Punto de restauración.

This commit is contained in:
2025-07-31 09:58:38 +02:00
parent e1e1441892
commit 5db8f5439d
14 changed files with 1382 additions and 497 deletions

4
.idea/PVPlant.iml generated
View File

@@ -5,4 +5,8 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PackageRequirementsSettings">
<option name="removeUnused" value="true" />
<option name="modifyBaseFiles" value="true" />
</component>
</module> </module>

View File

@@ -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 copy
import math import math
import ArchComponent
import Draft
import FreeCAD
import Part
import PVPlantFencePost
import PVPlantSite
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui import FreeCADGui
@@ -56,26 +58,28 @@ from PVPlantResources import DirIcons as DirIcons
EAST = FreeCAD.Vector(1, 0, 0) 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): def makePVPlantFence(section, post, path):
obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Fence') obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Fence')
_Fence(obj) Fence(obj)
obj.Post = post obj.Post = post
obj.Base = path obj.Base = path
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
_ViewProviderFence(obj.ViewObject) ViewProviderFence(obj.ViewObject)
hide(section) hide(section)
hide(post) hide(post)
hide(path) 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() FreeCAD.ActiveDocument.recompute()
return obj return obj
@@ -83,16 +87,8 @@ def hide(obj):
if hasattr(obj, 'ViewObject') and obj.ViewObject: if hasattr(obj, 'ViewObject') and obj.ViewObject:
obj.ViewObject.Visibility = False obj.ViewObject.Visibility = False
def getAngle(Line1, Line2): def get_parameter_from_v0_old(edge, offset):
v1 = Line1.Vertexes[1].Point - Line1.Vertexes[0].Point """ Return parameter at distance offset from edge.Vertexes[0].sb method in Part.TopoShapeEdge??? """
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???
"""
import DraftVecUtils import DraftVecUtils
lpt = edge.valueAt(edge.getParameterByLength(0)) lpt = edge.valueAt(edge.getParameterByLength(0))
@@ -106,14 +102,16 @@ def get_parameter_from_v0(edge, offset):
length = offset length = offset
return edge.getParameterByLength(length) 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): 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 = FreeCAD.Placement()
placement.Rotation = globalRotation placement.Rotation = globalRotation
placement.move(RefPt + xlate) placement.move(RefPt + xlate)
@@ -121,55 +119,23 @@ def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal
if not align: if not align:
return placement return placement
# unit +Z Probably defined elsewhere? t = edge.tangentAt(get_parameter_from_v0(edge, offset)).normalize()
z = FreeCAD.Vector(0, 0, 1) n = normal or FreeCAD.Vector(0, 0, 1)
# y = FreeCAD.Vector(0, 1, 0) # unit +Y b = t.cross(n).normalize()
x = FreeCAD.Vector(1, 0, 0) # unit +X
nullv = FreeCAD.Vector(0, 0, 0)
# get local coord system - tangent, normal, binormal, if possible # Asegurar sistema de coordenadas derecho
t = edge.tangentAt(get_parameter_from_v0(edge, offset)) if n.dot(t.cross(b)) < 0:
t.normalize() b = -b
n = normal
b = t.cross(n)
b.normalize()
lnodes = z.cross(b) # Construir matriz
try: rotation_matrix = FreeCAD.Matrix(
# Can't normalize null vector. t.x, b.x, n.x, 0,
lnodes.normalize() t.y, b.y, n.y, 0,
except: t.z, b.z, n.z, 0,
# pathological cases: 0, 0, 0, 1
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])
placement.Rotation = FreeCAD.Rotation(rotation_matrix)
return placement return placement
def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align): def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
@@ -183,12 +149,8 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
import DraftGeomUtils import DraftGeomUtils
closedpath = DraftGeomUtils.isReallyClosed(pathwire) closedpath = DraftGeomUtils.isReallyClosed(pathwire)
normal = DraftGeomUtils.getNormal(pathwire)
if normal: normal = FreeCAD.Vector(0, 0, 1)
if normal.z < 0: # asegurarse de que siempre se dibuje por encima del suelo
normal.z *= -1
else:
normal = FreeCAD.Vector(0, 0, 1)
path = Part.__sortEdges__(pathwire.Edges) path = Part.__sortEdges__(pathwire.Edges)
ends = [] ends = []
cdist = 0 cdist = 0
@@ -241,7 +203,7 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
return placements return placements
class _Fence(ArchComponent.Component): class Fence(ArchComponent.Component):
def __init__(self, obj): def __init__(self, obj):
ArchComponent.Component.__init__(self, obj) ArchComponent.Component.__init__(self, obj)
self.setProperties(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")) QT_TRANSLATE_NOOP("App::Property", "The number of posts used to build the fence"))
obj.setEditorMode("Length", 1) obj.setEditorMode("Length", 1)
self.Type = "PVPlatFence" self.Type = "PVPlatFence"
def __getstate__(self): def __getstate__(self):
@@ -361,74 +322,30 @@ class _Fence(ArchComponent.Component):
return None return None
def execute(self, obj): def execute(self, obj):
if not obj.Base or not obj.Post:
return
# 1. Preparar trazado base
pathwire = self.calculatePathWire(obj) pathwire = self.calculatePathWire(obj)
if pathwire is None: pathwire = utils.getProjected(pathwire, FreeCAD.Vector(0, 0, 1))
# FreeCAD.Console.PrintLog("ArchFence.execute: path " + obj.Base.Name + " has no edges\n") pathwire = utils.simplifyWire(pathwire)
return if not pathwire or not pathwire.Edges:
if not obj.Post:
FreeCAD.Console.PrintLog("ArchFence.execute: Post not set\n")
return return
# 2. Proyectar sobre terreno (con caché)
self.Posts = [] self.Posts = []
self.Foundations = [] 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: site = PVPlantSite.get()
land = site.Terrain.Shape segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))
pathwire = land.makeParallelProjection(pathwire, FreeCAD.Vector(0, 0, 1)) points=[]
for segment in segments:
points.extend(segment)
pathwire = Part.makePolygon(points)
if pathwire is None: if pathwire is None:
return 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 sectionLength = obj.Gap.Value
postLength = 0 #obj.Post.Diameter.Value #considerarlo 0 porque no influye postLength = 0 #obj.Post.Diameter.Value #considerarlo 0 porque no influye
postPlacements = [] postPlacements = []
@@ -456,18 +373,19 @@ class _Fence(ArchComponent.Component):
postPlacements.extend(placements) postPlacements.extend(placements)
# 5. Generar geometría
postShapes, postFoundation = self.calculatePosts(obj, postPlacements) postShapes, postFoundation = self.calculatePosts(obj, postPlacements)
sections, num = self.calculateSections(obj, postPlacements) mesh = self.calculate_sections(obj, postPlacements)
postShapes = Part.makeCompound(postShapes) postShapes = Part.makeCompound(postShapes)
postFoundation = Part.makeCompound(postFoundation) 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.NumberOfSections = count
obj.NumberOfPosts = obj.NumberOfSections + 1 obj.NumberOfPosts = count + 1
obj.Length = pathLength obj.Length = pathLength
obj.Concrete = count * postFoundation.SubShapes[0].Volume obj.Concrete = count * postFoundation.SubShapes[0].Volume
@@ -496,7 +414,7 @@ class _Fence(ArchComponent.Component):
def calculatePostPlacements(self, obj, pathwire, rotation): def calculatePostPlacements(self, obj, pathwire, rotation):
postWidth = obj.Post.Diameter.Value 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) placements = calculatePlacementsOnPath(rotation, pathwire, int(obj.NumberOfSections) + 1, transformationVector, True)
# The placement of the last object is always the second entry in the list. # The placement of the last object is always the second entry in the list.
# So we move it to the end: # So we move it to the end:
@@ -508,47 +426,36 @@ class _Fence(ArchComponent.Component):
posts = [] posts = []
foundations = [] foundations = []
for placement in postPlacements: for placement in postPlacements:
postCopy = obj.Post.Shape.copy() new_post = obj.Post.Shape.copy()
postCopy = Part.Solid(postCopy) new_post = Part.Solid(new_post)
postCopy.Placement = placement new_post.Placement = placement
postCopy.Placement.Base.z += 100 new_post.Placement.Base.z += 100
posts.append(postCopy) posts.append(new_post)
foundation = Part.makeCylinder(150, 700) foundation = Part.makeCylinder(150, 700)
foundation.Placement = placement foundation.Placement = placement
foundation.Placement.Base.z -= obj.Depth.Value foundation.Placement.Base.z -= obj.Depth.Value
foundation = foundation.cut(postCopy) #foundation = foundation.cut(new_post)
foundations.append(foundation) foundations.append(foundation)
return posts, foundations return posts, foundations
def calculateSections(self, obj, postPlacements): def calculate_sections(self, obj, postPlacements):
shapes = [] offsetz = FreeCAD.Vector(0, 0, obj.MeshOffsetZ.Value)
faceNumbers = [] meshHeight = FreeCAD.Vector(0, 0, obj.MeshHeight.Value)
offsetz = obj.MeshOffsetZ.Value points_down = []
meshHeight = obj.MeshHeight.Value points_up = []
for i in range(len(postPlacements) - 1): for i in range(len(postPlacements) - 1):
startPlacement = postPlacements[i] p1 = postPlacements[i].Base + offsetz
endPlacement = postPlacements[i + 1] 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) shape = Part.makeRuledSurface(Part.makePolygon(points_down), Part.makePolygon(points_up))
p2 = endPlacement.Base + FreeCAD.Vector(0, 0, offsetz) return shape
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)
def calculatePathWire(self, obj): def calculatePathWire(self, obj):
if obj.Base: if obj.Base:
@@ -561,7 +468,7 @@ class _Fence(ArchComponent.Component):
return None return None
class _ViewProviderFence(ArchComponent.ViewProviderComponent): class ViewProviderFence(ArchComponent.ViewProviderComponent):
"A View Provider for the Fence object" "A View Provider for the Fence object"
def __init__(self, vobj): def __init__(self, vobj):
@@ -641,7 +548,7 @@ class _ViewProviderFence(ArchComponent.ViewProviderComponent):
children.append(self.Object.Gate) children.append(self.Object.Gate)
return children return children
class _FenceTaskPanel: class FenceTaskPanel:
'''The TaskPanel to setup the fence''' '''The TaskPanel to setup the fence'''
def __init__(self): def __init__(self):
@@ -774,15 +681,8 @@ class _FenceTaskPanel:
self.form = [self.formFence, self.formPost, self.formFoundation] self.form = [self.formFence, self.formPost, self.formFoundation]
# valores iniciales y creación del la valla: # valores iniciales y creación del la valla:
import Draft self.post = PVPlantFencePost.makeFencePost()
self.post = PVPlantFencePost.makeFencePost() # Arch.makePipe()
self.post.Label = "Post" self.post.Label = "Post"
Draft.autogroup(self.post)
'''
self.section = self.makeGrid()
self.path = self.section.Base
'''
FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.recompute()
self.fence = makePVPlantFence(self.section, self.post, self.path) self.fence = makePVPlantFence(self.section, self.post, self.path)
@@ -858,7 +758,7 @@ class CommandPVPlantFence:
return not FreeCAD.ActiveDocument is None return not FreeCAD.ActiveDocument is None
def Activated(self): def Activated(self):
self.TaskPanel = _FenceTaskPanel() self.TaskPanel = FenceTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel) FreeCADGui.Control.showDialog(self.TaskPanel)
@@ -876,12 +776,13 @@ if FreeCAD.GuiUp:
} }
def IsActive(self): def IsActive(self):
site = FreeCAD.ActiveDocument.getObject("Site")
return (not (FreeCAD.ActiveDocument is None) and return (not (FreeCAD.ActiveDocument is None) and
not (FreeCAD.ActiveDocument.getObject("Site") is None) and not (site is None) and
not (FreeCAD.ActiveDocument.getObject("Terrain") is None)) not (site.Terrain is None))
import PVPlantFenceGate import Civil.Fence.PVPlantFenceGate as PVPlantFenceGate
FreeCADGui.addCommand('PVPlantFence', CommandPVPlantFence()) FreeCADGui.addCommand('PVPlantFence', CommandPVPlantFence())
FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate._CommandPVPlantGate()) FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate.CommandPVPlantGate())
FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost._CommandFencePost()) FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost.CommandFencePost())
#FreeCADGui.addCommand('PVPlantFenceGroup', CommandFenceGroup()) #FreeCADGui.addCommand('PVPlantFenceGroup', CommandFenceGroup())

View File

@@ -202,7 +202,7 @@ class ViewProviderGate:
children.append(self.Object.Base) children.append(self.Object.Base)
return children return children
class _CommandPVPlantGate: class CommandPVPlantGate:
"the PVPlant Fence command definition" "the PVPlant Fence command definition"
def __init__(self): def __init__(self):

View File

@@ -1,5 +1,6 @@
import ArchComponent
import FreeCAD import FreeCAD
import Part
import ArchComponent
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui import FreeCADGui
@@ -9,8 +10,6 @@ else:
# \cond # \cond
def translate(ctxt, txt): def translate(ctxt, txt):
return txt return txt
def QT_TRANSLATE_NOOP(ctxt, txt): def QT_TRANSLATE_NOOP(ctxt, txt):
return txt return txt
# \endcond # \endcond
@@ -21,20 +20,14 @@ except AttributeError:
def _fromUtf8(s): def _fromUtf8(s):
return s return s
def makeFencePost(diameter=48, length=3000, placement=None, name="FencePost"):
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
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
obj.Label = name obj.Label = name
_FencePost(obj) FencePost(obj)
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
_ViewProviderFencePost(obj.ViewObject) ViewProviderFencePost(obj.ViewObject)
obj.Length = length obj.Length = length
obj.Diameter = diameter 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"): 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 = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
obj.Label = name obj.Label = name
_FenceReinforcePostPost(obj) FenceReinforcePost(obj)
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
_ViewProviderFencePost(obj.ViewObject) ViewProviderFencePost(obj.ViewObject)
obj.Length = length obj.Length = length
obj.Diameter = diameter obj.Diameter = diameter
@@ -66,7 +54,7 @@ def makeFenceReinforcePost(diameter=48, length=3000, placement=None, name="Post"
return obj return obj
class _FencePost(ArchComponent.Component): class FencePost(ArchComponent.Component):
def __init__(self, obj): def __init__(self, obj):
ArchComponent.Component.__init__(self, obj) ArchComponent.Component.__init__(self, obj)
self.setProperties(obj) self.setProperties(obj)
@@ -80,10 +68,10 @@ class _FencePost(ArchComponent.Component):
obj.addProperty("App::PropertyLength", "Diameter", "Pipe", obj.addProperty("App::PropertyLength", "Diameter", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The diameter of this pipe, if not based on a profile") QT_TRANSLATE_NOOP("App::Property", "The diameter of this pipe, if not based on a profile")
).Diameter = 48 ).Diameter = 48
if not "Thickness" in pl: '''if not "Thickness" in pl:
obj.addProperty("App::PropertyLength", "Thickness", "Pipe", obj.addProperty("App::PropertyLength", "Thickness", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The Thickness of this pipe, if not based on a profile") 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: if not "Length" in pl:
obj.addProperty("App::PropertyLength", "Length", "Pipe", obj.addProperty("App::PropertyLength", "Length", "Pipe",
QT_TRANSLATE_NOOP("App::Property", "The length of this pipe, if not based on an edge") 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) ArchComponent.Component.onDocumentRestored(self, obj)
self.setProperties(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): def execute(self, obj):
import Part
pl = obj.Placement pl = obj.Placement
if obj.CloneOf: lip_heigth = 20
obj.Shape = obj.CloneOf.Shape radius = obj.Diameter.Value / 2
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
# 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 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) class FenceReinforcePost(ArchComponent.Component):
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):
def __init__(self, obj): def __init__(self, obj):
ArchComponent.Component.__init__(self, obj) ArchComponent.Component.__init__(self, obj)
self.setProperties(obj) self.setProperties(obj)
@@ -199,10 +150,18 @@ class _FenceReinforcePost(ArchComponent.Component):
self.setProperties(obj) self.setProperties(obj)
def execute(self, obj): def execute(self, obj):
pl = obj.Placement 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: try:
# sh = w.makePipeShell([p], True, False, 2) # 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 = 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 return w
class _ViewProviderFencePost(ArchComponent.ViewProviderComponent): class ViewProviderFencePost(ArchComponent.ViewProviderComponent):
"A View Provider for the Pipe object" "A View Provider for the Pipe object"
def __init__(self, vobj): def __init__(self, vobj):
@@ -254,7 +213,7 @@ class _ViewProviderFencePost(ArchComponent.ViewProviderComponent):
return ":/icons/Arch_Pipe_Tree.svg" return ":/icons/Arch_Pipe_Tree.svg"
class _CommandFencePost: class CommandFencePost:
"the Arch Pipe command definition" "the Arch Pipe command definition"
def GetResources(self): def GetResources(self):
@@ -269,17 +228,5 @@ class _CommandFencePost:
return not FreeCAD.ActiveDocument is None return not FreeCAD.ActiveDocument is None
def Activated(self): def Activated(self):
if True: makeFencePost()
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()
FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
FreeCADGui.addCommand('FencePost', _CommandFencePost())

View File

@@ -0,0 +1,397 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * 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 "<Object name=" in line:
n = re.findall('name=\"(.*?)\"',line)
if n:
name = n[0]
elif "<Property name=\"Label\"" in line:
writemode = True
elif writemode and "<String value=" in line:
n = re.findall('value=\"(.*?)\"',line)
if n:
label = n[0]
writemode = False
elif "<Property name=\"Shape\" type=\"Part::PropertyPartShape\"" in line:
writemode = True
elif writemode and "<Part file=" in line:
n = re.findall('file=\"(.*?)\"',line)
if n:
part = n[0]
writemode = False
elif "<Property name=\"MaterialsTable\" type=\"App::PropertyMap\"" in line:
writemode = True
elif writemode and "<Item key=" in line:
n = re.findall('key=\"(.*?)\"',line)
v = re.findall('value=\"(.*?)\"',line)
if n and v:
materials[n[0]] = v[0]
elif writemode and "</Map>" in line:
writemode = False
elif "</Object>" in line:
if name and label and part:
parts[name] = [label,part,materials]
name = None
label = None
part = None
materials = {}
writemode = False
return parts
def getColors(self,obj):
"returns the DiffuseColor of the referenced object"
filename = self.getFile(obj)
if not filename:
return None
part = obj.Part
if not obj.Part:
return None
zdoc = zipfile.ZipFile(filename)
if not "GuiDocument.xml" in zdoc.namelist():
return None
colorfile = None
with zdoc.open("GuiDocument.xml") as docf:
writemode1 = False
writemode2 = False
for line in docf:
line = line.decode("utf8")
if ("<ViewProvider name=" in line) and (part in line):
writemode1 = True
elif writemode1 and ("<Property name=\"DiffuseColor\"" in line):
writemode1 = False
writemode2 = True
elif writemode2 and ("<ColorList file=" in line):
n = re.findall('file=\"(.*?)\"',line)
if n:
colorfile = n[0]
break
if not colorfile:
return None
if not colorfile in zdoc.namelist():
return None
colors = []
cf = zdoc.open(colorfile)
buf = cf.read()
cf.close()
for i in range(1,int(len(buf)/4)):
colors.append((buf[i*4+3]/255.0,buf[i*4+2]/255.0,buf[i*4+1]/255.0,buf[i*4]/255.0))
if colors:
return colors
return None
def splitall(self,path):
"splits a path between its components"
allparts = []
while 1:
parts = os.path.split(path)
if parts[0] == path: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == path: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
path = parts[0]
allparts.insert(0, parts[1])
return allparts
class ViewProviderStringInverter(ArchComponent.ViewProviderComponent):
def __init__(self, vobj):
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, I",
'MenuText': "String Inverter",
'ToolTip': "String Placement",}
def Activated(self):
sinverter = makeStringInverter()
def IsActive(self):
active = not (FreeCAD.ActiveDocument is None)
return active
if FreeCAD.GuiUp:
FreeCADGui.addCommand('PowerConverter', CommandPowerConverter())

387
Electrical/group.py Normal file
View File

@@ -0,0 +1,387 @@
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_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)

View File

@@ -78,11 +78,12 @@ class _PVPlantPlacementTaskPanel:
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) 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.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.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): def addPVArea(self):
sel = FreeCADGui.Selection.getSelection() sel = FreeCADGui.Selection.getSelection()
@@ -95,6 +96,10 @@ class _PVPlantPlacementTaskPanel:
list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
list_item.setCheckState(QtCore.Qt.Checked) 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): def createFrameFromPoints(self, dataframe):
from Mechanical.Frame import PVPlantFrame from Mechanical.Frame import PVPlantFrame
try: try:
@@ -104,22 +109,37 @@ class _PVPlantPlacementTaskPanel:
MechanicalGroup.Label = "Frames" MechanicalGroup.Label = "Frames"
FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup)
if self.form.cbSubfolders.checked: if self.form.cbSubfolders.isChecked:
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", self.PVArea.Label) label = "Frames-" + self.PVArea.Label
group.Label = self.PVArea.Label if label in [obj.Label for obj in FreeCAD.ActiveDocument.Frames.Group]:
MechanicalGroup.addObject(group) MechanicalGroup = FreeCAD.ActiveDocument.getObject(label)[0]
MechanicalGroup = group else:
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", label)
placements = dataframe["placement"].tolist() group.Label = label
types = dataframe["type"].tolist() MechanicalGroup.addObject(group)
frames = [] MechanicalGroup = group
for idx in range(len(placements)): try:
newrack = PVPlantFrame.makeTracker(setup=types[idx]) placements = dataframe["placement"].tolist()
newrack.Label = "Tracker" types = dataframe["type"].tolist()
newrack.Visibility = False frames = []
newrack.Placement = placements[idx] for idx in range(len(placements)):
MechanicalGroup.addObject(newrack) newrack = PVPlantFrame.makeTracker(setup=types[idx])
frames.append(newrack) 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"): if self.PVArea.Name.startswith("FrameArea"):
self.PVArea.Frames = frames self.PVArea.Frames = frames
@@ -179,7 +199,7 @@ class _PVPlantPlacementTaskPanel:
return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ 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) np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64)
def adjustToTerrain(self, coordinates): def adjustToTerrain_old(self, coordinates):
mode = 1 mode = 1
terrain = self.Terrain.Mesh terrain = self.Terrain.Mesh
@@ -276,6 +296,106 @@ class _PVPlantPlacementTaskPanel:
placeRegion(df) placeRegion(df)
return 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): def isInside(self, frame, point):
if self.Area.isInside(point, 10, True): if self.Area.isInside(point, 10, True):
frame.Placement.Base = point frame.Placement.Base = point
@@ -349,86 +469,68 @@ class _PVPlantPlacementTaskPanel:
if countcols == self.form.editColCount.value(): if countcols == self.form.editColCount.value():
offsetcols += valcols offsetcols += valcols
countcols = 0 countcols = 0
print("/n/n")
print(cols)
return self.adjustToTerrain(cols) return self.adjustToTerrain(cols)
def calculateNonAlignedArray(self): def calculateNonAlignedArray(self):
gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value pointsx, pointsy = self.getAligments()
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
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) # variables for corridors:
countcols = 0
# TODO: revisar todo esto: ----------------------------------------------------------------- countrows = 0
sel = FreeCADGui.Selection.getSelectionEx()[0] offsetcols = 0 # ??
refh = None offsetrows = 0 # ??
refv = None valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy)
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 = [] pl = []
for point in pointsx: for point in pointsx:
p1 = FreeCAD.Vector(point, Area.BoundBox.YMax, 0.0) p1 = FreeCAD.Vector(point, self.Area.BoundBox.YMax, 0.0)
p2 = FreeCAD.Vector(point, Area.BoundBox.YMin, 0.0) p2 = FreeCAD.Vector(point, self.Area.BoundBox.YMin, 0.0)
line = Part.makePolygon([p1, p2]) 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 pts = [ver.Point for ver in inter.Vertexes] # todo: sort points
for i in range(0, len(pts), 2): for i in range(0, len(pts), 2):
line = Part.LineSegment(pts[i], pts[i + 1]) line = Part.LineSegment(pts[i], pts[i + 1])
if line.length() >= rec.BoundBox.YLength: if line.length() >= ref[1].BoundBox.YLength:
y1 = pts[i].y - rec.BoundBox.YLength y1 = pts[i].y - ref[1].BoundBox.YLength / 2
cp = rec.copy() cp = ref[1].copy()
cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, y1, 0.0) cp.Placement.Base = FreeCAD.Vector(pts[i].x, y1, 0.0)
inter = cp.cut([Area]) Part.show(cp)
y1 = min([ver.Point.y for ver in inter.Vertexes]) inter = cp.cut([self.Area])
pointsy = np.arange(y1, pts[i + 1].y, -gap_row) pts1 = [ver.Point for ver in inter.Vertexes]
for point in pointsy: if len(pts1) == 0:
cp = rec.copy() continue
cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, point, 0.0) y1 = min(pts1, key=lambda p: p.y).y
cut = cp.cut([Area], 0) pointsy = np.arange(y1, pts[i + 1].y, -self.gap_row)
if len(cut.Vertexes) == 0: continue
Part.show(cp) for pointy in pointsy:
pl.append(point) 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 return pl
def accept(self): def accept(self):
@@ -446,21 +548,6 @@ class _PVPlantPlacementTaskPanel:
if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked 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} 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.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True)
@@ -479,8 +566,14 @@ class _PVPlantPlacementTaskPanel:
dataframe = self.calculateNonAlignedArray() dataframe = self.calculateNonAlignedArray()
# 3. Adjust to terrain: # 3. Adjust to terrain:
self.createFrameFromPoints(dataframe) 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 FreeCAD.ActiveDocument.RecomputesFrozen = False
params.SetBool("AutoSaveEnabled", auto_save_enabled) params.SetBool("AutoSaveEnabled", auto_save_enabled)
@@ -489,6 +582,8 @@ class _PVPlantPlacementTaskPanel:
FreeCADGui.Control.closeDialog() FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.recompute()
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
# function AdjustToTerrain # function AdjustToTerrain
# Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh # Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh

View File

@@ -22,16 +22,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1">
<widget class="QListWidget" name="listFrameSetups">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>54</height>
</size>
</property>
</widget>
</item>
<item row="7" column="1"> <item row="7" column="1">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
@@ -60,20 +50,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item>
<widget class="QPushButton" name="buttonAddFrame">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonRemoveFrame">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@@ -335,7 +311,11 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="editInnerSpacing"/> <widget class="QLineEdit" name="editInnerSpacing">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
@@ -503,11 +483,29 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="editPVArea"/> <widget class="QLineEdit" name="editPVArea"/>
</item> </item>
<item row="0" column="1">
<widget class="QListWidget" name="listFrameSetups">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>54</height>
</size>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="cbSubfolders">
<property name="text">
<string>Organizar en subcarpetas</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>buttonAddFrame</tabstop>
<tabstop>buttonRemoveFrame</tabstop>
<tabstop>editPVArea</tabstop> <tabstop>editPVArea</tabstop>
<tabstop>buttonPVArea</tabstop> <tabstop>buttonPVArea</tabstop>
<tabstop>comboOrientation</tabstop> <tabstop>comboOrientation</tabstop>

View File

@@ -673,8 +673,7 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantTracker', PVPlantFrame.CommandTracker()) FreeCADGui.addCommand('PVPlantTracker', PVPlantFrame.CommandTracker())
FreeCADGui.addCommand('RackType', CommandRackGroup()) FreeCADGui.addCommand('RackType', CommandRackGroup())
from Civil.Fence import PVPlantFence
import PVPlantFence
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup()) FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
projectlist = [ # "Reload", projectlist = [ # "Reload",
@@ -712,4 +711,4 @@ pv_mechanical = [
] ]
objectlist = ['PVPlantTree', objectlist = ['PVPlantTree',
'PVPlantFence',] 'PVPlantFenceGroup',]

View File

@@ -69,6 +69,7 @@ class _Area:
''' Initialize the Area object ''' ''' Initialize the Area object '''
self.Type = None self.Type = None
self.obj = None self.obj = None
self.setProperties(obj)
def setProperties(self, obj): def setProperties(self, obj):
pl = obj.PropertiesList pl = obj.PropertiesList
@@ -101,18 +102,18 @@ class _Area:
def __setstate__(self, state): def __setstate__(self, state):
pass pass
def execute(self, obj):
''' Execute the area object '''
pass
class _ViewProviderArea: class _ViewProviderArea:
def __init__(self, vobj): def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self vobj.Proxy = self
def attach(self, vobj): def attach(self, vobj):
''' ''' Create Object visuals in 3D view. '''
Create Object visuals in 3D view. self.ViewObject = vobj
'''
self.Object = vobj.Object
return
def getIcon(self): def getIcon(self):
''' '''
@@ -120,6 +121,7 @@ class _ViewProviderArea:
''' '''
return str(os.path.join(DirIcons, "area.svg")) return str(os.path.join(DirIcons, "area.svg"))
''' '''
def claimChildren(self): def claimChildren(self):
""" """
@@ -159,17 +161,10 @@ class _ViewProviderArea:
pass pass
def __getstate__(self): def __getstate__(self):
"""
Save variables to file.
"""
return None return None
def __setstate__(self, state): def __setstate__(self, state):
""" pass
Get variables from file.
"""
return None
''' Frame Area ''' ''' Frame Area '''
@@ -311,17 +306,14 @@ class ViewProviderFrameArea(_ViewProviderArea):
''' offsets ''' ''' offsets '''
def makeOffsetArea(base = None, val=None): def makeOffsetArea(base = None, val=None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "OffsetArea") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "OffsetArea")
OffsetArea(obj) OffsetArea(obj)
obj.Base = base obj.Base = base
ViewProviderOffsetArea(obj.ViewObject) ViewProviderOffsetArea(obj.ViewObject)
if val: if val:
obj.Distance = val obj.OffsetDistance = val
offsets = None
try: try:
offsetsgroup = FreeCAD.ActiveDocument.Offsets offsetsgroup = FreeCAD.ActiveDocument.Offsets
except: except:
@@ -334,11 +326,13 @@ def makeOffsetArea(base = None, val=None):
class OffsetArea(_Area): class OffsetArea(_Area):
def __init__(self, obj): def __init__(self, obj):
_Area.__init__(self, obj) '''_Area.__init__(self, obj)
self.setProperties(obj) self.setProperties(obj)'''
super().__init__(obj) # Llama al constructor de _Area
def setProperties(self, obj): def setProperties(self, obj):
_Area.setProperties(self, obj) super().setProperties(obj) # Propiedades de la clase base
pl = obj.PropertiesList pl = obj.PropertiesList
if not ("OffsetDistance" in pl): if not ("OffsetDistance" in pl):
obj.addProperty("App::PropertyDistance", obj.addProperty("App::PropertyDistance",
@@ -354,24 +348,28 @@ class OffsetArea(_Area):
self.setProperties(obj) self.setProperties(obj)
def execute(self, 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 base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0, 0, 1) vec = FreeCAD.Vector(0, 0, 1)
wire = utils.getProjected(base, vec) wire = utils.getProjected(base, vec)
wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
tmp = mp.projectShapeOnMesh(wire, land, vec) sections = mp.projectShapeOnMesh(wire, land, vec)
pts = [] pts = []
for section in tmp: for section in sections:
pts.extend(section) pts.extend(section)
obj.Shape = Part.makePolygon(pts)
def __getstate__(self): # Crear forma solo si hay resultados
return None if sections:
obj.Shape = Part.makePolygon(pts)
def __setstate__(self, state): else:
pass obj.Shape = Part.Shape() # Forma vacía si falla
class ViewProviderOffsetArea(_ViewProviderArea): class ViewProviderOffsetArea(_ViewProviderArea):
@@ -382,14 +380,12 @@ class ViewProviderOffsetArea(_ViewProviderArea):
def claimChildren(self): def claimChildren(self):
""" Provides object grouping """ """ Provides object grouping """
children = [] children = []
if self.Object.Base: if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.Object.Base) children.append(self.ViewObject.Object.Base)
return children return children
''' Forbidden Area: ''' ''' Forbidden Area: '''
def makeProhibitedArea(base = None): def makeProhibitedArea(base = None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ExclusionArea") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ExclusionArea")
ProhibitedArea(obj) ProhibitedArea(obj)
@@ -416,33 +412,192 @@ class ProhibitedArea(OffsetArea):
self.Type = obj.Type = "ProhibitedArea" self.Type = obj.Type = "ProhibitedArea"
obj.Proxy = self obj.Proxy = self
def onDocumentRestored(self, obj): '''# Propiedades de color
"""Method run when the document is restored.""" if not hasattr(obj, "OriginalColor"):
self.setProperties(obj) obj.addProperty("App::PropertyColor",
"OriginalColor",
"Display",
"Color for original wire")
obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
def __getstate__(self): if not hasattr(obj, "OffsetColor"):
return None obj.addProperty("App::PropertyColor",
"OffsetColor",
"Display",
"Color for offset wire")
obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
def __setstate__(self, state): # Propiedades de grosor
pass 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): 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): def getIcon(self):
''' Return object treeview icon ''' ''' Return object treeview icon. '''
return str(os.path.join(DirIcons, "area_forbidden.svg")) return str(os.path.join(DirIcons, "area_forbidden.svg"))
def claimChildren(self): def claimChildren(self):
""" Provides object grouping """ """ Provides object grouping """
children = [] children = []
if self.Object.Base: if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.Object.Base) children.append(self.ViewObject.Object.Base)
return children 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: ''' ''' PV Area: '''
def makePVSubplant(): def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant") obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")
PVSubplant(obj) PVSubplant(obj)

View File

@@ -24,11 +24,15 @@ class _CommandReload:
def Activated(self): def Activated(self):
import PVPlantPlacement, \ import PVPlantPlacement, \
PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \ PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \
PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \ PVPlantSite, PVPlantRackChecking, PVPlantCreateTerrainMesh, \
PVPlantCreateTerrainMesh, \
PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \ PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \
PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \ PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \
GraphProfile 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 Civil import PVPlantTrench
from Vegetation import PVPlantTreeGenerator from Vegetation import PVPlantTreeGenerator
@@ -59,9 +63,11 @@ class _CommandReload:
importlib.reload(PVPlantSite) importlib.reload(PVPlantSite)
importlib.reload(PVPlantFrame) importlib.reload(PVPlantFrame)
importlib.reload(PVPlantRackChecking) importlib.reload(PVPlantRackChecking)
importlib.reload(PVPlantFence) importlib.reload(PVPlantFence)
importlib.reload(PVPlantFenceGate) importlib.reload(PVPlantFenceGate)
importlib.reload(PVPlantFencePost) importlib.reload(PVPlantFencePost)
importlib.reload(PVPlantFoundation) importlib.reload(PVPlantFoundation)
importlib.reload(PVPlantCreateTerrainMesh) importlib.reload(PVPlantCreateTerrainMesh)
importlib.reload(PVPlantTreeGenerator) importlib.reload(PVPlantTreeGenerator)

View File

@@ -9,16 +9,12 @@ setuptools~=68.2.2
laspy~=2.5.3 laspy~=2.5.3
geopy~=2.4.1 geopy~=2.4.1
lxml~=4.9.3 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 Pillow~=10.1.0
pyproj~=3.7.1 pyproj~=3.7.1
simplekml~=1.3.6 simplekml~=1.3.6
geojson~=3.1.0 geojson~=3.1.0
certifi~=2023.11.17 certifi~=2023.11.17
SciPy~=1.11.4 SciPy~=1.11.4
ezdxf~=1.4.1 pycollada~=0.7.2
shapely
rtree