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 6cef531..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,74 +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 - segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, 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 = [] @@ -456,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 @@ -496,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: @@ -508,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: @@ -561,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): @@ -641,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): @@ -774,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) @@ -858,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) @@ -876,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 99% rename from PVPlantFenceGate.py rename to Civil/Fence/PVPlantFenceGate.py index a468913..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): 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..23cc197 --- /dev/null +++ b/Electrical/PowerConverter/PowerConverter.py @@ -0,0 +1,397 @@ +# /********************************************************************** +# * * +# * 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 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") + + +def makePCS(): + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "StringInverter") + PowerConverter(obj) + ViewProviderStringInverter(obj.ViewObject) + + try: + folder = FreeCAD.ActiveDocument.StringInverters + except: + folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'StringInverters') + folder.Label = "StringInverters" + folder.addObject(obj) + return obj + + +class PowerConverter(ArchComponent.Component): + def __init__(self, obj): + ''' Initialize the Area object ''' + ArchComponent.Component.__init__(self, obj) + + self.oldMPPTs = 0 + + self.Type = None + self.obj = None + self.setProperties(obj) + + def setProperties(self, obj): + pl = obj.PropertiesList + + if not "File" in pl: + obj.addProperty("App::PropertyFile", + "File", + "Inverter", + "The base file this component is built upon") + + if not ("MPPTs" in pl): + obj.addProperty("App::PropertyQuantity", + "MPPTs", + "Inverter", + "Points that define the area" + ).MPPTs = 0 + + if not ("Generator" in pl): + obj.addProperty("App::PropertyEnumeration", + "Generator", + "Inverter", + "Points that define the area" + ).Generator = ["Generic", "Library"] + obj.Generator = "Generic" + + if not ("Type" in pl): + obj.addProperty("App::PropertyString", + "Type", + "Base", + "Points that define the area" + ).Type = "PowerConverter" + obj.setEditorMode("Type", 1) + + self.Type = obj.Type + obj.Proxy = self + + def onDocumentRestored(self, obj): + """ Method run when the document is restored """ + self.setProperties(obj) + + def onBeforeChange(self, obj, prop): + + if prop == "MPPTs": + self.oldMPPTs = int(obj.MPPTs) + + def onChanged(self, obj, prop): + ''' ''' + + if prop == "Generator": + if obj.Generator == "Generic": + obj.setEditorMode("MPPTs", 0) + else: + obj.setEditorMode("MPPTs", 1) + + if prop == "MPPTs": + ''' ''' + if self.oldMPPTs > obj.MPPTs: + ''' borrar sobrantes ''' + obj.removeProperty() + + elif self.oldMPPTs < obj.MPPTs: + ''' crear los faltantes ''' + for i in range(self.oldMPPTs, int(obj.MPPTs)): + ''' ''' + print(i) + else: + pass + + if (prop == "File") and obj.File: + ''' ''' + + def execute(self, obj): + ''' ''' + # obj.Shape: compound + # |- body: compound + # |-- inverter: solid + # |-- door: solid + # |-- holder: solid + + # |- connectors: compound + # |-- DC: compound + # |--- MPPT 1..x: compound + # |---- positive: compound + # |----- connector 1..y: ?? + # |---- negative 1..y: compound + # |----- connector 1..y: ?? + # |-- AC: compound + # |--- R,S,T,: ?? + # |-- Communication + + pl = obj.Placement + filename = self.getFile(obj) + if filename: + parts = self.getPartsList(obj) + if parts: + zdoc = zipfile.ZipFile(filename) + if zdoc: + f = zdoc.open(parts[list(parts.keys())[-1]][1]) + shapedata = f.read() + f.close() + shapedata = shapedata.decode("utf8") + shape = self.cleanShape(shapedata, obj, parts[list(parts.keys())[-1]][2]) + obj.Shape = shape + if not pl.isIdentity(): + obj.Placement = pl + obj.MPPTs = len(shape.SubShapes[1].SubShapes[0].SubShapes) + + def cleanShape(self, shapedata, obj, materials): + "cleans the imported shape" + + import Part + shape = Part.Shape() + shape.importBrepFromString(shapedata) + '''if obj.FuseArch and materials: + # separate lone edges + shapes = [] + for edge in shape.Edges: + found = False + for solid in shape.Solids: + for soledge in solid.Edges: + if edge.hashCode() == soledge.hashCode(): + found = True + break + if found: + break + if found: + break + else: + shapes.append(edge) + print("solids:",len(shape.Solids),"mattable:",materials) + for key,solindexes in materials.items(): + if key == "Undefined": + # do not join objects with no defined material + for solindex in [int(i) for i in solindexes.split(",")]: + shapes.append(shape.Solids[solindex]) + else: + fusion = None + for solindex in [int(i) for i in solindexes.split(",")]: + if not fusion: + fusion = shape.Solids[solindex] + else: + fusion = fusion.fuse(shape.Solids[solindex]) + if fusion: + shapes.append(fusion) + shape = Part.makeCompound(shapes) + try: + shape = shape.removeSplitter() + except Exception: + print(obj.Label,": error removing splitter")''' + return shape + + def getFile(self, obj, filename=None): + "gets a valid file, if possible" + + if not filename: + filename = obj.File + if not filename: + return None + if not filename.lower().endswith(".fcstd"): + return None + if not os.path.exists(filename): + # search for the file in the current directory if not found + basename = os.path.basename(filename) + currentdir = os.path.dirname(obj.Document.FileName) + altfile = os.path.join(currentdir,basename) + if altfile == obj.Document.FileName: + return None + elif os.path.exists(altfile): + return altfile + else: + # search for subpaths in current folder + altfile = None + subdirs = self.splitall(os.path.dirname(filename)) + for i in range(len(subdirs)): + subpath = [currentdir]+subdirs[-i:]+[basename] + altfile = os.path.join(*subpath) + if os.path.exists(altfile): + return altfile + return None + return filename + + def getPartsList(self, obj, filename=None): + + "returns a list of Part-based objects in a FCStd file" + + parts = {} + materials = {} + filename = self.getFile(obj,filename) + if not filename: + return parts + zdoc = zipfile.ZipFile(filename) + with zdoc.open("Document.xml") as docf: + name = None + label = None + part = None + materials = {} + writemode = False + for line in docf: + line = line.decode("utf8") + if "" in line: + writemode = False + elif "" in line: + if name and label and part: + parts[name] = [label,part,materials] + name = None + label = None + part = None + materials = {} + writemode = False + return parts + + def getColors(self,obj): + + "returns the DiffuseColor of the referenced object" + + filename = self.getFile(obj) + if not filename: + return None + part = obj.Part + if not obj.Part: + return None + zdoc = zipfile.ZipFile(filename) + if not "GuiDocument.xml" in zdoc.namelist(): + return None + colorfile = None + with zdoc.open("GuiDocument.xml") as docf: + writemode1 = False + writemode2 = False + for line in docf: + line = line.decode("utf8") + if (" 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_sphere = FreeCAD.ActiveDocument.addObject("Part::Sphere", f"CT_{i + 1}") + ct_sphere.Radius = 5000 # 2m de radio + ct_sphere.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0) + + # Añadir propiedades personalizadas + ct_sphere.addProperty("App::PropertyLinkList", "Trackers", "CT", + "Lista de trackers asociados a este CT") + ct_sphere.addProperty("App::PropertyFloat", "TotalPower", "CT", + "Potencia total del grupo (W)") + ct_sphere.addProperty("App::PropertyFloat", "NominalPower", "CT", + "Potencia nominal del transformador (W)") + ct_sphere.addProperty("App::PropertyFloat", "Utilization", "CT", + "Porcentaje de utilización (Total/Nominal)") + + # Establecer valores de las propiedades + ct_sphere.Trackers = group['trackers'] + ct_sphere.TotalPower = group['total_power'].Value + ct_sphere.NominalPower = transformer_power + ct_sphere.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_sphere.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_sphere.ViewObject.ShapeColor = color + ct_sphere.ViewObject.Transparency = 40 # 40% de transparencia + + # Añadir etiqueta con información + ct_sphere.ViewObject.DisplayMode = "Shaded" + ct_sphere.Label = f"CT {i + 1} ({ct_sphere.TotalPower / 1000:.1f}kW/{ct_sphere.NominalPower / 1000:.1f}kW)" + + # Añadir al grupo principal + transformer_group.addObject(ct_sphere) + + 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_spheres = [] + 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_spheres = transformers_group.Group + if not self.ct_spheres: + 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_spheres: + 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) + + 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/PVPlantPlacement.py b/PVPlantPlacement.py index 667ce1d..04e1a1f 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -78,11 +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.form.buttonPVArea.clicked.connect(self.addPVArea) - #self.form.buttonAddFrame.clicked.connect(self.addFrames) - #self.form.buttonRemoveFrame.clicked.connect(self.removeFrame) - 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() @@ -95,6 +96,10 @@ class _PVPlantPlacementTaskPanel: 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 try: @@ -104,22 +109,37 @@ class _PVPlantPlacementTaskPanel: MechanicalGroup.Label = "Frames" FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) - if self.form.cbSubfolders.checked: - group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", self.PVArea.Label) - group.Label = self.PVArea.Label - MechanicalGroup.addObject(group) - MechanicalGroup = group - - 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: + label = "Frames-" + self.PVArea.Label + if label in [obj.Label for obj in FreeCAD.ActiveDocument.Frames.Group]: + MechanicalGroup = FreeCAD.ActiveDocument.getObject(label)[0] + else: + group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", label) + group.Label = label + MechanicalGroup.addObject(group) + MechanicalGroup = group + 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] + MechanicalGroup.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] + MechanicalGroup.addObject(newrack) + frames.append(newrack) if self.PVArea.Name.startswith("FrameArea"): self.PVArea.Frames = frames @@ -179,7 +199,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 @@ -276,6 +296,106 @@ class _PVPlantPlacementTaskPanel: 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 @@ -349,86 +469,68 @@ 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() - Area = self.calculateWorkingArea() + 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 - rec = Part.makePlane(self.Rack.Shape.BoundBox.YLength, self.Rack.Shape.BoundBox.XLength) - - # 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() + # variables for corridors: + countcols = 0 + countrows = 0 + offsetcols = 0 # ?? + offsetrows = 0 # ?? + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) pl = [] for point in pointsx: - p1 = FreeCAD.Vector(point, Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(point, Area.BoundBox.YMin, 0.0) + p1 = FreeCAD.Vector(point, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(point, self.Area.BoundBox.YMin, 0.0) line = Part.makePolygon([p1, p2]) - inter = Area.section([line]) + inter = self.Area.section([line]) pts = [ver.Point for ver in inter.Vertexes] # todo: sort points 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: - Part.show(cp) - pl.append(point) + if line.length() >= ref[1].BoundBox.YLength: + y1 = pts[i].y - ref[1].BoundBox.YLength / 2 + cp = ref[1].copy() + cp.Placement.Base = FreeCAD.Vector(pts[i].x, y1, 0.0) + Part.show(cp) + inter = cp.cut([self.Area]) + pts1 = [ver.Point for ver in inter.Vertexes] + if len(pts1) == 0: + continue + y1 = min(pts1, key=lambda p: p.y).y + pointsy = np.arange(y1, pts[i + 1].y, -self.gap_row) + continue + for pointy in pointsy: + cp = ref[1].copy() + cp.Placement.Base = FreeCAD.Vector(pts[i].x + ref[1].BoundBox.XLength / 2, pointy, 0.0) + cut = cp.cut([self.Area], 0) + #print(y1, " - ", pointy, " - ", len(cut.Vertexes)) + #if len(cut.Vertexes) == 0: + Part.show(cp) + pl.append([ref[0], pointy]) return pl def accept(self): @@ -446,21 +548,6 @@ class _PVPlantPlacementTaskPanel: if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked ] - """seen_lengths = set() - tmpframes = [] - for frame in sorted(items, key=lambda rack: rack.Length, reverse=True): - if frame.Length not in seen_lengths: - seen_lengths.add(frame.Length) - tmpframes.append(frame) - '''found = False - for tmp in tmpframes: - if tmp.Length == frame.Length: - found = True - break - if not found: - tmpframes.append(frame)''' - self.FrameSetups = tmpframes.copy()""" - unique_frames = {frame.Length.Value: frame for frame in items} self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) @@ -479,8 +566,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) @@ -489,6 +582,8 @@ class _PVPlantPlacementTaskPanel: FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() + + # ---------------------------------------------------------------------------------------------------------------------- # function AdjustToTerrain # Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh diff --git a/PVPlantPlacement.ui b/PVPlantPlacement.ui index 49fc1d4..3e77bfe 100644 --- a/PVPlantPlacement.ui +++ b/PVPlantPlacement.ui @@ -22,16 +22,6 @@ - - - - - 16777215 - 54 - - - - @@ -60,20 +50,6 @@ 0 - - - - Add - - - - - - - Remove - - - @@ -335,7 +311,11 @@ - + + + true + + @@ -503,11 +483,29 @@ + + + + + 16777215 + 54 + + + + + + + + Organizar en subcarpetas + + + true + + + - buttonAddFrame - buttonRemoveFrame editPVArea buttonPVArea comboOrientation diff --git a/PVPlantTools.py b/PVPlantTools.py index 5115555..d46ebe1 100644 --- a/PVPlantTools.py +++ b/PVPlantTools.py @@ -673,8 +673,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 +711,4 @@ pv_mechanical = [ ] objectlist = ['PVPlantTree', - 'PVPlantFence',] \ No newline at end of file + 'PVPlantFenceGroup',] \ No newline at end of file diff --git a/Project/Area/PVPlantArea.py b/Project/Area/PVPlantArea.py index f541820..67227cc 100644 --- a/Project/Area/PVPlantArea.py +++ b/Project/Area/PVPlantArea.py @@ -69,6 +69,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 +102,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 +121,7 @@ class _ViewProviderArea: ''' return str(os.path.join(DirIcons, "area.svg")) + ''' def claimChildren(self): """ @@ -159,17 +161,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 +306,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 +326,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 +348,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) 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 sections: + obj.Shape = Part.makePolygon(pts) + else: + obj.Shape = Part.Shape() # Forma vacía si falla class ViewProviderOffsetArea(_ViewProviderArea): @@ -382,14 +380,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) @@ -416,33 +412,192 @@ class ProhibitedArea(OffsetArea): self.Type = obj.Type = "ProhibitedArea" obj.Proxy = self - def onDocumentRestored(self, obj): - """Method run when the document is restored.""" - self.setProperties(obj) + '''# Propiedades de color + if not hasattr(obj, "OriginalColor"): + obj.addProperty("App::PropertyColor", + "OriginalColor", + "Display", + "Color for original wire") + obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo - def __getstate__(self): - return None + if not hasattr(obj, "OffsetColor"): + obj.addProperty("App::PropertyColor", + "OffsetColor", + "Display", + "Color for offset wire") + obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja - def __setstate__(self, state): - pass + # Propiedades de grosor + if not hasattr(obj, "OriginalWidth"): + obj.addProperty("App::PropertyFloat", + "OriginalWidth", + "Display", + "Line width for original wire") + obj.OriginalWidth = 4.0 + + if not hasattr(obj, "OffsetWidth"): + obj.addProperty("App::PropertyFloat", + "OffsetWidth", + "Display", + "Line width for offset wire") + obj.OffsetWidth = 4.0''' + + def execute(self, obj): + # Comprobar dependencias + if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape: + return + if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain: + return + + base = obj.Base.Shape + land = PVPlantSite.get().Terrain.Mesh + vec = FreeCAD.Vector(0, 0, 1) + + # 1. Crear wire original + original_wire = utils.getProjected(base, vec) + sections_original = mp.projectShapeOnMesh(original_wire, land, vec) + + # 2. Crear wire offset + offset_wire = original_wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) + sections_offset = mp.projectShapeOnMesh(offset_wire, land, vec) + + # Crear formas compuestas + def make_polygon(sections): + if not sections: + return Part.Shape() + pts = [] + for section in sections: + pts.extend(section) + return Part.makePolygon(pts) + + compounds = [] + if sections_original: + compounds.append(make_polygon(sections_original)) + if sections_offset: + compounds.append(make_polygon(sections_offset)) + + if compounds: + obj.Shape = Part.makeCompound(compounds) + else: + obj.Shape = Part.Shape() + + # Actualizar colores en la vista + if FreeCAD.GuiUp and obj.ViewObject: + obj.ViewObject.Proxy.updateVisual() class ViewProviderForbiddenArea(_ViewProviderArea): + def __init__(self, vobj): + super().__init__(vobj) + # Valores por defecto + self.original_color = (1.0, 0.0, 0.0) # Rojo + self.offset_color = (1.0, 0.5, 0.0) # Naranja + self.original_width = 4.0 + self.offset_width = 4.0 + self.line_widths = [] # Almacenará los grosores por arista + + vobj.LineColor = (1.0, 0.0, 0.0) + vobj.LineWidth = 4 + vobj.PointColor = (1.0, 0.0, 0.0) + vobj.PointSize = 4 + def getIcon(self): - ''' Return object treeview icon ''' + ''' Return object treeview icon. ''' return str(os.path.join(DirIcons, "area_forbidden.svg")) 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 + def attach(self, vobj): + super().attach(vobj) + # Inicializar visualización + self.updateVisual() + + def updateVisual(self): + """Actualiza colores y grosores de línea""" + if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object: + return + + obj = self.ViewObject.Object + + # Obtener propiedades de color y grosor + try: + self.original_color = obj.OriginalColor + self.offset_color = obj.OffsetColor + self.original_width = obj.OriginalWidth + self.offset_width = obj.OffsetWidth + except: + pass + + # Actualizar colores si hay forma + if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): + if len(obj.Shape.SubShapes) >= 2: + # Asignar colores + colors = [] + colors.append(self.original_color) # Primer wire (original) + colors.append(self.offset_color) # Segundo wire (offset) + self.ViewObject.DiffuseColor = colors + + # Preparar grosores por arista + #self.prepareLineWidths() + + # Asignar grosores usando LineWidthArray + '''if self.line_widths: + self.ViewObject.LineWidthArray = self.line_widths''' + + # Establecer grosor global como respaldo + #self.ViewObject.LineWidth = max(self.original_width, self.offset_width) + + def prepareLineWidths(self): + """Prepara la lista de grosores para cada arista""" + self.line_widths = [] + obj = self.ViewObject.Object + + if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): + # Contar aristas en cada subforma + for i, subshape in enumerate(obj.Shape.SubShapes): + edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1 + + # Determinar grosor según tipo de wire + width = self.original_width if i == 0 else self.offset_width + + # Asignar el mismo grosor a todas las aristas de este wire + self.line_widths.extend([width] * edge_count) + + def onChanged(self, vobj, prop): + """Maneja cambios en propiedades de visualización""" + if prop in ["LineColor", "PointColor", "ShapeColor", "LineWidth"]: + self.updateVisual() + + def updateData(self, obj, prop): + """Actualiza cuando cambian los datos del objeto""" + if prop == "Shape": + self.updateVisual() + + '''def __getstate__(self): + return { + "original_color": self.original_color, + "offset_color": self.offset_color, + "original_width": self.original_width, + "offset_width": self.offset_width + } + + def __setstate__(self, state): + if "original_color" in state: + self.original_color = state["original_color"] + if "offset_color" in state: + self.offset_color = state["offset_color"] + if "original_width" in state: + self.original_width = state.get("original_width", 4.0) + if "offset_width" in state: + self.offset_width = state.get("offset_width", 4.0)''' + ''' PV Area: ''' - - def makePVSubplant(): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant") PVSubplant(obj) 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