diff --git a/.idea/PVPlant.iml b/.idea/PVPlant.iml
index d0876a7..cfa6fbe 100644
--- a/.idea/PVPlant.iml
+++ b/.idea/PVPlant.iml
@@ -5,4 +5,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/PVPlantFence.py b/Civil/Fence/PVPlantFence.py
similarity index 78%
rename from PVPlantFence.py
rename to Civil/Fence/PVPlantFence.py
index f78ab6e..b031b4e 100644
--- a/PVPlantFence.py
+++ b/Civil/Fence/PVPlantFence.py
@@ -20,16 +20,18 @@
# * *
# ***********************************************************************
+import FreeCAD
+import Part
+import Draft
+import MeshPart as mp
+import ArchComponent
+
+import Civil.Fence.PVPlantFencePost as PVPlantFencePost
+import PVPlantSite
+import Utils.PVPlantUtils as utils
import copy
import math
-import ArchComponent
-import Draft
-import FreeCAD
-import Part
-
-import PVPlantFencePost
-import PVPlantSite
if FreeCAD.GuiUp:
import FreeCADGui
@@ -56,26 +58,28 @@ from PVPlantResources import DirIcons as DirIcons
EAST = FreeCAD.Vector(1, 0, 0)
-def makeprojection(pathwire):
- site = FreeCAD.ActiveDocument.Site
- land = site.Terrain.Shape
- proj = land.makeParallelProjection(pathwire, FreeCAD.Vector(0, 0, 1))
- return proj
-
def makePVPlantFence(section, post, path):
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Fence')
- _Fence(obj)
+ Fence(obj)
obj.Post = post
obj.Base = path
if FreeCAD.GuiUp:
- _ViewProviderFence(obj.ViewObject)
+ ViewProviderFence(obj.ViewObject)
hide(section)
hide(post)
hide(path)
+ try:
+ fende_group = FreeCAD.ActiveDocument.Fences
+ except:
+ fende_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Fences')
+ fende_group.Label = "Fences"
+ FreeCAD.ActiveDocument.CivilGroup.addObject(fende_group)
+ fende_group.addObject(obj)
+
FreeCAD.ActiveDocument.recompute()
return obj
@@ -83,16 +87,8 @@ def hide(obj):
if hasattr(obj, 'ViewObject') and obj.ViewObject:
obj.ViewObject.Visibility = False
-def getAngle(Line1, Line2):
- v1 = Line1.Vertexes[1].Point - Line1.Vertexes[0].Point
- v2 = Line2.Vertexes[1].Point - Line2.Vertexes[0].Point
- return v1.getAngle(v2)
-
-
-def get_parameter_from_v0(edge, offset):
- """Return parameter at distance offset from edge.Vertexes[0].
- sb method in Part.TopoShapeEdge???
- """
+def get_parameter_from_v0_old(edge, offset):
+ """ Return parameter at distance offset from edge.Vertexes[0].sb method in Part.TopoShapeEdge??? """
import DraftVecUtils
lpt = edge.valueAt(edge.getParameterByLength(0))
@@ -106,14 +102,16 @@ def get_parameter_from_v0(edge, offset):
length = offset
return edge.getParameterByLength(length)
+def get_parameter_from_v0(edge, offset):
+ """Parámetro a distancia offset desde el primer vértice"""
+ lpt = edge.valueAt(edge.getParameterByLength(0))
+ vpt = edge.Vertexes[0].Point
+
+ if not vpt.isEqual(lpt, 1e-6):
+ return edge.getParameterByLength(edge.Length - offset)
+ return edge.getParameterByLength(offset)
def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal=None):
- """Orient shape to tangent at parm offset along edge."""
- import functools
- import DraftVecUtils
- # http://en.wikipedia.org/wiki/Euler_angles
- # start with null Placement point so translate goes to right place.
-
placement = FreeCAD.Placement()
placement.Rotation = globalRotation
placement.move(RefPt + xlate)
@@ -121,55 +119,23 @@ def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal
if not align:
return placement
- # unit +Z Probably defined elsewhere?
- z = FreeCAD.Vector(0, 0, 1)
- # y = FreeCAD.Vector(0, 1, 0) # unit +Y
- x = FreeCAD.Vector(1, 0, 0) # unit +X
- nullv = FreeCAD.Vector(0, 0, 0)
+ t = edge.tangentAt(get_parameter_from_v0(edge, offset)).normalize()
+ n = normal or FreeCAD.Vector(0, 0, 1)
+ b = t.cross(n).normalize()
- # get local coord system - tangent, normal, binormal, if possible
- t = edge.tangentAt(get_parameter_from_v0(edge, offset))
- t.normalize()
- n = normal
- b = t.cross(n)
- b.normalize()
+ # Asegurar sistema de coordenadas derecho
+ if n.dot(t.cross(b)) < 0:
+ b = -b
- lnodes = z.cross(b)
- try:
- # Can't normalize null vector.
- lnodes.normalize()
- except:
- # pathological cases:
- pass
-
- print(b, " - ", b.dot(z))
- if abs(b.dot(z)) == 1.0: # 2) binormal is || z
- # align shape to tangent only
- psi = math.degrees(DraftVecUtils.angle(x, t, z))
- theta = 0.0
- phi = 0.0
- FreeCAD.Console.PrintWarning("Draft PathArray.orientShape - Gimbal lock. Infinite lnodes. Change Path or Base.\n")
- else: # regular case
- psi = math.degrees(DraftVecUtils.angle(x, lnodes, z))
- theta = math.degrees(DraftVecUtils.angle(z, b, lnodes))
- phi = math.degrees(DraftVecUtils.angle(lnodes, t, b))
-
- rotations = [placement.Rotation]
-
- if psi != 0.0:
- rotations.insert(0, FreeCAD.Rotation(z, psi))
- if theta != 0.0:
- rotations.insert(0, FreeCAD.Rotation(lnodes, theta))
- if phi != 0.0:
- rotations.insert(0, FreeCAD.Rotation(b, phi))
-
- if len(rotations) == 1:
- finalRotation = rotations[0]
- else:
- finalRotation = functools.reduce(lambda rot1, rot2: rot1.multiply(rot2), rotations)
-
- placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), finalRotation.toEuler()[2])
+ # Construir matriz
+ rotation_matrix = FreeCAD.Matrix(
+ t.x, b.x, n.x, 0,
+ t.y, b.y, n.y, 0,
+ t.z, b.z, n.z, 0,
+ 0, 0, 0, 1
+ )
+ placement.Rotation = FreeCAD.Rotation(rotation_matrix)
return placement
def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
@@ -183,12 +149,8 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
import DraftGeomUtils
closedpath = DraftGeomUtils.isReallyClosed(pathwire)
- normal = DraftGeomUtils.getNormal(pathwire)
- if normal:
- if normal.z < 0: # asegurarse de que siempre se dibuje por encima del suelo
- normal.z *= -1
- else:
- normal = FreeCAD.Vector(0, 0, 1)
+
+ normal = FreeCAD.Vector(0, 0, 1)
path = Part.__sortEdges__(pathwire.Edges)
ends = []
cdist = 0
@@ -241,7 +203,7 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
return placements
-class _Fence(ArchComponent.Component):
+class Fence(ArchComponent.Component):
def __init__(self, obj):
ArchComponent.Component.__init__(self, obj)
self.setProperties(obj)
@@ -347,7 +309,6 @@ class _Fence(ArchComponent.Component):
QT_TRANSLATE_NOOP("App::Property", "The number of posts used to build the fence"))
obj.setEditorMode("Length", 1)
-
self.Type = "PVPlatFence"
def __getstate__(self):
@@ -361,75 +322,30 @@ class _Fence(ArchComponent.Component):
return None
def execute(self, obj):
+ if not obj.Base or not obj.Post:
+ return
+ # 1. Preparar trazado base
pathwire = self.calculatePathWire(obj)
- if pathwire is None:
- # FreeCAD.Console.PrintLog("ArchFence.execute: path " + obj.Base.Name + " has no edges\n")
- return
-
- if not obj.Post:
- FreeCAD.Console.PrintLog("ArchFence.execute: Post not set\n")
+ pathwire = utils.getProjected(pathwire, FreeCAD.Vector(0, 0, 1))
+ pathwire = utils.simplifyWire(pathwire)
+ if not pathwire or not pathwire.Edges:
return
+ # 2. Proyectar sobre terreno (con caché)
self.Posts = []
self.Foundations = []
- site = PVPlantSite.get()
- if True: # prueba
- import MeshPart as mp
- land = FreeCAD.ActiveDocument.Terrain.Mesh
- segments = mp.projectShapeOnMesh(pathwire, land, FreeCAD.Vector(0, 0, 1))
- points=[]
- for segment in segments:
- points.extend(segment)
- pathwire = Part.makePolygon(points)
- else:
- if PVPlantSite.get().Terrain.TypeId == 'Mesh::Feature':
- import MeshPart as mp
- land = PVPlantSite.get().Terrain.Mesh
- pathwire = mp.projectShapeOnMesh(pathwire, land, FreeCAD.Vector(0, 0, 1))
- else:
- land = site.Terrain.Shape
- pathwire = land.makeParallelProjection(pathwire, FreeCAD.Vector(0, 0, 1))
+ site = PVPlantSite.get()
+ segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))
+ points=[]
+ for segment in segments:
+ points.extend(segment)
+ pathwire = Part.makePolygon(points)
if pathwire is None:
return
- ''' no sirve:
- if len(pathwire.Wires) > 1:
- import draftgeoutils
- pathwire = draftgeoutils.wires.superWire(pathwire.Edges, True)
- Part.show(pathwire)
- '''
-
- ''' unir todas en una '''
- '''
- if len(pathwire.Wires) > 1:
- import Utils.PVPlantUtils as utils
- wires = pathwire.Wires
- new_wire = []
- to_compare = utils.getPoints(wires.pop(0))
- new_wire.extend(to_compare)
- while len(wires)>0:
- wire = wires[0]
- points = utils.getPoints(wire)
- to_remove = None
- if points[0] in to_compare:
- to_remove = points[0]
- if points[-1] in to_compare:
- to_remove = points[-1]
- if to_remove:
- to_compare = points.copy()
- points.remove(to_remove)
- new_wire.extend(points)
- wires.pop()
- continue
- wires.append(wires.pop())
- pathwire = Part.makePolygon(new_wire)
- #Part.show(pathwire)
- #return
- '''
-
sectionLength = obj.Gap.Value
postLength = 0 #obj.Post.Diameter.Value #considerarlo 0 porque no influye
postPlacements = []
@@ -457,18 +373,19 @@ class _Fence(ArchComponent.Component):
postPlacements.extend(placements)
+ # 5. Generar geometría
postShapes, postFoundation = self.calculatePosts(obj, postPlacements)
- sections, num = self.calculateSections(obj, postPlacements)
+ mesh = self.calculate_sections(obj, postPlacements)
postShapes = Part.makeCompound(postShapes)
postFoundation = Part.makeCompound(postFoundation)
- sections = Part.makeCompound(sections)
- compound = Part.makeCompound([postShapes, postFoundation, sections])
- obj.Shape = compound
- # Give information
+ # 6. Crear forma final
+ obj.Shape = Part.makeCompound([postShapes, postFoundation, mesh])
+
+ # 7. Actualizar propiedades
obj.NumberOfSections = count
- obj.NumberOfPosts = obj.NumberOfSections + 1
+ obj.NumberOfPosts = count + 1
obj.Length = pathLength
obj.Concrete = count * postFoundation.SubShapes[0].Volume
@@ -497,7 +414,7 @@ class _Fence(ArchComponent.Component):
def calculatePostPlacements(self, obj, pathwire, rotation):
postWidth = obj.Post.Diameter.Value
- transformationVector = FreeCAD.Vector(0, postWidth / 2, 0)
+ transformationVector = FreeCAD.Vector(0, - postWidth / 2, 0)
placements = calculatePlacementsOnPath(rotation, pathwire, int(obj.NumberOfSections) + 1, transformationVector, True)
# The placement of the last object is always the second entry in the list.
# So we move it to the end:
@@ -509,47 +426,36 @@ class _Fence(ArchComponent.Component):
posts = []
foundations = []
for placement in postPlacements:
- postCopy = obj.Post.Shape.copy()
- postCopy = Part.Solid(postCopy)
- postCopy.Placement = placement
- postCopy.Placement.Base.z += 100
- posts.append(postCopy)
+ new_post = obj.Post.Shape.copy()
+ new_post = Part.Solid(new_post)
+ new_post.Placement = placement
+ new_post.Placement.Base.z += 100
+ posts.append(new_post)
foundation = Part.makeCylinder(150, 700)
foundation.Placement = placement
foundation.Placement.Base.z -= obj.Depth.Value
- foundation = foundation.cut(postCopy)
+ #foundation = foundation.cut(new_post)
foundations.append(foundation)
return posts, foundations
- def calculateSections(self, obj, postPlacements):
- shapes = []
- faceNumbers = []
+ def calculate_sections(self, obj, postPlacements):
+ offsetz = FreeCAD.Vector(0, 0, obj.MeshOffsetZ.Value)
+ meshHeight = FreeCAD.Vector(0, 0, obj.MeshHeight.Value)
- offsetz = obj.MeshOffsetZ.Value
- meshHeight = obj.MeshHeight.Value
+ points_down = []
+ points_up = []
for i in range(len(postPlacements) - 1):
- startPlacement = postPlacements[i]
- endPlacement = postPlacements[i + 1]
+ p1 = postPlacements[i].Base + offsetz
+ p2 = postPlacements[i + 1].Base + offsetz
+ p3 = p1 + meshHeight
+ p4 = p2 + meshHeight
+ points_down.extend([p1, p2])
+ points_up.extend([p3, p4])
- p1 = startPlacement.Base + FreeCAD.Vector(0, 0, offsetz)
- p2 = endPlacement.Base + FreeCAD.Vector(0, 0, offsetz)
- p3 = p2 + FreeCAD.Vector(0, 0, meshHeight)
- p4 = p1 + FreeCAD.Vector(0, 0, meshHeight)
- pointlist = [p1, p2, p3, p4, p1]
-
- try:
- pol = Part.makePolygon(pointlist)
- face = Part.Face(pol)
- shapes.append(face)
- faceNumbers.append(1)
- except:
- print("No es posible crear la cara: ---------------------------------------------------")
- print(" +++++ Start: ", startPlacement.Base, " - end: ", endPlacement.Base)
- print(" +++++ algo: ", pointlist, "\n")
- print("---------------------------------------------------\n")
- return (shapes, faceNumbers)
+ shape = Part.makeRuledSurface(Part.makePolygon(points_down), Part.makePolygon(points_up))
+ return shape
def calculatePathWire(self, obj):
if obj.Base:
@@ -562,7 +468,7 @@ class _Fence(ArchComponent.Component):
return None
-class _ViewProviderFence(ArchComponent.ViewProviderComponent):
+class ViewProviderFence(ArchComponent.ViewProviderComponent):
"A View Provider for the Fence object"
def __init__(self, vobj):
@@ -642,7 +548,7 @@ class _ViewProviderFence(ArchComponent.ViewProviderComponent):
children.append(self.Object.Gate)
return children
-class _FenceTaskPanel:
+class FenceTaskPanel:
'''The TaskPanel to setup the fence'''
def __init__(self):
@@ -775,15 +681,8 @@ class _FenceTaskPanel:
self.form = [self.formFence, self.formPost, self.formFoundation]
# valores iniciales y creación del la valla:
- import Draft
- self.post = PVPlantFencePost.makeFencePost() # Arch.makePipe()
+ self.post = PVPlantFencePost.makeFencePost()
self.post.Label = "Post"
- Draft.autogroup(self.post)
-
- '''
- self.section = self.makeGrid()
- self.path = self.section.Base
- '''
FreeCAD.ActiveDocument.recompute()
self.fence = makePVPlantFence(self.section, self.post, self.path)
@@ -859,7 +758,7 @@ class CommandPVPlantFence:
return not FreeCAD.ActiveDocument is None
def Activated(self):
- self.TaskPanel = _FenceTaskPanel()
+ self.TaskPanel = FenceTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
@@ -877,12 +776,13 @@ if FreeCAD.GuiUp:
}
def IsActive(self):
+ site = FreeCAD.ActiveDocument.getObject("Site")
return (not (FreeCAD.ActiveDocument is None) and
- not (FreeCAD.ActiveDocument.getObject("Site") is None) and
- not (FreeCAD.ActiveDocument.getObject("Terrain") is None))
+ not (site is None) and
+ not (site.Terrain is None))
- import PVPlantFenceGate
+ import Civil.Fence.PVPlantFenceGate as PVPlantFenceGate
FreeCADGui.addCommand('PVPlantFence', CommandPVPlantFence())
- FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate._CommandPVPlantGate())
- FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost._CommandFencePost())
+ FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate.CommandPVPlantGate())
+ FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost.CommandFencePost())
#FreeCADGui.addCommand('PVPlantFenceGroup', CommandFenceGroup())
diff --git a/PVPlantFence.ui b/Civil/Fence/PVPlantFence.ui
similarity index 100%
rename from PVPlantFence.ui
rename to Civil/Fence/PVPlantFence.ui
diff --git a/PVPlantFenceGate.py b/Civil/Fence/PVPlantFenceGate.py
similarity index 98%
rename from PVPlantFenceGate.py
rename to Civil/Fence/PVPlantFenceGate.py
index ae57455..d5c6786 100644
--- a/PVPlantFenceGate.py
+++ b/Civil/Fence/PVPlantFenceGate.py
@@ -202,7 +202,7 @@ class ViewProviderGate:
children.append(self.Object.Base)
return children
-class _CommandPVPlantGate:
+class CommandPVPlantGate:
"the PVPlant Fence command definition"
def __init__(self):
@@ -256,7 +256,9 @@ class _CommandPVPlantGate:
gate = makePVPlantFence()
try:
import MeshPart as mp
- point1 = mp.projectPointsOnMesh([point1,], FreeCAD.ActiveDocument.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
+ import PVPlantSite
+ site = PVPlantSite.get()
+ point1 = mp.projectPointsOnMesh([point1,], site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
except:
FreeCAD.Console.PrintError("No se puede encontrar punto 3D.." + "\n")
diff --git a/PVPlantFenceGate.ui b/Civil/Fence/PVPlantFenceGate.ui
similarity index 100%
rename from PVPlantFenceGate.ui
rename to Civil/Fence/PVPlantFenceGate.ui
diff --git a/PVPlantFencePost.py b/Civil/Fence/PVPlantFencePost.py
similarity index 55%
rename from PVPlantFencePost.py
rename to Civil/Fence/PVPlantFencePost.py
index 55370e6..e46f0fe 100644
--- a/PVPlantFencePost.py
+++ b/Civil/Fence/PVPlantFencePost.py
@@ -1,5 +1,6 @@
-import ArchComponent
import FreeCAD
+import Part
+import ArchComponent
if FreeCAD.GuiUp:
import FreeCADGui
@@ -9,8 +10,6 @@ else:
# \cond
def translate(ctxt, txt):
return txt
-
-
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
@@ -21,20 +20,14 @@ except AttributeError:
def _fromUtf8(s):
return s
-
-def makeFencePost(diameter=48, length=3000, placement=None, name="Post"):
- "makePipe([baseobj,diamerter,length,placement,name]): creates an pipe object from the given base object"
-
- if not FreeCAD.ActiveDocument:
- FreeCAD.Console.PrintError("No active document. Aborting\n")
- return
+def makeFencePost(diameter=48, length=3000, placement=None, name="FencePost"):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
obj.Label = name
- _FencePost(obj)
+ FencePost(obj)
if FreeCAD.GuiUp:
- _ViewProviderFencePost(obj.ViewObject)
+ ViewProviderFencePost(obj.ViewObject)
obj.Length = length
obj.Diameter = diameter
@@ -45,18 +38,13 @@ def makeFencePost(diameter=48, length=3000, placement=None, name="Post"):
def makeFenceReinforcePost(diameter=48, length=3000, placement=None, name="Post"):
- "makePipe([baseobj,diamerter,length,placement,name]): creates an pipe object from the given base object"
-
- if not FreeCAD.ActiveDocument:
- FreeCAD.Console.PrintError("No active document. Aborting\n")
- return
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
obj.Label = name
- _FenceReinforcePostPost(obj)
+ FenceReinforcePost(obj)
if FreeCAD.GuiUp:
- _ViewProviderFencePost(obj.ViewObject)
+ ViewProviderFencePost(obj.ViewObject)
obj.Length = length
obj.Diameter = diameter
@@ -66,7 +54,7 @@ def makeFenceReinforcePost(diameter=48, length=3000, placement=None, name="Post"
return obj
-class _FencePost(ArchComponent.Component):
+class FencePost(ArchComponent.Component):
def __init__(self, obj):
ArchComponent.Component.__init__(self, obj)
self.setProperties(obj)
@@ -80,10 +68,10 @@ class _FencePost(ArchComponent.Component):
obj.addProperty("App::PropertyLength", "Diameter", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The diameter of this pipe, if not based on a profile")
).Diameter = 48
- if not "Thickness" in pl:
+ '''if not "Thickness" in pl:
obj.addProperty("App::PropertyLength", "Thickness", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The Thickness of this pipe, if not based on a profile")
- ).Thickness = 4
+ ).Thickness = 4'''
if not "Length" in pl:
obj.addProperty("App::PropertyLength", "Length", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The length of this pipe, if not based on an edge")
@@ -94,86 +82,49 @@ class _FencePost(ArchComponent.Component):
ArchComponent.Component.onDocumentRestored(self, obj)
self.setProperties(obj)
+ def get_axis(self, obj, lip_heigth):
+ wire = Part.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth))
+ #wire = Part.makePolygon(FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth),)
+ return Part.Wire(wire)
+
def execute(self, obj):
- import Part
pl = obj.Placement
- if obj.CloneOf:
- obj.Shape = obj.CloneOf.Shape
- else:
- w = self.getProfile(obj)
- try:
- # sh = w.makePipeShell([p], True, False, 2)
- sh = w.revolve(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Vector(0.0, 0.0, 1.0), 360)
- sh = Part.Solid(sh)
- except:
- FreeCAD.Console.PrintError("Unable to build the pipe \n")
- else:
- obj.Shape = sh
- obj.Placement = pl
+ lip_heigth = 20
+ radius = obj.Diameter.Value / 2
+ # para que sea una función que sirva para los postes rectos y con curva:
+ axis = self.get_axis(obj, lip_heigth)
+ profile = Part.Wire([Part.Circle(FreeCAD.Vector(0,0,0), FreeCAD.Vector(0,0,1), radius).toShape()])
+ post = axis.makePipeShell([profile, ],True,True,2)
+
+ lip = Part.makeCylinder(radius + 2, lip_heigth)
+ lip = lip.makeFillet(5, [lip.Edges[0]])
+
+ # Obtener caras
+ face_post = post.Faces[2] # Cara superior del cilindro largo
+ face_lip = lip.Faces[2] # Cara inferior del cilindro corto
+
+ # Calcular centro y normal de las caras
+ face_post_center = face_post.CenterOfMass
+ face_post_normal = face_post.normalAt(0, 0)
+ face_lip_center = face_lip.CenterOfMass
+ face_lip_normal = face_lip.normalAt(0, 0)
+
+ # Calcular rotación para alinear normales (ajustar dirección)
+ rotacion = FreeCAD.Rotation(face_lip_normal, -face_post_normal) # Invertir normal del cilindro corto
+ lip.Placement.Rotation = rotacion.multiply(lip.Placement.Rotation)
+
+ # Calcular traslación: mover centro del cilindro corto al centro del cilindro largo
+ traslacion = face_post_center - rotacion.multVec(face_lip_center)
+ lip.Placement.Base = traslacion #face_post_center
+
+ obj.Shape = post.fuse(lip)
+ obj.Placement = pl
return
- # ------------------------- Prueba para apoyos de refuerzo:
- import math
- L = math.pi / 2 * (obj.Diameter.Value - 2 * obj.Thickness.Value)
- v1 = FreeCAD.Vector(L / 2, 0, obj.Thickness.Value)
- vc1 = FreeCAD.Vector(L / 2 + obj.Thickness.Value, 0, 0)
- v2 = FreeCAD.Vector(L / 2, 0, -obj.Thickness.Value)
-
- v11 = FreeCAD.Vector(-L / 2, 0, obj.Thickness.Value)
- vc11 = FreeCAD.Vector(-(L / 2 + obj.Thickness.Value), 0, 0)
- v21 = FreeCAD.Vector(-L / 2, 0, -obj.Thickness.Value)
-
- arc1 = Part.Arc(v1, vc1, v2).toShape()
- arc11 = Part.Arc(v11, vc11, v21).toShape()
- line1 = Part.LineSegment(v11, v1).toShape()
- line2 = Part.LineSegment(v21, v2).toShape()
- w = Part.Wire([arc1, line2, arc11, line1])
- face = Part.Face(w)
- pro = face.extrude(FreeCAD.Vector(0, 40, 0))
-
- #Part.Circle(Center, Normal, Radius)
- cir1 = Part.Face(Part.Wire(Part.Circle(FreeCAD.Vector(0, -200, 0), FreeCAD.Vector(0, 1, 0), obj.Diameter.Value / 2).toShape()))
- ext = cir1.extrude(FreeCAD.Vector(0, 170, 0))
- cir2 = Part.Circle(FreeCAD.Vector(0, -30, 0), FreeCAD.Vector(0, 1, 0), obj.Diameter.Value/2).toShape()
- loft = Part.makeLoft([cir2, w], True)
- ext = ext.fuse([loft, pro])
- Part.show(ext)
-
-
- def getProfile(self, obj):
- import Part
- sin45 = 0.707106781
- radio = obj.Diameter.Value / 2
- taph = 20
- tapw = radio + 2
- chamfer = 5
- chamfer2 = chamfer * sin45
-
- edge1 = Part.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(radio, 0, 0))
- edge2 = Part.makeLine(FreeCAD.Vector(radio, 0, 0), FreeCAD.Vector(radio, 0, obj.Length.Value - taph))
- edge3 = Part.makeLine(FreeCAD.Vector(radio, 0, obj.Length.Value - taph),
- FreeCAD.Vector(tapw, 0, obj.Length.Value - taph))
- edge4 = Part.makeLine(FreeCAD.Vector(tapw, 0, obj.Length.Value - taph),
- FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer))
- if True:
- edge5 = Part.makeLine(FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer),
- FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value))
- else:
- edge5 = Part.Arc(FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer),
- FreeCAD.Vector(tapw - chamfer2, 0, obj.Length.Value - chamfer2),
- FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value)
- ).toShape()
- edge6 = Part.makeLine(FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value),
- FreeCAD.Vector(0, 0, obj.Length.Value))
- w = Part.Wire([edge1, edge2, edge3, edge4, edge5, edge6])
-
- return w
-
-
-class _FenceReinforcePost(ArchComponent.Component):
+class FenceReinforcePost(ArchComponent.Component):
def __init__(self, obj):
ArchComponent.Component.__init__(self, obj)
self.setProperties(obj)
@@ -199,10 +150,18 @@ class _FenceReinforcePost(ArchComponent.Component):
self.setProperties(obj)
def execute(self, obj):
-
pl = obj.Placement
- w = self.getWire(obj)
+ lip_heigth = 20
+ post = Part.makeCylinder(obj.Diameter.Value / 2, obj.Length.Value - lip_heigth)
+ lip = Part.makeCylinder(obj.Diameter.Value / 2 + 2, lip_heigth)
+ lip = lip.makeFillet(5, [lip.Edges[0]])
+ lip.translate(FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth))
+ obj.Shape = post.fuse(lip)
+ obj.Placement = pl
+ return
+
+ w = self.getWire(obj)
try:
# sh = w.makePipeShell([p], True, False, 2)
sh = w.revolve(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Vector(0.0, 0.0, 1.0), 360)
@@ -244,7 +203,7 @@ class _FenceReinforcePost(ArchComponent.Component):
return w
-class _ViewProviderFencePost(ArchComponent.ViewProviderComponent):
+class ViewProviderFencePost(ArchComponent.ViewProviderComponent):
"A View Provider for the Pipe object"
def __init__(self, vobj):
@@ -254,7 +213,7 @@ class _ViewProviderFencePost(ArchComponent.ViewProviderComponent):
return ":/icons/Arch_Pipe_Tree.svg"
-class _CommandFencePost:
+class CommandFencePost:
"the Arch Pipe command definition"
def GetResources(self):
@@ -269,17 +228,5 @@ class _CommandFencePost:
return not FreeCAD.ActiveDocument is None
def Activated(self):
- if True:
- makeFencePost()
- else:
- FreeCAD.ActiveDocument.openTransaction(translate("Arch", "Create Pipe"))
- FreeCADGui.addModule("Arch")
- FreeCADGui.doCommand("obj = Arch.makePipe()")
- FreeCADGui.addModule("Draft")
- FreeCADGui.doCommand("Draft.autogroup(obj)")
- FreeCAD.ActiveDocument.commitTransaction()
+ makeFencePost()
FreeCAD.ActiveDocument.recompute()
-
-
-if FreeCAD.GuiUp:
- FreeCADGui.addCommand('FencePost', _CommandFencePost())
diff --git a/Electrical/PowerConverter/PowerConverter.py b/Electrical/PowerConverter/PowerConverter.py
new file mode 100644
index 0000000..12e9576
--- /dev/null
+++ b/Electrical/PowerConverter/PowerConverter.py
@@ -0,0 +1,200 @@
+# /**********************************************************************
+# * *
+# * 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 ArchComponent
+import Part
+import os
+import zipfile
+import re
+
+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")
+
+vector = ["Y", "YN", "Z", "ZN", "D"]
+
+def makePCS():
+ obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PowerConversionSystem")
+ PowerConverter(obj)
+ ViewProviderPowerConverter(obj.ViewObject)
+
+ try:
+ folder = FreeCAD.ActiveDocument.PowerConversionSystemGroup
+ except:
+ folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'PowerConversionSystemGroup')
+ folder.Label = "PowerConversionSystemGroup"
+ folder.addObject(obj)
+ return obj
+
+
+class PowerConverter(ArchComponent.Component):
+ def __init__(self, obj):
+ ''' Initialize the Area object '''
+ ArchComponent.Component.__init__(self, obj)
+ self.Type = None
+ self.obj = None
+ self.setProperties(obj)
+
+ def setProperties(self, obj):
+ pl = obj.PropertiesList
+
+ # Transformer properties
+ if not "Technology" in pl:
+ obj.addProperty("App::PropertyEnumeration",
+ "Technology",
+ "Transformer",
+ "Number of phases and type of transformer"
+ ).Technology = ["Single Phase Transformer", "Three Phase Transformer"]
+
+ if not "PowerPrimary" in pl:
+ obj.addProperty("App::PropertyPower",
+ "PowerPrimary",
+ "Transformer",
+ "The base file this component is built upon").PowerPrimary = 6000000000
+
+ 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
+
+ def onDocumentRestored(self, obj):
+ """ Method run when the document is restored """
+ self.setProperties(obj)
+
+ def onBeforeChange(self, obj, prop):
+ ''' '''
+ # This method is called before a property is changed.
+ # 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 execute(self, obj):
+ ''' '''
+ # obj.Shape: compound
+ # |- body: compound
+ # |- transformer: solid
+ # |- primary switchgear: compound
+ # |- secundary 1 switchgear: compound
+ # |- secundary 2 switchgear: compound
+
+ pl = obj.Placement
+ obj.Shape = Part.makeBox(6058, 2438, 2591) # Placeholder for the shape
+ obj.Placement = pl
+
+
+
+class ViewProviderPowerConverter(ArchComponent.ViewProviderComponent):
+ def __init__(self, vobj):
+ ArchComponent.ViewProviderComponent.__init__(self, vobj)
+
+ def getIcon(self):
+ return str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg"))
+
+class CommandPowerConverter:
+
+ def GetResources(self):
+ return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg")),
+ 'Accel': "E, P",
+ 'MenuText': "Power Converter",
+ 'ToolTip': "Power Converter",}
+
+ def Activated(self):
+ sinverter = makePCS()
+
+ def IsActive(self):
+ active = not (FreeCAD.ActiveDocument is None)
+ return active
+
+if FreeCAD.GuiUp:
+ FreeCADGui.addCommand('PowerConverter', CommandPowerConverter())
+
diff --git a/Electrical/group.py b/Electrical/group.py
new file mode 100644
index 0000000..66672fc
--- /dev/null
+++ b/Electrical/group.py
@@ -0,0 +1,392 @@
+def groupTrackersToTransformers(transformer_power, max_distance):
+ import numpy as np
+ from scipy.spatial import KDTree
+ import FreeCAD
+ from collections import deque
+
+ # 1. Obtener todos los trackers válidos
+ valid_trackers = []
+ valid_points = []
+ valid_power = []
+
+ for tracker in FreeCAD.ActiveDocument.Objects:
+ if hasattr(tracker, 'Proxy') and (tracker.Proxy.Type == "Tracker"):
+ base = tracker.Placement.Base
+ if all(np.isfinite([base.x, base.y, base.z])):
+ valid_trackers.append(tracker)
+ valid_points.append([base.x, base.y])
+ valid_power.append(tracker.Setup.TotalPower)
+
+ if not valid_trackers:
+ FreeCAD.Console.PrintWarning("No se encontraron trackers válidos para agrupar\n")
+ return
+
+ # 2. Obtener parámetros de los CTs
+ target_power = transformer_power * 1.2
+ points = np.array(valid_points)
+ power_values = np.array(valid_power)
+
+ # 3. Determinar dirección de barrido (oeste a este por defecto)
+ min_x, min_y = np.min(points, axis=0)
+ max_x, max_y = np.max(points, axis=0)
+
+ # 4. Ordenar trackers de oeste a este (menor X a mayor X)
+ '''
+ Norte a Sur: sorted_indices = np.argsort(-points[:, 1]) (Y descendente)
+ Sur a Norte: sorted_indices = np.argsort(points[:, 1]) (Y ascendente)
+ Este a Oeste: sorted_indices = np.argsort(-points[:, 0]) (X descendente)
+ '''
+ sorted_indices = np.argsort(points[:, 0])
+ sorted_points = points[sorted_indices]
+ sorted_power = power_values[sorted_indices]
+ sorted_trackers = [valid_trackers[i] for i in sorted_indices]
+
+ # 5. Crear KDTree para búsquedas rápidas
+ kdtree = KDTree(sorted_points)
+
+ # 6. Algoritmo de barrido espacial
+ transformer_groups = []
+ used_indices = set()
+
+ # Función para expandir un grupo desde un punto inicial
+ def expand_group(start_idx):
+ group = []
+ total_power = 0
+ queue = deque([start_idx])
+
+ while queue and total_power < target_power:
+ idx = queue.popleft()
+ if idx in used_indices:
+ continue
+
+ # Añadir tracker al grupo si no excede la potencia
+ tracker_power = sorted_power[idx]
+ if total_power + tracker_power > target_power * 1.05:
+ continue
+
+ group.append(sorted_trackers[idx])
+ total_power += tracker_power
+ used_indices.add(idx)
+
+ # Buscar vecinos cercanos
+ neighbors = kdtree.query_ball_point(
+ sorted_points[idx],
+ max_distance
+ )
+
+ # Filtrar vecinos no usados y ordenar por proximidad al punto inicial
+ neighbors = [n for n in neighbors if n not in used_indices]
+ neighbors.sort(key=lambda n: abs(n - start_idx))
+ queue.extend(neighbors)
+
+ return group, total_power
+
+ # 7. Barrido principal de oeste a este
+ for i in range(len(sorted_points)):
+ if i in used_indices:
+ continue
+
+ group, total_power = expand_group(i)
+
+ if group:
+ # Calcular centro del grupo
+ group_points = np.array([t.Placement.Base[:2] for t in group])
+ center = np.mean(group_points, axis=0)
+
+ transformer_groups.append({
+ 'trackers': group,
+ 'total_power': total_power,
+ 'center': center
+ })
+
+ # 8. Manejar grupos residuales (si los hay)
+ unused_indices = set(range(len(sorted_points))) - used_indices
+ if unused_indices:
+ # Intentar añadir trackers residuales a grupos existentes
+ for idx in unused_indices:
+ point = sorted_points[idx]
+ tracker_power = sorted_power[idx]
+
+ # Buscar el grupo más cercano que pueda aceptar este tracker
+ best_group = None
+ min_distance = float('inf')
+
+ for group in transformer_groups:
+ if group['total_power'] + tracker_power <= target_power * 1.05:
+ dist = np.linalg.norm(point - group['center'])
+ if dist < min_distance and dist < max_distance * 1.5:
+ min_distance = dist
+ best_group = group
+
+ # Añadir al grupo si se encontró uno adecuado
+ if best_group:
+ best_group['trackers'].append(sorted_trackers[idx])
+ best_group['total_power'] += tracker_power
+ # Actualizar centro del grupo
+ group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']])
+ best_group['center'] = np.mean(group_points, axis=0)
+ else:
+ # Crear un nuevo grupo con este tracker residual
+ group = [sorted_trackers[idx]]
+ center = point
+ transformer_groups.append({
+ 'trackers': group,
+ 'total_power': tracker_power,
+ 'center': center
+ })
+
+ # 9. Crear los grupos en FreeCAD
+ transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers")
+ transformer_group.Label = "Centros de Transformación"
+
+ for i, group in enumerate(transformer_groups):
+ # Crear la esfera que representará el CT
+ ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
+ ct_shape.Length = 6058
+ 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
+ ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
+ "Lista de trackers asociados a este CT")
+ ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
+ "Potencia total del grupo (W)")
+ ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
+ "Potencia nominal del transformador (W)")
+ ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
+ "Porcentaje de utilización (Total/Nominal)")
+
+ # Establecer valores de las propiedades
+ ct_shape.Trackers = group['trackers']
+ ct_shape.TotalPower = group['total_power'].Value
+ ct_shape.NominalPower = transformer_power
+ ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100
+
+ # Configurar visualización
+ # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
+ utilization = ct_shape.Utilization
+ if utilization <= 100:
+ color = (0.0, 1.0, 0.0) # Verde
+ elif utilization <= 110:
+ color = (1.0, 1.0, 0.0) # Amarillo
+ else:
+ color = (1.0, 0.0, 0.0) # Rojo
+
+ ct_shape.ViewObject.ShapeColor = color
+ ct_shape.ViewObject.Transparency = 40 # 40% de transparencia
+
+ # Añadir etiqueta con información
+ ct_shape.ViewObject.DisplayMode = "Shaded"
+ ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"
+
+ # Añadir al grupo principal
+ transformer_group.addObject(ct_shape)
+
+ FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")
+
+ #onSelectGatePoint()
+
+
+import FreeCAD, FreeCADGui, Part
+import numpy as np
+from scipy.stats import linregress
+from PySide import QtGui
+
+class InternalPathCreator:
+ def __init__(self, gate_point, strategy=1, path_width=4000):
+ self.gate_point = gate_point
+ self.strategy = strategy
+ self.path_width = path_width
+ self.ct_shapes = []
+ self.ct_positions = []
+
+ def get_transformers(self):
+ transformers_group = FreeCAD.ActiveDocument.getObject("Transformers")
+ if not transformers_group:
+ FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
+ return False
+
+ self.ct_shapes = transformers_group.Group
+ if not self.ct_shapes:
+ FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
+ return False
+
+ # Obtener las posiciones de los CTs
+ for sphere in self.ct_shapes:
+ base = sphere.Placement.Base
+ self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
+ return True
+
+ def create_paths(self):
+ if not self.get_transformers():
+ return []
+
+ if self.strategy == 1:
+ return self.create_direct_paths()
+ elif self.strategy == 2:
+ return self.create_unified_path()
+ else:
+ FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n")
+ return []
+
+ def create_direct_paths(self):
+ """Estrategia 1: Caminos independientes desde cada CT hasta la puerta"""
+ paths = []
+ for ct in self.ct_positions:
+ paths.append([ct, self.gate_point])
+ return paths
+
+ def create_unified_path(self):
+ """Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal"""
+ if not self.ct_positions:
+ return []
+
+ all_points = self.ct_positions + [self.gate_point]
+ x = [p.x for p in all_points]
+ y = [p.y for p in all_points]
+
+ # Manejar caso de puntos alineados verticalmente
+ if np.std(x) < 1e-6:
+ sorted_points = sorted(all_points, key=lambda p: p.y)
+ paths = []
+ for i in range(len(sorted_points) - 1):
+ paths.append([sorted_points[i], sorted_points[i + 1]])
+ return paths
+
+ # Calcular regresión lineal
+ slope, intercept, _, _, _ = linregress(x, y)
+
+ # Función para proyectar puntos
+ def project_point(point):
+ x0, y0 = point.x, point.y
+ if abs(slope) > 1e6:
+ return FreeCAD.Vector(x0, intercept, 0)
+ x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2)
+ y_proj = slope * x_proj + intercept
+ return FreeCAD.Vector(x_proj, y_proj, 0)
+
+ # return slope * x + intercept --> desde placement
+
+ projected_points = [project_point(p) for p in all_points]
+
+ # Calcular distancias a lo largo de la línea
+ ref_point = projected_points[0]
+ direction_vector = FreeCAD.Vector(1, slope).normalize()
+ distances = []
+ for p in projected_points:
+ vec_to_point = p - ref_point
+ distance = vec_to_point.dot(direction_vector)
+ distances.append(distance)
+
+ # Ordenar por distancia
+ sorted_indices = np.argsort(distances)
+ sorted_points = [all_points[i] for i in sorted_indices]
+
+ # Crear caminos
+ paths = []
+ for i in range(len(sorted_points) - 1):
+ paths.append([sorted_points[i], sorted_points[i + 1]])
+ return paths
+
+ def create_3d_path(self, path_poly):
+ """Crea geometría 3D para el camino adaptada a orientación norte-sur"""
+ segments = []
+ for i in range(len(path_poly.Vertexes) - 1):
+ start = path_poly.Vertexes[i].Point
+ end = path_poly.Vertexes[i + 1].Point
+ direction = end - start
+
+ # Determinar orientación predominante
+ if abs(direction.x) > abs(direction.y):
+ normal = FreeCAD.Vector(0, 1, 0) # Norte-sur
+ else:
+ normal = FreeCAD.Vector(1, 0, 0) # Este-oeste
+
+ offset = normal * self.path_width / 2
+
+ # Crear puntos para la sección transversal
+ p1 = start + offset
+ p2 = start - offset
+ p3 = end - offset
+ p4 = end + offset
+
+ # Crear cara
+ wire = Part.makePolygon([p1, p2, p3, p4, p1])
+ face = Part.Face(wire)
+ segments.append(face)
+
+ if segments:
+ '''road_shape = segments[0].fuse(segments[1:])
+ return road_shape.removeSplitter()'''
+ return Part.makeCompound(segments)
+ return Part.Shape()
+
+ def build(self):
+ paths = self.create_paths()
+ if not paths:
+ return
+
+ path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths")
+ path_group.Label = f"Caminos Internos (Estrategia {self.strategy})"
+
+ for i, path in enumerate(paths):
+ poly = Part.makePolygon(path)
+
+ # Objeto para la línea central
+ path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}")
+ path_obj.Shape = poly
+ path_obj.ViewObject.LineWidth = 3.0
+ path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0)
+
+ # Objeto para la superficie 3D
+ road_shape = self.create_3d_path(poly)
+ road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}")
+ road_obj.Shape = road_shape
+ road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
+
+ path_group.addObject(path_obj)
+ path_group.addObject(road_obj)
+
+ FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n")
+
+
+# Función para mostrar el diálogo de estrategia
+def show_path_strategy_dialog(gate_point):
+ dialog = QtGui.QDialog()
+ dialog.setWindowTitle("Seleccionar Estrategia de Caminos")
+ layout = QtGui.QVBoxLayout(dialog)
+
+ label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:")
+ layout.addWidget(label)
+
+ rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta")
+ rb1.setChecked(True)
+ layout.addWidget(rb1)
+
+ rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta")
+ layout.addWidget(rb2)
+
+ btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
+ layout.addWidget(btn_box)
+
+ def on_accept():
+ strategy = 1 if rb1.isChecked() else 2
+ dialog.accept()
+ creator = InternalPathCreator(gate_point, strategy)
+ creator.build()
+
+ btn_box.accepted.connect(on_accept)
+ btn_box.rejected.connect(dialog.reject)
+
+ dialog.exec_()
+
+
+# Uso: seleccionar un punto para la puerta de entrada
+def onSelectGatePoint():
+ '''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0]
+ gate_point = gate.Placement.Base
+ show_path_strategy_dialog(gate_point)'''
+
+ sel = FreeCADGui.Selection.getSelectionEx()[0]
+ show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass)
\ No newline at end of file
diff --git a/Export/exportDXF.py b/Export/exportDXF.py
index 1dbad34..c2d5ce6 100644
--- a/Export/exportDXF.py
+++ b/Export/exportDXF.py
@@ -1,5 +1,6 @@
import math
import FreeCAD
+import Part
from Utils.PVPlantUtils import findObjects
if FreeCAD.GuiUp:
@@ -75,8 +76,10 @@ def getWire(wire, nospline=False, width=.0):
import DraftGeomUtils
import math
+ offset = FreeCAD.ActiveDocument.Site.Origin
+
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 = []
edges = Part.__sortEdges__(wire.Edges)
@@ -259,15 +262,60 @@ class exportDXF:
'rotation': rotation
})
- def createPolyline(self, wire):
+ def createPolyline(self, wire, layer=""):
try:
data = getWire(wire.Shape)
lwp = self.msp.add_lwpolyline(data)
+ if layer:
+ lwp.dxf.layer = layer
return lwp
except Exception as e:
print("Error creating polyline:", e)
return None
+ def createHatch(self, wire, pattern="SOLID", scale=1.0, angle=0, layer=None):
+ """Crea un sombreado (hatch) para un área"""
+ try:
+ # Obtener los puntos en metros
+ points = [(x, y) for (x, y, *_) in wire]
+
+ # Crear el hatch
+ hatch = self.msp.add_hatch(color=7, dxfattribs={'layer': layer})
+
+ if pattern == "SOLID":
+ # Sombreado sólido
+ hatch.set_solid_fill()
+ else:
+ # Patrón de sombreado
+ hatch.set_pattern_fill(name=pattern, scale=scale, angle=angle)
+
+ # Añadir el contorno
+ hatch.paths.add_polyline_path(points, is_closed=True)
+ return hatch
+ except Exception as e:
+ print("Error creating hatch:", e)
+ return None
+
+ def export_feature_image(self, feature, layer_name):
+ """Exporta una imagen del GeoFeature y la añade al DXF"""
+ try:
+ # Añadir la imagen al DXF
+ image_def = self.doc.add_image_def(feature.ImageFile, size_in_pixel=(feature.XSize, feature.YSize))
+ self.msp.add_image(image_def,
+ insert=(0, 0, 0),
+ size_in_units=(feature.XSize,
+ feature.YSize),
+ rotation=0,
+ dxfattribs={'layer': layer_name})
+
+ print(f"Imagen exportada para {feature.Label}")
+
+ # Eliminar el archivo temporal
+ import os
+ os.unlink(temp_img.name)
+ except Exception as e:
+ print(f"Error en exportación de imagen: {e}")
+
# =================================================================================
# INTERFAZ DE USUARIO
# =================================================================================
@@ -575,6 +623,59 @@ class LineTypeComboBox(QtWidgets.QComboBox):
finally:
painter.end() # Asegurar que el painter se cierre correctamente
+layers = [
+ ("Available area", QtGui.QColor(0, 204, 153), "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 Offset", 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 Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("Areas Offset", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+
+ ("Cable codes LV AC inverter", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("Cable codes LV string", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("Cable codes MV System", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("CABLES LV AC inverter 120 mm2", QtGui.QColor(255, 204, 0), "Continuous", "1", True),
+ ("CABLES LV AC inverter 185 mm2", QtGui.QColor(204, 153, 0), "Continuous", "1", True),
+ ("CABLES LV string 4 mm2", QtGui.QColor(255, 255, 0), "Continuous", "1", True),
+ ("CABLES LV string 10 mm2", QtGui.QColor(255, 255, 102), "Continuous", "1", True),
+ ("CABLES MV system 300 mm2", QtGui.QColor(102, 51, 0), "Continuous", "1", True),
+
+ ("CIVIL Fence", QtGui.QColor(102, 102, 102), "FENCELINE1", "1", True),
+ ("CIVIL External Roads", QtGui.QColor(91, 91, 91), "Continuous", "1", True),
+ ("CIVIL External Roads Axis", QtGui.QColor(255, 255, 192), "Dashed", "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 Axis", QtGui.QColor(192, 192, 192), "Dashed", "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),
+ ("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
+ ("Major contour value", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
+ ("Minor contour line", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
+ ("Minor contour value", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
+ ("Power Stations", QtGui.QColor(255, 0, 0), "Continuous", "1", True),
+ ("Power Stations Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("ST", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("ST Names", QtGui.QColor(255, 255, 0), "Continuous", "1", True),
+ ("String Inv", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("STRUC Structure 1", QtGui.QColor(0, 0, 255), "Continuous", "1", True),
+ ("STRUC Structure 2", QtGui.QColor(0, 0, 204), "Continuous", "1", True),
+ ("STRUC Structure 3", QtGui.QColor(0, 0, 153), "Continuous", "1", True),
+ ("STRUC Structure 4", QtGui.QColor(0, 0, 128), "Continuous", "1", True),
+ ("STRUC Structure 5", QtGui.QColor(0, 0, 102), "Continuous", "1", True),
+ ("STRUC Structure 6", QtGui.QColor(0, 0, 76), "Continuous", "1", True),
+ ("STRUC Structure 7", QtGui.QColor(0, 0, 51), "Continuous", "1", True),
+ ("STRUC Structure 8", QtGui.QColor(0, 0, 25), "Continuous", "1", True),
+ ("Structure Codes", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("TRENCHES Low voltage 400.0 x 1000.0 m", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
+ ("TRENCHES Medium voltage 400.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("TRENCHES Medium voltage 800.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+ ("TRENCHES Medium voltage 800.0 x 1500.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
+]
+
class _PVPlantExportDXF(QtGui.QWidget):
'''The editmode TaskPanel to select what you want to export'''
@@ -610,10 +711,8 @@ class _PVPlantExportDXF(QtGui.QWidget):
self.form.tableLayers.removeRow(row)
# Configuración de las capas por defecto
- self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True)
- self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "CONTINUOUS", "1", True)
- self.add_row("Internal_Roads", QtGui.QColor(128, 128, 128), "CONTINUOUS", "1", True)
- self.add_row("Frames", QtGui.QColor(0, 255, 0), "CONTINUOUS", "1", True)
+ for row in layers:
+ self.add_row(*row)
def save_project_settings(self):
"""Guarda la configuración actual en el proyecto de FreeCAD"""
@@ -778,25 +877,28 @@ class _PVPlantExportDXF(QtGui.QWidget):
)
def writeArea(self, exporter):
- exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "boundary")
-
- areas_types = ["Boundaries", "Exclusions", "Offsets"]
+ exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "Available area")
+ areas_types = [("Boundaries", "Available area"),
+ ("CadastralPlots", "Areas Cadastral Plot"),
+ ("Exclusions", "Areas Exclusion"),
+ ("Offsets", "Areas Offset")]
for area_type in areas_types:
- if hasattr(FreeCAD.ActiveDocument, area_type):
- for area in FreeCAD.ActiveDocument.Boundaries.Group:
- exporter.createPolyline(area, "Areas_Boundary")
+ if hasattr(FreeCAD.ActiveDocument, area_type[0]):
+ area_group = FreeCAD.ActiveDocument.getObjectsByLabel(area_type[0])
+ if len(area_group):
+ for area in area_group[0].Group:
+ tmp = exporter.createPolyline(area, area_type[1])
+ if area_type[0] == "Exclusions":
+ exporter.createHatch(
+ tmp,
+ pattern="ANSI37",
+ scale=0.3,
+ angle=0,
+ layer=area_type[1]
+ )
+ for obj in FreeCADGui.Selection.getSelection():
+ tmp = exporter.createPolyline(obj, areas_types[0][1])
- '''for area in FreeCAD.ActiveDocument.Boundaries.Group:
- pol = exporter.createPolyline(area)
- pol.dxf.layer = "Areas_Boundary"
-
- for area in FreeCAD.ActiveDocument.Exclusions.Group:
- pol = exporter.createPolyline(area)
- pol.dxf.layer = "Areas_Exclusion"
-
- for area in FreeCAD.ActiveDocument.Offsets.Group:
- pol = exporter.createPolyline(area)
- pol.dxf.layer = "Areas_Offsets"'''
def writeFrameSetups(self, exporter):
if not hasattr(FreeCAD.ActiveDocument, "Site"):
@@ -811,11 +913,11 @@ class _PVPlantExportDXF(QtGui.QWidget):
w.Placement.Base = w.Placement.Base.sub(center)
block.add_lwpolyline(getWire(w))
- block.add_circle((0, 0), 0.2, dxfattribs={'color': 2})
+ block.add_circle((0, 0), 0.2, dxfattribs={'layer': 'Structure Posts'}) #'color': 2,
p = math.sin(math.radians(45)) * 0.2
- block.add_line((-p, -p), (p, p), dxfattribs={"layer": "MyLines"})
- block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"})
+ block.add_line((-p, -p), (p, p), dxfattribs={'layer': 'Structure Posts'})
+ block.add_line((-p, p), (p, -p), dxfattribs={'layer': 'Structure Posts'})
# 2. Frames
for ts in FreeCAD.ActiveDocument.Site.Frames:
@@ -868,10 +970,10 @@ class _PVPlantExportDXF(QtGui.QWidget):
if FreeCAD.ActiveDocument.Transport:
for road in FreeCAD.ActiveDocument.Transport.Group:
- base = exporter.createPolyline(road, "External_Roads")
+ base = exporter.createPolyline(road, "CIVIL External Roads")
base.dxf.const_width = road.Width
- axis = exporter.createPolyline(road.Base, "External_Roads_Axis")
+ axis = exporter.createPolyline(road, "CIVIL External Roads Axis")
axis.dxf.const_width = .2
def writeTrenches(self, exporter):
@@ -976,8 +1078,12 @@ class _PVPlantExportDXF(QtGui.QWidget):
self.writeTrenches(exporter)
# Crear espacios de papel
- self.setup_layout4(exporter.doc)
- self.createPaperSpaces(exporter)
+ #self.setup_layout4(exporter.doc)
+ #self.createPaperSpaces(exporter)
+
+ if hasattr(FreeCAD.ActiveDocument, "Background"):
+ # Exportar como imagen en lugar de polilínea
+ exporter.export_feature_image(FreeCAD.ActiveDocument.Background, "Site_Image")
# Guardar archivo
exporter.save()
diff --git a/Export/exportPVSyst.py b/Export/exportPVSyst.py
index 9368f24..2828185 100644
--- a/Export/exportPVSyst.py
+++ b/Export/exportPVSyst.py
@@ -29,6 +29,15 @@ import Part
import numpy
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:
import FreeCADGui
from PySide import QtCore
@@ -63,6 +72,11 @@ def check_collada():
FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n")
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:
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)
-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 = 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"
from xml.etree.ElementTree import Element, SubElement
@@ -291,17 +604,18 @@ def exportToPVC(path, exportTerrain = False):
# xml: 1. Asset:
asset = SubElement(root, 'asset')
+
asset_contributor = SubElement(asset, 'contributor')
- asset_contributor_autor = SubElement(asset_contributor, 'autor')
- #asset_contributor_autor.text = author
+ asset_contributor_autor = SubElement(asset_contributor, 'author')
+ asset_contributor_autor.text = author
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_keywords = SubElement(asset, 'keywords')
asset_revision = SubElement(asset, 'revision')
asset_subject = SubElement(asset, 'subject')
asset_tittle = SubElement(asset, 'title')
- #asset_tittle.text = FreeCAD.ActiveDocument.Name
+ asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_unit = SubElement(asset, 'unit')
asset_unit.set('meter', '0.001')
asset_unit.set('name', 'millimeter')
@@ -359,7 +673,6 @@ def exportToPVC(path, exportTerrain = False):
# xml: 4. library_geometries:
library_geometries = SubElement(root, 'library_geometries')
def add_geometry(objtype, vindex, findex, objind = 0, centers = None):
-
isFrame = False
if objtype == 0:
geometryName = 'Frame'
@@ -505,36 +818,20 @@ def exportToPVC(path, exportTerrain = False):
end_time.text = '1.000000'
# xml: 6. scene:
- scene = SubElement(root, 'scene')
+ '''scene = SubElement(root, 'scene')
instance = SubElement(scene, 'instance_visual_scene')
- instance.set('url', '#')
-
- full_list_of_objects = FreeCAD.ActiveDocument.Objects
+ instance.set('url', '#')'''
# CASO 1 - FRAMES:
frameType = site.Frames
- frame_setup = {"type": [],
- "footprint": []}
- for obj in frameType:
- frame_setup["type"] = obj
- frame_setup["footprint"] = ""
-
objind = 0
# TODO: revisar
for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower()
- 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()
+ #isTracker = False
+ objectlist = utils.findObjects("Tracker")
for obj in objectlist:
if obj.Setup == typ:
findex = numpy.array([])
@@ -580,7 +877,6 @@ def exportToPVC(path, exportTerrain = False):
v = Topology[0][i]
vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale,
(v.y - center.y) * scale)
-
# 2. face indices
findex = numpy.empty(len(Topology[1]) * 3, numpy.int64)
for i in range(len(Topology[1])):
diff --git a/Importer/importOSM.py b/Importer/importOSM.py
index 024716b..05421b6 100644
--- a/Importer/importOSM.py
+++ b/Importer/importOSM.py
@@ -42,11 +42,10 @@ class OSMImporter:
}
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
- def transform_from_latlon(self, lat, lon):
- point = ImportElevation.getElevationFromOE([[lat, lon], ])
- return FreeCAD.Vector(point[0].x, point[0].y, point[0].z) * scale - self.Origin
- '''x, y, _, _ = utm.from_latlon(lat, lon)
- return FreeCAD.Vector(x, y, .0) * scale - self.Origin'''
+ def transform_from_latlon(self, coordinates):
+ points = ImportElevation.getElevationFromOE(coordinates)
+ pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points]
+ return pts
def get_osm_data(self, bbox):
query = f"""
@@ -70,7 +69,7 @@ class OSMImporter:
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
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):
if not FreeCAD.ActiveDocument.getObject(name):
@@ -81,11 +80,16 @@ class OSMImporter:
root = ET.fromstring(osm_data)
# Almacenar nodos transformados
+ coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')]
+ coordinates = self.transform_from_latlon(coordinates)
+ for i, node in enumerate(root.findall('node')):
+ self. nodes[node.attrib['id']] = coordinates[i]
+ '''return
for node in root.findall('node'):
self.nodes[node.attrib['id']] = self.transform_from_latlon(
float(node.attrib['lat']),
float(node.attrib['lon'])
- )
+ )'''
# Procesar ways
for way in root.findall('way'):
@@ -162,11 +166,10 @@ class OSMImporter:
def create_buildings(self):
building_layer = self.create_layer("Buildings")
for way_id, data in self.ways_data.items():
+ print(data)
if 'building' not in data['tags']:
continue
- print(data)
-
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
diff --git a/InitGui.py b/InitGui.py
index 27e402f..f5afc97 100644
--- a/InitGui.py
+++ b/InitGui.py
@@ -59,6 +59,7 @@ class PVPlantWorkbench(Workbench):
"PVPlantBuilding",
"PVPlantFenceGroup",
]'''
+ from Electrical.PowerConverter import PowerConverter
self.electricalList = ["PVPlantStringBox",
"PVPlantCable",
"PVPlanElectricalLine",
@@ -66,6 +67,8 @@ class PVPlantWorkbench(Workbench):
"Stringing",
"Separator",
"StringInverter",
+ "Separator",
+ "PowerConverter"
]
self.roads = ["PVPlantRoad",
diff --git a/Mechanical/Frame/PVPlantFrame.py b/Mechanical/Frame/PVPlantFrame.py
index 003db97..9782061 100644
--- a/Mechanical/Frame/PVPlantFrame.py
+++ b/Mechanical/Frame/PVPlantFrame.py
@@ -540,6 +540,7 @@ def makeTrackerSetup(name="TrackerSetup"):
pass
return obj
+
def getarray(array, numberofpoles):
if len(array) == 0:
newarray = [0] * numberofpoles
@@ -568,6 +569,7 @@ def getarray(array, numberofpoles):
newarray = [array[0]] * numberofpoles
return newarray
+
class TrackerSetup(FrameSetup):
"A 1 Axis Tracker Obcject"
@@ -589,7 +591,7 @@ class TrackerSetup(FrameSetup):
obj.addProperty("App::PropertyDistance",
"MotorGap",
"ModuleArray",
- QT_TRANSLATE_NOOP("App::Property", "Thse height of this object")
+ QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).MotorGap = 550
if not "UseGroupsOfModules" in pl:
@@ -880,6 +882,9 @@ class TrackerSetup(FrameSetup):
def CalculatePosts(self, obj, totalh, totalw):
# Temp: utilizar el uso de versiones:
+ if len(obj.PoleType) == 0:
+ return None, None
+
ver = 1
if ver == 0:
# versión 0:
@@ -906,8 +911,8 @@ class TrackerSetup(FrameSetup):
elif ver == 1:
# versión 1:
linetmp = Part.LineSegment(FreeCAD.Vector(0), FreeCAD.Vector(0, 10, 0)).toShape()
- compoundPoles = Part.makeCompound([])
- compoundAxis = Part.makeCompound([])
+ compound_poles = Part.makeCompound([])
+ compound_axis = Part.makeCompound([])
offsetX = - totalw / 2
arrayDistance = obj.DistancePole
@@ -915,15 +920,16 @@ class TrackerSetup(FrameSetup):
arrayPost = obj.PoleSequence
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]
- postCopy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(postCopy.BoundBox.ZLength - arrayAerial[x]))
- compoundPoles.add(postCopy)
+ post_copy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(post_copy.BoundBox.ZLength - arrayAerial[x]))
+ compound_poles.add(post_copy)
axis = linetmp.copy()
axis.Placement.Base = FreeCAD.Vector(offsetX, 0, arrayAerial[x])
- compoundAxis.add(axis)
- return compoundPoles, compoundAxis
+ compound_axis.add(axis)
+
+ return compound_poles, compound_axis
def execute(self, obj):
# obj.Shape: compound
@@ -1029,14 +1035,14 @@ class Tracker(ArchComponent.Component):
"AngleY",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
- ).AngleX = 0
+ ).AngleY = 0
if not ("AngleZ" in pl):
obj.addProperty("App::PropertyAngle",
"AngleZ",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
- ).AngleX = 0
+ ).AngleZ = 0
self.Type = "Tracker"
#obj.Type = self.Type
@@ -1056,12 +1062,15 @@ class Tracker(ArchComponent.Component):
if prop.startswith("Angle"):
base = obj.Placement.Base
angles = obj.Placement.Rotation.toEulerAngles("XYZ")
+
+ # Actualizar rotación según el ángulo modificado
if prop == "AngleX":
rot = FreeCAD.Rotation(angles[2], angles[1], obj.AngleX.Value)
elif prop == "AngleY":
rot = FreeCAD.Rotation(angles[2], obj.AngleY.Value, angles[0])
elif prop == "AngleZ":
rot = FreeCAD.Rotation(obj.AngleZ.Value, angles[1], angles[0])
+
obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0,0,0))
if hasattr(FreeCAD.ActiveDocument, "FramesChecking"):
@@ -1083,28 +1092,38 @@ class Tracker(ArchComponent.Component):
# |-- PoleAxes: Edge
if obj.Setup is None:
+ print("Warning: No Setup defined for tracker")
return
- pl = obj.Placement
- shape = obj.Setup.Shape.copy()
+ try:
+ pl = obj.Placement
+ shape = obj.Setup.Shape.copy()
- p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
- p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
- axis = p1 - p2
- modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
+ # Rotar módulos
+ p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
+ p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
+ axis = p1 - p2
+ modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
- angle = obj.Placement.Rotation.toEuler()[1]
- newpoles = Part.makeCompound([])
- for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
- pole = shape.SubShapes[1].SubShapes[0].SubShapes[i]
- axis = shape.SubShapes[1].SubShapes[1].SubShapes[i]
- base = axis.Vertexes[0].Point
- axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point
- newpoles.add(pole.rotate(base, axis, -angle))
- poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
+ # Rotar postes
+ angle = obj.Placement.Rotation.toEuler()[1]
+ newpoles = Part.makeCompound([])
+ for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
+ pole = shape.SubShapes[1].SubShapes[0].SubShapes[i]
+ axis = shape.SubShapes[1].SubShapes[1].SubShapes[i]
+ base = axis.Vertexes[0].Point
+ axis = axis.Vertexes[1].Point - axis.Vertexes[0].Point
+ newpoles.add(pole.rotate(base, axis, -angle))
+ poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
- obj.Shape = Part.makeCompound([modules, poles])
- obj.Placement = pl
- obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ")
+ # Crear forma final
+ obj.Shape = Part.makeCompound([modules, poles])
+ 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):
@@ -1271,6 +1290,7 @@ class CommandFixedRack:
#FreeCADGui.Control.showDialog(self.TaskPanel)
return
+
class CommandTrackerSetup:
"the Arch Building command definition"
@@ -1292,6 +1312,7 @@ class CommandTrackerSetup:
FreeCADGui.Control.showDialog(self.TaskPanel)
return
+
class CommandTracker:
"the Arch Building command definition"
diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py
index 3d3b4cd..a4ecca5 100644
--- a/PVPlantImportGrid.py
+++ b/PVPlantImportGrid.py
@@ -122,6 +122,7 @@ def getElevationFromOE(coordinates):
try:
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
except RequestException as e:
+ print(f"Error en la solicitud: {str(e)}")
points = []
for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1])
diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py
index 94fe41e..f130245 100644
--- a/PVPlantPlacement.py
+++ b/PVPlantPlacement.py
@@ -26,6 +26,7 @@ import Part
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore, QtGui
+ from PySide.QtGui import QListWidgetItem
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
@@ -58,11 +59,12 @@ def selectionFilter(sel, objtype):
return fil
-class _PVPlantPlacementTaskPanel:
+class _PVPlantPlacementTaskPanel_old:
'''The editmode TaskPanel for Schedules'''
def __init__(self, obj=None):
- self.Terrain = PVPlantSite.get().Terrain
+ self.site = PVPlantSite.get()
+ self.Terrain = self.site.Terrain
self.FrameSetups = None
self.PVArea = None
self.Area = None
@@ -76,9 +78,12 @@ class _PVPlantPlacementTaskPanel:
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui"))
self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg")))
+ self.addFrames()
+ self.maxWidth = max([frame.Width.Value for frame in self.site.Frames])
+
self.form.buttonPVArea.clicked.connect(self.addPVArea)
- self.form.buttonAddFrame.clicked.connect(self.addFrame)
- self.form.buttonRemoveFrame.clicked.connect(self.removeFrame)
+ self.form.editGapCols.valueChanged.connect(self.update_inner_spacing)
+ self.update_inner_spacing()
def addPVArea(self):
sel = FreeCADGui.Selection.getSelection()
@@ -86,68 +91,106 @@ class _PVPlantPlacementTaskPanel:
self.PVArea = sel[0]
self.form.editPVArea.setText(self.PVArea.Label)
- def addFrame(self):
- from Mechanical.Frame import PVPlantFrame
- selection = FreeCADGui.Selection.getSelection()
- self.FrameSetup = selectionFilter(selection, PVPlantFrame.TrackerSetup)
+ def addFrames(self):
+ for frame_setup in self.site.Frames:
+ list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
+ list_item.setCheckState(QtCore.Qt.Checked)
- if len(selection) > 0:
- items = []
- for x in range(self.form.listFrameSetups.count()):
- items.append(self.form.listFrameSetups.item(x).text())
- if not (selection[0].Name in items):
- self.form.listFrameSetups.addItem(selection[0].Name)
-
- def removeFrame(self):
- ''' remove select item from list '''
- self.form.listFrameSetups.takeItem(self.form.listFrameSetups.currentRow())
+ def update_inner_spacing(self):
+ self.form.editInnerSpacing.setText(
+ ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000))))
def createFrameFromPoints(self, dataframe):
from Mechanical.Frame import PVPlantFrame
- try:
+ '''try:
MechanicalGroup = FreeCAD.ActiveDocument.Frames
except:
MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames')
MechanicalGroup.Label = "Frames"
FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup)
- placements = dataframe["placement"].tolist()
- types = dataframe["type"].tolist()
- frames = []
- for idx in range(len(placements)):
- newrack = PVPlantFrame.makeTracker(setup=types[idx])
- newrack.Label = "Tracker"
- newrack.Visibility = False
- newrack.Placement = placements[idx]
- MechanicalGroup.addObject(newrack)
- frames.append(newrack)
+ if self.form.cbSubfolders.isChecked:
+ name = "Frames-" + self.PVArea.Label
+ if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]:
+ MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0]
+ else:
+ group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
+ group.Label = name
+ MechanicalGroup.addObject(group)
+ MechanicalGroup = group'''
+
+ doc = FreeCAD.ActiveDocument
+
+ # 1. Obtener o crear el grupo principal 'Frames'
+ main_group_name = "Frames"
+ main_group = doc.getObject(main_group_name)
+ if not main_group:
+ main_group = doc.addObject("App::DocumentObjectGroup", main_group_name)
+ main_group.Label = main_group_name
+ # Asumiendo que existe un grupo 'MechanicalGroup'
+ if hasattr(doc, 'MechanicalGroup'):
+ doc.MechanicalGroup.addObject(main_group)
+
+ # 2. Manejar subgrupo si es necesario
+ group = main_group # Grupo donde se añadirán los marcos
+ if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis!
+ subgroup_name = f"Frames-{self.PVArea.Label}"
+
+ # Buscar subgrupo existente
+ subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None)
+
+ if not subgroup:
+ subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name)
+ subgroup.Label = subgroup_name
+ main_group.addObject(subgroup)
+ group = subgroup
+
+ try:
+ placements = dataframe["placement"].tolist()
+ types = dataframe["type"].tolist()
+ frames = []
+ for idx in range(len(placements)):
+ newrack = PVPlantFrame.makeTracker(setup=types[idx])
+ newrack.Label = "Tracker"
+ newrack.Visibility = False
+ newrack.Placement = placements[idx]
+ group.addObject(newrack)
+ frames.append(newrack)
+ except:
+ placements = dataframe[0]
+ frames = []
+ for idx in placements:
+ print(idx)
+ newrack = PVPlantFrame.makeTracker(setup=idx[0])
+ newrack.Label = "Tracker"
+ newrack.Visibility = False
+ newrack.Placement = idx[1]
+ groupq.addObject(newrack)
+ frames.append(newrack)
+
if self.PVArea.Name.startswith("FrameArea"):
self.PVArea.Frames = frames
- # TODO: else
def getProjected(self, shape):
""" returns projected edges from a shape and a direction """
-
if shape.BoundBox.ZLength == 0:
- edges = shape.Edges
- return Part.Face(Part.Wire(edges))
- else:
- from Utils import PVPlantUtils as utils
- wire = utils.simplifyWire(utils.getProjected(shape))
- if wire.isClosed():
- wire = wire.removeSplitter()
- return Part.Face(wire)
+ return Part.Face(Part.Wire(shape.Edges))
+
+ from Utils import PVPlantUtils as utils
+ wire = utils.simplifyWire(utils.getProjected(shape))
+ return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire)
def calculateWorkingArea(self):
self.Area = self.getProjected(self.PVArea.Shape)
- tmp = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
- if len(tmp):
- ProhibitedAreas = list()
- for obj in tmp:
- face = self.getProjected(obj.Base.Shape)
+ exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
+
+ if exclusion_areas:
+ prohibited_faces = []
+ for obj in exclusion_areas:
+ face = self.getProjected(obj.Shape.SubShapes[1])
if face.isValid():
- ProhibitedAreas.append(face)
- self.Area = self.Area.cut(ProhibitedAreas)
+ prohibited_faces.append(face)
+ self.Area = self.Area.cut(prohibited_faces)
def getAligments(self):
# TODO: revisar todo esto: -----------------------------------------------------------------
@@ -183,7 +226,7 @@ class _PVPlantPlacementTaskPanel:
return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \
np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64)
- def adjustToTerrain(self, coordinates):
+ def adjustToTerrain_old(self, coordinates):
mode = 1
terrain = self.Terrain.Mesh
@@ -280,103 +323,105 @@ class _PVPlantPlacementTaskPanel:
placeRegion(df)
return df
- """def placeonregion_old(frames): # old
- for colnum, col in enumerate(frames):
- groups = list()
- groups.append([col[0]])
- for i in range(1, len(col)):
- group = groups[-1]
- long = (col[i].sub(group[-1])).Length
- long -= width
- if long <= dist:
- group.append(col[i])
- else:
- groups.append([col[i]])
- for group in groups:
- points = list()
- points.append(group[0].sub(vec1))
- for ind in range(0, len(group) - 1):
- points.append((group[ind].sub(vec1) + group[ind + 1].add(vec1)) / 2)
- points.append(group[-1].add(vec1))
- points3D = list()
- '''
- # v0
- for ind in range(len(points) - 1):
- line = Part.LineSegment(points[ind], points[ind + 1])
- tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1))
- if len(tmp.Vertexes) > 0:
- if ind == 0:
- points3D.append(tmp.Vertexes[0].Point)
- points3D.append(tmp.Vertexes[-1].Point)
- '''
- # V1
- if type == 0: # Mesh:
- import MeshPart as mp
- for point in points:
- point3D = mp.projectPointsOnMesh([point, ], terrain, FreeCAD.Vector(0, 0, 1))
- if len(point3D) > 0:
- points3D.append(point3D[0])
+ def _setup_terrain_interpolator(self):
+ """Prepara interpolador del terreno para ajuste rápido"""
+ import numpy as np
+ from scipy.interpolate import LinearNDInterpolator
- else: # Shape:
- line = Part.LineSegment(points[0], points[-1])
- tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1))
- if len(tmp.Vertexes) > 0:
- tmppoints = [ver.Point for ver in tmp.Vertexes]
- if mode == 1: # mode = normal
- for point in points:
- '''# OPTION 1:
- line = Part.Line(point, point + FreeCAD.Vector(0, 0, 10))
- for i in range(len(tmppoints) - 1):
- seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1])
- inter = line.intersect(seg)
- print(inter)
- if len(inter) > 0:
- points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z))
- '''
- # OPTION 2:
- plane = Part.Plane(point, self.Dir)
- for i in range(len(tmppoints) - 1):
- seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1])
- inter = plane.intersect(seg)
- if len(inter) > 0:
- if len(inter[0]):
- inter = inter[0]
- points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z))
- break
- else: # TODO: mode = Trend
- # TODO: check:
- from scipy import stats
- xx = list()
- yy = list()
- zz = list()
+ mesh = self.Terrain.Mesh
+ points = np.array([p.Vector for p in mesh.Points])
+ bbox = self.Area.BoundBox
- for pts in tmppoints:
- xx.append(pts.x)
- yy.append(pts.y)
- zz.append(pts.z)
+ # Filtrar puntos dentro del área de trabajo
+ in_bbox = [
+ p for p in points
+ if bbox.XMin <= p[0] <= bbox.XMax and
+ bbox.YMin <= p[1] <= bbox.YMax
+ ]
- slope, intercept, r, p, std_err = stats.linregress(yy, zz)
+ if not in_bbox:
+ return None
- def myfunc(x):
- return slope * x + intercept
+ coords = np.array(in_bbox)
+ return LinearNDInterpolator(coords[:, :2], coords[:, 2])
- x = list()
- x.append(yy[0])
- x.append(yy[-1])
- newzz = list(map(myfunc, x))
- points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0]))
- points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1]))
+ def adjustToTerrain(self, coordinates):
+ from scipy.ndimage import label as sclabel
+ import pandas as pd
+ import numpy as np
+ from scipy import stats
+ import MeshPart
- for ind in range(0, len(points3D) - 1):
- pl = FreeCAD.Placement()
- vec = points3D[ind] - points3D[ind + 1]
- pl.Base = FreeCAD.Vector(group[ind])
- p = (points3D[ind] + points3D[ind + 1]) / 2
- pl.Base.z = p.z
- rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec)
- pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
- placements.append(pl)
- return placements"""
+ # Crear matriz binaria
+ arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates])
+ labeled_array, num_features = sclabel(arr)
+
+ # Construir DataFrame optimizado
+ data = []
+ terrain_interp = self._setup_terrain_interpolator()
+
+ for label in range(1, num_features + 1):
+ cols, rows = np.where(labeled_array == label)
+ for idx, (col, row) in enumerate(zip(cols, rows)):
+ frame_type, placement = coordinates[col][row]
+ data.append({
+ 'ID': len(data) + 1,
+ 'region': label,
+ 'type': frame_type,
+ 'column': col,
+ 'row': row,
+ 'placement': placement
+ })
+
+ df = pd.DataFrame(data)
+
+ # Ajustar al terreno
+ for idx, row in df.iterrows():
+ pl = row['placement']
+ yl = row['type'].Length.Value / 2
+
+ # Calcular puntos extremos
+ top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0)
+ bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0)
+
+ # Usar interpolador si está disponible
+ if terrain_interp:
+ yy = np.linspace(bot_point.y, top_point.y, 10)
+ xx = np.full(10, pl.x)
+ zz = terrain_interp(xx, yy)
+
+ if not np.isnan(zz).all():
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+ else:
+ # Fallback a proyección directa
+ line = Part.LineSegment(bot_point, top_point).toShape()
+ projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
+ if len(projected) >= 2:
+ yy = [p.y for p in projected]
+ zz = [p.z for p in projected]
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+
+ # Actualizar placement
+ new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top)
+ new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot)
+
+ new_pl = FreeCAD.Placement()
+ new_pl.Base = (new_top + new_bot) / 2
+ new_pl.Rotation = FreeCAD.Rotation(
+ FreeCAD.Vector(-1, 0, 0),
+ new_top - new_bot
+ )
+ df.at[idx, 'placement'] = new_pl
+
+ return df
def isInside(self, frame, point):
if self.Area.isInside(point, 10, True):
@@ -451,87 +496,75 @@ class _PVPlantPlacementTaskPanel:
if countcols == self.form.editColCount.value():
offsetcols += valcols
countcols = 0
- print("/n/n")
- print(cols)
+
return self.adjustToTerrain(cols)
def calculateNonAlignedArray(self):
- gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
- gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + max(self.Rack.Shape.BoundBox.XLength,
- self.Rack.Shape.BoundBox.YLength)
- offset_x = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
- offset_y = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
+ pointsx, pointsy = self.getAligments()
+ if len(pointsx) == 0:
+ FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n")
+ return []
- Area = self.calculateWorkingArea()
+ footprints = []
+ for frame in self.FrameSetups:
+ l = frame.Length.Value
+ w = frame.Width.Value
+ l_med = l / 2
+ w_med = w / 2
+ rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0),
+ FreeCAD.Vector( l_med, -w_med, 0),
+ FreeCAD.Vector( l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, -w_med, 0)])
+ rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
+ footprints.append([frame, rec])
- rec = Part.makePlane(self.Rack.Shape.BoundBox.YLength, self.Rack.Shape.BoundBox.XLength)
+ corridor = self.form.groupCorridor.isChecked()
+ corridor_offset = 0
+ count = 0
- # TODO: revisar todo esto: -----------------------------------------------------------------
- sel = FreeCADGui.Selection.getSelectionEx()[0]
- refh = None
- refv = None
-
- if len(sel.SubObjects) == 0:
- refh = refv = Area.Edges[0]
-
- if len(sel.SubObjects) == 1:
- refh = refv = sel.SubObjects[0]
-
- if len(sel.SubObjects) == 2:
- if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength:
- refh = sel.SubObjects[0]
- else:
- refh = sel.SubObjects[1]
-
- if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength:
- refv = sel.SubObjects[0]
- else:
- refv = sel.SubObjects[1]
-
- steps = int((refv.BoundBox.XMax - Area.BoundBox.XMin + offset_x) / gap_col)
- startx = refv.BoundBox.XMax + offset_x - gap_col * steps
- # todo end ----------------------------------------------------------------------------------
-
- start = FreeCAD.Vector(startx, 0.0, 0.0)
- pointsx = np.arange(start.x, Area.BoundBox.XMax, gap_col)
-
- if self.form.groupCorridor.isChecked():
- if (self.form.editColCount.value() > 0):
- xlen = len(pointsx)
- count = self.form.editColCount.value()
- val = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (
- gap_col - min(self.Rack.Shape.BoundBox.XLength, self.Rack.Shape.BoundBox.YLength))
- while count <= xlen:
- for i, point in enumerate(pointsx):
- if i >= count:
- pointsx[i] += val
- count += self.form.editColCount.value()
-
- pl = []
- for point in pointsx:
- p1 = FreeCAD.Vector(point, Area.BoundBox.YMax, 0.0)
- p2 = FreeCAD.Vector(point, Area.BoundBox.YMin, 0.0)
+ cols = []
+ for x in pointsx:
+ col=[]
+ x += corridor_offset
+ p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0)
+ p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0)
line = Part.makePolygon([p1, p2])
-
- inter = Area.section([line])
- pts = [ver.Point for ver in inter.Vertexes] # todo: sort points
+ inter = self.Area.section([line])
+ pts = [ver.Point for ver in inter.Vertexes]
+ pts = sorted(pts, key=lambda p: p.y, reverse=True)
for i in range(0, len(pts), 2):
- line = Part.LineSegment(pts[i], pts[i + 1])
- if line.length() >= rec.BoundBox.YLength:
- y1 = pts[i].y - rec.BoundBox.YLength
- cp = rec.copy()
- cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, y1, 0.0)
- inter = cp.cut([Area])
- y1 = min([ver.Point.y for ver in inter.Vertexes])
- pointsy = np.arange(y1, pts[i + 1].y, -gap_row)
- for point in pointsy:
- cp = rec.copy()
- cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, point, 0.0)
- cut = cp.cut([Area], 0)
- if len(cut.Vertexes) == 0:
+ top = pts[i]
+ bootom = pts[i + 1]
+ if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength:
+ y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2)
+ cp = footprints[-1][1].copy()
+ cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0)
+ inter = cp.cut([self.Area])
+ vtx = [ver.Point for ver in inter.Vertexes]
+ mod = top.y
+ if len(vtx) != 0:
+ mod = min(vtx, key=lambda p: p.y).y
+ #y1 = cp.Placement.Base.y - mod
+
+ tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy')
+ for opt in tmp[0]:
+ mod -= (footprints[opt][1].BoundBox.YLength / 2)
+ pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0)
+ cp = footprints[opt][1].copy()
+ if self.isInside(cp, pl):
+ col.append([footprints[opt][0], pl])
+ mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500)
Part.show(cp)
- pl.append(point)
- return pl
+
+ if corridor and len(col) > 0:
+ count += 1
+ if count == self.form.editColCount.value():
+ corridor_offset += 12000
+ count = 0
+
+ cols.append(cols)
+ return self.adjustToTerrain(cols)
def accept(self):
from datetime import datetime
@@ -542,23 +575,17 @@ class _PVPlantPlacementTaskPanel:
params.SetBool("AutoSaveEnabled", False)
FreeCAD.ActiveDocument.RecomputesFrozen = True
- items = []
- for x in range(self.form.listFrameSetups.count()):
- items.append(FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(x).text()))
- tmpframes = list()
- for frame in sorted(items, key=lambda rack: rack.Length, reverse=True):
- found = False
- for tmp in tmpframes:
- if tmp.Length == frame.Length:
- found = True
- break
- if not found:
- tmpframes.append(frame)
- self.FrameSetups = tmpframes.copy()
- longerFrame = self.FrameSetups[0]
+ items = [
+ FreeCAD.ActiveDocument.getObject(item.text())
+ for i in range(self.form.listFrameSetups.count())
+ if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked
+ ]
+
+ unique_frames = {frame.Length.Value: frame for frame in items}
+ self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True)
self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
- self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + longerFrame.Length.Value
+ self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value
self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
@@ -572,8 +599,14 @@ class _PVPlantPlacementTaskPanel:
dataframe = self.calculateNonAlignedArray()
# 3. Adjust to terrain:
self.createFrameFromPoints(dataframe)
- FreeCAD.ActiveDocument.commitTransaction()
+ import Electrical.group as egroup
+ import importlib
+ importlib.reload(egroup)
+ egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value)
+
+
+ FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.RecomputesFrozen = False
params.SetBool("AutoSaveEnabled", auto_save_enabled)
@@ -581,7 +614,1164 @@ class _PVPlantPlacementTaskPanel:
print(" -- Tiempo tardado:", total_time)
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
- #return True
+
+
+class _PVPlantPlacementTaskPanel_new1:
+ '''The editmode TaskPanel for Schedules'''
+
+ def __init__(self, obj=None):
+ self.site = PVPlantSite.get()
+ self.Terrain = self.site.Terrain
+ self.FrameSetups = None
+ self.PVArea = None
+ self.Area = None
+ self.gap_col = .0
+ self.gap_row = .0
+ self.offsetX = .0
+ self.offsetY = .0
+ self.Dir = FreeCAD.Vector(0, -1, 0) # Norte a sur
+
+ # self.form:
+ self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui"))
+ self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg")))
+
+ self.addFrames()
+ self.maxWidth = max([frame.Width.Value for frame in self.site.Frames])
+
+ self.form.buttonPVArea.clicked.connect(self.addPVArea)
+ self.form.editGapCols.valueChanged.connect(self.update_inner_spacing)
+ self.update_inner_spacing()
+
+ def addPVArea(self):
+ sel = FreeCADGui.Selection.getSelection()
+ if len(sel) > 0:
+ self.PVArea = sel[0]
+ self.form.editPVArea.setText(self.PVArea.Label)
+
+ def addFrames(self):
+ for frame_setup in self.site.Frames:
+ list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
+ list_item.setCheckState(QtCore.Qt.Checked)
+
+ def update_inner_spacing(self):
+ self.form.editInnerSpacing.setText(
+ ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000))))
+
+ def createFrameFromPoints(self, dataframe):
+ from Mechanical.Frame import PVPlantFrame
+ doc = FreeCAD.ActiveDocument
+
+ # 1. Obtener o crear el grupo principal 'Frames'
+ main_group_name = "Frames"
+ main_group = doc.getObject(main_group_name)
+ if not main_group:
+ main_group = doc.addObject("App::DocumentObjectGroup", main_group_name)
+ main_group.Label = main_group_name
+ # Asumiendo que existe un grupo 'MechanicalGroup'
+ if hasattr(doc, 'MechanicalGroup'):
+ doc.MechanicalGroup.addObject(main_group)
+
+ # 2. Manejar subgrupo si es necesario
+ group = main_group # Grupo donde se añadirán los marcos
+ if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis!
+ subgroup_name = f"Frames-{self.PVArea.Label}"
+
+ # Buscar subgrupo existente
+ subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None)
+
+ if not subgroup:
+ subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name)
+ subgroup.Label = subgroup_name
+ main_group.addObject(subgroup)
+ group = subgroup
+
+ try:
+ placements = dataframe["placement"].tolist()
+ types = dataframe["type"].tolist()
+ frames = []
+ for idx in range(len(placements)):
+ newrack = PVPlantFrame.makeTracker(setup=types[idx])
+ newrack.Label = "Tracker"
+ newrack.Visibility = False
+ newrack.Placement = placements[idx]
+ group.addObject(newrack)
+ frames.append(newrack)
+ except:
+ placements = dataframe[0]
+ frames = []
+ for idx in placements:
+ print(idx)
+ newrack = PVPlantFrame.makeTracker(setup=idx[0])
+ newrack.Label = "Tracker"
+ newrack.Visibility = False
+ newrack.Placement = idx[1]
+ groupq.addObject(newrack)
+ frames.append(newrack)
+
+ if self.PVArea.Name.startswith("FrameArea"):
+ self.PVArea.Frames = frames
+
+ def getProjected(self, shape):
+ """ returns projected edges from a shape and a direction """
+ if shape.BoundBox.ZLength == 0:
+ return Part.Face(Part.Wire(shape.Edges))
+
+ from Utils import PVPlantUtils as utils
+ wire = utils.simplifyWire(utils.getProjected(shape))
+ return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire)
+
+ def calculateWorkingArea(self):
+ self.Area = self.getProjected(self.PVArea.Shape)
+ exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
+
+ if exclusion_areas:
+ prohibited_faces = []
+ for obj in exclusion_areas:
+ face = self.getProjected(obj.Shape.SubShapes[1])
+ if face.isValid():
+ prohibited_faces.append(face)
+ self.Area = self.Area.cut(prohibited_faces)
+
+ def getAligments(self):
+ # TODO: revisar todo esto: -----------------------------------------------------------------
+ sel = FreeCADGui.Selection.getSelectionEx()[0]
+ refh = None
+ refv = None
+
+ if len(sel.SubObjects) == 0:
+ return
+
+ elif len(sel.SubObjects) == 1:
+ # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma
+ refh = refv = sel.SubObjects[0]
+
+ elif len(sel.SubObjects) > 1:
+ # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma
+ if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength:
+ refh = sel.SubObjects[0]
+ else:
+ refh = sel.SubObjects[1]
+
+ if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength:
+ refv = sel.SubObjects[0]
+ else:
+ refv = sel.SubObjects[1]
+
+ steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col)
+ startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps)
+ steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row)
+ starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps)
+ # todo end ----------------------------------------------------------------------------------
+
+ return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \
+ np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64)
+
+ def adjustToTerrain_old(self, coordinates):
+ mode = 1
+ terrain = self.Terrain.Mesh
+
+ def placeRegion(df): # TODO: new
+ import MeshPart as mp
+ from scipy import stats
+ linregression = []
+ for colnum in df.column.unique().tolist():
+ dftmp = df[df["column"] == colnum]
+ for id in dftmp["ID"].tolist():
+ data = df.loc[df['ID'] == id]
+ frametype = data["type"].tolist()[0]
+ # col = data["column"]
+ # row = data["row"]
+ base = data["placement"].tolist()[0]
+
+ yl = frametype.Length.Value / 2
+ ptop = FreeCAD.Vector(base)
+ ptop.y += yl
+ pbot = FreeCAD.Vector(base)
+ pbot.y -= yl
+ line = Part.LineSegment(ptop, pbot).toShape()
+ profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0]
+ '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo.
+ tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1))
+ profilepoints = [ver.Point for ver in tmp.Vertexes]'''
+
+ xx = list()
+ yy = list()
+ zz = list()
+ for pts in profilepoints:
+ xx.append(pts.x)
+ yy.append(pts.y)
+ zz.append(pts.z)
+ slope, intercept, r, p, std_err = stats.linregress(yy, zz)
+
+ # linregression.append(slope, intercept, r, p, std_err)
+ def myfunc(x):
+ return slope * x + intercept
+
+ newzz = list(map(myfunc, [yy[0], yy[-1]]))
+ points3D = list()
+ points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0]))
+ points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1]))
+ linregression.append(points3D)
+
+ # for ind in range(0, len(points3D) - 1):
+ pl = FreeCAD.Placement()
+ pl.Base = (points3D[0] + points3D[1]) / 2
+ rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1])
+ pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
+ df.at[id - 1, "placement"] = pl
+ df["regression"] = linregression
+
+ # 01. Grouping:
+ from scipy.ndimage import label as sclabel
+ import pandas as pd
+ tmp = []
+ for c, col in enumerate(coordinates):
+ tmpcol = []
+ for n, obj in enumerate(col):
+ if obj != 0:
+ tmpcol.append(1)
+ else:
+ tmpcol.append(0)
+ tmp.append(tmpcol)
+
+ data = {"ID": [],
+ "region": [],
+ "type": [],
+ "column": [],
+ "row": [],
+ "placement": []}
+
+ arr = np.array(tmp)
+ labeled_array, num_features = sclabel(arr)
+ id = 1
+ for label in range(1, num_features + 1):
+ cols, rows = np.where(labeled_array == label)
+ unique, counts = np.unique(cols, return_counts=True)
+ result = np.column_stack((unique, counts))
+ cnt = 0
+ for val, count in result:
+ for c in range(count):
+ data["ID"].append(id)
+ data["region"].append(label)
+ data["type"].append(coordinates[val][rows[cnt]][0])
+ data["column"].append(val)
+ data["row"].append(rows[cnt])
+ data["placement"].append(coordinates[val][rows[cnt]][1])
+ cnt += 1
+ id += 1
+ df = pd.DataFrame(data)
+ placeRegion(df)
+ return df
+
+ def _setup_terrain_interpolator(self):
+ """Prepara interpolador del terreno para ajuste rápido"""
+ import numpy as np
+ from scipy.interpolate import LinearNDInterpolator
+
+ mesh = self.Terrain.Mesh
+ points = np.array([p.Vector for p in mesh.Points])
+ bbox = self.Area.BoundBox
+
+ # Filtrar puntos dentro del área de trabajo
+ in_bbox = [
+ p for p in points
+ if bbox.XMin <= p[0] <= bbox.XMax and
+ bbox.YMin <= p[1] <= bbox.YMax
+ ]
+
+ if not in_bbox:
+ return None
+
+ coords = np.array(in_bbox)
+ return LinearNDInterpolator(coords[:, :2], coords[:, 2])
+
+ def adjustToTerrain(self, coordinates):
+ from scipy.ndimage import label as sclabel
+ import pandas as pd
+ import numpy as np
+ from scipy import stats
+ import MeshPart
+
+ # Crear matriz binaria
+ arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates])
+ labeled_array, num_features = sclabel(arr)
+
+ # Construir DataFrame optimizado
+ data = []
+ terrain_interp = self._setup_terrain_interpolator()
+
+ for label in range(1, num_features + 1):
+ cols, rows = np.where(labeled_array == label)
+ for idx, (col, row) in enumerate(zip(cols, rows)):
+ frame_type, placement = coordinates[col][row]
+ data.append({
+ 'ID': len(data) + 1,
+ 'region': label,
+ 'type': frame_type,
+ 'column': col,
+ 'row': row,
+ 'placement': placement
+ })
+
+ df = pd.DataFrame(data)
+
+ # Ajustar al terreno
+ for idx, row in df.iterrows():
+ pl = row['placement']
+ yl = row['type'].Length.Value / 2
+
+ # Calcular puntos extremos
+ top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0)
+ bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0)
+
+ # Usar interpolador si está disponible
+ if terrain_interp:
+ yy = np.linspace(bot_point.y, top_point.y, 10)
+ xx = np.full(10, pl.x)
+ zz = terrain_interp(xx, yy)
+
+ if not np.isnan(zz).all():
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+ else:
+ # Fallback a proyección directa
+ line = Part.LineSegment(bot_point, top_point).toShape()
+ projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
+ if len(projected) >= 2:
+ yy = [p.y for p in projected]
+ zz = [p.z for p in projected]
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+
+ # Actualizar placement
+ new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top)
+ new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot)
+
+ new_pl = FreeCAD.Placement()
+ new_pl.Base = (new_top + new_bot) / 2
+ new_pl.Rotation = FreeCAD.Rotation(
+ FreeCAD.Vector(-1, 0, 0),
+ new_top - new_bot
+ )
+ df.at[idx, 'placement'] = new_pl
+
+ return df
+
+ def isInside(self, frame, point):
+ if self.Area.isInside(point, 10, True):
+ frame.Placement.Base = point
+ cut = frame.cut([self.Area])
+ if len(cut.Vertexes) == 0:
+ return True
+ return False
+
+ def calculateAlignedArray(self):
+ import FreeCAD
+ pointsx, pointsy = self.getAligments()
+
+ footprints = []
+ for frame in self.FrameSetups:
+ xx = frame.Length.Value
+ yy = frame.Width.Value
+ xx_med = xx / 2
+ yy_med = yy / 2
+ rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0),
+ FreeCAD.Vector(xx_med, -yy_med, 0),
+ FreeCAD.Vector(xx_med, yy_med, 0),
+ FreeCAD.Vector(-xx_med, yy_med, 0),
+ FreeCAD.Vector(-xx_med, -yy_med, 0)])
+ rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
+ footprints.append([frame, rec])
+ ref = footprints.pop(0)
+ xx = ref[0].Length.Value
+ yy = ref[0].Width.Value
+ xx_med = xx / 2
+ yy_med = yy / 2
+
+ # variables for corridors:
+ countcols = 0
+ countrows = 0
+ offsetcols = 0 # ??
+ offsetrows = 0 # ??
+ valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy)
+
+ cols = []
+ for x in pointsx:
+ col = []
+ for y in pointsy:
+ found = False
+ point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0)
+ if self.isInside(ref[1], point):
+ col.append([ref[0], point])
+ found = True
+ continue
+ else:
+ for footprint in footprints:
+ l = int((ref[0].Length - footprint[0].Length) / 2)
+ for i in range(2):
+ point1 = FreeCAD.Vector(point)
+ point1.y = point1.y + l
+ if self.isInside(footprint[1], point1):
+ col.append([footprint[0], point1])
+ found = True
+ break
+ l = -l
+ if found:
+ break
+ if not found:
+ col.append(0)
+ cols.append(col)
+
+ # if len(col) > 0:
+ # code for vertical corridors:
+ if self.form.groupCorridor.isChecked():
+ if self.form.editColCount.value() > 0:
+ countcols += 1
+ if countcols == self.form.editColCount.value():
+ offsetcols += valcols
+ countcols = 0
+
+ return self.adjustToTerrain(cols)
+
+ def calculateNonAlignedArray(self):
+ pointsx, pointsy = self.getAligments()
+ if len(pointsx) == 0:
+ FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n")
+ return []
+
+ footprints = []
+ for frame in self.FrameSetups:
+ l = frame.Length.Value
+ w = frame.Width.Value
+ l_med = l / 2
+ w_med = w / 2
+ rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0),
+ FreeCAD.Vector( l_med, -w_med, 0),
+ FreeCAD.Vector( l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, -w_med, 0)])
+ rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
+ footprints.append([frame, rec])
+
+ corridor = self.form.groupCorridor.isChecked()
+ corridor_offset = 0
+ count = 0
+
+ cols = []
+ for x in pointsx:
+ col=[]
+ x += corridor_offset
+ p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0)
+ p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0)
+ line = Part.makePolygon([p1, p2])
+ inter = self.Area.section([line])
+ pts = [ver.Point for ver in inter.Vertexes]
+ pts = sorted(pts, key=lambda p: p.y, reverse=True)
+ for i in range(0, len(pts), 2):
+ top = pts[i]
+ bootom = pts[i + 1]
+ if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength:
+ y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2)
+ cp = footprints[-1][1].copy()
+ cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0)
+ inter = cp.cut([self.Area])
+ vtx = [ver.Point for ver in inter.Vertexes]
+ mod = top.y
+ if len(vtx) != 0:
+ mod = min(vtx, key=lambda p: p.y).y
+ #y1 = cp.Placement.Base.y - mod
+
+ tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy')
+ for opt in tmp[0]:
+ mod -= (footprints[opt][1].BoundBox.YLength / 2)
+ pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0)
+ cp = footprints[opt][1].copy()
+ if self.isInside(cp, pl):
+ col.append([footprints[opt][0], pl])
+ mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500)
+ Part.show(cp)
+
+ if corridor and len(col) > 0:
+ count += 1
+ if count == self.form.editColCount.value():
+ corridor_offset += 12000
+ count = 0
+
+ cols.append(col)
+ return self.adjustToTerrain(cols)
+
+ def accept(self):
+ from datetime import datetime
+ starttime = datetime.now()
+
+ params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document")
+ auto_save_enabled = params.GetBool("AutoSaveEnabled")
+ params.SetBool("AutoSaveEnabled", False)
+ FreeCAD.ActiveDocument.RecomputesFrozen = True
+
+ items = [
+ FreeCAD.ActiveDocument.getObject(item.text())
+ for i in range(self.form.listFrameSetups.count())
+ if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked
+ ]
+
+ unique_frames = {frame.Length.Value: frame for frame in items}
+ self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True)
+
+ self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
+ self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value
+ self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
+ self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
+
+ FreeCAD.ActiveDocument.openTransaction("Create Placement")
+ # 1. Calculate working area:
+ self.calculateWorkingArea()
+ # 2. Calculate aligned array:
+ if self.form.cbAlignFrames.isChecked():
+ dataframe = self.calculateAlignedArray()
+ else:
+ dataframe = self.calculateNonAlignedArray()
+ # 3. Adjust to terrain:
+ self.createFrameFromPoints(dataframe)
+
+ import Electrical.group as egroup
+ import importlib
+ importlib.reload(egroup)
+ egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value)
+
+
+ FreeCAD.ActiveDocument.commitTransaction()
+ FreeCAD.ActiveDocument.RecomputesFrozen = False
+ params.SetBool("AutoSaveEnabled", auto_save_enabled)
+
+ total_time = datetime.now() - starttime
+ print(" -- Tiempo tardado:", total_time)
+ FreeCADGui.Control.closeDialog()
+ FreeCAD.ActiveDocument.recompute()
+
+
+import numpy as np
+import pandas as pd
+from scipy.ndimage import label as sclabel
+from scipy import stats
+from scipy.interpolate import LinearNDInterpolator
+import Part
+import FreeCAD
+import FreeCADGui
+from PySide2 import QtCore, QtGui
+from PySide2.QtWidgets import QListWidgetItem
+import os
+import PVPlantResources
+
+
+class _PVPlantPlacementTaskPanel:
+ '''The editmode TaskPanel for Schedules'''
+
+ def __init__(self, obj=None):
+ self.site = PVPlantSite.get()
+ self.Terrain = self.site.Terrain
+ self.FrameSetups = None
+ self.PVArea = None
+ self.Area = None
+ self.gap_col = .0
+ self.gap_row = .0
+ self.offsetX = .0
+ self.offsetY = .0
+ self.Dir = FreeCAD.Vector(0, -1, 0)
+ self._terrain_interpolator = None
+ self._frame_footprints_cache = {}
+
+ # UI setup
+ self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui"))
+ self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg")))
+
+ self.addFrames()
+ self.maxWidth = max([frame.Width.Value for frame in self.site.Frames])
+
+ self.form.buttonPVArea.clicked.connect(self.addPVArea)
+ self.form.editGapCols.valueChanged.connect(self.update_inner_spacing)
+ self.update_inner_spacing()
+
+ def addPVArea(self):
+ sel = FreeCADGui.Selection.getSelection()
+ if sel:
+ self.PVArea = sel[0]
+ self.form.editPVArea.setText(self.PVArea.Label)
+
+ def addFrames(self):
+ for frame_setup in self.site.Frames:
+ list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
+ list_item.setCheckState(QtCore.Qt.Checked)
+
+ def update_inner_spacing(self):
+ self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m")
+
+ def _get_or_create_frame_group(self):
+ """Optimized group creation and management"""
+ doc = FreeCAD.ActiveDocument
+
+ # Get or create main group
+ main_group = doc.getObject("Frames") or doc.addObject("App::DocumentObjectGroup", "Frames")
+ if not main_group.Label == "Frames":
+ main_group.Label = "Frames"
+
+ # Add to MechanicalGroup if exists
+ if not hasattr(doc, 'MechanicalGroup') and hasattr(doc, 'getObject') and doc.getObject('MechanicalGroup'):
+ doc.MechanicalGroup.addObject(main_group)
+
+ # Handle subfolder
+ if self.form.cbSubfolders.isChecked() and self.PVArea:
+ subgroup_name = f"Frames-{self.PVArea.Label}"
+ subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None)
+ if not subgroup:
+ subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name)
+ subgroup.Label = subgroup_name
+ main_group.addObject(subgroup)
+ return subgroup
+
+ return main_group
+
+ def createFrameFromPoints(self, dataframe):
+ from Mechanical.Frame import PVPlantFrame
+ doc = FreeCAD.ActiveDocument
+
+ group = self._get_or_create_frame_group()
+
+ frames = []
+ placements_key = "placement" if "placement" in dataframe.columns else 0
+
+ if placements_key == "placement":
+ placements = dataframe["placement"].tolist()
+ types = dataframe["type"].tolist()
+
+ for idx, (placement, frame_type) in enumerate(zip(placements, types)):
+ newrack = PVPlantFrame.makeTracker(setup=frame_type)
+ newrack.Label = "Tracker"
+ newrack.Visibility = False
+ newrack.Placement = placement
+ group.addObject(newrack)
+ frames.append(newrack)
+
+ if self.PVArea and self.PVArea.Name.startswith("FrameArea"):
+ self.PVArea.Frames = frames
+
+ def getProjected(self, shape):
+ """Optimized projection calculation"""
+ if shape.BoundBox.ZLength == 0:
+ return Part.Face(Part.Wire(shape.Edges))
+
+ from Utils import PVPlantUtils as utils
+ wire = utils.simplifyWire(utils.getProjected(shape))
+ return Part.Face(wire.removeSplitter()) if wire and wire.isClosed() else Part.Face(wire)
+
+ def calculateWorkingArea(self):
+ """Optimized working area calculation"""
+ self.Area = self.getProjected(self.PVArea.Shape)
+ exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
+
+ if exclusion_areas:
+ prohibited_faces = []
+ for obj in exclusion_areas:
+ face = self.getProjected(obj.Shape.SubShapes[1])
+ if face and face.isValid():
+ prohibited_faces.append(face)
+
+ if prohibited_faces:
+ self.Area = self.Area.cut(prohibited_faces)
+
+ # Clear terrain interpolator cache when area changes
+ self._terrain_interpolator = None
+
+ def _setup_terrain_interpolator(self):
+ """Cached terrain interpolator"""
+ if self._terrain_interpolator is not None:
+ return self._terrain_interpolator
+
+ mesh = self.Terrain.Mesh
+ points = np.array([v.Vector for v in mesh.Points])
+ bbox = self.Area.BoundBox
+
+ # Filter points within working area efficiently
+ mask = ((points[:, 0] >= bbox.XMin) & (points[:, 0] <= bbox.XMax) &
+ (points[:, 1] >= bbox.YMin) & (points[:, 1] <= bbox.YMax))
+ filtered_points = points[mask]
+
+ if len(filtered_points) == 0:
+ self._terrain_interpolator = None
+ return None
+
+ try:
+ self._terrain_interpolator = LinearNDInterpolator(
+ filtered_points[:, :2], filtered_points[:, 2]
+ )
+ except:
+ self._terrain_interpolator = None
+
+ return self._terrain_interpolator
+
+ def _get_frame_footprint(self, frame):
+ """Cached footprint calculation"""
+ frame_key = (frame.Length.Value, frame.Width.Value)
+ if frame_key not in self._frame_footprints_cache:
+ l, w = frame.Length.Value, frame.Width.Value
+ l_med, w_med = l / 2, w / 2
+
+ footprint = Part.makePolygon([
+ FreeCAD.Vector(-l_med, -w_med, 0),
+ FreeCAD.Vector(l_med, -w_med, 0),
+ FreeCAD.Vector(l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, w_med, 0),
+ FreeCAD.Vector(-l_med, -w_med, 0)
+ ])
+ footprint.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
+
+ self._frame_footprints_cache[frame_key] = footprint
+
+ return self._frame_footprints_cache[frame_key]
+
+ def _calculate_terrain_adjustment_batch(self, points_data):
+ """Process terrain adjustments in batches for better performance"""
+ terrain_interp = self._setup_terrain_interpolator()
+ results = []
+
+ for frame_type, base_point in points_data:
+ yl = frame_type.Length.Value / 2
+ top_point = FreeCAD.Vector(base_point.x, base_point.y + yl, 0)
+ bot_point = FreeCAD.Vector(base_point.x, base_point.y - yl, 0)
+
+ if terrain_interp:
+ # Use interpolator for faster elevation calculation
+ yy = np.linspace(bot_point.y, top_point.y, 6) # Reduced points for speed
+ xx = np.full_like(yy, base_point.x)
+ try:
+ zz = terrain_interp(xx, yy)
+ if not np.isnan(zz).all():
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+ except:
+ z_top = z_bot = 0
+ else:
+ # Fallback to direct projection (slower)
+ line = Part.LineSegment(bot_point, top_point).toShape()
+ try:
+ import MeshPart
+ projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
+ if len(projected) >= 2:
+ yy = [p.y for p in projected]
+ zz = [p.z for p in projected]
+ slope, intercept, *_ = stats.linregress(yy, zz)
+ z_top = slope * top_point.y + intercept
+ z_bot = slope * bot_point.y + intercept
+ else:
+ z_top = z_bot = 0
+ except:
+ z_top = z_bot = 0
+
+ new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top)
+ new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot)
+
+ new_pl = FreeCAD.Placement()
+ new_pl.Base = (new_top + new_bot) / 2
+ new_pl.Rotation = FreeCAD.Rotation(
+ FreeCAD.Vector(-1, 0, 0), new_top - new_bot
+ )
+
+ results.append((frame_type, new_pl))
+
+ return results
+
+ def adjustToTerrain(self, coordinates):
+ """Unified terrain adjustment function for both aligned and non-aligned arrays"""
+ # Create binary array efficiently
+ arr = np.array([[int(obj != 0) for obj in col] for col in coordinates], dtype=np.uint8)
+ labeled_array, num_features = sclabel(arr)
+
+ # Build DataFrame efficiently
+ data = []
+ for label in range(1, num_features + 1):
+ cols, rows = np.where(labeled_array == label)
+ for col, row in zip(cols, rows):
+ frame_type, placement = coordinates[col][row]
+ data.append({
+ 'ID': len(data) + 1,
+ 'region': label,
+ 'type': frame_type,
+ 'column': col,
+ 'row': row,
+ 'placement': placement
+ })
+
+ if not data:
+ return pd.DataFrame(columns=['ID', 'region', 'type', 'column', 'row', 'placement'])
+
+ df = pd.DataFrame(data)
+
+ # Process terrain adjustments in batches
+ points_data = [(row['type'], row['placement']) for _, row in df.iterrows()]
+ adjusted_results = self._calculate_terrain_adjustment_batch(points_data)
+
+ # Update placements in DataFrame
+ for idx, (frame_type, new_placement) in enumerate(adjusted_results):
+ df.at[idx, 'placement'] = new_placement
+
+ return df
+
+ def isInside(self, frame, point):
+ """Optimized inside check with early termination"""
+ if not self.Area.isInside(point, 1e-6, True): # Reduced tolerance for speed
+ return False
+
+ frame_footprint = self._get_frame_footprint(frame)
+ frame_footprint.Placement.Base = point
+
+ try:
+ cut = frame_footprint.cut([self.Area])
+ return len(cut.Vertexes) == 0
+ except:
+ return False
+
+ def getAligments(self):
+ """Optimized alignment calculation"""
+ sel = FreeCADGui.Selection.getSelectionEx()
+ if not sel or not sel[0].SubObjects:
+ return np.array([]), np.array([])
+
+ sub_objects = sel[0].SubObjects
+
+ if len(sub_objects) == 1:
+ refh = refv = sub_objects[0]
+ else:
+ # Choose references based on bounding box dimensions
+ refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength)
+ refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength)
+
+ # Calculate ranges efficiently
+ startx = refv.BoundBox.XMin + self.offsetX - self.gap_col * int(
+ (refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col
+ )
+
+ starty = refh.BoundBox.YMin + self.offsetY + self.gap_row * int(
+ (refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row
+ )
+
+ x_range = np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.float64)
+ y_range = np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.float64)
+
+ return x_range, y_range
+
+ def calculateAlignedArray(self):
+ """Optimized aligned array calculation"""
+ pointsx, pointsy = self.getAligments()
+ if len(pointsx) == 0 or len(pointsy) == 0:
+ return pd.DataFrame()
+
+ # Precompute footprints once
+ footprints = []
+ for frame in self.FrameSetups:
+ footprint = self._get_frame_footprint(frame)
+ footprints.append((frame, footprint))
+
+ ref_frame, ref_footprint = footprints[0]
+ ref_length = ref_frame.Length.Value
+ ref_width = ref_frame.Width.Value
+
+ # Corridor variables
+ countcols = 0
+ valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - ref_width)
+ corridor_enabled = self.form.groupCorridor.isChecked()
+
+ cols = []
+ for x in pointsx:
+ col = []
+ corridor_offset = 0
+
+ for y in pointsy:
+ point = FreeCAD.Vector(x + ref_width / 2 + corridor_offset, y - ref_length / 2, 0.0)
+ found = False
+
+ # Check reference frame first (most common case)
+ if self.isInside(ref_frame, point):
+ col.append([ref_frame, point])
+ found = True
+ else:
+ # Check alternative frames
+ for frame, footprint in footprints[1:]:
+ length_diff = (ref_frame.Length.Value - frame.Length.Value) / 2
+ for offset in [length_diff, -length_diff]:
+ test_point = FreeCAD.Vector(point.x, point.y + offset, 0.0)
+ if self.isInside(frame, test_point):
+ col.append([frame, test_point])
+ found = True
+ break
+ if found:
+ break
+
+ if not found:
+ col.append(0)
+
+ # Handle corridors
+ if corridor_enabled and col:
+ countcols += 1
+ if countcols >= self.form.editColCount.value():
+ corridor_offset += valcols
+ countcols = 0
+
+ cols.append(col)
+
+ return self.adjustToTerrain(cols)
+
+ def calculateNonAlignedArray(self):
+ """Optimized non-aligned array calculation"""
+ pointsx, pointsy = self.getAligments()
+ if len(pointsx) == 0:
+ FreeCAD.Console.PrintWarning("No X alignments found.\n")
+ return pd.DataFrame()
+
+ # Precompute footprints
+ footprints = []
+ for frame in self.FrameSetups:
+ footprint = self._get_frame_footprint(frame)
+ footprints.append((frame, footprint))
+
+ corridor_enabled = self.form.groupCorridor.isChecked()
+ corridor_count = 0
+ corridor_offset = 0
+
+ cols = []
+ for x in pointsx:
+ col = []
+ current_x = x + corridor_offset
+
+ # Create vertical line for intersection
+ p1 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMax, 0.0)
+ p2 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMin, 0.0)
+ line = Part.LineSegment(p1, p2).toShape()
+
+ # Get intersections with area
+ try:
+ inter = self.Area.section(line)
+ pts = sorted([v.Point for v in inter.Vertexes], key=lambda p: p.y, reverse=True)
+
+ for i in range(0, len(pts) - 1, 2):
+ top, bottom = pts[i], pts[i + 1]
+ available_height = top.y - bottom.y
+
+ if available_height > footprints[-1][0].Width.Value:
+ # Use optimized placement algorithm
+ self._place_frames_in_segment(col, footprints, current_x, top, bottom)
+
+ except Exception as e:
+ FreeCAD.Console.PrintWarning(f"Error in segment processing: {e}\n")
+
+ # Handle corridor offset
+ if corridor_enabled and col:
+ corridor_count += 1
+ if corridor_count >= self.form.editColCount.value():
+ corridor_offset += 12000 # 12m corridor
+ corridor_count = 0
+
+ cols.append(col)
+
+ return self.adjustToTerrain(cols)
+
+ def _place_frames_in_segment(self, col, footprints, x, top, bottom):
+ """Optimized frame placement within a segment"""
+ current_y = top.y
+ frame_heights = [ftp[0].Width.Value for ftp in footprints]
+ min_frame_height = min(frame_heights)
+
+ while current_y - bottom.y > min_frame_height:
+ placed = False
+
+ for frame, footprint in footprints:
+ test_y = current_y - frame.Width.Value / 2
+ test_point = FreeCAD.Vector(x, test_y, 0.0)
+
+ if self.isInside(frame, test_point):
+ col.append([frame, test_point])
+ current_y -= frame.Width.Value
+ placed = True
+ break
+
+ if not placed:
+ break
+
+ def accept(self):
+ from datetime import datetime
+ starttime = datetime.now()
+
+ # Document optimization
+ params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document")
+ auto_save_enabled = params.GetBool("AutoSaveEnabled")
+ params.SetBool("AutoSaveEnabled", False)
+ FreeCAD.ActiveDocument.RecomputesFrozen = True
+
+ try:
+ # Get selected frames
+ items = [
+ FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text())
+ for i in range(self.form.listFrameSetups.count())
+ if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked
+ ]
+
+ # Remove duplicates efficiently
+ self.FrameSetups = list({frame.Length.Value: frame for frame in items}.values())
+ self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True)
+
+ # Parse parameters
+ self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
+ self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value
+ self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
+ self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
+
+ FreeCAD.ActiveDocument.openTransaction("Create Placement")
+
+ # Main processing
+ self.calculateWorkingArea()
+
+ if self.form.cbAlignFrames.isChecked():
+ dataframe = self.calculateAlignedArray()
+ else:
+ dataframe = self.calculateNonAlignedArray()
+
+ if not dataframe.empty:
+ self.createFrameFromPoints(dataframe)
+
+ # Group trackers
+ import Electrical.group as egroup
+ import importlib
+ importlib.reload(egroup)
+ egroup.groupTrackersToTransformers(5000000, self.gap_row)
+
+ FreeCAD.ActiveDocument.commitTransaction()
+
+ finally:
+ # Restore document settings
+ FreeCAD.ActiveDocument.RecomputesFrozen = False
+ params.SetBool("AutoSaveEnabled", auto_save_enabled)
+
+ total_time = datetime.now() - starttime
+ print(f" -- Total time: {total_time}")
+ FreeCADGui.Control.closeDialog()
+ FreeCAD.ActiveDocument.recompute()
+
+def optimized_cut(L_total, piezas, margen=0, metodo='auto'):
+ """
+ Encuentra la combinación óptima de piezas para minimizar el desperdicio,
+ considerando un margen entre piezas.
+
+ Args:
+ L_total (int): Longitud total del material.
+ piezas (list): Lista de longitudes de los patrones de corte.
+ margen (int): Espacio perdido entre piezas consecutivas.
+ metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática.
+
+ Returns:
+ tuple: (piezas_seleccionadas, desperdicio)
+ """
+ # Filtrar piezas inválidas
+ piezas = [p for p in piezas if 0 < p <= L_total]
+ if not piezas:
+ return [], L_total
+
+ # Transformar longitudes y longitud total con margen
+ longitudes_aumentadas = [p + margen for p in piezas]
+ L_total_aumentado = L_total + margen
+
+ # Selección automática de método
+ if metodo == 'auto':
+ if L_total_aumentado <= 10000 and len(piezas) <= 100:
+ metodo = 'dp'
+ else:
+ metodo = 'greedy'
+
+ if metodo == 'dp':
+ n = len(piezas)
+ dp = [0] * (L_total_aumentado + 1)
+ parent = [-1] * (L_total_aumentado + 1) # Almacena índices de piezas usadas
+
+ # Llenar la tabla dp y parent
+ for j in range(1, L_total_aumentado + 1):
+ for i in range(n):
+ p_aum = longitudes_aumentadas[i]
+ if p_aum <= j:
+ if dp[j] < dp[j - p_aum] + p_aum:
+ dp[j] = dp[j - p_aum] + p_aum
+ parent[j] = i # Guardar índice de la pieza
+
+ # Reconstruir solución desde el final
+ current = L_total_aumentado
+ seleccion_indices = []
+ while current > 0 and parent[current] != -1:
+ i = parent[current]
+ seleccion_indices.append(i)
+ current -= longitudes_aumentadas[i]
+
+ # Calcular desperdicio real
+ k = len(seleccion_indices)
+ if k == 0:
+ desperdicio = L_total
+ else:
+ suma_original = sum(piezas[i] for i in seleccion_indices)
+ desperdicio = L_total - suma_original - margen * (k - 1)
+
+ return seleccion_indices, desperdicio
+
+ elif metodo == 'greedy':
+ # Crear lista con índices y longitudes aumentadas
+ lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))]
+ lista_con_indices.sort(key=lambda x: x[0], reverse=True) # Ordenar descendente
+
+ seleccion_indices = []
+ restante = L_total_aumentado
+
+ # Seleccionar piezas vorazmente
+ for p_aum, i in lista_con_indices:
+ while restante >= p_aum:
+ seleccion_indices.append(i)
+ restante -= p_aum
+
+ # Calcular desperdicio real
+ k = len(seleccion_indices)
+ if k == 0:
+ desperdicio = L_total
+ else:
+ suma_original = sum(piezas[i] for i in seleccion_indices)
+ desperdicio = L_total - suma_original - margen * (k - 1)
+
+ return seleccion_indices, desperdicio
+
+
+# Ejemplo de uso
+'''if __name__ == "__main__":
+ L_total = 100
+ piezas = [25, 35, 40, 20, 15, 30, 50]
+ margen = 5
+
+ print("Solución óptima con margen (programación dinámica):")
+ seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp')
+ print(f"Piezas usadas: {seleccion}")
+ print(f"Margen entre piezas: {margen} cm")
+ print(f"Material útil: {sum(seleccion)} cm")
+ print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm")
+ print(f"Desperdicio total: {desperd} cm")
+
+ print("\nSolución aproximada con margen (algoritmo voraz):")
+ seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy')
+ print(f"Piezas usadas: {seleccion_g}")
+ print(f"Margen entre piezas: {margen} cm")
+ print(f"Material útil: {sum(seleccion_g)} cm")
+ print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm")
+ print(f"Desperdicio total: {desperd_g} cm")'''
# ----------------------------------------------------------------------------------------------------------------------
diff --git a/PVPlantPlacement.ui b/PVPlantPlacement.ui
index 05e553d..3e77bfe 100644
--- a/PVPlantPlacement.ui
+++ b/PVPlantPlacement.ui
@@ -15,6 +15,318 @@
Park Settings
+ -
+
+
+ Estructura:
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ -
+
+
+ Add
+
+
+
+ -
+
+
+ Configuración
+
+
+
+ 10
+
+
+ 6
+
+
-
+
+
-
+
+ De arriba a abajo
+
+
+ -
+
+ De abajo a arriba
+
+
+ -
+
+ Del centro a los lados
+
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Dirección Horizontal
+
+
+
+ -
+
+
-
+
+ De izquierda a derecha
+
+
+ -
+
+ De derecha a izquiera
+
+
+ -
+
+ De centro a los lados
+
+
+
+
+ -
+
+
+ Alinear estructuras
+
+
+ true
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ mm
+
+
+ 10000
+
+
+ 500
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Pitch
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ m
+
+
+ 3
+
+
+ -10000.000000000000000
+
+
+ 10000.000000000000000
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Orientación
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Offset Horizontal
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Offset Vertical
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ m
+
+
+ 3
+
+
+ -10000.000000000000000
+
+
+ 10000.000000000000000
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ m
+
+
+ 3
+
+
+ 100.000000000000000
+
+
+ 5.000000000000000
+
+
+
+ -
+
+
-
+
+ Norte - Sur
+
+
+ -
+
+ Este - Oeste
+
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Dirección Vertical
+
+
+
+ -
+
+
+
+ 120
+ 16777215
+
+
+
+ Espacio entre filas
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ - Inner Spacing
+
+
+
+
+
+
-
@@ -59,10 +371,10 @@
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- QAbstractSpinBox::NoButtons
+ QAbstractSpinBox::ButtonSymbols::NoButtons
@@ -110,10 +422,10 @@
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- QAbstractSpinBox::NoButtons
+ QAbstractSpinBox::ButtonSymbols::NoButtons
@@ -135,10 +447,10 @@
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- QAbstractSpinBox::NoButtons
+ QAbstractSpinBox::ButtonSymbols::NoButtons
4
@@ -148,10 +460,10 @@
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- QAbstractSpinBox::NoButtons
+ QAbstractSpinBox::ButtonSymbols::NoButtons
8
@@ -161,45 +473,6 @@
- -
-
-
- Add
-
-
-
- -
-
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
- Add
-
-
-
- -
-
-
- Remove
-
-
-
-
-
-
-
@@ -207,282 +480,9 @@
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
- -
-
-
- Configuración
-
-
-
- 10
-
-
- 6
-
-
-
-
-
-
- 120
- 16777215
-
-
-
- Offset Vertical
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Orientación
-
-
-
- -
-
-
- Alinear estructuras
-
-
- true
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Espacio entre filas
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Dirección Horizontal
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Offset Horizontal
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Pitch
-
-
-
- -
-
-
-
- 120
- 16777215
-
-
-
- Dirección Vertical
-
-
-
- -
-
-
-
-
- Norte - Sur
-
-
- -
-
- Este - Oeste
-
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- m
-
-
- 3
-
-
- 100.000000000000000
-
-
- 5.000000000000000
-
-
-
- -
-
-
-
-
- De izquierda a derecha
-
-
- -
-
- De derecha a izquiera
-
-
- -
-
- De centro a los lados
-
-
-
-
- -
-
-
-
-
- De arriba a abajo
-
-
- -
-
- De abajo a arriba
-
-
- -
-
- Del centro a los lados
-
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- m
-
-
- 3
-
-
- -10000.000000000000000
-
-
- 10000.000000000000000
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- m
-
-
- 3
-
-
- -10000.000000000000000
-
-
- 10000.000000000000000
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- mm
-
-
- 10000
-
-
- 500
-
-
-
-
-
-
- -
-
-
- Estructura:
-
-
-
-
@@ -493,11 +493,19 @@
+ -
+
+
+ Organizar en subcarpetas
+
+
+ true
+
+
+
- buttonAddFrame
- buttonRemoveFrame
editPVArea
buttonPVArea
comboOrientation
diff --git a/PVPlantSite.py b/PVPlantSite.py
index dc177b4..242b666 100644
--- a/PVPlantSite.py
+++ b/PVPlantSite.py
@@ -771,12 +771,12 @@ class _PVPlantSite(ArchSite._Site):
import PVPlantImportGrid
x, y, zone_number, zone_letter = utm.from_latlon(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
- zz = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
- self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz[0].z)
- #self.obj.OriginOffset = FreeCAD.Vector(x * 1000, y * 1000, 0) #??
+
+ point = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
+ self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z)
self.obj.Latitude = lat
self.obj.Longitude = lon
- self.obj.Elevation = zz[0].z
+ self.obj.Elevation = point[0].z
class _ViewProviderSite(ArchSite._ViewProviderSite):
diff --git a/PVPlantTerrain.py b/PVPlantTerrain.py
index 8043e0f..91f8bcc 100644
--- a/PVPlantTerrain.py
+++ b/PVPlantTerrain.py
@@ -73,6 +73,42 @@ line_patterns = {
"Dot (.5x) ...............................": 0x5555,
"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"):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain")
obj.Label = name
@@ -81,7 +117,6 @@ def makeTerrain(name="Terrain"):
FreeCAD.ActiveDocument.recompute()
return obj
-
class Terrain(ArchComponent.Component):
"A Shadow Terrain Obcject"
@@ -161,101 +196,110 @@ class Terrain(ArchComponent.Component):
if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime
if obj.DEM and obj.CuttingBoundary:
- '''
- Parámetro Descripción Requisitos
- NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
- 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.
- 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.
- NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999
- '''
- grid_space = 1
- file = open(obj.DEM, "r")
- templist = [line.split() for line in file.readlines()]
- file.close()
- del file
+ from pathlib import Path
+ suffix = Path(obj.DEM).suffix
+ if suffix == '.asc':
+ '''
+ ASC format:
+
+ Parámetro Descripción Requisitos
+ NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
+ 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.
+ 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.
+ NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999
+ '''
+ grid_space = 1
+ file = open(obj.DEM, "r")
+ templist = [line.split() for line in file.readlines()]
+ file.close()
+ del file
- # Read meta data:
- meta = templist[0:6]
- nx = int(meta[0][1]) # NCOLS
- ny = int(meta[1][1]) # NROWS
- xllref = meta[2][0] # XLLCENTER / XLLCORNER
- xllvalue = round(float(meta[2][1]), 3)
- yllref = meta[3][0] # YLLCENTER / XLLCORNER
- yllvalue = round(float(meta[3][1]), 3)
- cellsize = round(float(meta[4][1]), 3) # CELLSIZE
- nodata_value = float(meta[5][1]) # NODATA_VALUE
+ # Read meta data:
+ meta = templist[0:6]
+ nx = int(meta[0][1]) # NCOLS
+ ny = int(meta[1][1]) # NROWS
+ xllref = meta[2][0] # XLLCENTER / XLLCORNER
+ xllvalue = round(float(meta[2][1]), 3)
+ yllref = meta[3][0] # YLLCENTER / XLLCORNER
+ yllvalue = round(float(meta[3][1]), 3)
+ cellsize = round(float(meta[4][1]), 3) # CELLSIZE
+ nodata_value = float(meta[5][1]) # NODATA_VALUE
- # set coarse_factor
- coarse_factor = max(round(grid_space / cellsize), 1)
+ # set coarse_factor
+ coarse_factor = max(round(grid_space / cellsize), 1)
- # Get z values
- templist = templist[6:(6 + ny)]
- templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)]
- datavals = np.array(templist).astype(float)
- del templist
+ # Get z values
+ templist = templist[6:(6 + ny)]
+ templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)]
+ datavals = np.array(templist).astype(float)
+ del templist
- # create xy coordinates
- offset = self.site.Origin
- 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
- datavals = datavals * 1000 # Ajuste de altura
+ # create xy coordinates
+ offset = self.site.Origin
+ 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
+ datavals = datavals * 1000 # Ajuste de altura
- # remove points out of area
- # 1. coarse:
- if obj.CuttingBoundary:
- inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 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),
- x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
- x_max = np.ndarray.max(tmp)
- x_min = np.ndarray.min(tmp)
+ # remove points out of area
+ # 1. coarse:
+ if obj.CuttingBoundary:
+ inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 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),
+ x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
+ x_max = np.ndarray.max(tmp)
+ x_min = np.ndarray.min(tmp)
- tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y),
- y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0]
- y_max = np.ndarray.max(tmp)
- y_min = np.ndarray.min(tmp)
- del tmp
+ tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y),
+ y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0]
+ y_max = np.ndarray.max(tmp)
+ y_min = np.ndarray.min(tmp)
+ del tmp
- x = x[x_min:x_max+1]
- y = y[y_min:y_max+1]
- datavals = datavals[y_min:y_max+1, x_min:x_max+1]
+ x = x[x_min:x_max+1]
+ y = y[y_min:y_max+1]
+ datavals = datavals[y_min:y_max+1, x_min:x_max+1]
- # Create mesh - surface:
- import MeshTools.Triangulation as Triangulation
- import Mesh
- stepsize = 75
- stepx = math.ceil(nx / stepsize)
- stepy = math.ceil(ny / stepsize)
+ # Create mesh - surface:
+ import MeshTools.Triangulation as Triangulation
+ import Mesh
+ stepsize = 75
+ stepx = math.ceil(nx / stepsize)
+ stepy = math.ceil(ny / stepsize)
- mesh = Mesh.Mesh()
- for indx in range(stepx):
- inix = indx * stepsize - 1
- finx = min([stepsize * (indx + 1), len(x)-1])
- for indy in range(stepy):
- iniy = indy * stepsize - 1
- finy = min([stepsize * (indy + 1), len(y) - 1])
- pts = []
- for i in range(inix, finx):
- for j in range(iniy, finy):
- if datavals[j][i] != nodata_value:
- if obj.CuttingBoundary:
- if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
+ mesh = Mesh.Mesh()
+ for indx in range(stepx):
+ inix = indx * stepsize - 1
+ finx = min([stepsize * (indx + 1), len(x)-1])
+ for indy in range(stepy):
+ iniy = indy * stepsize - 1
+ finy = min([stepsize * (indy + 1), len(y) - 1])
+ pts = []
+ for i in range(inix, finx):
+ for j in range(iniy, finy):
+ if datavals[j][i] != nodata_value:
+ if obj.CuttingBoundary:
+ 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]])
- else:
- pts.append([x[i], y[j], datavals[j][i]])
- if len(pts) > 3:
- try:
- triangulated = Triangulation.Triangulate(pts)
- mesh.addMesh(triangulated)
- except TypeError:
- print(f"Error al procesar {len(pts)} puntos: {str(e)}")
+ if len(pts) > 3:
+ try:
+ triangulated = Triangulation.Triangulate(pts)
+ mesh.addMesh(triangulated)
+ 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 obj.PointsGroup and obj.CuttingBoundary:
diff --git a/PVPlantTools.py b/PVPlantTools.py
index 5115555..8240e0d 100644
--- a/PVPlantTools.py
+++ b/PVPlantTools.py
@@ -54,30 +54,6 @@ class CommandPVPlantSite:
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:
@staticmethod
def GetResources():
@@ -673,8 +649,7 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantTracker', PVPlantFrame.CommandTracker())
FreeCADGui.addCommand('RackType', CommandRackGroup())
-
-import PVPlantFence
+from Civil.Fence import PVPlantFence
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
projectlist = [ # "Reload",
@@ -712,4 +687,4 @@ pv_mechanical = [
]
objectlist = ['PVPlantTree',
- 'PVPlantFence',]
\ No newline at end of file
+ 'PVPlantFenceGroup',]
\ No newline at end of file
diff --git a/cidownloader.py b/Plugins/cidownloader.py
similarity index 100%
rename from cidownloader.py
rename to Plugins/cidownloader.py
diff --git a/Project/Area/PVPlantArea.py b/Project/Area/PVPlantArea.py
index f541820..f6eb818 100644
--- a/Project/Area/PVPlantArea.py
+++ b/Project/Area/PVPlantArea.py
@@ -26,6 +26,9 @@ import PVPlantSite
import Utils.PVPlantUtils as utils
import MeshPart as mp
+import pivy
+from pivy import coin
+
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
@@ -69,6 +72,7 @@ class _Area:
''' Initialize the Area object '''
self.Type = None
self.obj = None
+ self.setProperties(obj)
def setProperties(self, obj):
pl = obj.PropertiesList
@@ -101,18 +105,18 @@ class _Area:
def __setstate__(self, state):
pass
+ def execute(self, obj):
+ ''' Execute the area object '''
+ pass
+
class _ViewProviderArea:
def __init__(self, vobj):
- self.Object = vobj.Object
vobj.Proxy = self
def attach(self, vobj):
- '''
- Create Object visuals in 3D view.
- '''
- self.Object = vobj.Object
- return
+ ''' Create Object visuals in 3D view. '''
+ self.ViewObject = vobj
def getIcon(self):
'''
@@ -120,6 +124,7 @@ class _ViewProviderArea:
'''
return str(os.path.join(DirIcons, "area.svg"))
+
'''
def claimChildren(self):
"""
@@ -159,17 +164,10 @@ class _ViewProviderArea:
pass
def __getstate__(self):
- """
- Save variables to file.
- """
return None
def __setstate__(self, state):
- """
- Get variables from file.
- """
- return None
-
+ pass
''' Frame Area '''
@@ -311,17 +309,14 @@ class ViewProviderFrameArea(_ViewProviderArea):
''' 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.Distance = val
+ obj.OffsetDistance = val
- offsets = None
try:
offsetsgroup = FreeCAD.ActiveDocument.Offsets
except:
@@ -334,11 +329,13 @@ def makeOffsetArea(base = None, val=None):
class OffsetArea(_Area):
def __init__(self, obj):
- _Area.__init__(self, obj)
- self.setProperties(obj)
+ '''_Area.__init__(self, obj)
+ self.setProperties(obj)'''
+ super().__init__(obj) # Llama al constructor de _Area
def setProperties(self, obj):
- _Area.setProperties(self, obj)
+ super().setProperties(obj) # Propiedades de la clase base
+
pl = obj.PropertiesList
if not ("OffsetDistance" in pl):
obj.addProperty("App::PropertyDistance",
@@ -354,24 +351,28 @@ class OffsetArea(_Area):
self.setProperties(obj)
def execute(self, obj):
- import Utils.PVPlantUtils as utils
+ # 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)
- tmp = mp.projectShapeOnMesh(wire, land, vec)
+ sections = mp.projectShapeOnMesh(wire, land, vec)
+ print(" javi ", sections)
pts = []
- for section in tmp:
+ for section in sections:
pts.extend(section)
- obj.Shape = Part.makePolygon(pts)
-
- def __getstate__(self):
- return None
-
- def __setstate__(self, state):
- pass
+ # Crear forma solo si hay resultados
+ if len(pts)>0:
+ obj.Shape = Part.makePolygon(pts)
+ else:
+ obj.Shape = Part.Shape() # Forma vacía si falla
class ViewProviderOffsetArea(_ViewProviderArea):
@@ -382,14 +383,12 @@ class ViewProviderOffsetArea(_ViewProviderArea):
def claimChildren(self):
""" Provides object grouping """
children = []
- if self.Object.Base:
- children.append(self.Object.Base)
+ 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)
@@ -420,29 +419,443 @@ class ProhibitedArea(OffsetArea):
"""Method run when the document is restored."""
self.setProperties(obj)
+ def execute(self, obj):
+ # Comprobar dependencias
+ if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape:
+ return
+ if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain:
+ return
+
+ base = obj.Base.Shape
+ land = PVPlantSite.get().Terrain.Mesh
+ vec = FreeCAD.Vector(0, 0, 1)
+
+ # 1. Crear wire original
+ original_wire = utils.getProjected(base, vec)
+ sections_original = mp.projectShapeOnMesh(original_wire, land, vec)
+
+ # 2. Crear wire offset
+ offset_wire = original_wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
+ sections_offset = mp.projectShapeOnMesh(offset_wire, land, vec)
+
+ # Crear formas compuestas
+ def make_polygon(sections):
+ if not sections:
+ return Part.Shape()
+ pts = []
+ for section in sections:
+ pts.extend(section)
+ return Part.makePolygon(pts)
+
+ compounds = []
+ if sections_original:
+ compounds.append(make_polygon(sections_original))
+ if sections_offset:
+ compounds.append(make_polygon(sections_offset))
+
+ if compounds:
+ obj.Shape = Part.makeCompound(compounds)
+ else:
+ obj.Shape = Part.Shape()
+
+ # Actualizar colores en la vista
+ """if FreeCAD.GuiUp and obj.ViewObject:
+ obj.ViewObject.Proxy.updateVisual()"""
+
+
+class ViewProviderForbiddenArea_old:
+ def __init__(self, vobj):
+ vobj.Proxy = self
+ self.setProperties(vobj)
+
+ def setProperties(self, vobj):
+ # Propiedades de color
+ if not hasattr(vobj, "OriginalColor"):
+ vobj.addProperty("App::PropertyColor",
+ "OriginalColor",
+ "ObjectStyle",
+ "Color for original wire")
+ vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
+
+ if not hasattr(vobj, "OffsetColor"):
+ vobj.addProperty("App::PropertyColor",
+ "OffsetColor",
+ "ObjectStyle",
+ "Color for offset wire")
+ vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo
+
+ # Propiedades de grosor
+ if not hasattr(vobj, "OriginalWidth"):
+ vobj.addProperty("App::PropertyFloat",
+ "OriginalWidth",
+ "ObjectStyle",
+ "Line width for original wire")
+ vobj.OriginalWidth = 4.0
+
+ if not hasattr(vobj, "OffsetWidth"):
+ vobj.addProperty("App::PropertyFloat",
+ "OffsetWidth",
+ "ObjectStyle",
+ "Line width for offset wire")
+ vobj.OffsetWidth = 4.0
+
+ # Deshabilitar el color por defecto
+ vobj.setPropertyStatus("LineColor", "Hidden")
+ vobj.setPropertyStatus("PointColor", "Hidden")
+ vobj.setPropertyStatus("ShapeAppearance", "Hidden")
+
+ def attach(self, vobj):
+ self.ViewObject = vobj
+ self.Object = vobj.Object
+
+ # Crear la estructura de escena Coin3D
+ self.root = coin.SoGroup()
+
+ # Switch para habilitar/deshabilitar la selección
+ self.switch = coin.SoSwitch()
+ self.switch.whichChild = coin.SO_SWITCH_ALL
+
+ # Separador para el wire original
+ self.original_sep = coin.SoSeparator()
+ self.original_color = coin.SoBaseColor()
+ self.original_coords = coin.SoCoordinate3()
+ self.original_line_set = coin.SoLineSet()
+ self.original_draw_style = coin.SoDrawStyle()
+
+ # Separador para el wire offset
+ self.offset_sep = coin.SoSeparator()
+ self.offset_color = coin.SoBaseColor()
+ self.offset_coords = coin.SoCoordinate3()
+ self.offset_line_set = coin.SoLineSet()
+ self.offset_draw_style = coin.SoDrawStyle()
+
+ # Construir la jerarquía de escena
+ self.original_sep.addChild(self.original_color)
+ self.original_sep.addChild(self.original_draw_style)
+ self.original_sep.addChild(self.original_coords)
+ self.original_sep.addChild(self.original_line_set)
+
+ self.offset_sep.addChild(self.offset_color)
+ self.offset_sep.addChild(self.offset_draw_style)
+ self.offset_sep.addChild(self.offset_coords)
+ self.offset_sep.addChild(self.offset_line_set)
+
+ self.switch.addChild(self.original_sep)
+ self.switch.addChild(self.offset_sep)
+ self.root.addChild(self.switch)
+
+ vobj.addDisplayMode(self.root, "Wireframe")
+
+ # Inicializar estilos de dibujo
+ self.original_draw_style.style = coin.SoDrawStyle.LINES
+ self.offset_draw_style.style = coin.SoDrawStyle.LINES
+
+ # Actualizar visualización inicial
+ if hasattr(self.Object, 'Shape'):
+ self.updateData(self.Object, "Shape")
+ self.updateVisual()
+
+ def updateData(self, obj, prop):
+ if prop == "Shape" and obj.Shape and not obj.Shape.isNull():
+ self.updateGeometry()
+
+ def updateGeometry(self):
+ """Actualiza la geometría en la escena 3D"""
+ if not hasattr(self, 'Object') or not self.Object.Shape or self.Object.Shape.isNull():
+ return
+
+ # Limpiar coordenadas existentes
+ self.original_coords.point.deleteValues(0)
+ self.offset_coords.point.deleteValues(0)
+
+ # Obtener los sub-shapes
+ subshapes = []
+ if hasattr(self.Object.Shape, 'SubShapes') and self.Object.Shape.SubShapes:
+ subshapes = self.Object.Shape.SubShapes
+ elif hasattr(self.Object.Shape, 'ChildShapes') and self.Object.Shape.ChildShapes:
+ subshapes = self.Object.Shape.ChildShapes
+
+ # Procesar wire original (primer sub-shape)
+ if len(subshapes) > 0:
+ self.processShape(subshapes[0], self.original_coords, self.original_line_set)
+
+ # Procesar wire offset (segundo sub-shape)
+ if len(subshapes) > 1:
+ self.processShape(subshapes[1], self.offset_coords, self.offset_line_set)
+
+ # Actualizar colores y grosores
+ self.updateVisual()
+
+ def processShape(self, shape, coords_node, lineset_node):
+ """Procesa una forma y la añade al nodo de coordenadas"""
+ if not shape or shape.isNull():
+ return
+
+ points = []
+ line_indices = []
+ current_index = 0
+
+ # Obtener todos los edges de la forma
+ edges = []
+ if hasattr(shape, 'Edges'):
+ edges = shape.Edges
+ elif hasattr(shape, 'ChildShapes'):
+ for child in shape.ChildShapes:
+ if hasattr(child, 'Edges'):
+ edges.extend(child.Edges)
+
+ for edge in edges:
+ try:
+ # Discretizar la curva para obtener puntos
+ vertices = edge.discretize(Number=50)
+
+ for i, vertex in enumerate(vertices):
+ points.append([vertex.x, vertex.y, vertex.z])
+ line_indices.append(current_index)
+ current_index += 1
+
+ # Añadir -1 para indicar fin de línea
+ line_indices.append(-1)
+
+ except Exception as e:
+ print(f"Error processing edge: {e}")
+ continue
+
+ # Configurar coordenadas y líneas
+ if points:
+ coords_node.point.setValues(0, len(points), points)
+ lineset_node.numVertices.deleteValues(0)
+ lineset_node.numVertices.setValues(0, len(line_indices), line_indices)
+
+ def updateVisual(self):
+ """Actualiza colores y grosores según las propiedades"""
+ if not hasattr(self, 'ViewObject') or not self.ViewObject:
+ return
+
+ vobj = self.ViewObject
+
+ try:
+ # Configurar wire original
+ if hasattr(vobj, "OriginalColor"):
+ original_color = vobj.OriginalColor
+ self.original_color.rgb.setValue(original_color[0], original_color[1], original_color[2])
+
+ if hasattr(vobj, "OriginalWidth"):
+ self.original_draw_style.lineWidth = vobj.OriginalWidth
+
+ # Configurar wire offset
+ if hasattr(vobj, "OffsetColor"):
+ offset_color = vobj.OffsetColor
+ self.offset_color.rgb.setValue(offset_color[0], offset_color[1], offset_color[2])
+
+ if hasattr(vobj, "OffsetWidth"):
+ self.offset_draw_style.lineWidth = vobj.OffsetWidth
+
+ except Exception as e:
+ print(f"Error updating visual: {e}")
+
+ def onChanged(self, vobj, prop):
+ """Maneja cambios en propiedades"""
+ if prop in ["OriginalColor", "OffsetColor", "OriginalWidth", "OffsetWidth"]:
+ self.updateVisual()
+
+ def getDisplayModes(self, obj):
+ return ["Wireframe"]
+
+ def getDefaultDisplayMode(self):
+ return "Wireframe"
+
+ def setDisplayMode(self, mode):
+ return mode
+
+ def claimChildren(self):
+ """Proporciona agrupamiento de objetos"""
+ children = []
+ if hasattr(self, 'Object') and self.Object and hasattr(self.Object, "Base"):
+ children.append(self.Object.Base)
+ return children
+
+ def getIcon(self):
+ '''Return object treeview icon'''
+ return str(os.path.join(DirIcons, "area_forbidden.svg"))
+
+ def onDocumentRestored(self, vobj):
+ """Método ejecutado cuando el documento es restaurado"""
+ self.ViewObject = vobj
+ self.Object = vobj.Object
+ self.setProperties(vobj)
+ self.attach(vobj)
+
def __getstate__(self):
return None
def __setstate__(self, state):
- pass
+ return None
-class ViewProviderForbiddenArea(_ViewProviderArea):
+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 object treeview icon '''
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):
- """ Provides object grouping """
children = []
- if self.Object.Base:
- children.append(self.Object.Base)
+ if hasattr(self, 'ViewObject') and self.ViewObject and hasattr(self.ViewObject.Object, 'Base'):
+ children.append(self.ViewObject.Object.Base)
return children
+ def dumps(self):
+ return None
+
+ def loads(self, state):
+ return None
''' PV Area: '''
-
-
def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")
PVSubplant(obj)
diff --git a/Project/ProjectSetup.ui b/Project/ProjectSetup.ui
index 80a9b3c..e22643b 100644
--- a/Project/ProjectSetup.ui
+++ b/Project/ProjectSetup.ui
@@ -39,6 +39,16 @@
9
+ -
+
+
+ Maximum west-east slope:
+
+
+
+ -
+
+
-
@@ -46,17 +56,20 @@
- -
-
+
-
+
- Frame coloring:
+ South facing
+
+
+ Qt::AlignmentFlag::AlignCenter
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -66,71 +79,20 @@
- -
-
+
-
+
- Maximum west-east slope:
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- º
-
-
- 90.000000000000000
-
-
- 8.000000000000000
-
-
-
- -
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- QAbstractSpinBox::NoButtons
-
-
- º
-
-
- 90.000000000000000
-
-
- 2.800000000000000
-
-
-
- -
-
-
- -
-
-
- South facing
-
-
- Qt::AlignCenter
+ Frame coloring:
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- QAbstractSpinBox::NoButtons
+ QAbstractSpinBox::ButtonSymbols::NoButtons
º
@@ -149,7 +111,45 @@
North Facing
- Qt::AlignCenter
+ Qt::AlignmentFlag::AlignCenter
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ º
+
+
+ 90.000000000000000
+
+
+ 2.800000000000000
+
+
+
+ -
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ º
+
+
+ 90.000000000000000
+
+
+ 8.000000000000000
diff --git a/Utils/PVPlantUtils.py b/Utils/PVPlantUtils.py
index 2c51fc0..049684d 100644
--- a/Utils/PVPlantUtils.py
+++ b/Utils/PVPlantUtils.py
@@ -224,14 +224,20 @@ def getProjected(shape, direction=FreeCAD.Vector(0, 0, 1)): # Based on Draft / s
return ow
-def findObjects(classtype):
+'''def findObjects(classtype):
objects = FreeCAD.ActiveDocument.Objects
objlist = list()
for object in objects:
if hasattr(object, "Proxy"):
if object.Proxy.Type == classtype:
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):
'''
diff --git a/package.xml b/package.xml
index 4adc7ac..ced4267 100644
--- a/package.xml
+++ b/package.xml
@@ -2,17 +2,18 @@
PVPlant
FreeCAD Fotovoltaic Power Plant Toolkit
- 2025.07.06
- 2025.07.06
+ 2025.11.20
+ 2025.11.20
Javier Braña
LGPL-2.1-or-later
- https://homehud.duckdns.org/javier/PVPlant
+ https://homehud.duckdns.org/javier/PVPlant
https://homehud.duckdns.org/javier/PVPlant/issues
+ https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md
PVPlant/Resources/Icons/PVPlantWorkbench.svg
- RoadWorkbench
+ PVPlantWorkbench
./
diff --git a/reload.py b/reload.py
index 030da1f..b2b3752 100644
--- a/reload.py
+++ b/reload.py
@@ -24,11 +24,15 @@ class _CommandReload:
def Activated(self):
import PVPlantPlacement, \
PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \
- PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \
- PVPlantCreateTerrainMesh, \
+ PVPlantSite, PVPlantRackChecking, PVPlantCreateTerrainMesh, \
PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \
PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \
GraphProfile
+
+ from Civil.Fence import PVPlantFenceGate as PVPlantFenceGate
+ from Civil.Fence import PVPlantFence as PVPlantFence
+ from Civil.Fence import PVPlantFencePost as PVPlantFencePost
+
from Civil import PVPlantTrench
from Vegetation import PVPlantTreeGenerator
@@ -59,9 +63,11 @@ class _CommandReload:
importlib.reload(PVPlantSite)
importlib.reload(PVPlantFrame)
importlib.reload(PVPlantRackChecking)
+
importlib.reload(PVPlantFence)
importlib.reload(PVPlantFenceGate)
importlib.reload(PVPlantFencePost)
+
importlib.reload(PVPlantFoundation)
importlib.reload(PVPlantCreateTerrainMesh)
importlib.reload(PVPlantTreeGenerator)
diff --git a/requirements.txt b/requirements.txt
index 8bb799c..b5ac3db 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,16 +9,12 @@ setuptools~=68.2.2
laspy~=2.5.3
geopy~=2.4.1
lxml~=4.9.3
-pip~=23.3.2
-wheel~=0.42.0
-Brotli~=1.1.0
-PySocks~=1.7.1
-typing_extensions~=4.9.0
-docutils~=0.20.1
Pillow~=10.1.0
pyproj~=3.7.1
simplekml~=1.3.6
geojson~=3.1.0
certifi~=2023.11.17
SciPy~=1.11.4
-ezdxf~=1.4.1
\ No newline at end of file
+pycollada~=0.7.2
+shapely
+rtree
\ No newline at end of file