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