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:
+ 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