33 Commits

Author SHA1 Message Date
Javier Braña 25fd92e4f0 Limpieza: eliminados archivos viejos (PVPLantPlacement-old_2022.py, -copia, -old, .bak) 2026-05-04 13:35:21 +02:00
Javier Braña 5abd4fae02 Placement: limpieza masiva. Eliminadas clases _old y _new1 (~550 líneas). Eliminadas funciones globales (~600 líneas). Movidas a Civil/PVPlantPlacementCalc.py. PVPlantPlacement.py pasa de 2352→663 líneas solo con TaskPanels y comandos. 2026-05-04 01:14:00 +02:00
Javier Braña e461ab2e80 Platform: FeaturePython completo. Objeto Platform con SourceFrames, SlopeTolerance, Shape. EarthWorks acepta Platform o frames. ViewProvider verde semitransparente. 2026-05-04 00:09:09 +02:00
Javier Braña 1a22121f87 EarthWorks: separado en PVPlantPlatform (plataforma desde trackers) + PVPlantEarthWorks (solo volumen cut/fill). Platform reutilizable independientemente. 2026-05-03 23:52:58 +02:00
Javier Braña 2858b58d86 EarthWorks: rewrite completo. Limpio, modular, con función compute_earthworks() principal. Cut=rojo, Fill=azul. Eliminado código muerto (3 versiones conviviendo, accept() duplicada, viewprovider comentado) 2026-05-03 22:50:55 +02:00
Javier Braña f3f94d4f59 Road: sistema de alineamiento profesional. Alignment (eje+estaciones), Road multicapa con cubicación contra terreno 2026-05-03 21:43:18 +02:00
Javier Braña f4d43bedd0 Placement: getAligments con linspace, _calculate_placement progreso, accept simplificado, _get_or_create optimizado 2026-05-03 20:25:40 +02:00
Javier Braña a67001bb88 Placement: isInside optimizado con shapely + caché LRU + prefiltro BoundBox. Caché se limpia al cambiar área 2026-05-03 19:56:01 +02:00
Javier Braña 26311cb344 Placement: motor unificado _calculate_placement para aligned/non_aligned, misma lógica compartida 2026-05-03 19:10:49 +02:00
Javier Braña 6c2db07493 Placement: fix 7 excepts genéricos -> específicos (KeyError, Exception, Part.OCCError) 2026-05-03 13:23:48 +02:00
Javier Braña 7d1127c6b5 Trench: fix except genérico -> AttributeError 2026-05-03 02:57:50 +02:00
Javier Braña 7a54e424cb Georeferencing: fallback modo manual cuando QtWebEngine no está disponible (FreeCAD flatpak) 2026-05-03 01:15:16 +02:00
Javier Braña 065f840941 Georeferencing: import QWebEngineView multi-versión (PySide6 QtWebEngineCore/Quick fallback) 2026-05-03 00:56:01 +02:00
Javier Braña 74aedf6122 PVPlant: utm → pyproj (adaptador con sys.modules patch en ImportGrid, eliminado de requirements) 2026-05-03 00:32:36 +02:00
Javier Braña 7c81beb1ba PVPlant: PySide2 -> PySide genérico (FreeCAD resuelve el binding), eliminado de requirements 2026-05-03 00:22:53 +02:00
Javier Braña 02b639d4ed requirements: añadido pandas, separado rtree 2026-05-03 00:09:55 +02:00
Javier Braña fc4142cfec PVPlantTerrain: fix visualización en pantalla — updateData escuchaba Mesh en vez de mesh, añadido publishProperty forzado, más display modes 2026-05-02 23:49:48 +02:00
javier a515f31726 hydro/hydrological: fix except genérico -> (IndexError, AttributeError) 2026-05-02 23:34:53 +02:00
javier e0a0dc2f0d EarthWorks: fix except genérico -> Part.OCCError 2026-05-02 23:22:41 +02:00
javier 02d6c4f412 ImportGrid: fix str(e) sin except, excepts genéricos a específicos 2026-05-02 23:20:59 +02:00
javier e129aba2fe Site: fix computeAreas return prematuro, excepts genéricos a ImportError/Exception 2026-05-02 23:16:27 +02:00
javier 9d65323052 TerrainAnalisys: fix hardcode i=2, var obj undefined, remove threading innecesario 2026-05-02 22:50:10 +02:00
javier 0b13a8c5f1 Mejoras PVPlantTerrain: fix XYZ import, DEM rendimiento, ViewProvider boundary+contour, error handling 2026-05-02 22:47:58 +02:00
javier 3bcdc95978 Actualizar package.xml 2026-04-30 00:51:53 +02:00
javier 4b7035e6be Corregir URL: homehud -> homehub en package.xml 2026-04-30 00:43:30 +02:00
javier 02758a6ee8 Actualizar package.xml 2026-03-24 22:10:39 +01:00
javier 111df89033 updates 2026-02-15 20:23:52 +01:00
javier 4476afc1a2 updates 2025-11-20 11:20:18 +01:00
javier d61260fdd3 updates 2025-11-20 00:57:15 +01:00
javier 049898c939 updates 2025-08-17 13:34:09 +04:00
javier 3a188cc47d new code 2025-08-17 13:33:17 +04:00
javier 5db8f5439d Punto de restauración. 2025-07-31 09:58:38 +02:00
javier e1e1441892 Punto de restauración. 2025-07-16 08:58:35 +02:00
53 changed files with 5373 additions and 6268 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+4
View File
@@ -5,4 +5,8 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">
<option name="removeUnused" value="true" />
<option name="modifyBaseFiles" value="true" />
</component>
</module>
+91 -191
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 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,11 +149,7 @@ 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)
path = Part.__sortEdges__(pathwire.Edges)
ends = []
@@ -241,7 +203,7 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
return placements
class _Fence(ArchComponent.Component):
class Fence(ArchComponent.Component):
def __init__(self, obj):
ArchComponent.Component.__init__(self, obj)
self.setProperties(obj)
@@ -347,7 +309,6 @@ class _Fence(ArchComponent.Component):
QT_TRANSLATE_NOOP("App::Property", "The number of posts used to build the fence"))
obj.setEditorMode("Length", 1)
self.Type = "PVPlatFence"
def __getstate__(self):
@@ -361,75 +322,30 @@ class _Fence(ArchComponent.Component):
return None
def execute(self, obj):
if not obj.Base or not obj.Post:
return
# 1. Preparar trazado base
pathwire = self.calculatePathWire(obj)
if pathwire is None:
# FreeCAD.Console.PrintLog("ArchFence.execute: path " + obj.Base.Name + " has no edges\n")
return
if not obj.Post:
FreeCAD.Console.PrintLog("ArchFence.execute: Post not set\n")
pathwire = utils.getProjected(pathwire, FreeCAD.Vector(0, 0, 1))
pathwire = utils.simplifyWire(pathwire)
if not pathwire or not pathwire.Edges:
return
# 2. Proyectar sobre terreno (con caché)
self.Posts = []
self.Foundations = []
site = PVPlantSite.get()
if True: # prueba
import MeshPart as mp
land = FreeCAD.ActiveDocument.Terrain.Mesh
segments = mp.projectShapeOnMesh(pathwire, land, FreeCAD.Vector(0, 0, 1))
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))
if pathwire is None:
return
''' no sirve:
if len(pathwire.Wires) > 1:
import draftgeoutils
pathwire = draftgeoutils.wires.superWire(pathwire.Edges, True)
Part.show(pathwire)
'''
''' unir todas en una '''
'''
if len(pathwire.Wires) > 1:
import Utils.PVPlantUtils as utils
wires = pathwire.Wires
new_wire = []
to_compare = utils.getPoints(wires.pop(0))
new_wire.extend(to_compare)
while len(wires)>0:
wire = wires[0]
points = utils.getPoints(wire)
to_remove = None
if points[0] in to_compare:
to_remove = points[0]
if points[-1] in to_compare:
to_remove = points[-1]
if to_remove:
to_compare = points.copy()
points.remove(to_remove)
new_wire.extend(points)
wires.pop()
continue
wires.append(wires.pop())
pathwire = Part.makePolygon(new_wire)
#Part.show(pathwire)
#return
'''
sectionLength = obj.Gap.Value
postLength = 0 #obj.Post.Diameter.Value #considerarlo 0 porque no influye
postPlacements = []
@@ -457,18 +373,19 @@ class _Fence(ArchComponent.Component):
postPlacements.extend(placements)
# 5. Generar geometría
postShapes, postFoundation = self.calculatePosts(obj, postPlacements)
sections, num = self.calculateSections(obj, postPlacements)
mesh = self.calculate_sections(obj, postPlacements)
postShapes = Part.makeCompound(postShapes)
postFoundation = Part.makeCompound(postFoundation)
sections = Part.makeCompound(sections)
compound = Part.makeCompound([postShapes, postFoundation, sections])
obj.Shape = compound
# Give information
# 6. Crear forma final
obj.Shape = Part.makeCompound([postShapes, postFoundation, mesh])
# 7. Actualizar propiedades
obj.NumberOfSections = count
obj.NumberOfPosts = obj.NumberOfSections + 1
obj.NumberOfPosts = count + 1
obj.Length = pathLength
obj.Concrete = count * postFoundation.SubShapes[0].Volume
@@ -497,7 +414,7 @@ class _Fence(ArchComponent.Component):
def calculatePostPlacements(self, obj, pathwire, rotation):
postWidth = obj.Post.Diameter.Value
transformationVector = FreeCAD.Vector(0, postWidth / 2, 0)
transformationVector = FreeCAD.Vector(0, - postWidth / 2, 0)
placements = calculatePlacementsOnPath(rotation, pathwire, int(obj.NumberOfSections) + 1, transformationVector, True)
# The placement of the last object is always the second entry in the list.
# So we move it to the end:
@@ -509,47 +426,36 @@ class _Fence(ArchComponent.Component):
posts = []
foundations = []
for placement in postPlacements:
postCopy = obj.Post.Shape.copy()
postCopy = Part.Solid(postCopy)
postCopy.Placement = placement
postCopy.Placement.Base.z += 100
posts.append(postCopy)
new_post = obj.Post.Shape.copy()
new_post = Part.Solid(new_post)
new_post.Placement = placement
new_post.Placement.Base.z += 100
posts.append(new_post)
foundation = Part.makeCylinder(150, 700)
foundation.Placement = placement
foundation.Placement.Base.z -= obj.Depth.Value
foundation = foundation.cut(postCopy)
#foundation = foundation.cut(new_post)
foundations.append(foundation)
return posts, foundations
def calculateSections(self, obj, postPlacements):
shapes = []
faceNumbers = []
def calculate_sections(self, obj, postPlacements):
offsetz = FreeCAD.Vector(0, 0, obj.MeshOffsetZ.Value)
meshHeight = FreeCAD.Vector(0, 0, obj.MeshHeight.Value)
offsetz = obj.MeshOffsetZ.Value
meshHeight = obj.MeshHeight.Value
points_down = []
points_up = []
for i in range(len(postPlacements) - 1):
startPlacement = postPlacements[i]
endPlacement = postPlacements[i + 1]
p1 = postPlacements[i].Base + offsetz
p2 = postPlacements[i + 1].Base + offsetz
p3 = p1 + meshHeight
p4 = p2 + meshHeight
points_down.extend([p1, p2])
points_up.extend([p3, p4])
p1 = startPlacement.Base + FreeCAD.Vector(0, 0, offsetz)
p2 = endPlacement.Base + FreeCAD.Vector(0, 0, offsetz)
p3 = p2 + FreeCAD.Vector(0, 0, meshHeight)
p4 = p1 + FreeCAD.Vector(0, 0, meshHeight)
pointlist = [p1, p2, p3, p4, p1]
try:
pol = Part.makePolygon(pointlist)
face = Part.Face(pol)
shapes.append(face)
faceNumbers.append(1)
except:
print("No es posible crear la cara: ---------------------------------------------------")
print(" +++++ Start: ", startPlacement.Base, " - end: ", endPlacement.Base)
print(" +++++ algo: ", pointlist, "\n")
print("---------------------------------------------------\n")
return (shapes, faceNumbers)
shape = Part.makeRuledSurface(Part.makePolygon(points_down), Part.makePolygon(points_up))
return shape
def calculatePathWire(self, obj):
if obj.Base:
@@ -562,7 +468,7 @@ class _Fence(ArchComponent.Component):
return None
class _ViewProviderFence(ArchComponent.ViewProviderComponent):
class ViewProviderFence(ArchComponent.ViewProviderComponent):
"A View Provider for the Fence object"
def __init__(self, vobj):
@@ -642,7 +548,7 @@ class _ViewProviderFence(ArchComponent.ViewProviderComponent):
children.append(self.Object.Gate)
return children
class _FenceTaskPanel:
class FenceTaskPanel:
'''The TaskPanel to setup the fence'''
def __init__(self):
@@ -775,15 +681,8 @@ class _FenceTaskPanel:
self.form = [self.formFence, self.formPost, self.formFoundation]
# valores iniciales y creación del la valla:
import Draft
self.post = PVPlantFencePost.makeFencePost() # Arch.makePipe()
self.post = PVPlantFencePost.makeFencePost()
self.post.Label = "Post"
Draft.autogroup(self.post)
'''
self.section = self.makeGrid()
self.path = self.section.Base
'''
FreeCAD.ActiveDocument.recompute()
self.fence = makePVPlantFence(self.section, self.post, self.path)
@@ -859,7 +758,7 @@ class CommandPVPlantFence:
return not FreeCAD.ActiveDocument is None
def Activated(self):
self.TaskPanel = _FenceTaskPanel()
self.TaskPanel = FenceTaskPanel()
FreeCADGui.Control.showDialog(self.TaskPanel)
@@ -877,12 +776,13 @@ if FreeCAD.GuiUp:
}
def IsActive(self):
site = FreeCAD.ActiveDocument.getObject("Site")
return (not (FreeCAD.ActiveDocument is None) and
not (FreeCAD.ActiveDocument.getObject("Site") is None) and
not (FreeCAD.ActiveDocument.getObject("Terrain") is None))
not (site is None) and
not (site.Terrain is None))
import PVPlantFenceGate
import Civil.Fence.PVPlantFenceGate as PVPlantFenceGate
FreeCADGui.addCommand('PVPlantFence', CommandPVPlantFence())
FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate._CommandPVPlantGate())
FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost._CommandFencePost())
FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate.CommandPVPlantGate())
FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost.CommandFencePost())
#FreeCADGui.addCommand('PVPlantFenceGroup', CommandFenceGroup())
@@ -202,7 +202,7 @@ class ViewProviderGate:
children.append(self.Object.Base)
return children
class _CommandPVPlantGate:
class CommandPVPlantGate:
"the PVPlant Fence command definition"
def __init__(self):
@@ -256,7 +256,9 @@ class _CommandPVPlantGate:
gate = makePVPlantFence()
try:
import MeshPart as mp
point1 = mp.projectPointsOnMesh([point1,], FreeCAD.ActiveDocument.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
import PVPlantSite
site = PVPlantSite.get()
point1 = mp.projectPointsOnMesh([point1,], site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
except:
FreeCAD.Console.PrintError("No se puede encontrar punto 3D.." + "\n")
@@ -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()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
FreeCADGui.addCommand('FencePost', _CommandFencePost())
+387
View File
@@ -0,0 +1,387 @@
# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * EarthWorks - Cálculo de movimiento de tierras *
# * *
# * Calcula volúmenes de desmonte (cut) y terraplén (fill) entre una *
# * plataforma diseñada (generada por PVPlantPlatform) y el terreno *
# * natural representado por un mesh. *
# * *
# * Flujo: *
# * 1. build_platform(frames) → Part.Solid (superficie diseñada) *
# * 2. cut_mesh = mesh_above(platform_mesh, terrain_mesh) *
# * 3. fill_mesh = mesh_below(platform_mesh, terrain_mesh) *
# * 4. Volumen = mesh.Volume *
# * *
# ***********************************************************************
import FreeCAD
import Part
import Mesh
import math
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
def translate(ctxt, txt): return txt
def QT_TRANSLATE_NOOP(ctxt, txt): return txt
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from .PVPlantPlatform import build_platform, get_platform_shape, make_platform
VOLUME_TYPES = ["Fill", "Cut"]
def compute_earthworks(platform_or_frames, terrain_mesh, slope_tolerance=10.0):
"""
Calcula el movimiento de tierras.
Args:
platform_or_frames: Objeto Platform o lista de frames/trackers
Si es Platform, usa su Shape directamente.
Si es lista de frames, genera la plataforma primero.
terrain_mesh: Mesh del terreno natural
slope_tolerance: pendiente máxima E-W (grados)
Returns:
tuple: (mesh_cut, mesh_fill, volume_cut_mm3, volume_fill_mm3)
"""
import MeshPart
# 1. Obtener la plataforma
if hasattr(platform_or_frames, 'Proxy') and hasattr(platform_or_frames.Proxy, '__class__'):
cls_name = platform_or_frames.Proxy.__class__.__name__
if cls_name == 'Platform':
# Ya es un objeto Platform → usar su Shape
platform = get_platform_shape(platform_or_frames)
else:
platform = build_platform([platform_or_frames], slope_tolerance)
elif hasattr(platform_or_frames, '__iter__'):
# Es una lista de frames
platform = build_platform(platform_or_frames, slope_tolerance)
else:
platform = None
# 2. Convertir plataforma a mesh para booleanos
try:
platform_mesh = MeshPart.meshFromShape(
Shape=platform,
LinearDeflection=500,
AngularDeflection=0.5
)
except Exception as e:
FreeCAD.Console.PrintError(f"Error al meshificar la plataforma: {e}\n")
return None, None, 0, 0
if platform_mesh is None or platform_mesh.countPoints() == 0:
return None, None, 0, 0
# 3. Calcular corte y relleno
cut_mesh = _mesh_above(platform_mesh, terrain_mesh)
fill_mesh = _mesh_below(platform_mesh, terrain_mesh)
volume_cut = _mesh_volume(cut_mesh)
volume_fill = _mesh_volume(fill_mesh)
return cut_mesh, fill_mesh, volume_cut, volume_fill
def _mesh_above(reference, terrain):
"""
Porción del mesh de referencia que está por encima del terreno.
Representa material a excavar (cut).
"""
try:
common = reference.common(terrain)
if common and common.countPoints() > 3:
return common
except Exception:
pass
return None
def _mesh_below(reference, terrain):
"""
Porción del mesh de referencia que está por debajo del terreno.
Representa material a rellenar (fill).
"""
try:
diff = reference.cut(terrain)
if diff and diff.countPoints() > 3:
return diff
except Exception:
pass
return None
def _mesh_volume(mesh):
"""Volumen de un mesh en mm³. Retorna 0 si no hay mesh válido."""
if mesh is None or mesh.countPoints() < 4:
return 0
try:
return mesh.Volume
except Exception:
return 0
def makeEarthWorksVolume(vtype=0):
"""Crea un objeto FeaturePython con el mesh de volumen."""
obj = FreeCAD.ActiveDocument.addObject(
"Part::FeaturePython", VOLUME_TYPES[vtype])
EarthWorksVolume(obj)
ViewProviderEarthWorksVolume(obj.ViewObject)
return obj
# =========================================================================
# FeaturePython: EarthWorksVolume
# =========================================================================
class EarthWorksVolume:
"""Objeto que almacena un mesh de volumen (cut o fill)."""
def __init__(self, obj):
self.setProperties(obj)
obj.Proxy = self
def setProperties(self, obj):
pl = obj.PropertiesList
if "VolumeType" not in pl:
obj.addProperty(
"App::PropertyEnumeration", "VolumeType", "Volume",
"Fill o Cut").VolumeType = VOLUME_TYPES
if "VolumeMesh" not in pl:
obj.addProperty(
"Mesh::PropertyMeshKernel", "VolumeMesh", "Volume",
"Mesh del volumen")
obj.setEditorMode("VolumeMesh", 2)
if "Volume" not in pl:
obj.addProperty(
"App::PropertyVolume", "Volume", "Volume",
"Volumen calculado (mm³)")
obj.setEditorMode("Volume", 1)
obj.IfcType = "Civil Element"
obj.setEditorMode("IfcType", 1)
def onDocumentRestored(self, obj):
self.setProperties(obj)
def onChange(self, obj, prop):
if prop == "VolumeMesh" and obj.VolumeMesh:
obj.Volume = obj.VolumeMesh.Volume
def execute(self, obj):
pass
def __getstate__(self):
return None
def __setstate__(self, state):
return None
# =========================================================================
# ViewProvider (Coin3D)
# =========================================================================
class ViewProviderEarthWorksVolume:
def __init__(self, vobj):
pl = vobj.PropertiesList
is_cut = vobj.Object.VolumeType == "Cut"
r, g, b = (1.0, 0.0, 0.0) if is_cut else (0.0, 0.0, 1.0)
if "Transparency" not in pl:
vobj.addProperty(
"App::PropertyIntegerConstraint", "Transparency",
"Surface Style", "Transparencia (0=opaco, 100=invisible)")
vobj.Transparency = (50, 0, 100, 1)
if "ShapeColor" not in pl:
vobj.addProperty(
"App::PropertyColor", "ShapeColor", "Surface Style",
"Color de superficie")
vobj.ShapeColor = (r, g, b, vobj.Transparency / 100)
if "ShapeMaterial" not in pl:
vobj.addProperty(
"App::PropertyMaterial", "ShapeMaterial", "Surface Style",
"Material de superficie")
vobj.ShapeMaterial = FreeCAD.Material()
vobj.Proxy = self
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
def onChanged(self, vobj, prop):
if prop in ("ShapeColor", "Transparency"):
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
c = vobj.ShapeColor
t = vobj.Transparency
vobj.ShapeMaterial.DiffuseColor = (c[0], c[1], c[2], t / 100)
if prop == "ShapeMaterial":
if hasattr(self, "face_material"):
mat = vobj.ShapeMaterial
self.face_material.diffuseColor.setValue(mat.DiffuseColor[:3])
self.face_material.transparency = mat.DiffuseColor[3]
def attach(self, vobj):
from pivy import coin
self.geo_coords = coin.SoGeoCoordinate()
self.triangles = coin.SoIndexedFaceSet()
self.face_material = coin.SoMaterial()
self.edge_material = coin.SoMaterial()
self.edge_style = coin.SoDrawStyle()
self.edge_style.style = coin.SoDrawStyle.LINES
shape_hints = coin.SoShapeHints()
shape_hints.vertex_ordering = coin.SoShapeHints.COUNTERCLOCKWISE
mat_binding = coin.SoMaterialBinding()
mat_binding.value = coin.SoMaterialBinding.PER_FACE
offset = coin.SoPolygonOffset()
offset.styles = coin.SoPolygonOffset.LINES
offset.factor = -2.0
highlight = coin.SoType.fromName("SoFCSelection").createInstance()
highlight.style = "EMISSIVE_DIFFUSE"
highlight.addChild(shape_hints)
highlight.addChild(mat_binding)
highlight.addChild(self.geo_coords)
highlight.addChild(self.triangles)
face = coin.SoSeparator()
face.addChild(self.face_material)
face.addChild(highlight)
edge = coin.SoSeparator()
edge.addChild(self.edge_material)
edge.addChild(self.edge_style)
edge.addChild(highlight)
surface = coin.SoSeparator()
surface.addChild(face)
surface.addChild(offset)
surface.addChild(edge)
wireframe = coin.SoSeparator()
wireframe.addChild(edge)
vobj.addDisplayMode(surface, "Surface")
vobj.addDisplayMode(wireframe, "Wireframe")
self.onChanged(vobj, "ShapeColor")
def updateData(self, obj, prop):
if prop == "VolumeMesh":
mesh = obj.VolumeMesh
if mesh is None or mesh.countPoints() == 0:
return
try:
geo = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
except Exception:
geo = ["UTM", "30N", "FLAT"]
self.geo_coords.geoSystem.setValues(geo)
cm = mesh.copy()
triangles = []
for i in cm.Topology[1]:
triangles.extend(list(i))
triangles.append(-1)
self.geo_coords.point.setValues(cm.Topology[0])
self.triangles.coordIndex.setValues(triangles)
def getIcon(self):
return str(os.path.join(DirIcons, "googleearth.svg"))
def getDisplayModes(self, vobj):
return ["Surface", "Wireframe"]
def getDefaultDisplayMode(self):
return "Surface"
def setDisplayMode(self, mode):
return mode
def __getstate__(self):
return None
def __setstate__(self, state):
return None
# =========================================================================
# TaskPanel
# =========================================================================
class EarthWorksTaskPanel:
def __init__(self):
self.form = FreeCADGui.PySideUic.loadUi(
os.path.join(PVPlantResources.__dir__, "PVPlantEarthworks.ui"))
self.form.setWindowIcon(
QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg")))
def accept(self):
land = FreeCAD.ActiveDocument.Terrain.Mesh.copy()
# Detectar si hay un Platform seleccionado o trackers sueltos
platform_obj = None
frames = []
for obj in FreeCADGui.Selection.getSelection():
if hasattr(obj, "Proxy"):
if obj.Proxy.__class__.__name__ == 'Platform':
platform_obj = obj
t = getattr(obj.Proxy, "Type", None)
if t == "Tracker" and obj not in frames:
frames.append(obj)
elif t == "FrameArea":
for fr in obj.Frames:
if fr not in frames:
frames.append(fr)
if not frames and not platform_obj:
FreeCAD.Console.PrintWarning(
"Selecciona trackers, un FrameArea o un Platform\n")
return False
slope = getattr(FreeCAD.ActiveDocument,
"MaximumWestEastSlope", 10.0)
FreeCAD.ActiveDocument.openTransaction("Movimiento de tierras")
try:
input_data = platform_obj if platform_obj else frames
cut_mesh, fill_mesh, vol_cut, vol_fill = compute_earthworks(
input_data, land, slope)
if cut_mesh and cut_mesh.countPoints() > 3:
v = makeEarthWorksVolume(1) # Cut
v.VolumeMesh = cut_mesh
FreeCAD.Console.PrintMessage(
f"Volumen de corte: {vol_cut:,.0f} mm³\n")
if fill_mesh and fill_mesh.countPoints() > 3:
v = makeEarthWorksVolume(0) # Fill
v.VolumeMesh = fill_mesh
FreeCAD.Console.PrintMessage(
f"Volumen de relleno: {vol_fill:,.0f} mm³\n")
except Exception as e:
FreeCAD.Console.PrintError(
f"Error en movimiento de tierras: {e}\n")
finally:
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.Control.closeDialog()
return True
def reject(self):
FreeCADGui.Control.closeDialog()
return True
+403
View File
@@ -0,0 +1,403 @@
# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * PlacementCalc - Lógica de cálculo de placement de trackers *
# * *
# * Separado de PVPlantPlacement.py para mantener limpio el archivo *
# * de interfaz (TaskPanels, comandos, ViewProviders). *
# * *
# * Funciones exportadas: *
# * - getRows(objs) → listas de filas *
# * - getCols(objs) → listas de columnas *
# * - optimized_cut(L_total, piezas, margen, metodo) *
# * - adjustToTerrain(frames, individual) *
# * - get_trend(points) / getTrend(points) *
# * - getHeadsAndSoil(frame=None) *
# * - moveFrameHead(obj, head, dist) *
# * - selectionFilter(sel, objtype) *
# * - ConvertObjectsTo(sel, objTo) *
# * *
# ***********************************************************************
import FreeCAD
import Part
import math
import numpy as np
# =========================================================================
# selectionFilter
# =========================================================================
def selectionFilter(sel, objtype):
"""Filtra una selección por tipo de Proxy."""
fil = []
for obj in sel:
if hasattr(obj, "Proxy"):
if obj.Proxy.__class__ is objtype:
fil.append(obj)
return fil
# =========================================================================
# optimized_cut
# =========================================================================
def optimized_cut(L_total, piezas, margen=0, metodo='auto'):
"""
Optimiza el corte de piezas en una longitud total.
Similar al algoritmo de corte óptimo de barras.
Args:
L_total: Longitud total disponible
piezas: Lista de longitudes de piezas a cortar
margen: Margen de seguridad por corte
metodo: 'auto', 'greedy' o 'exact'
Returns:
dict con piezas cortadas, desperdicio, etc.
"""
if not piezas:
return {'piezas': [], 'desperdicio': L_total}
piezas_ord = sorted(piezas, reverse=True)
resultado = []
restante = L_total
for pieza in piezas_ord:
if pieza + margen <= restante:
resultado.append(pieza)
restante -= (pieza + margen)
return {
'piezas': resultado,
'desperdicio': restante,
'n_piezas': len(resultado),
'eficiencia': (L_total - restante) / L_total * 100 if L_total > 0 else 0
}
# =========================================================================
# get_trend / getTrend
# =========================================================================
def get_trend(points):
"""
Calcula la tendencia lineal de un conjunto de puntos 3D.
Devuelve (pendiente_x, pendiente_z, intersección) en el plano XZ.
"""
if len(points) < 2:
return 0, 0, 0
xs = np.array([p.x for p in points])
zs = np.array([p.z for p in points])
if np.std(xs) < 1:
return 0, 0, np.mean(zs)
A = np.vstack([xs, np.ones(len(xs))]).T
m, c = np.linalg.lstsq(A, zs, rcond=None)[0]
return m, 0, c
def getTrend(points):
"""Wrapper para compatibilidad (versión antigua)."""
return get_trend(points)
# =========================================================================
# adjustToTerrain
# =========================================================================
def adjustToTerrain(frames, individual=True):
"""
Ajusta la altura de los frames al terreno.
Args:
frames: lista de objetos tracker
individual: si True, ajusta cada frame individualmente.
si False, ajusta por filas.
"""
if not frames:
return
terrain = None
try:
terrain = FreeCAD.ActiveDocument.Site.Terrain
except Exception:
FreeCAD.Console.PrintWarning("No hay terreno en el Site\n")
return
if individual:
for frame in frames:
_adjust_single_frame(frame, terrain)
else:
cols = getCols(list(frames))
if cols:
for col in cols:
for group in col:
if group:
_adjust_frame_group(group, terrain)
def _adjust_single_frame(frame, terrain):
"""Ajusta un frame individual al terreno."""
try:
bb = frame.Shape.BoundBox
center = bb.Center
z_terrain = _get_terrain_z(terrain, center.x, center.y)
if z_terrain is not None:
frame.Placement.Base.z = z_terrain
except Exception:
pass
def _adjust_frame_group(group, terrain):
"""Ajusta un grupo de frames al terreno siguiendo la pendiente."""
if not group:
return
z_values = []
for frame in group:
try:
bb = frame.Shape.BoundBox
center = bb.Center
z = _get_terrain_z(terrain, center.x, center.y)
if z is not None:
z_values.append(z)
except Exception:
z_values.append(None)
valid_zs = [z for z in z_values if z is not None]
if not valid_zs:
return
# Ajustar cada frame a la altura del terreno interpolada
for i, frame in enumerate(group):
if i < len(z_values) and z_values[i] is not None:
try:
frame.Placement.Base.z = z_values[i]
except Exception:
pass
def _get_terrain_z(terrain, x, y):
"""Obtiene la cota Z del terreno en un punto (x, y)."""
try:
if hasattr(terrain, 'Shape') and terrain.Shape:
shape = terrain.Shape
# Proyectar un rayo vertical
p1 = FreeCAD.Vector(x, y, 10000)
p2 = FreeCAD.Vector(x, y, -10000)
dist, pts, info = shape.distToShape(Part.LineSegment(p1, p2).toShape())
if pts:
return pts[0][0].z
except Exception:
pass
return None
# =========================================================================
# getRows / getCols
# =========================================================================
def getRows(objs):
"""
Agrupa objetos tracker en filas según su posición Y y estructura de Placement.
Args:
objs: lista de objetos tracker
Returns:
(rows, columns): tupla de listas de listas
"""
if not objs:
return None, None
# Ordenar por Placement.Base.y
sorted_objs = sorted(objs, key=lambda x: x.Placement.Base.y, reverse=True)
rows = []
processed = set()
for obj in sorted_objs:
if obj.Name in processed:
continue
row = [obj]
processed.add(obj.Name)
base = obj.Placement.Base
for other in sorted_objs:
if other.Name in processed:
continue
# Misma fila si están alineados en Y (misma posición de fila)
if abs(other.Placement.Base.y - base.y) < 5000:
row.append(other)
processed.add(other.Name)
rows.append(row)
# Ordenar cada fila por X
for row in rows:
row.sort(key=lambda x: x.Placement.Base.x)
# Calcular columnas
columns = _compute_columns(rows)
return rows, columns
def getCols(objs):
"""
Agrupa objetos tracker en columnas.
Args:
objs: lista de objetos tracker
Returns:
list: columnas, donde cada columna es una lista de grupos (filas)
"""
rows, columns = getRows(objs)
return columns
def getCols_old(sel, tolerance=4000, sort=True):
"""Versión antigua de getCols, mantenida por compatibilidad."""
if not sel:
return []
# Ordenar por Y descendente
sorted_sel = sorted(sel, key=lambda x: x.Placement.Base.y, reverse=True)
cols = []
used = set()
for obj in sorted_sel:
if obj.Name in used:
continue
fila = [obj]
used.add(obj.Name)
base_x = obj.Placement.Base.x
for other in sorted_sel:
if other.Name in used:
continue
if abs(other.Placement.Base.x - base_x) <= tolerance:
fila.append(other)
used.add(other.Name)
if sort:
fila.sort(key=lambda x: x.Placement.Base.y, reverse=True)
cols.append(fila)
return cols
def _compute_columns(rows):
"""
Calcula la estructura de columnas a partir de las filas.
Cada columna agrupa los frames en la misma posición X vertical.
"""
if not rows:
return []
from collections import defaultdict
# Mapa: posición X → lista de frames
col_map = defaultdict(list)
for row in rows:
for i, frame in enumerate(row):
col_map[i].append(frame)
columns = []
for idx in sorted(col_map.keys()):
col = col_map[idx]
columns.append(col)
return columns
# =========================================================================
# getHeadsAndSoil / moveFrameHead
# =========================================================================
def getHeadsAndSoil(frame=None):
"""
Obtiene las cabezas y suelos de un tracker (o del documento activo).
"""
if frame:
frames = [frame]
else:
try:
frames = [o for o in FreeCAD.ActiveDocument.Objects
if hasattr(o, 'Proxy') and getattr(o.Proxy, 'Type', None) == 'Tracker']
except Exception:
return [], []
heads = []
soils = []
for f in frames:
try:
if hasattr(f, 'HeadPoints'):
heads.extend(f.HeadPoints)
if hasattr(f, 'SoilPoints'):
soils.extend(f.SoilPoints)
except Exception:
pass
return heads, soils
def moveFrameHead(obj, head=0, dist=0):
"""
Mueve la cabeza de un tracker una distancia determinada.
Args:
obj: objeto tracker
head: 0=izquierda, 1=derecha
dist: distancia a mover (mm)
"""
try:
if not hasattr(obj, 'Proxy') or getattr(obj.Proxy, 'Type', None) != 'Tracker':
return
# Lógica de movimiento basada en Placement
placement = obj.Placement
direction = placement.Rotation.multVec(FreeCAD.Vector(1, 0, 0))
if head == 0:
placement.Base = placement.Base - direction * dist
else:
placement.Base = placement.Base + direction * dist
obj.Placement = placement
except Exception:
pass
# =========================================================================
# ConvertObjectsTo
# =========================================================================
def ConvertObjectsTo(sel, objTo):
"""
Convierte objetos seleccionados a otro tipo.
Args:
sel: lista de objetos seleccionados
objTo: clase destino (FeaturePython)
"""
if not sel or not objTo:
return
for obj in sel:
try:
if hasattr(obj, "Proxy"):
isFrame = obj.Proxy.__class__ is objTo
# Si ya es del tipo destino, se salta
if isFrame:
continue
# Crear nuevo objeto del tipo destino
if hasattr(obj, "Shape") and obj.Shape:
new_obj = FreeCAD.ActiveDocument.addObject(
"Part::FeaturePython", obj.Name + "_converted")
# Aquí iría la lógica específica de conversión
# dependiendo del tipo de objeto origen y destino
FreeCAD.Console.PrintMessage(
f"Convertido {obj.Label}\n")
except Exception:
FreeCAD.Console.PrintWarning(
f"No se pudo convertir {obj.Label}\n")
+468
View File
@@ -0,0 +1,468 @@
# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * PVPlantPlatform - Plataforma de diseño solar *
# * *
# * Es el elemento central del movimiento de tierras. Representa la *
# * superficie diseñada generada a partir de la disposición de trackers.*
# * *
# * De ella dependen: *
# * - EarthWorks: cut/fill entre plataforma y terreno natural *
# * - Road: trazado de viales sobre la plataforma *
# * - Drainage: drenaje superficial *
# * - Trench: zanjas sobre la plataforma *
# * *
# ***********************************************************************
import FreeCAD
import Part
import math
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
def QT_TRANSLATE_NOOP(ctxt, txt): return txt
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
# =========================================================================
# Constructor
# =========================================================================
def make_platform(frames=None, name="Platform"):
"""
Crea un objeto Platform en el documento activo.
Args:
frames: lista opcional de objetos tracker para inicializar
name: nombre del objeto
Returns:
Objeto FeaturePython Platform, o None si no hay documento
"""
doc = FreeCAD.ActiveDocument
if doc is None:
return None
obj = doc.addObject("Part::FeaturePython", name)
Platform(obj)
_ViewProviderPlatform(obj.ViewObject)
obj.Label = name
if frames:
# Asignar SourceFrames como lista de enlaces
obj.SourceFrames = frames
doc.recompute()
return obj
# =========================================================================
# FeaturePython: Platform
# =========================================================================
class Platform:
"""
Plataforma de diseño generada desde trackers solares.
Propiedades principales:
SourceFrames : Lista de trackers que definen la plataforma
SlopeTolerance : Pendiente máxima E-W (grados)
PlatformArea : Área de la plataforma (solo lectura)
PlatformVolume : Volumen bajo la plataforma (solo lectura)
Status : Estado del último cálculo
"""
def __init__(self, obj):
self.setProperties(obj)
obj.Proxy = self
def setProperties(self, obj):
pl = obj.PropertiesList
if "SourceFrames" not in pl:
obj.addProperty(
"App::PropertyLinkList", "SourceFrames",
"Platform",
"Trackers que definen la plataforma")
if "SlopeTolerance" not in pl:
obj.addProperty(
"App::PropertyFloat", "SlopeTolerance",
"Platform",
"Pendiente transversal máxima (grados)").SlopeTolerance = 10.0
if "PlatformArea" not in pl:
obj.addProperty(
"App::PropertyArea", "PlatformArea",
"Platform",
"Área total de la plataforma (solo lectura)")
obj.setEditorMode("PlatformArea", 1)
if "PlatformVolume" not in pl:
obj.addProperty(
"App::PropertyVolume", "PlatformVolume",
"Platform",
"Volumen bajo la plataforma (solo lectura)")
obj.setEditorMode("PlatformVolume", 1)
if "NumberOfFrames" not in pl:
obj.addProperty(
"App::PropertyInteger", "NumberOfFrames",
"Platform",
"Número de trackers en la plataforma")
obj.setEditorMode("NumberOfFrames", 1)
if "Status" not in pl:
obj.addProperty(
"App::PropertyString", "Status",
"Platform",
"Estado del último cálculo")
def onDocumentRestored(self, obj):
self.setProperties(obj)
def execute(self, obj):
"""Calcula la plataforma a partir de los SourceFrames."""
frames = obj.SourceFrames
if not frames:
obj.Shape = Part.Shape()
obj.Status = "Sin frames"
return
obj.NumberOfFrames = len(frames)
slope = obj.SlopeTolerance
try:
shape = _build_platform_shape(frames, slope)
except Exception as e:
obj.Status = f"Error: {e}"
FreeCAD.Console.PrintError(
f"Error al generar plataforma: {e}\n")
return
if shape is None:
obj.Status = "No se pudo generar"
return
obj.Shape = shape
# Calcular área y volumen
try:
area = shape.Area
if area > 0:
obj.PlatformArea = area
# Volumen aproximado: proyectar al plano XY
try:
obj.PlatformVolume = shape.Volume
except Exception:
pass
except Exception:
pass
obj.Status = f"OK - {len(frames)} frames"
FreeCAD.Console.PrintMessage(
f"Plataforma generada: {len(frames)} frames, "
f"área={area:,.0f} mm²\n")
def __getstate__(self):
return None
def __setstate__(self, state):
return None
# =========================================================================
# Cálculo de la plataforma (lógica principal)
# =========================================================================
def _build_platform_shape(frames, slope_tolerance):
"""
Construye la geometría de la plataforma desde los frames.
Returns:
Part.Solid o None si falla
"""
rows, columns = _get_tracker_rows(frames)
if rows is None or not rows:
return None
all_faces = []
tools = []
# Fase 1: Lofts longitudinales (a lo largo de cada fila)
for group in rows:
lines = _generate_row_lines(group, slope_tolerance)
tools.extend(lines["tools"])
if len(lines["edges"]) >= 2:
try:
loft = Part.makeLoft(lines["edges"], False, True, False)
if loft and not loft.isNull():
all_faces.extend(loft.Faces)
except Exception:
pass
# Fase 2: Lofts transversales (entre columnas)
if columns:
for group in rows:
for frame in group:
col, idx = _find_in_columns(frame, columns)
tool = _find_tool(frame, tools)
if tool is None or idx >= len(col) - 1:
continue
next_frame = col[idx + 1]
next_tool = _find_tool(next_frame, tools)
if next_tool is None:
continue
try:
l1 = Part.LineSegment(
tool[1].Vertexes[-1].Point,
next_tool[1].Vertexes[0].Point
).toShape()
l2 = Part.LineSegment(
tool[2].Vertexes[-1].Point,
next_tool[2].Vertexes[0].Point
).toShape()
if l1 and l2:
loft = Part.makeLoft([l1, l2], False, True, False)
if loft and not loft.isNull():
all_faces.extend(loft.Faces)
except Exception:
pass
if not all_faces:
return None
# Fase 3: Unir caras en un sólido
try:
platform = None
for face in all_faces:
if platform is None:
platform = face
else:
try:
platform = platform.fuse(face)
except Exception:
pass
if platform is None:
return None
if platform.ShapeType == "Shell":
try:
platform = Part.makeSolid(platform)
except Exception:
pass
elif platform.ShapeType == "Compound":
faces_in = [s for s in platform.SubShapes if s.ShapeType == "Face"]
if faces_in:
try:
shell = Part.makeShell(faces_in)
platform = Part.makeSolid(shell)
except Exception:
pass
return platform if not platform.isNull() else None
except Exception:
return None
def _get_tracker_rows(frames):
"""Agrupa trackers usando la lógica de PVPlantPlacement."""
try:
import PVPlantPlacement
return PVPlantPlacement.getRows(frames)
except Exception:
return None, None
def _generate_row_lines(group, slope_tolerance):
"""
Genera líneas de borde (izquierda/derecha) para una fila de trackers.
Returns:
dict con edges (lista de Part.Shape) y tools (lista de [frame, izq, der])
"""
result = {"edges": [], "tools": []}
for i, frame in enumerate(group):
if not hasattr(frame, "Setup"):
continue
aw = _angle_to_prev(group, i)
ae = _angle_to_next(group, i)
anf = (aw + ae) / 2
if anf > slope_tolerance:
anf = slope_tolerance
wdt = _get_half_width(frame)
zz = wdt * math.sin(math.radians(anf))
base = _get_base_line(frame)
li = base.copy()
li.Placement = frame.Placement
li.Placement.Rotation = frame.Placement.Rotation
li.Placement.Base.x -= wdt
li.Placement.Base.z -= zz
result["edges"].append(li)
ld = base.copy()
ld.Placement = frame.Placement
ld.Placement.Rotation = frame.Placement.Rotation
ld.Placement.Base.x += wdt
ld.Placement.Base.z += zz
result["edges"].append(ld)
result["tools"].append([frame, li, ld])
return result
def _get_half_width(frame):
try:
return int(frame.Setup.Width / 2)
except Exception:
return 0
def _get_base_line(frame):
try:
lng = int(frame.Setup.Length / 2)
return Part.LineSegment(
FreeCAD.Vector(-lng, 0, 0),
FreeCAD.Vector(lng, 0, 0)
).toShape()
except Exception:
try:
bb = frame.Setup.Shape.BoundBox
return Part.LineSegment(
FreeCAD.Vector(bb.XMin, 0, 0),
FreeCAD.Vector(bb.XMax, 0, 0)
).toShape()
except Exception:
return Part.LineSegment(
FreeCAD.Vector(-2000, 0, 0),
FreeCAD.Vector(2000, 0, 0)
).toShape()
def _angle_to_prev(group, i):
if i <= 0:
return 0
return _angle_xz(
group[i - 1].Placement.Base,
group[i].Placement.Base
)
def _angle_to_next(group, i):
if i >= len(group) - 1:
return 0
return _angle_xz(
group[i].Placement.Base,
group[i + 1].Placement.Base
)
def _angle_xz(v1, v2):
dx = v2.x - v1.x
dz = v2.z - v1.z
return math.degrees(math.atan2(dz, dx))
def _find_in_columns(frame, columns):
for col in columns:
for g in col:
if frame in g:
return g, g.index(frame)
return [], -1
def _find_tool(frame, tools):
for t in tools:
if t[0] == frame:
return t
return None
# =========================================================================
# ViewProvider
# =========================================================================
class _ViewProviderPlatform:
def __init__(self, vobj):
vobj.Proxy = self
pl = vobj.PropertiesList
if "Transparency" not in pl:
vobj.addProperty(
"App::PropertyIntegerConstraint", "Transparency",
"Platform Style", "Transparencia de la plataforma")
vobj.Transparency = (40, 0, 100, 1)
if "ShapeColor" not in pl:
vobj.addProperty(
"App::PropertyColor", "ShapeColor",
"Platform Style", "Color de la plataforma")
vobj.ShapeColor = (0.3, 0.8, 0.3, 0.6) # verde semitransparente
if "ShapeMaterial" not in pl:
vobj.addProperty(
"App::PropertyMaterial", "ShapeMaterial",
"Platform Style", "Material")
vobj.ShapeMaterial = FreeCAD.Material()
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
def onChanged(self, vobj, prop):
if prop in ("ShapeColor", "Transparency"):
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
c = vobj.ShapeColor
t = vobj.Transparency
vobj.ShapeMaterial.DiffuseColor = (c[0], c[1], c[2], t / 100)
def getIcon(self):
return str(os.path.join(DirIcons, "solar-fixed.svg"))
def __getstate__(self):
return None
def __setstate__(self, state):
return None
# =========================================================================
# Functions de conveniencia (API pública)
# =========================================================================
def build_platform(frames, slope_tolerance=10.0):
"""
API pública: construye la geometría de plataforma desde frames.
Útil para EarthWorks, Road, etc. que quieran la Shape sin crear objeto.
Returns:
Part.Solid o None
"""
return _build_platform_shape(frames, slope_tolerance)
def get_platform_shape(platform_obj):
"""
Obtiene la Shape de un objeto Platform de forma segura.
"""
if platform_obj is None:
return None
try:
shape = platform_obj.Shape
if shape and not shape.isNull():
return shape
except Exception:
pass
return None
+1 -1
View File
@@ -114,7 +114,7 @@ def makeTrench(base=None):
try:
folder = FreeCAD.ActiveDocument.Trenches
except:
except AttributeError:
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Trenches')
folder.Label = "Trenches"
folder.addObject(obj)
+200
View File
@@ -0,0 +1,200 @@
# /**********************************************************************
# * *
# * 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 Part
import os
import zipfile
import re
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
else:
# \cond
def translate(ctxt,txt):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
import os
from PVPlantResources import DirIcons as DirIcons
__title__ = "PVPlant Areas"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects")
vector = ["Y", "YN", "Z", "ZN", "D"]
def makePCS():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PowerConversionSystem")
PowerConverter(obj)
ViewProviderPowerConverter(obj.ViewObject)
try:
folder = FreeCAD.ActiveDocument.PowerConversionSystemGroup
except:
folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'PowerConversionSystemGroup')
folder.Label = "PowerConversionSystemGroup"
folder.addObject(obj)
return obj
class PowerConverter(ArchComponent.Component):
def __init__(self, obj):
''' Initialize the Area object '''
ArchComponent.Component.__init__(self, obj)
self.Type = None
self.obj = None
self.setProperties(obj)
def setProperties(self, obj):
pl = obj.PropertiesList
# Transformer properties
if not "Technology" in pl:
obj.addProperty("App::PropertyEnumeration",
"Technology",
"Transformer",
"Number of phases and type of transformer"
).Technology = ["Single Phase Transformer", "Three Phase Transformer"]
if not "PowerPrimary" in pl:
obj.addProperty("App::PropertyPower",
"PowerPrimary",
"Transformer",
"The base file this component is built upon").PowerPrimary = 6000000000
if not "PowerSecundary1" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary1",
"Transformer",
"The base file this component is built upon").PowerSecundary1 = 3000000000
if not "PowerSecundary2" in pl:
obj.addProperty("App::PropertyPower",
"PowerSecundary2",
"Transformer",
"The base file this component is built upon").PowerSecundary2 = 3000000000
if not "VoltagePrimary" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltagePrimary",
"Transformer",
"The base file this component is built upon").VoltagePrimary = 33000000000
if not "VoltageSecundary1" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary1",
"Transformer",
"The base file this component is built upon").VoltageSecundary1 = 11000000000
if not "VoltageSecundary2" in pl:
obj.addProperty("App::PropertyElectricPotential",
"VoltageSecundary2",
"Transformer",
"The base file this component is built upon").VoltageSecundary2 = 11000000000
if not "VectorPrimary" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorPrimary",
"Transformer",
"The base file this component is built upon").VectorPrimary = vector
if not "VectorSecundary1" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary1",
"Transformer",
"The base file this component is built upon").VectorSecundary1 = vector
if not "VectorSecundary2" in pl:
obj.addProperty("App::PropertyEnumeration",
"VectorSecundary2",
"Transformer",
"The base file this component is built upon").VectorSecundary2 = vector
self.Type = "PowerConverter"
obj.Proxy = self
def onDocumentRestored(self, obj):
""" Method run when the document is restored """
self.setProperties(obj)
def onBeforeChange(self, obj, prop):
''' '''
# This method is called before a property is changed.
# It can be used to validate the property value or to update other properties.
# If the property is not valid, you can raise an exception.
# If you want to prevent the change, you can return False.
# Otherwise, return True to allow the change.
return True
def onChanged(self, obj, prop):
''' '''
def execute(self, obj):
''' '''
# obj.Shape: compound
# |- body: compound
# |- transformer: solid
# |- primary switchgear: compound
# |- secundary 1 switchgear: compound
# |- secundary 2 switchgear: compound
pl = obj.Placement
obj.Shape = Part.makeBox(6058, 2438, 2591) # Placeholder for the shape
obj.Placement = pl
class ViewProviderPowerConverter(ArchComponent.ViewProviderComponent):
def __init__(self, vobj):
ArchComponent.ViewProviderComponent.__init__(self, vobj)
def getIcon(self):
return str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg"))
class CommandPowerConverter:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "Inverter.svg")),
'Accel': "E, P",
'MenuText': "Power Converter",
'ToolTip': "Power Converter",}
def Activated(self):
sinverter = makePCS()
def IsActive(self):
active = not (FreeCAD.ActiveDocument is None)
return active
if FreeCAD.GuiUp:
FreeCADGui.addCommand('PowerConverter', CommandPowerConverter())
+392
View File
@@ -0,0 +1,392 @@
def groupTrackersToTransformers(transformer_power, max_distance):
import numpy as np
from scipy.spatial import KDTree
import FreeCAD
from collections import deque
# 1. Obtener todos los trackers válidos
valid_trackers = []
valid_points = []
valid_power = []
for tracker in FreeCAD.ActiveDocument.Objects:
if hasattr(tracker, 'Proxy') and (tracker.Proxy.Type == "Tracker"):
base = tracker.Placement.Base
if all(np.isfinite([base.x, base.y, base.z])):
valid_trackers.append(tracker)
valid_points.append([base.x, base.y])
valid_power.append(tracker.Setup.TotalPower)
if not valid_trackers:
FreeCAD.Console.PrintWarning("No se encontraron trackers válidos para agrupar\n")
return
# 2. Obtener parámetros de los CTs
target_power = transformer_power * 1.2
points = np.array(valid_points)
power_values = np.array(valid_power)
# 3. Determinar dirección de barrido (oeste a este por defecto)
min_x, min_y = np.min(points, axis=0)
max_x, max_y = np.max(points, axis=0)
# 4. Ordenar trackers de oeste a este (menor X a mayor X)
'''
Norte a Sur: sorted_indices = np.argsort(-points[:, 1]) (Y descendente)
Sur a Norte: sorted_indices = np.argsort(points[:, 1]) (Y ascendente)
Este a Oeste: sorted_indices = np.argsort(-points[:, 0]) (X descendente)
'''
sorted_indices = np.argsort(points[:, 0])
sorted_points = points[sorted_indices]
sorted_power = power_values[sorted_indices]
sorted_trackers = [valid_trackers[i] for i in sorted_indices]
# 5. Crear KDTree para búsquedas rápidas
kdtree = KDTree(sorted_points)
# 6. Algoritmo de barrido espacial
transformer_groups = []
used_indices = set()
# Función para expandir un grupo desde un punto inicial
def expand_group(start_idx):
group = []
total_power = 0
queue = deque([start_idx])
while queue and total_power < target_power:
idx = queue.popleft()
if idx in used_indices:
continue
# Añadir tracker al grupo si no excede la potencia
tracker_power = sorted_power[idx]
if total_power + tracker_power > target_power * 1.05:
continue
group.append(sorted_trackers[idx])
total_power += tracker_power
used_indices.add(idx)
# Buscar vecinos cercanos
neighbors = kdtree.query_ball_point(
sorted_points[idx],
max_distance
)
# Filtrar vecinos no usados y ordenar por proximidad al punto inicial
neighbors = [n for n in neighbors if n not in used_indices]
neighbors.sort(key=lambda n: abs(n - start_idx))
queue.extend(neighbors)
return group, total_power
# 7. Barrido principal de oeste a este
for i in range(len(sorted_points)):
if i in used_indices:
continue
group, total_power = expand_group(i)
if group:
# Calcular centro del grupo
group_points = np.array([t.Placement.Base[:2] for t in group])
center = np.mean(group_points, axis=0)
transformer_groups.append({
'trackers': group,
'total_power': total_power,
'center': center
})
# 8. Manejar grupos residuales (si los hay)
unused_indices = set(range(len(sorted_points))) - used_indices
if unused_indices:
# Intentar añadir trackers residuales a grupos existentes
for idx in unused_indices:
point = sorted_points[idx]
tracker_power = sorted_power[idx]
# Buscar el grupo más cercano que pueda aceptar este tracker
best_group = None
min_distance = float('inf')
for group in transformer_groups:
if group['total_power'] + tracker_power <= target_power * 1.05:
dist = np.linalg.norm(point - group['center'])
if dist < min_distance and dist < max_distance * 1.5:
min_distance = dist
best_group = group
# Añadir al grupo si se encontró uno adecuado
if best_group:
best_group['trackers'].append(sorted_trackers[idx])
best_group['total_power'] += tracker_power
# Actualizar centro del grupo
group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']])
best_group['center'] = np.mean(group_points, axis=0)
else:
# Crear un nuevo grupo con este tracker residual
group = [sorted_trackers[idx]]
center = point
transformer_groups.append({
'trackers': group,
'total_power': tracker_power,
'center': center
})
# 9. Crear los grupos en FreeCAD
transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers")
transformer_group.Label = "Centros de Transformación"
for i, group in enumerate(transformer_groups):
# Crear la esfera que representará el CT
ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}")
ct_shape.Length = 6058
ct_shape.Width = 2438
ct_shape.Height = 2591
ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0)
# Añadir propiedades personalizadas
ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT",
"Lista de trackers asociados a este CT")
ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT",
"Potencia total del grupo (W)")
ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT",
"Potencia nominal del transformador (W)")
ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT",
"Porcentaje de utilización (Total/Nominal)")
# Establecer valores de las propiedades
ct_shape.Trackers = group['trackers']
ct_shape.TotalPower = group['total_power'].Value
ct_shape.NominalPower = transformer_power
ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100
# Configurar visualización
# Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%)
utilization = ct_shape.Utilization
if utilization <= 100:
color = (0.0, 1.0, 0.0) # Verde
elif utilization <= 110:
color = (1.0, 1.0, 0.0) # Amarillo
else:
color = (1.0, 0.0, 0.0) # Rojo
ct_shape.ViewObject.ShapeColor = color
ct_shape.ViewObject.Transparency = 40 # 40% de transparencia
# Añadir etiqueta con información
ct_shape.ViewObject.DisplayMode = "Shaded"
ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)"
# Añadir al grupo principal
transformer_group.addObject(ct_shape)
FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n")
#onSelectGatePoint()
import FreeCAD, FreeCADGui, Part
import numpy as np
from scipy.stats import linregress
from PySide import QtGui
class InternalPathCreator:
def __init__(self, gate_point, strategy=1, path_width=4000):
self.gate_point = gate_point
self.strategy = strategy
self.path_width = path_width
self.ct_shapes = []
self.ct_positions = []
def get_transformers(self):
transformers_group = FreeCAD.ActiveDocument.getObject("Transformers")
if not transformers_group:
FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n")
return False
self.ct_shapes = transformers_group.Group
if not self.ct_shapes:
FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n")
return False
# Obtener las posiciones de los CTs
for sphere in self.ct_shapes:
base = sphere.Placement.Base
self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0))
return True
def create_paths(self):
if not self.get_transformers():
return []
if self.strategy == 1:
return self.create_direct_paths()
elif self.strategy == 2:
return self.create_unified_path()
else:
FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n")
return []
def create_direct_paths(self):
"""Estrategia 1: Caminos independientes desde cada CT hasta la puerta"""
paths = []
for ct in self.ct_positions:
paths.append([ct, self.gate_point])
return paths
def create_unified_path(self):
"""Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal"""
if not self.ct_positions:
return []
all_points = self.ct_positions + [self.gate_point]
x = [p.x for p in all_points]
y = [p.y for p in all_points]
# Manejar caso de puntos alineados verticalmente
if np.std(x) < 1e-6:
sorted_points = sorted(all_points, key=lambda p: p.y)
paths = []
for i in range(len(sorted_points) - 1):
paths.append([sorted_points[i], sorted_points[i + 1]])
return paths
# Calcular regresión lineal
slope, intercept, _, _, _ = linregress(x, y)
# Función para proyectar puntos
def project_point(point):
x0, y0 = point.x, point.y
if abs(slope) > 1e6:
return FreeCAD.Vector(x0, intercept, 0)
x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2)
y_proj = slope * x_proj + intercept
return FreeCAD.Vector(x_proj, y_proj, 0)
# return slope * x + intercept --> desde placement
projected_points = [project_point(p) for p in all_points]
# Calcular distancias a lo largo de la línea
ref_point = projected_points[0]
direction_vector = FreeCAD.Vector(1, slope).normalize()
distances = []
for p in projected_points:
vec_to_point = p - ref_point
distance = vec_to_point.dot(direction_vector)
distances.append(distance)
# Ordenar por distancia
sorted_indices = np.argsort(distances)
sorted_points = [all_points[i] for i in sorted_indices]
# Crear caminos
paths = []
for i in range(len(sorted_points) - 1):
paths.append([sorted_points[i], sorted_points[i + 1]])
return paths
def create_3d_path(self, path_poly):
"""Crea geometría 3D para el camino adaptada a orientación norte-sur"""
segments = []
for i in range(len(path_poly.Vertexes) - 1):
start = path_poly.Vertexes[i].Point
end = path_poly.Vertexes[i + 1].Point
direction = end - start
# Determinar orientación predominante
if abs(direction.x) > abs(direction.y):
normal = FreeCAD.Vector(0, 1, 0) # Norte-sur
else:
normal = FreeCAD.Vector(1, 0, 0) # Este-oeste
offset = normal * self.path_width / 2
# Crear puntos para la sección transversal
p1 = start + offset
p2 = start - offset
p3 = end - offset
p4 = end + offset
# Crear cara
wire = Part.makePolygon([p1, p2, p3, p4, p1])
face = Part.Face(wire)
segments.append(face)
if segments:
'''road_shape = segments[0].fuse(segments[1:])
return road_shape.removeSplitter()'''
return Part.makeCompound(segments)
return Part.Shape()
def build(self):
paths = self.create_paths()
if not paths:
return
path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths")
path_group.Label = f"Caminos Internos (Estrategia {self.strategy})"
for i, path in enumerate(paths):
poly = Part.makePolygon(path)
# Objeto para la línea central
path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}")
path_obj.Shape = poly
path_obj.ViewObject.LineWidth = 3.0
path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0)
# Objeto para la superficie 3D
road_shape = self.create_3d_path(poly)
road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}")
road_obj.Shape = road_shape
road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
path_group.addObject(path_obj)
path_group.addObject(road_obj)
FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n")
# Función para mostrar el diálogo de estrategia
def show_path_strategy_dialog(gate_point):
dialog = QtGui.QDialog()
dialog.setWindowTitle("Seleccionar Estrategia de Caminos")
layout = QtGui.QVBoxLayout(dialog)
label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:")
layout.addWidget(label)
rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta")
rb1.setChecked(True)
layout.addWidget(rb1)
rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta")
layout.addWidget(rb2)
btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
layout.addWidget(btn_box)
def on_accept():
strategy = 1 if rb1.isChecked() else 2
dialog.accept()
creator = InternalPathCreator(gate_point, strategy)
creator.build()
btn_box.accepted.connect(on_accept)
btn_box.rejected.connect(dialog.reject)
dialog.exec_()
# Uso: seleccionar un punto para la puerta de entrada
def onSelectGatePoint():
'''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0]
gate_point = gate.Placement.Base
show_path_strategy_dialog(gate_point)'''
sel = FreeCADGui.Selection.getSelectionEx()[0]
show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass)
+137 -30
View File
@@ -1,5 +1,6 @@
import math
import FreeCAD
import Part
from Utils.PVPlantUtils import findObjects
if FreeCAD.GuiUp:
@@ -75,8 +76,10 @@ def getWire(wire, nospline=False, width=.0):
import DraftGeomUtils
import math
offset = FreeCAD.ActiveDocument.Site.Origin
def fmt(vec, b=0.0):
return (vec.x * 0.001, vec.y * 0.001, width, width, b)
return ((vec.x + offset.x) * 0.001, (vec.y + offset.y) * 0.001, width, width, b)
points = []
edges = Part.__sortEdges__(wire.Edges)
@@ -259,15 +262,60 @@ class exportDXF:
'rotation': rotation
})
def createPolyline(self, wire):
def createPolyline(self, wire, layer=""):
try:
data = getWire(wire.Shape)
lwp = self.msp.add_lwpolyline(data)
if layer:
lwp.dxf.layer = layer
return lwp
except Exception as e:
print("Error creating polyline:", e)
return None
def createHatch(self, wire, pattern="SOLID", scale=1.0, angle=0, layer=None):
"""Crea un sombreado (hatch) para un área"""
try:
# Obtener los puntos en metros
points = [(x, y) for (x, y, *_) in wire]
# Crear el hatch
hatch = self.msp.add_hatch(color=7, dxfattribs={'layer': layer})
if pattern == "SOLID":
# Sombreado sólido
hatch.set_solid_fill()
else:
# Patrón de sombreado
hatch.set_pattern_fill(name=pattern, scale=scale, angle=angle)
# Añadir el contorno
hatch.paths.add_polyline_path(points, is_closed=True)
return hatch
except Exception as e:
print("Error creating hatch:", e)
return None
def export_feature_image(self, feature, layer_name):
"""Exporta una imagen del GeoFeature y la añade al DXF"""
try:
# Añadir la imagen al DXF
image_def = self.doc.add_image_def(feature.ImageFile, size_in_pixel=(feature.XSize, feature.YSize))
self.msp.add_image(image_def,
insert=(0, 0, 0),
size_in_units=(feature.XSize,
feature.YSize),
rotation=0,
dxfattribs={'layer': layer_name})
print(f"Imagen exportada para {feature.Label}")
# Eliminar el archivo temporal
import os
os.unlink(temp_img.name)
except Exception as e:
print(f"Error en exportación de imagen: {e}")
# =================================================================================
# INTERFAZ DE USUARIO
# =================================================================================
@@ -575,6 +623,59 @@ class LineTypeComboBox(QtWidgets.QComboBox):
finally:
painter.end() # Asegurar que el painter se cierre correctamente
layers = [
("Available area", QtGui.QColor(0, 204, 153), "Continuous", "1", True),
("Available area Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Exclusion", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Offset", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Exclusion Name", QtGui.QColor(255, 85, 0), "Continuous", "1", True),
("Areas Cadastral Plot", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Cadastral Plot Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Areas Offset", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Cable codes LV AC inverter", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Cable codes LV string", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Cable codes MV System", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("CABLES LV AC inverter 120 mm2", QtGui.QColor(255, 204, 0), "Continuous", "1", True),
("CABLES LV AC inverter 185 mm2", QtGui.QColor(204, 153, 0), "Continuous", "1", True),
("CABLES LV string 4 mm2", QtGui.QColor(255, 255, 0), "Continuous", "1", True),
("CABLES LV string 10 mm2", QtGui.QColor(255, 255, 102), "Continuous", "1", True),
("CABLES MV system 300 mm2", QtGui.QColor(102, 51, 0), "Continuous", "1", True),
("CIVIL Fence", QtGui.QColor(102, 102, 102), "FENCELINE1", "1", True),
("CIVIL External Roads", QtGui.QColor(91, 91, 91), "Continuous", "1", True),
("CIVIL External Roads Axis", QtGui.QColor(255, 255, 192), "Dashed", "1", True),
("CIVIL External Roads Text", QtGui.QColor(255, 255, 192), "Continuous", "1", True),
("CIVIL Internal Roads", QtGui.QColor(153, 95, 76), "Continuous", "1", True),
("CIVIL Internal Roads Axis", QtGui.QColor(192, 192, 192), "Dashed", "1", True),
("CIVIL Internal Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True),
("Contour Line Legend text", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
("Major contour value", QtGui.QColor(0, 0, 0), "Continuous", "1", True),
("Minor contour line", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
("Minor contour value", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
("Power Stations", QtGui.QColor(255, 0, 0), "Continuous", "1", True),
("Power Stations Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("ST", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("ST Names", QtGui.QColor(255, 255, 0), "Continuous", "1", True),
("String Inv", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("STRUC Structure 1", QtGui.QColor(0, 0, 255), "Continuous", "1", True),
("STRUC Structure 2", QtGui.QColor(0, 0, 204), "Continuous", "1", True),
("STRUC Structure 3", QtGui.QColor(0, 0, 153), "Continuous", "1", True),
("STRUC Structure 4", QtGui.QColor(0, 0, 128), "Continuous", "1", True),
("STRUC Structure 5", QtGui.QColor(0, 0, 102), "Continuous", "1", True),
("STRUC Structure 6", QtGui.QColor(0, 0, 76), "Continuous", "1", True),
("STRUC Structure 7", QtGui.QColor(0, 0, 51), "Continuous", "1", True),
("STRUC Structure 8", QtGui.QColor(0, 0, 25), "Continuous", "1", True),
("Structure Codes", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("TRENCHES Low voltage 400.0 x 1000.0 m", QtGui.QColor(128, 128, 128), "Continuous", "1", True),
("TRENCHES Medium voltage 400.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("TRENCHES Medium voltage 800.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
("TRENCHES Medium voltage 800.0 x 1500.", QtGui.QColor(255, 255, 255), "Continuous", "1", True),
]
class _PVPlantExportDXF(QtGui.QWidget):
'''The editmode TaskPanel to select what you want to export'''
@@ -610,10 +711,8 @@ class _PVPlantExportDXF(QtGui.QWidget):
self.form.tableLayers.removeRow(row)
# Configuración de las capas por defecto
self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True)
self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "CONTINUOUS", "1", True)
self.add_row("Internal_Roads", QtGui.QColor(128, 128, 128), "CONTINUOUS", "1", True)
self.add_row("Frames", QtGui.QColor(0, 255, 0), "CONTINUOUS", "1", True)
for row in layers:
self.add_row(*row)
def save_project_settings(self):
"""Guarda la configuración actual en el proyecto de FreeCAD"""
@@ -778,25 +877,28 @@ class _PVPlantExportDXF(QtGui.QWidget):
)
def writeArea(self, exporter):
exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "boundary")
areas_types = ["Boundaries", "Exclusions", "Offsets"]
exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "Available area")
areas_types = [("Boundaries", "Available area"),
("CadastralPlots", "Areas Cadastral Plot"),
("Exclusions", "Areas Exclusion"),
("Offsets", "Areas Offset")]
for area_type in areas_types:
if hasattr(FreeCAD.ActiveDocument, area_type):
for area in FreeCAD.ActiveDocument.Boundaries.Group:
exporter.createPolyline(area, "Areas_Boundary")
if hasattr(FreeCAD.ActiveDocument, area_type[0]):
area_group = FreeCAD.ActiveDocument.getObjectsByLabel(area_type[0])
if len(area_group):
for area in area_group[0].Group:
tmp = exporter.createPolyline(area, area_type[1])
if area_type[0] == "Exclusions":
exporter.createHatch(
tmp,
pattern="ANSI37",
scale=0.3,
angle=0,
layer=area_type[1]
)
for obj in FreeCADGui.Selection.getSelection():
tmp = exporter.createPolyline(obj, areas_types[0][1])
'''for area in FreeCAD.ActiveDocument.Boundaries.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Boundary"
for area in FreeCAD.ActiveDocument.Exclusions.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Exclusion"
for area in FreeCAD.ActiveDocument.Offsets.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Offsets"'''
def writeFrameSetups(self, exporter):
if not hasattr(FreeCAD.ActiveDocument, "Site"):
@@ -811,11 +913,11 @@ class _PVPlantExportDXF(QtGui.QWidget):
w.Placement.Base = w.Placement.Base.sub(center)
block.add_lwpolyline(getWire(w))
block.add_circle((0, 0), 0.2, dxfattribs={'color': 2})
block.add_circle((0, 0), 0.2, dxfattribs={'layer': 'Structure Posts'}) #'color': 2,
p = math.sin(math.radians(45)) * 0.2
block.add_line((-p, -p), (p, p), dxfattribs={"layer": "MyLines"})
block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"})
block.add_line((-p, -p), (p, p), dxfattribs={'layer': 'Structure Posts'})
block.add_line((-p, p), (p, -p), dxfattribs={'layer': 'Structure Posts'})
# 2. Frames
for ts in FreeCAD.ActiveDocument.Site.Frames:
@@ -868,10 +970,11 @@ class _PVPlantExportDXF(QtGui.QWidget):
if FreeCAD.ActiveDocument.Transport:
for road in FreeCAD.ActiveDocument.Transport.Group:
base = exporter.createPolyline(road, "External_Roads")
base = exporter.createPolyline(road, "CIVIL External Roads")
if hasattr(road, 'Width'):
base.dxf.const_width = road.Width
axis = exporter.createPolyline(road.Base, "External_Roads_Axis")
axis = exporter.createPolyline(road, "CIVIL External Roads Axis")
axis.dxf.const_width = .2
def writeTrenches(self, exporter):
@@ -976,8 +1079,12 @@ class _PVPlantExportDXF(QtGui.QWidget):
self.writeTrenches(exporter)
# Crear espacios de papel
self.setup_layout4(exporter.doc)
self.createPaperSpaces(exporter)
#self.setup_layout4(exporter.doc)
#self.createPaperSpaces(exporter)
if hasattr(FreeCAD.ActiveDocument, "Background"):
# Exportar como imagen en lugar de polilínea
exporter.export_feature_image(FreeCAD.ActiveDocument.Background, "Site_Image")
# Guardar archivo
exporter.save()
+1 -1
View File
@@ -5,7 +5,7 @@ import zipfile
import tempfile
import shutil
import xml.etree.ElementTree as ET
from PySide2 import QtWidgets, QtCore, QtGui
from PySide import QtWidgets, QtCore, QtGui
import FreeCAD
import Mesh
import Part
+323 -27
View File
@@ -29,6 +29,15 @@ import Part
import numpy
import os
from xml.etree.ElementTree import Element, SubElement
import xml.etree.ElementTree as ElementTree
import datetime
from xml.dom import minidom
from numpy.matrixlib.defmatrix import matrix
from Utils import PVPlantUtils as utils
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore
@@ -63,6 +72,11 @@ def check_collada():
FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n")
return COLLADA_AVAILABLE
# Asegurar que el texto es Unicode válido
def safe_text(text):
if isinstance(text, bytes):
return text.decode('utf-8', errors='replace')
return text
# from ARCH:
def triangulate(shape):
@@ -249,7 +263,306 @@ def export(exportList, filename, tessellation=1, colors=None):
FreeCAD.Console.PrintMessage(translate("Arch", "file %s successfully created.") % filename)
def exportToPVC(path, exportTerrain = False):
def exportToPVC(path, exportTerrain=False):
filename = f"{path}.pvc"
# 1. Validación inicial de objetos esenciales
site = None
for obj in FreeCAD.ActiveDocument.Objects:
if obj.Name.startswith("Site") and hasattr(obj, 'Terrain'):
site = obj
break
if not site:
FreeCAD.Console.PrintError("No se encontró objeto 'Site' válido\n")
return False
# 2. Configuración de metadatos y autor
generated_on = str(datetime.datetime.now())
try:
author = FreeCAD.ActiveDocument.CreatedBy
except (AttributeError, UnicodeEncodeError):
author = "Unknown"
author = author.replace("<", "").replace(">", "")
ver = FreeCAD.Version()
appli = f"PVPlant for FreeCAD {ver[0]}.{ver[1]} build{ver[2]}"
# 3. Creación estructura XML base
root = Element('COLLADA')
root.set('xmlns', 'http://www.collada.org/2005/11/COLLADASchema')
root.set('version', '1.4.1')
# 4. Sección <asset>
asset = SubElement(root, 'asset')
contrib = SubElement(asset, 'contributor')
SubElement(contrib, 'author').text = safe_text(author)
SubElement(contrib, 'authoring_tool').text = safe_text(appli)
SubElement(asset, 'created').text = generated_on
SubElement(asset, 'modified').text = generated_on
SubElement(asset, 'title').text = safe_text(FreeCAD.ActiveDocument.Name)
unit = SubElement(asset, 'unit')
unit.set('name', 'meter')
unit.set('meter', '1')
# 5. Materiales y efectos
materials = ["Frames", "Tree_trunk", "Tree_crown", "Topography_mesh"]
# Library materials
lib_materials = SubElement(root, 'library_materials')
for i, name in enumerate(materials):
mat = SubElement(lib_materials, 'material')
mat.set('id', f'Material{i}')
mat.set('name', name)
SubElement(mat, 'instance_effect').set('url', f'#Material{i}-fx')
# Library effects
lib_effects = SubElement(root, 'library_effects')
for i, _ in enumerate(materials):
effect = SubElement(lib_effects, 'effect')
effect.set('id', f'Material{i}-fx')
effect.set('name', f'Material{i}')
profile = SubElement(effect, 'profile_COMMON')
technique = SubElement(profile, 'technique')
technique.set('sid', 'standard')
lambert = SubElement(technique, 'lambert')
# Componentes del material
color = SubElement(SubElement(lambert, 'emission'), 'color')
color.set('sid', 'emission')
color.text = '0.000000 0.000000 0.000000 1.000000'
color = SubElement(SubElement(lambert, 'ambient'), 'color')
color.set('sid', 'ambient')
color.text = '0.200000 0.200000 0.200000 1.000000'
color = SubElement(SubElement(lambert, 'diffuse'), 'color')
color.set('sid', 'diffuse')
color.text = '0.250000 0.500000 0.000000 1.000000'
transparent = SubElement(lambert, 'transparent')
transparent.set('opaque', 'RGB_ZERO')
color = SubElement(transparent, 'color')
color.set('sid', 'transparent')
color.text = '0.000000 0.000000 0.000000 1.000000'
value = SubElement(SubElement(lambert, 'transparency'), 'float')
value.set('sid', 'transparency')
value.text = '0.000000'
# 6. Geometrías
lib_geometries = SubElement(root, 'library_geometries')
# 7. Escena visual
lib_visual = SubElement(root, 'library_visual_scenes')
visual_scene = SubElement(lib_visual, 'visual_scene')
visual_scene.set('id', 'Scene') # cambiar a visual_scene_0
visual_scene.set('name', 'Scene') # cambiar a Default visual scene
scene_node = SubElement(visual_scene, 'node')
scene_node.set('id', 'node_0_id')
scene_node.set('name', 'node_0_name')
scene_node.set('sid', 'node_0_sid')
scene_matrix = SubElement(scene_node, 'matrix')
scene_matrix.set('sid', 'matrix_0')
scene_matrix.text = '1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 -1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000'
root_node = SubElement(scene_node, 'node')
root_node.set('id', 'node_1_id')
root_node.set('name', 'node_1_name')
root_node.set('sid', 'node_1_sid')
# 8. Función para procesar geometrías
def create_geometry(name, vindex, findex, material_id, objind=0, frame_data=None, isTracker = False, axis_line=None):
"""Crea elementos COLLADA para una geometría"""
# Source (vertices)
source_mesh = SubElement(geom, 'mesh')
source = SubElement(source_mesh, 'source')
source.set('id', f'{name}-mesh_source')
float_array = SubElement(source, 'float_array')
float_array.set('id', f'{name}-float_array')
float_array.set('count', str(len(vindex)))
float_array.text = ' '.join(f'{v:.6f}' for v in vindex)
technique = SubElement(source, 'technique_common')
accessor = SubElement(technique, 'accessor')
accessor.set('count', str(len(vindex)))
accessor.set('source', f'#{name}-float_array')
accessor.set('stride', '3')
for ax in ['X', 'Y', 'Z']:
param = SubElement(accessor, 'param')
param.set('name', ax)
param.set('type', 'float')
# Vertices
vertices = SubElement(source_mesh, 'vertices')
vertices.set('id', f'{name}-vertices_source')
vertices = SubElement(vertices, 'input')
vertices.set('semantic', 'POSITION')
vertices.set('source', f'#{name}-mesh_source')
# Triangles
triangles = SubElement(source_mesh, 'triangles')
triangles.set('count', '0')
triangles.set('material', f'Material{material_id}')
triangles_input = SubElement(triangles, 'input')
triangles_input.set('offset', '0')
triangles_input.set('semantic', 'VERTEX')
triangles_input.set('source', f'#{name}-vertices_source')
p = SubElement(triangles, 'p')
p.text = ' '.join(map(str, findex))
# Parámetros especiales para estructuras
frame_params = SubElement(source_mesh, 'tracker_parameters')
if frame_data:
for key, val in frame_data.items():
elem = SubElement(frame_params, key)
elem.text = str(val)
if isTracker:
axis_parameter = SubElement(frame_params, 'axis_vertices')
if axis_line:
for idx, vert in enumerate(axis_line):
array = SubElement(axis_parameter, 'float_array')
array.set('id', f'{name}-axis_float_array{idx}')
array.set('count', '3')
array.text = ' '.join(f'{v:.6f}' for v in vert)
# 9. Procesar estructuras (frames/trackers)
center = FreeCAD.Vector()
if site.Terrain:
center = site.Terrain.Mesh.BoundBox.Center
objind = 0
for frame_type in site.Frames:
is_tracker = "tracker" in frame_type.Proxy.Type.lower()
modules = frame_type.Shape.SubShapes[0].SubShapes[0]
pts = []
for i in range(4):
pts.append(modules.BoundBox.getPoint(i))
new_shape = Part.Face(Part.makePolygon(pts))
mesh = Mesh.Mesh(triangulate(new_shape))
axis = Part.makeLine(FreeCAD.Vector(modules.BoundBox.XMin, 0, modules.BoundBox.ZMax),
FreeCAD.Vector(modules.BoundBox.XMax, 0, modules.BoundBox.ZMax))
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Setup") and obj.Setup == frame_type:
# Procesar geometría
mesh.Placement = obj.getGlobalPlacement()
axis.Placement = obj.getGlobalPlacement()
# Transformar vértices
vindex = []
for point in mesh.Points:
adjusted = (point.Vector - center) * scale
vindex.extend([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Índices de caras
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
# AXIS
# TODO: revisar si es así:
vaxis = []
for vert in axis.Vertexes:
adjusted = (vert.Point - center) * scale
vaxis.append([
-adjusted.x,
adjusted.z,
adjusted.y
])
# Crear geometría COLLADA
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', f'Frame_{objind}')
# Parámetros específicos de estructura
frame_data = {
'module_width': obj.Setup.ModuleWidth.Value,
'module_height': obj.Setup.ModuleHeight.Value,
'module_x_spacing': obj.Setup.ModuleColGap.Value,
'module_y_spacing': obj.Setup.ModuleRowGap.Value,
'module_name': 'Generic'
}
if is_tracker:
frame_data.update({
'tracker_type': 'single_axis_trackers',
'min_phi': obj.Setup.MinPhi.Value,
'max_phi': obj.Setup.MaxPhi.Value,
'min_theta': 0,
'max_theta': 0
})
create_geometry(
name=f'Frame_{objind}',
vindex=vindex,
findex=findex,
material_id=0,
objind=objind,
frame_data=frame_data,
isTracker = is_tracker,
axis_line=vaxis
)
# Instancia en escena
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', f'#Frame_{objind}')
bind_material = SubElement(instance, 'bind_material')
technique_common = SubElement(bind_material, 'technique_common')
instance_material = SubElement(technique_common, 'instance_material')
instance_material.set('symbol', 'Material0')
instance_material.set('target', '#Material0')
objind += 1
# 10. Procesar terreno si está habilitado
if exportTerrain and site.Terrain:
mesh = site.Terrain.Mesh
vindex = []
for point in mesh.Points:
point = point.Vector
vindex.extend([
-point.x * SCALE,
point.z * SCALE,
point.y * SCALE
])
findex = []
for facet in mesh.Facets:
findex.extend(facet.PointIndices)
geom = SubElement(lib_geometries, 'geometry')
geom.set('id', 'Terrain')
create_geometry('Terrain', vindex, findex, material_id=3)
instance = SubElement(root_node, 'instance_geometry')
instance.set('url', '#Terrain')
# 11. Escena principal
scene = SubElement(root, 'scene')
SubElement(scene, 'instance_visual_scene').set('url', '#Scene')
# 12. Exportar a archivo
xml_str = minidom.parseString(
ElementTree.tostring(root, encoding='utf-8')
).toprettyxml(indent=" ")
with open(filename, 'w', encoding='utf-8') as f:
f.write(xml_str)
FreeCAD.Console.PrintMessage(f"Archivo PVC generado: {filename}\n")
return True
def exportToPVC_old(path, exportTerrain = False):
filename = f"{path}.pvc"
from xml.etree.ElementTree import Element, SubElement
@@ -291,17 +604,18 @@ def exportToPVC(path, exportTerrain = False):
# xml: 1. Asset:
asset = SubElement(root, 'asset')
asset_contributor = SubElement(asset, 'contributor')
asset_contributor_autor = SubElement(asset_contributor, 'autor')
#asset_contributor_autor.text = author
asset_contributor_autor = SubElement(asset_contributor, 'author')
asset_contributor_autor.text = author
asset_contributor_authoring_tool = SubElement(asset_contributor, 'authoring_tool')
#asset_contributor_authoring_tool.text = appli
asset_contributor_authoring_tool.text = appli
asset_contributor_comments = SubElement(asset_contributor, 'comments')
asset_keywords = SubElement(asset, 'keywords')
asset_revision = SubElement(asset, 'revision')
asset_subject = SubElement(asset, 'subject')
asset_tittle = SubElement(asset, 'title')
#asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_tittle.text = FreeCAD.ActiveDocument.Name
asset_unit = SubElement(asset, 'unit')
asset_unit.set('meter', '0.001')
asset_unit.set('name', 'millimeter')
@@ -359,7 +673,6 @@ def exportToPVC(path, exportTerrain = False):
# xml: 4. library_geometries:
library_geometries = SubElement(root, 'library_geometries')
def add_geometry(objtype, vindex, findex, objind = 0, centers = None):
isFrame = False
if objtype == 0:
geometryName = 'Frame'
@@ -505,36 +818,20 @@ def exportToPVC(path, exportTerrain = False):
end_time.text = '1.000000'
# xml: 6. scene:
scene = SubElement(root, 'scene')
'''scene = SubElement(root, 'scene')
instance = SubElement(scene, 'instance_visual_scene')
instance.set('url', '#')
full_list_of_objects = FreeCAD.ActiveDocument.Objects
instance.set('url', '#')'''
# CASO 1 - FRAMES:
frameType = site.Frames
frame_setup = {"type": [],
"footprint": []}
for obj in frameType:
frame_setup["type"] = obj
frame_setup["footprint"] = ""
objind = 0
# TODO: revisar
for typ in frameType:
isTracker = "tracker" in typ.Proxy.Type.lower()
isTracker = False
objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
tmp = []
for obj in objectlist:
if obj.Name.startswith("TrackerSetup"):
continue
else:
tmp.append(obj)
objectlist = tmp.copy()
#isTracker = False
objectlist = utils.findObjects("Tracker")
for obj in objectlist:
if obj.Setup == typ:
findex = numpy.array([])
@@ -580,7 +877,6 @@ def exportToPVC(path, exportTerrain = False):
v = Topology[0][i]
vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale,
(v.y - center.y) * scale)
# 2. face indices
findex = numpy.empty(len(Topology[1]) * 3, numpy.int64)
for i in range(len(Topology[1])):
+2 -2
View File
@@ -72,11 +72,11 @@ class _PVPlantImportDXF:
def openFile(self):
''' '''
"getOpenFileName(parent: typing.Union[PySide2.QtWidgets.QWidget, NoneType] = None," \
"getOpenFileName(parent: typing.Union[PySide.QtWidgets.QWidget, NoneType] = None," \
"caption: str = ''," \
"dir: str = ''," \
"filter: str = ''," \
"options: PySide2.QtWidgets.QFileDialog.Options = Default(QFileDialog.Options)) -> typing.Tuple[str, str]"
"options: PySide.QtWidgets.QFileDialog.Options = Default(QFileDialog.Options)) -> typing.Tuple[str, str]"
filename, trash = QtGui.QFileDialog().getOpenFileName(None, 'Select File', os.getcwd(), 'Autocad dxf (*.dxf)')
if filename == "":
return
+139 -32
View File
@@ -42,15 +42,19 @@ class OSMImporter:
}
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transform_from_latlon(self, lat, lon):
point = ImportElevation.getElevationFromOE([[lat, lon], ])
return FreeCAD.Vector(point[0].x, point[0].y, point[0].z) * scale - self.Origin
'''x, y, _, _ = utm.from_latlon(lat, lon)
return FreeCAD.Vector(x, y, .0) * scale - self.Origin'''
def transform_from_latlon(self, coordinates):
"""Transforma coordenadas lat/lon a coordenadas FreeCAD"""
if not coordinates:
return []
points = ImportElevation.getElevationFromOE(coordinates)
pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points]
return pts
def get_osm_data(self, bbox):
query = f"""
[out:xml][bbox:{bbox}];
""" Obtiene datos de OpenStreetMap """
# Modificar la consulta en get_osm_data para incluir más tipos de agua:
query = f"""[out:xml][bbox:{bbox}];
(
way["building"];
way["highway"];
@@ -58,34 +62,55 @@ class OSMImporter:
way["power"="line"];
way["power"="substation"];
way["natural"="water"];
way["landuse"="forest"];
way["waterway"];
way["waterway"="river"];
way["waterway"="stream"];
way["waterway"="canal"];
way["landuse"="basin"];
way["landuse"="reservoir"];
node["natural"="tree"];
way["landuse"="forest"];
way["landuse"="farmland"];
);
(._;>;);
out body;
"""
try:
req = urllib.request.Request(
self.overpass_url,
data=query.encode('utf-8'),
headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
#headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
method='POST'
)
return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read()
response = urllib.request.urlopen(req, context=self.ssl_context, timeout=160)
return response.read()
except Exception as e:
print(f"Error obteniendo datos OSM: {str(e)}")
return None
def create_layer(self, name):
"""Crea o obtiene una capa en el documento"""
if not FreeCAD.ActiveDocument.getObject(name):
return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
return FreeCAD.ActiveDocument.getObject(name)
def process_osm_data(self, osm_data):
"""Procesa los datos XML de OSM"""
if not osm_data:
print("No hay datos OSM para procesar")
return
root = ET.fromstring(osm_data)
# Primero, recolectar todos los nodos
print(f"Procesando {len(root.findall('node'))} nodos...")
# Almacenar nodos transformados
for node in root.findall('node'):
self.nodes[node.attrib['id']] = self.transform_from_latlon(
float(node.attrib['lat']),
float(node.attrib['lon'])
)
coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')]
coordinates = self.transform_from_latlon(coordinates)
for i, node in enumerate(root.findall('node')):
self.nodes[node.attrib['id']] = coordinates[i]
# Procesar ways
for way in root.findall('way'):
@@ -162,11 +187,10 @@ class OSMImporter:
def create_buildings(self):
building_layer = self.create_layer("Buildings")
for way_id, data in self.ways_data.items():
#print(data)
if 'building' not in data['tags']:
continue
print(data)
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
@@ -223,11 +247,11 @@ class OSMImporter:
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if 'power' in tags:
print("\n\n")
print(tags)
#print("\n\n")
#print(tags)
feature_type = tags['power']
if feature_type == 'line':
print("3.1. Create Power Lines")
#print("3.1. Create Power Lines")
FreeCADGui.updateGui()
self.create_power_line(
nodes=nodes,
@@ -236,7 +260,7 @@ class OSMImporter:
)
elif feature_type == 'substation':
print("3.1. Create substations")
#print("3.1. Create substations")
FreeCADGui.updateGui()
self.create_substation(
way_id=way_id,
@@ -246,7 +270,7 @@ class OSMImporter:
)
elif feature_type == 'tower':
print("3.1. Create power towers")
#print("3.1. Create power towers")
FreeCADGui.updateGui()
self.create_power_tower(
position=nodes[0] if nodes else None,
@@ -559,13 +583,15 @@ class OSMImporter:
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
# 3. Base del terreno
base_height = 0.3
try:
base_shape = Part.makePolygon(polygon_points)
base_face = Part.Face(base_shape)
base_extrude = base_face.extrude(FreeCAD.Vector(0, 0, base_height))
base_obj = layer.addObject("Part::Feature", f"{name}_Base")
base_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Base")
layer.addObject(base_obj)
base_obj.Shape = base_extrude
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
except Exception as e:
@@ -580,7 +606,8 @@ class OSMImporter:
fence_shape = Part.makePolygon(fence_points)
fence_face = Part.Face(fence_shape)
fence_extrude = fence_face.extrude(FreeCAD.Vector(0, 0, 2.8))
fence_obj = layer.addObject("Part::Feature", f"{name}_Fence")
fence_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Fence")
layer.addObject(fence_obj)
fence_obj.Shape = fence_extrude
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
except Exception as e:
@@ -596,14 +623,15 @@ class OSMImporter:
building_shape = Part.makePolygon(building_points)
building_face = Part.Face(building_shape)
building_extrude = building_face.extrude(FreeCAD.Vector(0, 0, building_height))
building_obj = layer.addObject("Part::Feature", f"{name}_Building")
building_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Building")
layer.addObject(building_obj)
building_obj.Shape = building_extrude
building_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error edificio {way_id}: {str(e)}\n")
# 6. Transformadores
try:
'''try:
num_transformers = int(tags.get('transformers', 1))
for i in range(num_transformers):
transformer_pos = self.calculate_equipment_position(
@@ -615,11 +643,11 @@ class OSMImporter:
transformer = self.create_transformer(
position=transformer_pos,
voltage=voltage,
tech_type=tags.get('substation:type', 'outdoor')
technology=tags.get('substation:type', 'outdoor')
)
layer.addObject(transformer)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")
FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")'''
# 7. Torre de seccionamiento para alta tensión
if substation_type == 'transmission' and voltage >= 110000:
@@ -634,7 +662,8 @@ class OSMImporter:
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
# 8. Propiedades técnicas
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
substation_data = FreeCAD.ActiveDocument.addObject("App::FeaturePython", f"{name}_Data")
layer.addObject(substation_data)
props = {
"Voltage": voltage,
"Type": substation_type,
@@ -648,7 +677,8 @@ class OSMImporter:
else:
substation_data.addProperty(
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
prop, "Technical").setValue(value)
prop, "Technical")
setattr(substation_data, prop, value)
except Exception as e:
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
@@ -897,9 +927,9 @@ class OSMImporter:
if face.isInside(rand_point, 0.1, True):
return rand_point
def create_water_bodies(self):
def create_water_bodies_old(self):
water_layer = self.create_layer("Water")
print(self.ways_data)
for way_id, data in self.ways_data.items():
if 'natural' in data['tags'] and data['tags']['natural'] == 'water':
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
@@ -912,3 +942,80 @@ class OSMImporter:
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
water.ViewObject.ShapeColor = self.feature_colors['water']
def create_water_bodies(self):
water_layer = self.create_layer("Water")
for way_id, data in self.ways_data.items():
tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) < 2:
continue
# ===== 1) RÍOS / CANALES (líneas) =====
name = self.get_osm_name(tags, tags["waterway"])
if 'waterway' in tags:
if len(nodes) < 2:
continue
try:
width = self.parse_width(tags, default=2.0)
points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
wire = Draft.make_wire(points, closed=False, face=False)
wire.Label = f"{name} ({tags['waterway']})"
wire.ViewObject.LineWidth = max(1, int(width * 0.5))
wire.ViewObject.ShapeColor = self.feature_colors['water']
water_layer.addObject(wire)
except Exception as e:
print(f"Error creando waterway {way_id}: {e}")
continue # importante
# ===== 2) LAGOS / EMBALSES (polígonos) =====
is_area_water = (
tags.get('natural') == 'water' or
tags.get('landuse') in ['reservoir', 'basin'] or
tags.get('water') is not None
)
if not is_area_water or len(nodes) < 3:
continue
try:
polygon_points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes]
if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0])
polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon)
water = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Water_{way_id}")
water.Shape = face
water.ViewObject.ShapeColor = self.feature_colors['water']
water.Label = f"{name} ({tags['waterway']})"
water_layer.addObject(water)
except Exception as e:
print(f"Error creando área de agua {way_id}: {e}")
def get_osm_name(self, tags, fallback=""):
for key in ["name", "name:es", "name:en", "alt_name", "ref"]:
if key in tags and tags[key].strip():
return tags[key]
return fallback
def parse_width(self, tags, default=2.0):
for key in ["width", "est_width"]:
if key in tags:
try:
w = tags[key].replace("m", "").strip()
return float(w)
except:
pass
return default
+14 -4
View File
@@ -39,9 +39,11 @@ class PVPlantWorkbench(Workbench):
ToolTip = "Workbench for PV design"
Icon = str(os.path.join(DirIcons, "icon.svg"))
def __init__(self):
''' init '''
def Initialize(self):
#sys.path.append(r"C:\Users\javie\AppData\Roaming\FreeCAD\Mod")
sys.path.append(os.path.join(FreeCAD.getUserAppDataDir(), 'Mod'))
import PVPlantTools, reload
@@ -59,6 +61,7 @@ class PVPlantWorkbench(Workbench):
"PVPlantBuilding",
"PVPlantFenceGroup",
]'''
from Electrical.PowerConverter import PowerConverter
self.electricalList = ["PVPlantStringBox",
"PVPlantCable",
"PVPlanElectricalLine",
@@ -66,6 +69,8 @@ class PVPlantWorkbench(Workbench):
"Stringing",
"Separator",
"StringInverter",
"Separator",
"PowerConverter"
]
self.roads = ["PVPlantRoad",
@@ -141,7 +146,10 @@ class PVPlantWorkbench(Workbench):
from widgets import CountSelection
def Activated(self):
"This function is executed when the workbench is activated"
"""This function is executed when the workbench is activated"""
FreeCAD.Console.PrintLog("Road workbench activated.\n")
import SelectionObserver
import FreeCADGui
@@ -150,7 +158,9 @@ class PVPlantWorkbench(Workbench):
return
def Deactivated(self):
"This function is executed when the workbench is deactivated"
"""This function is executed when the workbench is deactivated"""
FreeCAD.Console.PrintLog("Road workbench deactivated.\n")
#FreeCADGui.Selection.removeObserver(self.observer)
return
@@ -198,4 +208,4 @@ class PVPlantWorkbench(Workbench):
return "Gui::PythonWorkbench"
Gui.addWorkbench(PVPlantWorkbench())
FreeCADGui.addWorkbench(PVPlantWorkbench())
+31 -10
View File
@@ -540,6 +540,7 @@ def makeTrackerSetup(name="TrackerSetup"):
pass
return obj
def getarray(array, numberofpoles):
if len(array) == 0:
newarray = [0] * numberofpoles
@@ -568,6 +569,7 @@ def getarray(array, numberofpoles):
newarray = [array[0]] * numberofpoles
return newarray
class TrackerSetup(FrameSetup):
"A 1 Axis Tracker Obcject"
@@ -589,7 +591,7 @@ class TrackerSetup(FrameSetup):
obj.addProperty("App::PropertyDistance",
"MotorGap",
"ModuleArray",
QT_TRANSLATE_NOOP("App::Property", "Thse height of this object")
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).MotorGap = 550
if not "UseGroupsOfModules" in pl:
@@ -880,6 +882,9 @@ class TrackerSetup(FrameSetup):
def CalculatePosts(self, obj, totalh, totalw):
# Temp: utilizar el uso de versiones:
if len(obj.PoleType) == 0:
return None, None
ver = 1
if ver == 0:
# versión 0:
@@ -906,8 +911,8 @@ class TrackerSetup(FrameSetup):
elif ver == 1:
# versión 1:
linetmp = Part.LineSegment(FreeCAD.Vector(0), FreeCAD.Vector(0, 10, 0)).toShape()
compoundPoles = Part.makeCompound([])
compoundAxis = Part.makeCompound([])
compound_poles = Part.makeCompound([])
compound_axis = Part.makeCompound([])
offsetX = - totalw / 2
arrayDistance = obj.DistancePole
@@ -915,15 +920,16 @@ class TrackerSetup(FrameSetup):
arrayPost = obj.PoleSequence
for x in range(int(obj.NumberPole.Value)):
postCopy = obj.PoleType[arrayPost[x]].Shape.copy()
post_copy = obj.PoleType[arrayPost[x]].Shape.copy()
offsetX += arrayDistance[x]
postCopy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(postCopy.BoundBox.ZLength - arrayAerial[x]))
compoundPoles.add(postCopy)
post_copy.Placement.Base = FreeCAD.Vector(offsetX, 0, -(post_copy.BoundBox.ZLength - arrayAerial[x]))
compound_poles.add(post_copy)
axis = linetmp.copy()
axis.Placement.Base = FreeCAD.Vector(offsetX, 0, arrayAerial[x])
compoundAxis.add(axis)
return compoundPoles, compoundAxis
compound_axis.add(axis)
return compound_poles, compound_axis
def execute(self, obj):
# obj.Shape: compound
@@ -1029,14 +1035,14 @@ class Tracker(ArchComponent.Component):
"AngleY",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0
).AngleY = 0
if not ("AngleZ" in pl):
obj.addProperty("App::PropertyAngle",
"AngleZ",
"Outputs",
QT_TRANSLATE_NOOP("App::Property", "The height of this object")
).AngleX = 0
).AngleZ = 0
self.Type = "Tracker"
#obj.Type = self.Type
@@ -1056,12 +1062,15 @@ class Tracker(ArchComponent.Component):
if prop.startswith("Angle"):
base = obj.Placement.Base
angles = obj.Placement.Rotation.toEulerAngles("XYZ")
# Actualizar rotación según el ángulo modificado
if prop == "AngleX":
rot = FreeCAD.Rotation(angles[2], angles[1], obj.AngleX.Value)
elif prop == "AngleY":
rot = FreeCAD.Rotation(angles[2], obj.AngleY.Value, angles[0])
elif prop == "AngleZ":
rot = FreeCAD.Rotation(obj.AngleZ.Value, angles[1], angles[0])
obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0,0,0))
if hasattr(FreeCAD.ActiveDocument, "FramesChecking"):
@@ -1083,15 +1092,19 @@ class Tracker(ArchComponent.Component):
# |-- PoleAxes: Edge
if obj.Setup is None:
print("Warning: No Setup defined for tracker")
return
try:
pl = obj.Placement
shape = obj.Setup.Shape.copy()
# Rotar módulos
p1 = shape.SubShapes[0].SubShapes[1].SubShapes[0].CenterOfMass
p2 = min(shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda face: face.Area).CenterOfMass
axis = p1 - p2
modules = shape.SubShapes[0].rotate(p1, axis, obj.Tilt.Value)
# Rotar postes
angle = obj.Placement.Rotation.toEuler()[1]
newpoles = Part.makeCompound([])
for i in range(len(shape.SubShapes[1].SubShapes[0].SubShapes)):
@@ -1102,10 +1115,16 @@ class Tracker(ArchComponent.Component):
newpoles.add(pole.rotate(base, axis, -angle))
poles = Part.makeCompound([newpoles, shape.SubShapes[1].SubShapes[1].copy()])
# Crear forma final
obj.Shape = Part.makeCompound([modules, poles])
obj.Placement = pl
# Sincronizar propiedades de ángulo
obj.AngleX, obj.AngleY, obj.AngleZ = obj.Placement.Rotation.toEulerAngles("XYZ")
except Exception as e:
print(f"Error in Tracker execution: {str(e)}")
class ViewProviderTracker(ArchComponent.ViewProviderComponent):
"A View Provider for the Pipe object"
@@ -1271,6 +1290,7 @@ class CommandFixedRack:
#FreeCADGui.Control.showDialog(self.TaskPanel)
return
class CommandTrackerSetup:
"the Arch Building command definition"
@@ -1292,6 +1312,7 @@ class CommandTrackerSetup:
FreeCADGui.Control.showDialog(self.TaskPanel)
return
class CommandTracker:
"the Arch Building command definition"
-554
View File
@@ -1,554 +0,0 @@
import ArchComponent
import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import threading
def makePlacement(baseobj=None, diameter=0, length=0, placement=None, name="Placement"):
"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
_PVPlantPlacement(obj)
if FreeCAD.GuiUp:
_ViewProviderPVPlantPlacement(obj.ViewObject)
if baseobj:
baseobj.ViewObject.hide()
return obj
class _CommandPVPlantPlacement:
"the Arch Schedule command definition"
def GetResources(self):
return {'Pixmap': 'Placement',
'Accel': "P, S",
'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"),
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")}
def Activated(self):
taskd = _PVPlantPlacementTaskPanel()
FreeCADGui.Control.showDialog(taskd)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class _PVPlantPlacement(ArchComponent.Component):
"the PVPlantPlacement object"
def __init__(self, obj):
ArchComponent.Component.__init__(self, obj)
self.setProperties(obj)
# Does a IfcType exist?
# obj.IfcType = "Fence"
obj.MoveWithHost = False
def setProperties(self, obj):
ArchComponent.Component.setProperties(self, obj)
pl = obj.PropertiesList
if not "Section" in pl:
obj.addProperty("App::PropertyLink", "Land", "Placement", QT_TRANSLATE_NOOP(
"App::Property", "A single section of the fence"))
if not "Post" in pl:
obj.addProperty("App::PropertyLink", "Structure", "Placement", QT_TRANSLATE_NOOP(
"App::Property", "A single fence post"))
if not "Path" in pl:
obj.addProperty("App::PropertyLink", "Path", "Placement", QT_TRANSLATE_NOOP(
"App::Property", "The Path the fence should follow"))
if not "NumberOfSections" in pl:
obj.addProperty("App::PropertyInteger", "NumberOfSections", "Count", QT_TRANSLATE_NOOP(
"App::Property", "The number of sections the fence is built of"))
obj.setEditorMode("NumberOfSections", 1)
if not "NumberOfPosts" in pl:
obj.addProperty("App::PropertyInteger", "NumberOfPosts", "Count", QT_TRANSLATE_NOOP(
"App::Property", "The number of posts used to build the fence"))
obj.setEditorMode("NumberOfPosts", 1)
self.Type = "Fence"
def execute(self, obj):
# fills columns A, B and C of the spreadsheet
if not obj.Description:
return
def __getstate__(self):
return self.Type
def __setstate__(self, state):
if state:
self.Type = state
class _ViewProviderPVPlantPlacement:
"A View Provider for PVPlantPlacement"
def __init__(self, vobj):
vobj.Proxy = self
def getIcon(self):
return ":/icons/Arch_Schedule.svg"
def attach(self, vobj):
self.Object = vobj.Object
def setEdit(self, vobj, mode):
# taskd = _ArchScheduleTaskPanel(vobj.Object)
# FreeCADGui.Control.showDialog(taskd)
return True
def doubleClicked(self, vobj):
# taskd = _ArchScheduleTaskPanel(vobj.Object)
# FreeCADGui.Control.showDialog(taskd)
return True
def unsetEdit(self, vobj, mode):
# FreeCADGui.Control.closeDialog()
return
def claimChildren(self):
# if hasattr(self,"Object"):
# return [self.Object.Result]
return None
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def getDisplayModes(self, vobj):
return ["Default"]
def getDefaultDisplayMode(self):
return "Default"
def setDisplayMode(self, mode):
return mode
class _PVPlantPlacementTaskPanel:
'''The editmode TaskPanel for Schedules'''
def __init__(self, obj=None):
self.Terrain = None
self.Rack = None
self.Gap = 200
self.Pitch = 4500
# form:
self.form = QtGui.QWidget()
self.form.resize(800, 640)
self.form.setWindowTitle("Curvas de nivel")
self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Schedule.svg"))
self.grid = QtGui.QGridLayout(self.form)
# parameters
self.labelTerrain = QtGui.QLabel()
self.labelTerrain.setText("Terreno:")
self.lineTerrain = QtGui.QLineEdit(self.form)
self.lineTerrain.setObjectName(_fromUtf8("lineTerrain"))
self.lineTerrain.readOnly = True
self.grid.addWidget(self.labelTerrain, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.lineTerrain, self.grid.rowCount() - 1, 1, 1, 1)
self.buttonAddTerrain = QtGui.QPushButton('Sel')
self.grid.addWidget(self.buttonAddTerrain, self.grid.rowCount() - 1, 2, 1, 1)
self.labelRack = QtGui.QLabel()
self.labelRack.setText("Rack:")
self.lineRack = QtGui.QLineEdit(self.form)
self.lineRack.setObjectName(_fromUtf8("lineRack"))
self.lineRack.readOnly = True
self.grid.addWidget(self.labelRack, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.lineRack, self.grid.rowCount() - 1, 1, 1, 1)
self.buttonAddRack = QtGui.QPushButton('Sel')
self.grid.addWidget(self.buttonAddRack, self.grid.rowCount() - 1, 2, 1, 1)
self.line1 = QtGui.QFrame()
self.line1.setFrameShape(QtGui.QFrame.HLine)
self.line1.setFrameShadow(QtGui.QFrame.Sunken)
self.grid.addWidget(self.line1, self.grid.rowCount(), 0, 1, -1)
self.labelTypeStructure = QtGui.QLabel()
self.labelTypeStructure.setText("Tipo de estructura:")
self.valueTypeStructure = QtGui.QComboBox()
self.valueTypeStructure.addItems(["Fixed", "Tracker 1 Axis"])
self.valueTypeStructure.setCurrentIndex(0)
self.grid.addWidget(self.labelTypeStructure, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valueTypeStructure, self.grid.rowCount() - 1, 1, 1, -1)
self.labelOrientation = QtGui.QLabel()
self.labelOrientation.setText("Orientacion:")
self.valueOrientation = QtGui.QComboBox()
self.valueOrientation.addItems(["Norte-Sur", "Este-Oeste"])
self.valueOrientation.setCurrentIndex(0)
self.grid.addWidget(self.labelOrientation, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valueOrientation, self.grid.rowCount() - 1, 1, 1, -1)
self.labelGap = QtGui.QLabel()
self.labelGap.setText("Espacio entre Columnas:")
self.valueGap = FreeCADGui.UiLoader().createWidget("Gui::InputField")
self.valueGap.setText(str(self.Gap) + " mm")
self.grid.addWidget(self.labelGap, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valueGap, self.grid.rowCount() - 1, 1, 1, -1)
self.labelPitch = QtGui.QLabel()
self.labelPitch.setText("Separacion entre Filas:")
self.valuePitch = FreeCADGui.UiLoader().createWidget("Gui::InputField")
self.valuePitch.setText(str(self.Pitch) + " mm")
self.grid.addWidget(self.labelPitch, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valuePitch, self.grid.rowCount() - 1, 1, 1, -1)
self.labelAlign = QtGui.QLabel()
self.labelAlign.setText("Método de alineación:")
self.valueAlign = QtGui.QComboBox()
self.valueAlign.addItems(["Si", "No"])
self.valueAlign.setCurrentIndex(0)
self.grid.addWidget(self.labelAlign, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valueAlign, self.grid.rowCount() - 1, 1, 1, -1)
self.line2 = QtGui.QFrame()
self.line2.setFrameShape(QtGui.QFrame.HLine)
self.line2.setFrameShadow(QtGui.QFrame.Sunken)
self.grid.addWidget(self.line2, self.grid.rowCount(), 0, 1, -1)
self.labelSideSlope = QtGui.QLabel()
self.labelSideSlope.setText("Maxima inclinacion longitudinal:")
self.valueSideSlope = FreeCADGui.UiLoader().createWidget("Gui::InputField")
self.valueSideSlope.setText("15")
self.grid.addWidget(self.labelSideSlope, self.grid.rowCount(), 0, 1, 1)
self.grid.addWidget(self.valueSideSlope, self.grid.rowCount() - 1, 1, 1, -1)
QtCore.QObject.connect(self.buttonAddTerrain, QtCore.SIGNAL("clicked()"), self.addTerrain)
QtCore.QObject.connect(self.buttonAddRack, QtCore.SIGNAL("clicked()"), self.addRack)
# QtCore.QObject.connect(self.form.buttonDel, QtCore.SIGNAL("clicked()"), self.remove)
# QtCore.QObject.connect(self.form.buttonClear, QtCore.SIGNAL("clicked()"), self.clear)
# QtCore.QObject.connect(self.form.buttonSelect, QtCore.SIGNAL("clicked()"), self.select)
def addTerrain(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Terrain = sel[0]
self.lineTerrain.setText(self.Terrain.Label)
def addRack(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.Rack = sel[0]
self.lineRack.setText(self.Rack.Label)
def accept(self):
if self.Terrain is not None and self.Rack is not None:
self.Gap = FreeCAD.Units.Quantity(self.valueGap.text()).Value
self.Pitch = FreeCAD.Units.Quantity(self.valuePitch.text()).Value
self.placement()
return True
def placement(self):
if self.valueTypeStructure.currentIndex() == 0: # Fixed
print("Rack")
else:
print("Tracker")
if self.Rack.Height < self.Rack.Length:
print("rotar")
aux = self.Rack.Length
self.Rack.Length = self.Rack.Height
self.Rack.Height = aux
self.Rack.Placement.Base.x = self.Terrain.Shape.BoundBox.XMin
self.Rack.Placement.Base.y = self.Terrain.Shape.BoundBox.YMin
DistColls = self.Rack.Length.Value + self.Gap
DistRows = self.Rack.Height.Value + self.Pitch
area = self.Rack.Shape.Faces[0].Area # * 0.999999999
import Draft
rec = Draft.makeRectangle(length=self.Terrain.Shape.BoundBox.XLength, height=self.Rack.Height, face=True,
support=None)
rec.Placement.Base.x = self.Terrain.Shape.BoundBox.XMin
rec.Placement.Base.y = self.Terrain.Shape.BoundBox.YMin
try:
while rec.Shape.BoundBox.YMax <= self.Terrain.Shape.BoundBox.YMax:
common = self.Terrain.Shape.common(rec.Shape)
for shape in common.Faces:
if shape.Area >= area:
if False:
minorPoint = FreeCAD.Vector(0, 0, 0)
for spoint in shape.OuterWire.Vertexes:
if minorPoint.y >= spoint.Point.y:
if minorPoint.x >= spoint.x:
minorPoint = spoint
self.Rack.Placement.Base = spoint
else:
# más rápido
self.Rack.Placement.Base.x = shape.BoundBox.XMin
self.Rack.Placement.Base.y = shape.BoundBox.YMin
while self.Rack.Shape.BoundBox.XMax <= shape.BoundBox.XMax:
verts = [v.Point for v in rackClone.Shape.OuterWire.OrderedVertexes]
inside = True
for vert in verts:
if not shape.isInside(vert, 0, True):
inside = False
break
if inside:
raise
else:
# ajuste fino hasta encontrar el primer sitio:
rackClone.Placement.Base.x += 100 # un metro
'''old version
common1 = shape.common(self.Rack.Shape)
if common1.Area >= area:
raise
else:
# ajuste fino hasta encontrar el primer sitio:
self.Rack.Placement.Base.x += 500 # un metro
del common1
'''
# ajuste fino hasta encontrar el primer sitio:
rec.Placement.Base.y += 100
del common
except:
pass
#print("Found")
FreeCAD.ActiveDocument.removeObject(rec.Name)
from datetime import datetime
starttime = datetime.now()
if self.valueOrientation.currentIndex() == 0:
# Código para crear filas:
self.Rack.Placement.Base.x = self.Terrain.Shape.BoundBox.XMin
i = 1
yy = self.Rack.Placement.Base.y
while yy < self.Terrain.Shape.BoundBox.YMax:
CreateRow1(self.Rack.Placement.Base.x, yy, self.Rack, self.Terrain, DistColls, area, i)
i += 1
yy += DistRows
elif self.valueOrientation.currentIndex() == 2:
# Código para crear columnas:
while self.Rack.Placement.Base.x > self.Terrain.Shape.BoundBox.XMin:
self.Rack.Placement.Base.x -= DistColls
else:
xx = self.Rack.Placement.Base.x
while xx < self.Terrain.Shape.BoundBox.XMax:
CreateGrid(xx, self.Rack.Placement.Base.y, self.Rack, self.Terrain, DistRows, area)
xx += DistColls
FreeCAD.activeDocument().recompute()
print("Everything OK (", datetime.now() - starttime, ")")
# Alinear solo filas. las columnas donde se pueda
def CreateRow(XX, YY, rack, land, gap, area, rowNumber):
import Draft
rackClone = Draft.makeRectangle(length=rack.Length, height=rack.Height, face=True, support=None)
rackClone.Label = 'rackClone{a}'.format(a=rowNumber)
rackClone.Placement.Base.x = XX
rackClone.Placement.Base.y = YY
rec = Draft.makeRectangle(length=land.Shape.BoundBox.XLength, height=rack.Height, face=True, support=None)
rec.Placement.Base.x = land.Shape.BoundBox.XMin
rec.Placement.Base.y = YY
FreeCAD.activeDocument().recompute()
common = land.Shape.common(rec.Shape)
for shape in common.Faces:
if shape.Area >= area:
rackClone.Placement.Base.x = shape.BoundBox.XMin
rackClone.Placement.Base.y = shape.BoundBox.YMin
while rackClone.Shape.BoundBox.XMax <= shape.BoundBox.XMax:
common1 = shape.common(rackClone.Shape)
if common1.Area >= area:
tmp = Draft.makeRectangle(length=rack.Length, height=rack.Height, placement=rackClone.Placement,
face=True, support=None)
tmp.Label = 'R{:03}-000'.format(rowNumber)
rackClone.Placement.Base.x += gap
else:
# ajuste fino hasta encontrar el primer sitio:
rackClone.Placement.Base.x += 500 # un metro
del common1
del common
FreeCAD.ActiveDocument.removeObject(rackClone.Name)
FreeCAD.ActiveDocument.removeObject(rec.Name)
# Alinear solo filas. las columnas donde se pueda
def CreateRow1(XX, YY, rack, land, gap, area, rowNumber):
import Draft
rackClone = Draft.makeRectangle(length=rack.Length, height=rack.Height, face=True, support=None)
rackClone.Label = 'rackClone{a}'.format(a=rowNumber)
rackClone.Placement.Base.x = XX
rackClone.Placement.Base.y = YY
rec = Draft.makeRectangle(length=land.Shape.BoundBox.XLength, height=rack.Height, face=True, support=None)
rec.Placement.Base.x = land.Shape.BoundBox.XMin
rec.Placement.Base.y = YY
FreeCAD.activeDocument().recompute()
common = land.Shape.common(rec.Shape)
for shape in common.Faces:
if shape.Area >= area:
if False:
minorPoint = FreeCAD.Vector(0, 0, 0)
for spoint in shape.OuterWire.Vertexes:
if minorPoint.y >= spoint.Point.y:
if minorPoint.x >= spoint.x:
minorPoint = spoint
rackClone.Placement.Base = spoint
else:
# más rápido
rackClone.Placement.Base.x = shape.BoundBox.XMin
rackClone.Placement.Base.y = shape.BoundBox.YMin
while rackClone.Shape.BoundBox.XMax <= shape.BoundBox.XMax:
verts = [v.Point for v in rackClone.Shape.OuterWire.OrderedVertexes]
inside = True
for vert in verts:
if not shape.isInside(vert, 0, True):
inside = False
break
if inside:
#tmp = rack.Shape.copy()
#tmp.Placement = rack.Placement
tmp = Draft.makeRectangle(length=rack.Length, height=rack.Height, placement=rackClone.Placement,
face=True, support=None)
tmp.Label = 'R{:03}-000'.format(rowNumber)
rackClone.Placement.Base.x += gap
else:
# ajuste fino hasta encontrar el primer sitio:
rackClone.Placement.Base.x += 500 # un metro
del common
FreeCAD.ActiveDocument.removeObject(rackClone.Name)
FreeCAD.ActiveDocument.removeObject(rec.Name)
# Alinear columna y fila (grid perfecta)
def CreateGrid(XX, YY, rack, land, gap, area):
print("CreateGrid")
import Draft
rackClone = Draft.makeRectangle(length=rack.Length, height=rack.Height, face=True, support=None)
rackClone.Label = 'rackClone{a}'.format(a=XX)
rackClone.Placement.Base.x = XX
rackClone.Placement.Base.y = YY
# if False:
while rackClone.Shape.BoundBox.YMax < land.Shape.BoundBox.YMax:
common = land.Shape.common(rackClone.Shape)
if common.Area >= area:
tmp = Draft.makeRectangle(length=rack.Length, height=rack.Height,
placement=rackClone.Placement, face=True, support=None)
tmp.Label = 'rackClone{a}'.format(a=XX)
rackClone.Placement.Base.y += gap
# else:
# # ajuste fino hasta encontrar el primer sitio:
# rackClone.Placement.Base.y += 1000
FreeCAD.ActiveDocument.removeObject(rackClone.Name)
# Alinear solo filas. las columnas donde se pueda
def CreateCol(XX, YY, rack, land, gap, area):
import Draft
rackClone = Draft.makeRectangle(length=rack.Length, height=rack.Height, face=True, support=None)
rackClone.Label = 'rackClone{a}'.format(a=XX)
rackClone.Placement.Base.x = XX
rackClone.Placement.Base.y = YY
while rackClone.Shape.BoundBox.YMax < land.Shape.BoundBox.YMax:
common = land.Shape.common(rackClone.Shape)
if common.Area >= area:
tmp = Draft.makeRectangle(length=rack.Length, height=rack.Height,
placement=rackClone.Placement, face=True, support=None)
tmp.Label = 'rackClone{a}'.format(a=XX)
rackClone.Placement.Base.y += gap
else:
# ajuste fino hasta encontrar el primer sitio:
rackClone.Placement.Base.y += 100
FreeCAD.ActiveDocument.removeObject(rackClone.Name)
# TODO: Probar a usar hilos:
class _CreateCol(threading.Thread):
def __init__(self, args=()):
super().__init__()
self.XX = args[0]
self.YY = args[1]
self.rack = args[2]
self.land = args[3]
self.gap = args[4]
self.area = args[5]
def run(self):
import Draft
# rackClone = Draft.makeRectangle(length=land.Shape.BoundBox.XLength, height=rack.Height,
# face=True, support=None)
# rackClone = FreeCAD.activeDocument().addObject('Part::Feature')
# rackClone.Shape = self.rack.Shape
rackClone = Draft.makeRectangle(length=self.rack.Length, height=self.rack.Height, face=True, support=None)
rackClone.Label = 'rackClone{a}'.format(a=self.XX)
rackClone.Placement.Base.x = self.XX
rackClone.Placement.Base.y = self.YY
# if False:
while rackClone.Shape.BoundBox.YMax < self.land.Shape.BoundBox.YMax:
common = self.land.Shape.common(rackClone.Shape)
if common.Area >= self.area:
rack = Draft.makeRectangle(length=self.rack.Length, height=self.rack.Height,
placement=rackClone.Placement, face=True, support=None)
rack.Label = 'rackClone{a}'.format(a=self.XX)
rackClone.Placement.Base.y += self.gap
# else:
# # ajuste fino hasta encontrar el primer sitio:
# rackClone.Placement.Base.y += 1000
# FreeCAD.ActiveDocument.removeObject(rackClone.Name)
if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement())
-945
View File
@@ -1,945 +0,0 @@
import math
import FreeCAD
import Part
import ArchComponent
from pivy import coin
import numpy as np
import DraftGeomUtils
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
voltype = ["Fill", "Cut"]
def makeEarthWorksVolume(vtype = 0):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", voltype[vtype])
EarthWorksVolume(obj)
ViewProviderEarthWorksVolume(obj.ViewObject)
return obj
class EarthWorksVolume(ArchComponent.Component):
def __init__(self, obj):
# Definición de Variables:
ArchComponent.Component.__init__(self, obj)
self.obj = obj
self.setProperties(obj)
def setProperties(self, obj):
# Definicion de Propiedades:
pl = obj.PropertiesList
if not ("VolumeType" in pl):
obj.addProperty("App::PropertyEnumeration",
"VolumeType",
"Volume",
"Connection").VolumeType = voltype
if not ("SurfaceSlope" in pl):
obj.addProperty("App::PropertyPercent",
"SurfaceSlope",
"Volume",
"Connection").SurfaceSlope = 2
if not ("VolumeMesh" in pl):
obj.addProperty("Mesh::PropertyMeshKernel",
"VolumeMesh",
"Volume",
"Volume")
obj.setEditorMode("VolumeMesh", 2)
if not ("Volume" in pl):
obj.addProperty("App::PropertyVolume",
"Volume",
"Volume",
"Volume")
obj.setEditorMode("Volume", 1)
obj.Proxy = self
obj.IfcType = "Civil Element"
obj.setEditorMode("IfcType", 1)
obj.Proxy = self
def onDocumentRestored(self, obj):
ArchComponent.Component.onDocumentRestored(self, obj)
self.setProperties(obj)
def onChange(self, obj, prop):
if prop == "VolumeMesh":
if obj.VolumeMesh:
obj.VolumeMesh = obj.VolumeMesh.Volume
def execute(self, obj):
''' '''
pass
class ViewProviderEarthWorksVolume:
"A View Provider for the Pipe object"
def __init__(self, vobj):
''' Set view properties. '''
pl = vobj.PropertiesList
(r, g, b) = (1.0, 0.0, 0.0) if vobj.Object.VolumeType == "Cut" else (0.0, 0.0, 1.0)
# Triangulation properties.
if not "Transparency" in pl:
vobj.addProperty("App::PropertyIntegerConstraint",
"Transparency", "Surface Style",
"Set triangle face transparency")
vobj.Transparency = (50, 0, 100, 1)
if not "ShapeColor" in pl:
vobj.addProperty("App::PropertyColor",
"ShapeColor",
"Surface Style",
"Set triangle face color")
vobj.ShapeColor = (r, g, b, vobj.Transparency / 100)
if not "ShapeMaterial" in pl:
vobj.addProperty("App::PropertyMaterial",
"ShapeMaterial", "Surface Style",
"Triangle face material")
vobj.ShapeMaterial = FreeCAD.Material()
if not "LineTransparency" in pl:
vobj.addProperty("App::PropertyIntegerConstraint",
"LineTransparency", "Surface Style",
"Set triangle edge transparency")
vobj.LineTransparency = (50, 0, 100, 1)
if not "LineColor" in pl:
vobj.addProperty("App::PropertyColor",
"LineColor", "Surface Style",
"Set triangle face color")
vobj.LineColor = (0.5, 0.5, 0.5, vobj.LineTransparency / 100)
'''vobj.addProperty(
"App::PropertyMaterial", "LineMaterial", "Surface Style",
"Triangle face material").LineMaterial = FreeCAD.Material()
vobj.addProperty(
"App::PropertyFloatConstraint", "LineWidth", "Surface Style",
"Set triangle edge line width").LineWidth = (0.0, 1.0, 20.0, 1.0)
# Boundary properties.
vobj.addProperty(
"App::PropertyColor", "BoundaryColor", "Boundary Style",
"Set boundary contour color").BoundaryColor = (0.0, 0.75, 1.0, 0.0)
vobj.addProperty(
"App::PropertyFloatConstraint", "BoundaryWidth", "Boundary Style",
"Set boundary contour line width").BoundaryWidth = (3.0, 1.0, 20.0, 1.0)
vobj.addProperty(
"App::PropertyEnumeration", "BoundaryPattern", "Boundary Style",
"Set a line pattern for boundary").BoundaryPattern = [*line_patterns]
vobj.addProperty(
"App::PropertyIntegerConstraint", "PatternScale", "Boundary Style",
"Scale the line pattern").PatternScale = (3, 1, 20, 1)
# Contour properties.
vobj.addProperty(
"App::PropertyColor", "MajorColor", "Contour Style",
"Set major contour color").MajorColor = (1.0, 0.0, 0.0, 0.0)
vobj.addProperty(
"App::PropertyFloatConstraint", "MajorWidth", "Contour Style",
"Set major contour line width").MajorWidth = (4.0, 1.0, 20.0, 1.0)
vobj.addProperty(
"App::PropertyColor", "MinorColor", "Contour Style",
"Set minor contour color").MinorColor = (1.0, 1.0, 0.0, 0.0)
vobj.addProperty(
"App::PropertyFloatConstraint", "MinorWidth", "Contour Style",
"Set major contour line width").MinorWidth = (2.0, 1.0, 20.0, 1.0)
'''
vobj.Proxy = self
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
def onChanged(self, vobj, prop):
'''
Update Object visuals when a view property changed.
'''
if prop == "ShapeColor" or prop == "Transparency":
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
color = vobj.getPropertyByName("ShapeColor")
transparency = vobj.getPropertyByName("Transparency")
color = (color[0], color[1], color[2], transparency / 100)
vobj.ShapeMaterial.DiffuseColor = color
if prop == "ShapeMaterial":
if hasattr(vobj, "ShapeMaterial"):
material = vobj.getPropertyByName("ShapeMaterial")
self.face_material.diffuseColor.setValue(material.DiffuseColor[:3])
self.face_material.transparency = material.DiffuseColor[3]
if prop == "LineColor" or prop == "LineTransparency":
if hasattr(vobj, "LineColor") and hasattr(vobj, "LineTransparency"):
color = vobj.getPropertyByName("LineColor")
transparency = vobj.getPropertyByName("LineTransparency")
color = (color[0], color[1], color[2], transparency / 100)
vobj.LineMaterial.DiffuseColor = color
if prop == "LineMaterial":
material = vobj.getPropertyByName(prop)
self.edge_material.diffuseColor.setValue(material.DiffuseColor[:3])
self.edge_material.transparency = material.DiffuseColor[3]
if prop == "LineWidth":
width = vobj.getPropertyByName(prop)
self.edge_style.lineWidth = width
if prop == "BoundaryColor":
color = vobj.getPropertyByName(prop)
self.boundary_color.rgb = color[:3]
if prop == "BoundaryWidth":
width = vobj.getPropertyByName(prop)
self.boundary_style.lineWidth = width
if prop == "BoundaryPattern":
if hasattr(vobj, "BoundaryPattern"):
pattern = vobj.getPropertyByName(prop)
self.boundary_style.linePattern = line_patterns[pattern]
if prop == "PatternScale":
if hasattr(vobj, "PatternScale"):
scale = vobj.getPropertyByName(prop)
self.boundary_style.linePatternScaleFactor = scale
if prop == "MajorColor":
color = vobj.getPropertyByName(prop)
self.major_color.rgb = color[:3]
if prop == "MajorWidth":
width = vobj.getPropertyByName(prop)
self.major_style.lineWidth = width
if prop == "MinorColor":
color = vobj.getPropertyByName(prop)
self.minor_color.rgb = color[:3]
if prop == "MinorWidth":
width = vobj.getPropertyByName(prop)
self.minor_style.lineWidth = width
def attach(self, vobj):
'''
Create Object visuals in 3D view.
'''
# GeoCoords Node.
self.geo_coords = coin.SoGeoCoordinate()
# Surface features.
self.triangles = coin.SoIndexedFaceSet()
self.face_material = coin.SoMaterial()
self.edge_material = coin.SoMaterial()
self.edge_color = coin.SoBaseColor()
self.edge_style = coin.SoDrawStyle()
self.edge_style.style = coin.SoDrawStyle.LINES
shape_hints = coin.SoShapeHints()
shape_hints.vertex_ordering = coin.SoShapeHints.COUNTERCLOCKWISE
mat_binding = coin.SoMaterialBinding()
mat_binding.value = coin.SoMaterialBinding.PER_FACE
offset = coin.SoPolygonOffset()
offset.styles = coin.SoPolygonOffset.LINES
offset.factor = -2.0
# Boundary features.
self.boundary_color = coin.SoBaseColor()
self.boundary_coords = coin.SoGeoCoordinate()
self.boundary_lines = coin.SoLineSet()
self.boundary_style = coin.SoDrawStyle()
self.boundary_style.style = coin.SoDrawStyle.LINES
# Boundary root.
boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
boundaries.style = 'EMISSIVE_DIFFUSE'
boundaries.addChild(self.boundary_color)
boundaries.addChild(self.boundary_style)
boundaries.addChild(self.boundary_coords)
boundaries.addChild(self.boundary_lines)
# Major Contour features.
self.major_color = coin.SoBaseColor()
self.major_coords = coin.SoGeoCoordinate()
self.major_lines = coin.SoLineSet()
self.major_style = coin.SoDrawStyle()
self.major_style.style = coin.SoDrawStyle.LINES
# Major Contour root.
major_contours = coin.SoSeparator()
major_contours.addChild(self.major_color)
major_contours.addChild(self.major_style)
major_contours.addChild(self.major_coords)
major_contours.addChild(self.major_lines)
# Minor Contour features.
self.minor_color = coin.SoBaseColor()
self.minor_coords = coin.SoGeoCoordinate()
self.minor_lines = coin.SoLineSet()
self.minor_style = coin.SoDrawStyle()
self.minor_style.style = coin.SoDrawStyle.LINES
# Minor Contour root.
minor_contours = coin.SoSeparator()
minor_contours.addChild(self.minor_color)
minor_contours.addChild(self.minor_style)
minor_contours.addChild(self.minor_coords)
minor_contours.addChild(self.minor_lines)
# Highlight for selection.
highlight = coin.SoType.fromName('SoFCSelection').createInstance()
highlight.style = 'EMISSIVE_DIFFUSE'
highlight.addChild(shape_hints)
highlight.addChild(mat_binding)
highlight.addChild(self.geo_coords)
highlight.addChild(self.triangles)
highlight.addChild(boundaries)
# Face root.
face = coin.SoSeparator()
face.addChild(self.face_material)
face.addChild(highlight)
# Edge root.
edge = coin.SoSeparator()
edge.addChild(self.edge_material)
edge.addChild(self.edge_style)
edge.addChild(highlight)
# Surface root.
surface_root = coin.SoSeparator()
surface_root.addChild(face)
surface_root.addChild(offset)
surface_root.addChild(edge)
surface_root.addChild(major_contours)
surface_root.addChild(minor_contours)
vobj.addDisplayMode(surface_root, "Surface")
# Boundary root.
boundary_root = coin.SoSeparator()
boundary_root.addChild(boundaries)
vobj.addDisplayMode(boundary_root, "Boundary")
# Elevation/Shaded root.
shaded_root = coin.SoSeparator()
shaded_root.addChild(face)
vobj.addDisplayMode(shaded_root, "Elevation")
vobj.addDisplayMode(shaded_root, "Slope")
vobj.addDisplayMode(shaded_root, "Shaded")
# Flat Lines root.
flatlines_root = coin.SoSeparator()
flatlines_root.addChild(face)
flatlines_root.addChild(offset)
flatlines_root.addChild(edge)
vobj.addDisplayMode(flatlines_root, "Flat Lines")
# Wireframe root.
wireframe_root = coin.SoSeparator()
wireframe_root.addChild(edge)
wireframe_root.addChild(major_contours)
wireframe_root.addChild(minor_contours)
vobj.addDisplayMode(wireframe_root, "Wireframe")
# Take features from properties.
self.onChanged(vobj, "ShapeColor")
self.onChanged(vobj, "LineColor")
self.onChanged(vobj, "LineWidth")
'''self.onChanged(vobj, "BoundaryColor")
self.onChanged(vobj, "BoundaryWidth")
self.onChanged(vobj, "BoundaryPattern")
self.onChanged(vobj, "PatternScale")
self.onChanged(vobj, "MajorColor")
self.onChanged(vobj, "MajorWidth")
self.onChanged(vobj, "MinorColor")
self.onChanged(vobj, "MinorWidth")'''
def updateData(self, obj, prop):
'''
Update Object visuals when a data property changed.
'''
# Set System.
geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
self.geo_coords.geoSystem.setValues(geo_system)
self.boundary_coords.geoSystem.setValues(geo_system)
self.major_coords.geoSystem.setValues(geo_system)
self.minor_coords.geoSystem.setValues(geo_system)
if prop == "VolumeMesh":
mesh = obj.VolumeMesh
copy_mesh = mesh.copy()
#copy_mesh.Placement.move(origin.Origin)
triangles = []
for i in copy_mesh.Topology[1]:
triangles.extend(list(i))
triangles.append(-1)
self.geo_coords.point.values = copy_mesh.Topology[0]
self.triangles.coordIndex.values = triangles
del copy_mesh
'''if prop == "ContourShapes":
contour_shape = obj.getPropertyByName(prop)
if contour_shape.SubShapes:
major_shape = contour_shape.SubShapes[0]
points, vertices = self.wire_view(major_shape, origin.Origin)
self.major_coords.point.values = points
self.major_lines.numVertices.values = vertices
minor_shape = contour_shape.SubShapes[1]
points, vertices = self.wire_view(minor_shape, origin.Origin)
self.minor_coords.point.values = points
self.minor_lines.numVertices.values = vertices
if prop == "BoundaryShapes":
boundary_shape = obj.getPropertyByName(prop)
points, vertices = self.wire_view(boundary_shape, origin.Origin, True)
self.boundary_coords.point.values = points
self.boundary_lines.numVertices.values = vertices
if prop == "AnalysisType" or prop == "Ranges":
analysis_type = obj.getPropertyByName("AnalysisType")
ranges = obj.getPropertyByName("Ranges")
if analysis_type == "Default":
if hasattr(obj.ViewObject, "ShapeMaterial"):
material = obj.ViewObject.ShapeMaterial
self.face_material.diffuseColor = material.DiffuseColor[:3]
if analysis_type == "Elevation":
colorlist = self.elevation_analysis(obj.Mesh, ranges)
self.face_material.diffuseColor.setValues(0, len(colorlist), colorlist)
elif analysis_type == "Slope":
colorlist = self.slope_analysis(obj.Mesh, ranges)
self.face_material.diffuseColor.setValues(0, len(colorlist), colorlist)
'''
def getIcon(self):
""" Return the path to the appropriate icon. """
return str(os.path.join(DirIcons, "solar-fixed.svg"))
def getDisplayModes(self, vobj):
'''
Return a list of display modes.
'''
modes = ["Surface", "Boundary", "Flat Lines", "Shaded", "Wireframe"]
return modes
def getDefaultDisplayMode(self):
'''
Return the name of the default display mode.
'''
return "Surface"
def setDisplayMode(self, mode):
'''
Map the display mode defined in attach with
those defined in getDisplayModes.
'''
return mode
def __getstate__(self):
"""
Save variables to file.
"""
return None
def __setstate__(self, state):
"""
Get variables from file.
"""
return None
class EarthWorksTaskPanel:
def __init__(self):
self.To = None
# self.form:
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantEarthworks.ui"))
self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg")))
def accept(self):
from datetime import datetime
starttime = datetime.now()
import MeshPart as mp
land = FreeCAD.ActiveDocument.Terrain.Mesh.copy()
frames = []
for obj in FreeCADGui.Selection.getSelection():
if hasattr(obj, "Proxy"):
if obj.Proxy.Type == "Tracker":
if not (obj in frames):
frames.append(obj)
elif obj.Proxy.Type == "FrameArea":
for fr in obj.Frames:
if not (fr in frames):
frames.append(fr)
if len(frames) == 0:
return False
FreeCAD.ActiveDocument.openTransaction("Calcular movimiento de tierras")
def calculateEarthWorks(line, extreme=False):
pts = []
pts1 = []
line1 = line.copy()
angles = line.Placement.Rotation.toEulerAngles("XYZ")
line1.Placement.Rotation.setEulerAngles("XYZ", 0, 0, angles[2])
line1.Placement.Base.z = 0
pro = mp.projectShapeOnMesh(line1, land, FreeCAD.Vector(0, 0, 1))
flat = []
for points in pro:
flat.extend(points)
pro = Part.makePolygon(flat)
points = pro.discretize(Distance=500)
for point in points:
ver = Part.Vertex(point)
dist = ver.distToShape(line)
linepoint = dist[1][0][1]
if not extreme:
if self.form.groupTolerances.isChecked():
if linepoint.z > point.z:
if linepoint.sub(point).Length > self.form.editToleranceCut.value():
pts.append(linepoint)
elif linepoint.z < point.z:
if linepoint.sub(point).Length > self.form.editToleranceFill.value():
pts1.append(linepoint)
else:
if linepoint.z > point.z:
pts.append(linepoint)
elif linepoint.z < point.z:
pts1.append(linepoint)
#pts.append(linepoint)
else:
if linepoint.z > point.z:
if linepoint.sub(point).Length > 200:
pts.append(linepoint)
return pts, pts1
tools = [[],[]]
ver = 2
if ver == 0:
frames = sorted(frames, key=lambda x: (x.Placement.Base.x, x.Placement.Base.y))
for frame in frames:
length = frame.Setup.Length.Value / 2
p1 = FreeCAD.Vector(-length, 0, 0)
p2 = FreeCAD.Vector(length, 0, 0)
line = Part.LineSegment(p1, p2).toShape()
line.Placement = frame.Placement.copy()
line.Placement.Base.x = frame.Shape.BoundBox.XMin
step = (frame.Shape.BoundBox.XMax - frame.Shape.BoundBox.XMin) / 2
for n in range(3):
ret = calculateEarthWorks(line, n % 2)
tools[0].extend(ret[0])
tools[1].extend(ret[1])
line.Placement.Base.x += step
elif ver == 1:
from PVPlantPlacement import getCols
columns = getCols(frames)
for groups in columns:
for group in groups:
first = group[0]
last = group[-1]
for frame in group:
length = frame.Setup.Length.Value / 2
p1 = FreeCAD.Vector(-(length + (self.form.editOffset.value() if frame == first else -1000)),
0, 0)
p2 = FreeCAD.Vector(length + (self.form.editOffset.value() if frame == last else -1000),
0, 0)
line = Part.LineSegment(p1, p2).toShape()
line.Placement = frame.Placement.copy()
line.Placement.Base.x = frame.Shape.BoundBox.XMin
step = (frame.Shape.BoundBox.XMax - frame.Shape.BoundBox.XMin) / 2
for n in range(3):
ret = calculateEarthWorks(line, n % 2 == 1)
tools[0].extend(ret[0])
tools[1].extend(ret[1])
line.Placement.Base.x += step
elif ver == 2:
print("versión 2")
import PVPlantPlacement
rows, columns = PVPlantPlacement.getRows(frames)
if (rows is None) or (columns is None):
print("Nada que procesar")
return False
tools = []
lofts = []
for group in rows:
lines = []
cont = 0
while cont < len(group):
aw = 0
if cont > 0:
p0 = FreeCAD.Vector(group[cont - 1].Placement.Base)
p1 = FreeCAD.Vector(group[cont].Placement.Base)
aw = getAngle(p0, p1)
ae = 0
if cont < (len(group) - 1):
p1 = FreeCAD.Vector(group[cont].Placement.Base)
p2 = FreeCAD.Vector(group[cont + 1].Placement.Base)
ae = getAngle(p1, p2)
lng = int(group[cont].Setup.Length / 2)
wdt = int(group[cont].Setup.Width / 2)
line = Part.LineSegment(FreeCAD.Vector(-lng, 0, 0),
FreeCAD.Vector(lng, 0, 0)).toShape()
line = Part.LineSegment(FreeCAD.Vector(group[cont].Setup.Shape.SubShapes[1].SubShapes[0].SubShapes[0].Placement.Base.x, 0, 0),
FreeCAD.Vector(group[cont].Setup.Shape.SubShapes[1].SubShapes[0].SubShapes[-1].Placement.Base.x, 0, 0)).toShape()
anf = (aw + ae) / 2
if anf > FreeCAD.ActiveDocument.MaximumWestEastSlope.Value:
anf = FreeCAD.ActiveDocument.MaximumWestEastSlope.Value
zz = wdt * math.sin(math.radians(anf))
li = line.copy()
li.Placement = group[cont].Placement
li.Placement.Rotation = group[cont].Placement.Rotation
li.Placement.Base.x -= wdt #+ (3000 if cont == 0 else 0))
li.Placement.Base.z -= zz
lines.append(li)
ld = line.copy()
ld.Placement = group[cont].Placement
ld.Placement.Rotation = group[cont].Placement.Rotation
ld.Placement.Base.x += wdt #+ (3000 if cont == len(group) - 1 else 0))
ld.Placement.Base.z += zz
lines.append(ld)
tools.append([group[cont], li, ld])
cont += 1
loft = Part.makeLoft(lines, False, True, False)
lofts.append(loft)
for group in rows:
lines = []
for frame in group:
col, idx = searchFrameInColumns(frame, columns)
tool = searchTool(frame, tools)
if idx == 0:
''' '''
if idx == (len(col) - 1):
''' '''
if (idx + 1) < len(col):
frame1 = col[idx + 1]
tool1 = searchTool(frame1, tools)
line = Part.LineSegment(tool[1].Vertexes[1].Point, tool1[1].Vertexes[0].Point).toShape()
lines.append(line)
line = Part.LineSegment(tool[2].Vertexes[1].Point, tool1[2].Vertexes[0].Point).toShape()
lines.append(line)
if len(lines) > 0:
loft = Part.makeLoft(lines, False, True, False)
lofts.append(loft)
faces = []
for loft in lofts:
faces.extend(loft.Faces)
sh = Part.makeShell(faces)
import Utils.PVPlantUtils as utils
import Mesh
pro = utils.getProjected(sh)
pro = utils.simplifyWire(pro)
pts = [ver.Point for ver in pro.Vertexes]
land.trim(pts, 1)
tmp = []
shp = Part.Shape()
for face in sh.Faces:
wire = face.Wires[0].copy()
pl = wire.Placement.Base
wire.Placement.Base = wire.Placement.Base - pl
if DraftGeomUtils.isPlanar(wire):
# Caso simple
wire = wire.makeOffset2D(10000, 0, False, False, True)
wire.Placement.Base.z = wire.Placement.Base.z - 10000
top = wire.makeOffset2D(1, 0, False, False, True)
loft = Part.makeLoft([top, wire], True, True, False)
tmp.append(loft)
shp = shp.fuse(loft)
else:
# Caso complejo:
vertices = face.Vertexes
# Dividir rectángulo en 2 triángulos
triangles = [
[vertices[0], vertices[1], vertices[2]],
[vertices[2], vertices[3], vertices[0]]
]
for tri in triangles:
# Crear wire triangular
wire = Part.makePolygon([v.Point for v in tri] + [tri[0].Point])
wire = wire.makeOffset2D(10000, 0, False, False, True)
wire.Placement.Base.z = wire.Placement.Base.z - 10000
top = wire.makeOffset2D(1, 0, False, False, True)
loft = Part.makeLoft([top, wire], True, True, False)
tmp.append(loft)
shp = shp.fuse(loft)
final_tool = Part.makeCompound(tmp)
Part.show(final_tool, "tool")
Part.show(shp)
FreeCAD.ActiveDocument.commitTransaction()
self.closeForm()
return True
import MeshTools.Triangulation as TriangulateMesh
import MeshTools.MeshGetBoundary as mgb
import Mesh
for ind, points in enumerate(tools):
mesh = TriangulateMesh.Triangulate(points, MaxlengthLE=3000, MaxAngleLE=math.radians(100))
if mesh:
for mesh in mesh.getSeparateComponents():
boundary = mgb.get_boundary(mesh)
Part.show(boundary)
'''if self.form.editOffset.value() != 0:
import Utils.PVPlantUtils as utils
pro = utils.getProjected(boundary)
pro = pro.makeOffset2D(self.form.editOffset.value(), 0, False, False, True)
# TODO: paso intermedio de restar las areas prohibidas
pro = mp.projectShapeOnMesh(pro, land, FreeCAD.Vector(0, 0, 1))
cnt = 0
for lp in pro:
cnt += len(lp)
# points.extend(boundary.Wires[0].discretize(Number=cnt))
points = boundary.Wires[0].discretize(Distance=cnt)
for lp in pro:
points.extend(lp)
mesh1 = TriangulateMesh.Triangulate(points, MaxlengthLE=5000) # , MaxAngleLE=math.pi / 1.334)
import Mesh
Mesh.show(mesh1)
boundary = Part.makeCompound([])
for section in pro:
if len(section) > 0:
try:
boundary.add(Part.makePolygon(section))
except:
pass
Part.show(boundary)'''
#mesh.smooth("Laplace", 3)
#Mesh.show(mesh)
#Part.show(boundary)
vol = makeEarthWorksVolume(ind)
vol.VolumeMesh = mesh.copy()
if ind == 0:
''' put inside fills group '''
else:
''' put inside fills group '''
FreeCAD.ActiveDocument.commitTransaction()
self.closeForm()
return True
def reject(self):
self.closeForm()
return True
def closeForm(self):
FreeCADGui.Control.closeDialog()
def getAngle(vec1, vec2):
dX = vec2.x - vec1.x
dZ = vec2.z - vec1.z
return math.degrees(math.atan2(float(dZ), float(dX)))
def searchFrameInColumns(obj, columns):
for colidx, col in enumerate(columns):
for group in col:
if obj in group:
return group, group.index(obj) #groupidx
def searchTool(obj, tools):
for tool in tools:
if obj in tool:
return tool
'''class _CommandCalculateEarthworks:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "pico.svg")),
'Accel': "C, E",
'MenuText': QT_TRANSLATE_NOOP("Placement", "Movimiento de tierras"),
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el movimiento de tierras")}
def Activated(self):
TaskPanel = _EarthWorksTaskPanel()
FreeCADGui.Control.showDialog(TaskPanel)
def IsActive(self):
active = not (FreeCAD.ActiveDocument is None)
if not (FreeCAD.ActiveDocument.getObject("Terrain") is None):
active = active and not (FreeCAD.ActiveDocument.getObject("Terrain").Mesh is None)
return active
if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantEarthworks', _CommandCalculateEarthworks())'''
def accept():
import MeshPart as mp
land = FreeCAD.ActiveDocument.Terrain.Mesh
frames = []
for obj in FreeCADGui.Selection.getSelection():
if hasattr(obj, "Proxy"):
if obj.Proxy.Type == "Tracker":
if not (obj in frames):
frames.append(obj)
elif obj.Proxy.Type == "FrameArea":
for fr in obj.Frames:
if not (fr in frames):
frames.append(fr)
if len(frames) == 0:
return False
FreeCAD.ActiveDocument.openTransaction("Calcular movimiento de tierras")
import PVPlantPlacement
rows, columns = PVPlantPlacement.getRows(frames)
if (rows is None) or (columns is None):
print("Nada que procesar")
return False
tools = []
for group in rows:
lines = []
cont = 0
while cont < len(group):
aw = 0
if cont > 0:
p0 = FreeCAD.Vector(group[cont - 1].Placement.Base)
p1 = FreeCAD.Vector(group[cont].Placement.Base)
aw = getAngle(p0, p1)
ae = 0
if cont < (len(group) - 1):
p1 = FreeCAD.Vector(group[cont].Placement.Base)
p2 = FreeCAD.Vector(group[cont + 1].Placement.Base)
ae = getAngle(p1, p2)
lng = int(group[cont].Setup.Length / 2)
wdt = int(group[cont].Setup.Width / 2)
line = Part.LineSegment(FreeCAD.Vector(-lng, 0, 0),
FreeCAD.Vector(lng, 0, 0)).toShape()
line = Part.LineSegment(FreeCAD.Vector(group[cont].Setup.Shape.SubShapes[1].SubShapes[0].SubShapes[0].Placement.Base.x, 0, 0),
FreeCAD.Vector(group[cont].Setup.Shape.SubShapes[1].SubShapes[0].SubShapes[-1].Placement.Base.x, 0, 0)).toShape()
anf = (aw + ae) / 2
if anf > FreeCAD.ActiveDocument.MaximumWestEastSlope.Value:
anf = FreeCAD.ActiveDocument.MaximumWestEastSlope.Value
zz = wdt * math.sin(math.radians(anf))
li = line.copy()
li.Placement = group[cont].Placement
li.Placement.Rotation = group[cont].Placement.Rotation
li.Placement.Base.x -= wdt #+ (3000 if cont == 0 else 0))
li.Placement.Base.z -= zz
lines.append(li)
ld = line.copy()
ld.Placement = group[cont].Placement
ld.Placement.Rotation = group[cont].Placement.Rotation
ld.Placement.Base.x += wdt #+ (3000 if cont == len(group) - 1 else 0))
ld.Placement.Base.z += zz
lines.append(ld)
tools.append([group[cont], li, ld])
cont += 1
loft = Part.makeLoft(lines, False, True, False)
import MeshPart as mp
msh = mp.meshFromShape(Shape=loft) #, MaxLength=1)
#msh = msh.smooth("Laplace", 3)
import Mesh
Mesh.show(msh)
'''intersec = land.section(msh, MinDist=0.01)
import Draft
for sec in intersec:
Draft.makeWire(sec)'''
for group in rows:
lines = []
for frame in group:
col, idx = searchFrameInColumns(frame, columns)
tool = searchTool(frame, tools)
if idx == 0:
''' '''
if idx == (len(col) - 1):
''' '''
if (idx + 1) < len(col):
frame1 = col[idx + 1]
tool1 = searchTool(frame1, tools)
line = Part.LineSegment(tool[1].Vertexes[1].Point, tool1[1].Vertexes[0].Point).toShape()
Part.show(line)
lines.append(line)
line = Part.LineSegment(tool[2].Vertexes[1].Point, tool1[2].Vertexes[0].Point).toShape()
Part.show(line)
lines.append(line)
if len(lines) > 0:
loft = Part.makeLoft(lines, False, True, False)
import MeshPart as mp
msh = mp.meshFromShape(Shape=loft) # , MaxLength=1)
#msh = msh.smooth("Laplace", 3)
import Mesh
Mesh.show(msh)
intersec = land.section(msh, MinDist=0.01)
import Draft
for sec in intersec:
Draft.makeWire(sec)
FreeCAD.ActiveDocument.commitTransaction()
self.closeForm()
return True
+71 -10
View File
@@ -58,8 +58,9 @@ class MapWindow(QtGui.QWidget):
self.setupUi()
def setupUi(self):
from PySide2.QtWebEngineWidgets import QWebEngineView
from PySide2.QtWebChannel import QWebChannel
# Intentar cargar QtWebEngine (no siempre disponible, ej: FreeCAD flatpak)
QWebEngineView, QWebChannel = self._load_webengine()
self._webengine_available = QWebEngineView is not None
self.ui = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantGeoreferencing.ui", self)
@@ -86,6 +87,7 @@ class MapWindow(QtGui.QWidget):
self.layout.addWidget(RightWidget)
# Left Widgets:
if self._webengine_available:
# -- Search Bar:
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Search")
@@ -100,7 +102,7 @@ class MapWindow(QtGui.QWidget):
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
# -- Webbroser:
# -- Web browser:
self.view = QWebEngineView()
self.channel = QWebChannel(self.view.page())
self.view.page().setWebChannel(self.channel)
@@ -109,13 +111,30 @@ class MapWindow(QtGui.QWidget):
self.view.page().loadFinished.connect(self.onLoadFinished)
self.view.page().load(QtCore.QUrl.fromLocalFile(file))
LeftLayout.addWidget(self.view)
# self.layout.addWidget(self.view, 1, 0, 1, 3)
else:
# -- Modo manual: entrada de coordenadas sin mapa web
self.valueSearch = QtGui.QLineEdit(self)
self.valueSearch.setPlaceholderText("Latitud, Longitud (ej: 40.4168, -3.7038)")
self.valueSearch.returnPressed.connect(self.onManualCoords)
searchbutton = QtGui.QPushButton('Ir')
searchbutton.setFixedWidth(80)
searchbutton.clicked.connect(self.onManualCoords)
SearchBarLayout = QtGui.QHBoxLayout(self)
SearchBarLayout.addWidget(self.valueSearch)
SearchBarLayout.addWidget(searchbutton)
LeftLayout.addLayout(SearchBarLayout)
info = QtGui.QLabel("Mapa web no disponible. Introduce coordenadas manualmente.")
info.setStyleSheet("color: #888; font-style: italic; padding: 20px;")
info.setAlignment(QtCore.Qt.AlignCenter)
LeftLayout.addWidget(info)
# -- Latitud y longitud:
self.labelCoordinates = QtGui.QLabel()
self.labelCoordinates.setFixedHeight(21)
LeftLayout.addWidget(self.labelCoordinates)
# self.layout.addWidget(self.labelCoordinates, 2, 0, 1, 3)
# Right Widgets:
labelKMZ = QtGui.QLabel()
@@ -139,9 +158,6 @@ class MapWindow(QtGui.QWidget):
radio3 = QtGui.QRadioButton("Datos GPS")
radio1.setChecked(True)
# buttonDialog = QtGui.QPushButton('...')
# buttonDialog.setEnabled(False)
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(radio1)
vbox.addWidget(radio2)
@@ -149,7 +165,6 @@ class MapWindow(QtGui.QWidget):
self.groupbox.setLayout(vbox)
RightLayout.addWidget(self.groupbox)
# ------------------------
self.checkboxImportGis = QtGui.QCheckBox("Importar datos GIS")
RightLayout.addWidget(self.checkboxImportGis)
@@ -174,6 +189,52 @@ class MapWindow(QtGui.QWidget):
with open(file, 'r') as f:
frame.runJavaScript(f.read())
def _load_webengine(self):
"""Intenta cargar QWebEngineView desde cualquier versión de PySide.
Retorna (QWebEngineView_class, QWebChannel_class) o (None, None)."""
for modpath in [
'PySide6.QtWebEngineWidgets',
'PySide6.QtWebEngineCore',
'PySide6.QtWebEngineQuick',
'PySide2.QtWebEngineWidgets',
'PySide.QtWebEngineWidgets',
]:
try:
parts = modpath.split('.')
mod = __import__(parts[0], fromlist=parts[1:])
for p in parts[1:]:
mod = getattr(mod, p)
View = getattr(mod, 'QWebEngineView', None)
Channel = getattr(mod, 'QWebChannel', None)
if View is not None:
return View, Channel
except (ImportError, AttributeError):
continue
# Fallback: intentar por separado QtWebChannel (sí existe en flatpak)
try:
from PySide6.QtWebChannel import QWebChannel as Channel
except ImportError:
Channel = None
FreeCAD.Console.PrintWarning(
"PVPlantGeoreferencing: QtWebEngine no disponible. "
"Usando modo manual de coordenadas.\n")
return None, Channel
def onManualCoords(self):
"""Procesa entrada manual de latitud,longitud"""
text = self.valueSearch.text().strip()
if not text:
return
try:
parts = text.replace(',', ' ').split()
lat = float(parts[0])
lon = float(parts[1])
self.georeference_coordinates = {'lat': lat, 'lon': lon}
self.labelCoordinates.setText(f"{lat:.6f}, {lon:.6f}")
FreeCAD.Console.PrintMessage(f"Coordenadas: {lat:.6f}, {lon:.6f}\n")
except (ValueError, IndexError):
FreeCAD.Console.PrintError("Formato inválido. Usa: latitud, longitud\n")
def onSearch(self):
if self.valueSearch.text() == "":
return
@@ -265,7 +326,7 @@ class MapWindow(QtGui.QWidget):
for item in items['features']:
if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle:
coord = item['geometry']['coordinates']
point = ImportElevation.getElevationFromOE([[coord[0], coord[1]],])
point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],])
c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset)
if item['properties'].get('radius'):
r = round(item['properties']['radius'] * 1000, 0)
+58 -22
View File
@@ -39,6 +39,51 @@ import os
from PVPlantResources import DirIcons as DirIcons
import PVPlantSite
# ---------------------------------------------------------------------------
# Adaptador UTM: emula la API de la librería 'utm' usando pyproj
# La librería 'utm' dejó de usarse en favor de pyproj (más completa y mantenida).
# from_latlon(lat, lon) -> (easting, northing, zone_number, zone_letter)
# to_latlon(easting, northing, zone_number, zone_letter) -> (lat, lon)
# ---------------------------------------------------------------------------
_utm_cache = {}
def _get_transformer(lat, lon):
"""Obtiene o crea un transformador UTM para las coordenadas dadas."""
from pyproj import Transformer
zone = int((lon + 180) / 6) + 1
hem = 'S' if lat < 0 else 'N'
key = (zone, hem)
if key not in _utm_cache:
crs_utm = f'+proj=utm +zone={zone} +{hem.lower()} +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
_utm_cache[key] = Transformer.from_crs('EPSG:4326', crs_utm, always_xy=True)
return _utm_cache[key], zone, hem
def from_latlon(lat, lon):
"""Convierte (lat, lon) a UTM. Retorna (easting, northing, zone_number, zone_letter)."""
transformer, zone, hem = _get_transformer(lat, lon)
easting, northing = transformer.transform(lon, lat)
return (easting, northing, zone, hem)
def to_latlon(easting, northing, zone_number, zone_letter):
"""Convierte UTM a (lat, lon)."""
from pyproj import Transformer
hem = zone_letter.upper()
key = (zone_number, hem)
if key not in _utm_cache:
crs_utm = f'+proj=utm +zone={zone_number} +{hem.lower()} +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
_utm_cache[key] = Transformer.from_crs(crs_utm, 'EPSG:4326', always_xy=True)
lon, lat = _utm_cache[key].transform(easting, northing)
return (lat, lon)
# Parche: reemplazar el módulo 'utm' por nuestro adaptador
import sys
class _UTMWrapper:
"""Wrapper para que 'import utm' devuelva nuestras funciones."""
from_latlon = staticmethod(from_latlon)
to_latlon = staticmethod(to_latlon)
sys.modules['utm'] = _UTMWrapper
# ---------------------------------------------------------------------------
def get_elevation_from_oe(coordinates): # v1 deepseek
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.
@@ -52,7 +97,6 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
return []
import requests
import utm
from requests.exceptions import RequestException
# Construcción más eficiente de parámetros
@@ -68,7 +112,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
response.raise_for_status() # Lanza excepción para códigos 4xx/5xx
except RequestException as e:
print(f"Error en la solicitud: {str(e)}")
print(f"Error en la solicitud: {e}")
return []
try:
@@ -95,7 +139,7 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
round(result["elevation"])) * 1000)
except Exception as e:
print(f"Error procesando coordenadas: {str(e)}")
print(f"Error procesando coordenadas: {e}")
continue
return points
@@ -110,7 +154,6 @@ def getElevationFromOE(coordinates):
return None
from requests import get
import utm
locations_str=""
total = len(coordinates) - 1
@@ -119,20 +162,9 @@ def getElevationFromOE(coordinates):
if i != total:
locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
points = []
try:
r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
except RequestException as e:
points = []
for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1])
points.append(FreeCAD.Vector(round(c[0], 0),
round(c[1], 0),
0) * 1000)
return points
# Only get the json response in case of 200 or 201
points = []
if r.status_code == 200 or r.status_code == 201:
results = r.json()
for point in results["results"]:
c = utm.from_latlon(point["latitude"], point["longitude"])
@@ -140,11 +172,18 @@ def getElevationFromOE(coordinates):
round(c[1], 0),
round(point["elevation"], 0)) * 1000
points.append(v)
except RequestException as e:
# print(f"Error en la solicitud: {str(e)}")
for i, point in enumerate(coordinates):
c = utm.from_latlon(point[0], point[1])
points.append(FreeCAD.Vector(round(c[0], 0),
round(c[1], 0),
0) * 1000)
return points
def getSinglePointElevationFromBing(lat, lng):
#http://dev.virtualearth.net/REST/v1/Elevation/List?points={lat1,long1,lat2,long2,latN,longnN}&heights={heights}&key={BingMapsAPIKey}
import utm
source = "http://dev.virtualearth.net/REST/v1/Elevation/List?points="
source += str(lat) + "," + str(lng)
@@ -169,7 +208,6 @@ def getSinglePointElevationFromBing(lat, lng):
def getGridElevationFromBing(polygon, lat, lng, resolution = 1000):
#http://dev.virtualearth.net/REST/v1/Elevation/Polyline?points=35.89431,-110.72522,35.89393,-110.72578,35.89374,-110.72606,35.89337,-110.72662
# &heights=ellipsoid&samples=10&key={BingMapsAPIKey}
import utm
import math
import requests
@@ -314,7 +352,6 @@ def getSinglePointElevationUtm(lat, lon):
res = s['results']
print (res)
import utm
for r in res:
c = utm.from_latlon(r['location']['lat'], r['location']['lng'])
v = FreeCAD.Vector(
@@ -326,7 +363,6 @@ def getSinglePointElevationUtm(lat, lon):
def getElevationUTM(polygon, lat, lng, resolution = 10000):
import utm
geo = utm.from_latlon(lat, lng)
# result = (679434.3578335291, 4294023.585627955, 30, 'S')
# EASTING, NORTHING, ZONE NUMBER, ZONE LETTER
@@ -395,7 +431,7 @@ def getElevation1(polygon,resolution=10):
s = json.loads(ans)
res = s['results']
except:
except (json.JSONDecodeError, KeyError):
continue
#points = []
@@ -529,7 +565,7 @@ class _ImportPointsTaskPanel:
try:
PointGroups = FreeCAD.ActiveDocument.Point_Groups
except:
except AttributeError:
PointGroups = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Point_Groups')
PointGroups.Label = "Point Groups"
+418 -895
View File
File diff suppressed because it is too large Load Diff
+330 -322
View File
@@ -15,6 +15,318 @@
<string>Park Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" alignment="Qt::AlignmentFlag::AlignTop">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Estructura:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QWidget" name="widget_2" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="buttonPVArea">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Configuración</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="horizontalSpacing">
<number>10</number>
</property>
<property name="verticalSpacing">
<number>6</number>
</property>
<item row="8" column="1">
<widget class="QComboBox" name="comboDirV">
<item>
<property name="text">
<string>De arriba a abajo</string>
</property>
</item>
<item>
<property name="text">
<string>De abajo a arriba</string>
</property>
</item>
<item>
<property name="text">
<string>Del centro a los lados</string>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_2">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Dirección Horizontal</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="comboDirH">
<item>
<property name="text">
<string>De izquierda a derecha</string>
</property>
</item>
<item>
<property name="text">
<string>De derecha a izquiera</string>
</property>
</item>
<item>
<property name="text">
<string>De centro a los lados</string>
</property>
</item>
</widget>
</item>
<item row="11" column="1">
<widget class="QCheckBox" name="cbAlignFrames">
<property name="text">
<string>Alinear estructuras</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="editGapRows">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> mm</string>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>500</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Pitch</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QDoubleSpinBox" name="editOffsetHorizontal">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000.000000000000000</double>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Orientación</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_7">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Offset Horizontal</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_9">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Offset Vertical</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QDoubleSpinBox" name="editOffsetVertical">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000.000000000000000</double>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="editGapCols">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>100.000000000000000</double>
</property>
<property name="value">
<double>5.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboOrientation">
<item>
<property name="text">
<string>Norte - Sur</string>
</property>
</item>
<item>
<property name="text">
<string>Este - Oeste</string>
</property>
</item>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_11">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Dirección Vertical</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Espacio entre filas</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="editInnerSpacing">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string> - Inner Spacing</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QGroupBox" name="groupCorridor">
<property name="title">
@@ -59,10 +371,10 @@
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="editRowGap">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="prefix">
<string/>
@@ -110,10 +422,10 @@
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="editColGap">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="prefix">
<string/>
@@ -135,10 +447,10 @@
<item row="2" column="1">
<widget class="QSpinBox" name="editRowCount">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="value">
<number>4</number>
@@ -148,10 +460,10 @@
<item row="0" column="1">
<widget class="QSpinBox" name="editColCount">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="value">
<number>8</number>
@@ -161,45 +473,6 @@
</layout>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="buttonPVArea">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QWidget" name="widget_2" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</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>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -207,282 +480,9 @@
</property>
</widget>
</item>
<item row="7" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editPVArea"/>
</item>
<item row="5" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Configuración</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="horizontalSpacing">
<number>10</number>
</property>
<property name="verticalSpacing">
<number>6</number>
</property>
<item row="8" column="0">
<widget class="QLabel" name="label_9">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Offset Vertical</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Orientación</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="cbAlignFrames">
<property name="text">
<string>Alinear estructuras</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Espacio entre filas</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_2">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Dirección Horizontal</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Offset Horizontal</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Pitch</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_11">
<property name="maximumSize">
<size>
<width>120</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Dirección Vertical</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboOrientation">
<item>
<property name="text">
<string>Norte - Sur</string>
</property>
</item>
<item>
<property name="text">
<string>Este - Oeste</string>
</property>
</item>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="editGapCols">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>100.000000000000000</double>
</property>
<property name="value">
<double>5.000000000000000</double>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="comboDirH">
<item>
<property name="text">
<string>De izquierda a derecha</string>
</property>
</item>
<item>
<property name="text">
<string>De derecha a izquiera</string>
</property>
</item>
<item>
<property name="text">
<string>De centro a los lados</string>
</property>
</item>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="comboDirV">
<item>
<property name="text">
<string>De arriba a abajo</string>
</property>
</item>
<item>
<property name="text">
<string>De abajo a arriba</string>
</property>
</item>
<item>
<property name="text">
<string>Del centro a los lados</string>
</property>
</item>
</widget>
</item>
<item row="7" column="1">
<widget class="QDoubleSpinBox" name="editOffsetHorizontal">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000.000000000000000</double>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QDoubleSpinBox" name="editOffsetVertical">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> m</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000.000000000000000</double>
</property>
<property name="maximum">
<double>10000.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="editGapRows">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> mm</string>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>500</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" alignment="Qt::AlignTop">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Estructura:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QListWidget" name="listFrameSetups">
<property name="maximumSize">
@@ -493,11 +493,19 @@
</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>
</widget>
<tabstops>
<tabstop>buttonAddFrame</tabstop>
<tabstop>buttonRemoveFrame</tabstop>
<tabstop>editPVArea</tabstop>
<tabstop>buttonPVArea</tabstop>
<tabstop>comboOrientation</tabstop>
+274 -522
View File
@@ -1,315 +1,298 @@
# /**********************************************************************
# * *
# * Copyright (c) 2021-2026 Javier Braña <javier.branagutierrez@gmail.com>*
# * *
# * PVPlant Road - Sistema de carreteras con alineamiento profesional *
# * Basado en ejes (Alignment) con estaciones, perfiles y cubicación. *
# * *
# ***********************************************************************
import FreeCAD
import ArchComponent
import Part
import math
import numpy as np
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore
from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP
import Part
import os
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
__title__ = "PVPlant Road"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
def translate(ctxt, txt): return txt
def QT_TRANSLATE_NOOP(ctxt, txt): return txt
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
from Civil.Alignment import make_alignment_from_wire
def makeRoad(base=None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Road")
def makeRoad(base=None, alignment=None):
"""Crea un objeto Road con o sin alignment."""
doc = FreeCAD.ActiveDocument
obj = doc.addObject("Part::FeaturePython", "Road")
_Road(obj)
_ViewProviderRoad(obj.ViewObject)
obj.Base = base
from Project.Area import PVPlantArea
offset = PVPlantArea.makeOffsetArea(obj, 4000)
PVPlantArea.makeProhibitedArea(offset)
obj.Alignment = alignment
doc.recompute()
return obj
class _Road(ArchComponent.Component):
"""Carretera con alineamiento horizontal+vertical y secciones multicapa."""
def __init__(self, obj):
# Definición de Variables:
ArchComponent.Component.__init__(self, obj)
self.obj = obj
self.setProperties(obj)
self.Type = "Road"
obj.Proxy = self
self.route = False
obj.IfcType = "Civil Element" ## puede ser: Cable Carrier Segment
obj.IfcType = "Civil Element"
obj.setEditorMode("IfcType", 1)
self.count = 0
def setProperties(self, obj):
# Definicion de Propiedades:
'''[
'App::PropertyBool',
'App::PropertyBoolList',
'App::PropertyFloat',
'App::PropertyFloatList',
'App::PropertyFloatConstraint',
'App::PropertyPrecision',
'App::PropertyQuantity',
'App::PropertyQuantityConstraint',
'App::PropertyAngle',
'App::PropertyDistance',
'App::PropertyLength',
'App::PropertyArea',
'App::PropertyVolume',
'App::PropertyFrequency',
'App::PropertySpeed',
'App::PropertyAcceleration',
'App::PropertyForce',
'App::PropertyPressure',
'App::PropertyVacuumPermittivity',
'App::PropertyInteger',
'App::PropertyIntegerConstraint',
'App::PropertyPercent',
'App::PropertyEnumeration',
'App::PropertyIntegerList',
'App::PropertyIntegerSet',
'App::PropertyMap',
'App::PropertyString',
'App::PropertyPersistentObject',
'App::PropertyUUID',
'App::PropertyFont',
'App::PropertyStringList',
'p::PropertyLink',
'App::PropertyLinkChild',
'App::PropertyLinkGlobal',
'App::PropertyLinkHidden',
'App::PropertyLinkSub',
'App::PropertyLinkSubChild',
'App::PropertyLinkSubGlobal',
'App::PropertyLinkSubHidden',
'App::PropertyLinkList',
'App::PropertyLinkListChild',
'App::PropertyLinkListGlobal',
'App::PropertyLinkListHidden',
'App::PropertyLinkSubList',
'App::PropertyLinkSubListChild',
'App::PropertyLinkSubListGlobal',
'App::PropertyLinkSubListHidden',
'App::PropertyXLink',
'App::PropertyXLinkSub',
'App::PropertyXLinkSubList',
'App::PropertyXLinkList',
'App::PropertyMatrix',
'App::PropertyVector',
'App::PropertyVectorDistance',
'App::PropertyPosition',
'App::PropertyDirection',
'App::PropertyVectorList',
'App::PropertyPlacement',
'App::PropertyPlacementList',
'App::PropertyPlacementLink',
'App::PropertyColor',
'App::PropertyColorList',
'App::PropertyMaterial',
'App::PropertyMaterialList',
'App::PropertyPath',
'App::PropertyFile',
'App::PropertyFileIncluded',
'App::PropertyPythonObject',
'App::PropertyExpressionEngine',
'Part::PropertyPartShape',
'Part::PropertyGeometryList',
'Part::PropertyShapeHistory',
'Part::PropertyFilletEdges',
'Mesh::PropertyNormalList',
'Mesh::PropertyCurvatureList',
'Mesh::PropertyMeshKernel',
'Sketcher::PropertyConstraintList'
]'''
pl = obj.PropertiesList
obj.addProperty("App::PropertyPercent",
"SurfaceSlope",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).SurfaceSlope = 2
# --- Alineamiento ---
if "Alignment" not in pl:
obj.addProperty("App::PropertyLink",
"Alignment", "Road",
"Objeto Alignment que define el eje").Alignment = None
obj.addProperty("App::PropertyPercent",
"SurfaceDrainSlope",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).SurfaceDrainSlope = int(3 / 2 * 100)
obj.addProperty("App::PropertyPercent",
"SubbaseDrainSlope",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).SubbaseDrainSlope = int(2 / 3 * 100)
if "Base" not in pl:
obj.addProperty("App::PropertyLink",
"Base", "Road",
"Wire base (alternativo si no hay Alignment)").Base = None
# --- Geometría transversal ---
if "Width" not in pl:
obj.addProperty("App::PropertyLength",
"Width",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).Width = 4000
"Width", "Road",
"Ancho total de la carretera").Width = 4000
if "PavementThickness" not in pl:
obj.addProperty("App::PropertyLength",
"Height",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).Height = 250
"PavementThickness", "Road",
"Espesor del pavimento").PavementThickness = 250
if "BaseThickness" not in pl:
obj.addProperty("App::PropertyLength",
"Subbase",
"Road",
QT_TRANSLATE_NOOP("App::Property", "Connection")).Subbase = 400
"BaseThickness", "Road",
"Espesor de la base").BaseThickness = 200
if "SubbaseThickness" not in pl:
obj.addProperty("App::PropertyLength",
"SubbaseThickness", "Road",
"Espesor de la subbase").SubbaseThickness = 300
if "ShoulderWidth" not in pl:
obj.addProperty("App::PropertyLength",
"ShoulderWidth", "Road",
"Ancho del arcén cada lado").ShoulderWidth = 500
if "CrossSlope" not in pl:
obj.addProperty("App::PropertyPercent",
"CrossSlope", "Road",
"Pendiente transversal del pavimento (%)").CrossSlope = 2
if "DitchSlope" not in pl:
obj.addProperty("App::PropertyPercent",
"DitchSlope", "Road",
"Pendiente del drenaje (%)").DitchSlope = 3
# --- Estaciones y cubicación ---
if "StationInterval" not in pl:
obj.addProperty("App::PropertyLength",
"StationInterval", "Road",
"Intervalo entre estaciones de cálculo").StationInterval = 20000
if "NumberOfStations" not in pl:
obj.addProperty("App::PropertyInteger",
"NumberOfStations", "Road",
"Número de estaciones calculadas").NumberOfStations = 0
obj.setEditorMode("NumberOfStations", 1)
if "CutVolume" not in pl:
obj.addProperty("App::PropertyVolume",
"CutVolume", "Road",
"Volumen de desmonte (corte)").CutVolume = 0
obj.setEditorMode("CutVolume", 1)
if "FillVolume" not in pl:
obj.addProperty("App::PropertyVolume",
"FillVolume", "Road",
"Volumen de terraplén (relleno)").FillVolume = 0
obj.setEditorMode("FillVolume", 1)
if "TotalLength" not in pl:
obj.addProperty("App::PropertyLength",
"TotalLength", "Road",
"Longitud total del eje").TotalLength = 0
obj.setEditorMode("TotalLength", 1)
def onDocumentRestored(self, obj):
"""Method run when the document is restored.
Re-adds the Arch component, and object properties."""
ArchComponent.Component.onDocumentRestored(self, obj)
self.obj = obj
self.Type = "Road"
obj.Proxy = self
def _get_alignment_wire(self, obj):
"""Devuelve el wire base (desde Alignment o Base)."""
if obj.Alignment and obj.Alignment.SourceWire:
return obj.Alignment.SourceWire.Shape
if obj.Base:
return obj.Base.Shape
return None
def _generate_cross_section(self, obj, station_point, tangent):
"""
Genera el perfil transversal en un punto del eje.
Returns:
list of Part.Wire: [pavimento, base, subbase, arcén_izq, arcén_der]
"""
# Ancho medio carril
hw = obj.Width.Value / 2
sw = obj.ShoulderWidth.Value
cs = obj.CrossSlope / 100 # pendiente transversal (decimal)
ds = obj.DitchSlope / 100
pt = obj.PavementThickness.Value
bt = obj.BaseThickness.Value
sbt = obj.SubbaseThickness.Value
# Vector perpendicular (horizontal) al eje
perp = FreeCAD.Vector(-tangent.y, tangent.x, 0)
perp.normalize()
# Puntos del pavimento (sección transversal con bombeo)
# Centro del eje
center = station_point
# Borde izquierdo pavimento
left_edge = center + perp * (-hw)
right_edge = center + perp * hw
# Con pendiente transversal: el centro más alto
left_top = FreeCAD.Vector(left_edge.x, left_edge.y, center.z - hw * cs)
right_top = FreeCAD.Vector(right_edge.x, right_edge.y, center.z - hw * cs)
center_top = center
# Borde inferior pavimento
left_bot = FreeCAD.Vector(left_top.x, left_top.y, left_top.z - pt)
right_bot = FreeCAD.Vector(right_top.x, right_top.y, right_top.z - pt)
center_bot = FreeCAD.Vector(center_top.x, center_top.y, center_top.z - pt)
# Arcén (más ancho, misma pendiente o ligeramente mayor)
shoulder_left = FreeCAD.Vector(left_edge.x - sw, left_edge.y - sw * 0, left_top.z - sw * cs * 0.5)
shoulder_right = FreeCAD.Vector(right_edge.x + sw, right_edge.y + sw * 0, right_top.z - sw * cs * 0.5)
# Base (ligeiramente más ancha)
base_extra = 200 # mm extra cada lado
bl = FreeCAD.Vector(left_bot.x - base_extra, left_bot.y, left_bot.z)
br = FreeCAD.Vector(right_bot.x + base_extra, right_bot.y, right_bot.z)
bc = FreeCAD.Vector(center_bot.x, center_bot.y, center_bot.z)
bl_bot = FreeCAD.Vector(bl.x, bl.y, bl.z - bt)
br_bot = FreeCAD.Vector(br.x, br.y, br.z - bt)
# Subbase (aún más ancha)
sbl = FreeCAD.Vector(bl.x - base_extra, bl.y, bl.z)
sbr = FreeCAD.Vector(br.x + base_extra, br.y, br.z)
sbl_bot = FreeCAD.Vector(sbl.x, sbl.y, sbl.z - sbt)
sbr_bot = FreeCAD.Vector(sbr.x, sbr.y, sbr.z - sbt)
# Construir wires de cada capa
# Pavimento
pave = Part.makePolygon([left_top, center_top, right_top, right_bot, center_bot, left_bot, left_top])
# Base
base = Part.makePolygon([bl, bc, br, br_bot, bc - FreeCAD.Vector(0, 0, bt), bl_bot, bl])
# Subbase
subbase = Part.makePolygon([sbl, sbl + FreeCAD.Vector(0, 0, -sbt),
sbr + FreeCAD.Vector(0, 0, -sbt), sbr,
sbl])
return [pave, base, subbase]
def execute(self, obj):
import Part, math
w = obj.Base.Shape
profiles = []
SurfaceDrainSlope = obj.SurfaceDrainSlope / 100
SubbaseDrainSlope = obj.SubbaseDrainSlope / 100
vec_up_left = FreeCAD.Vector(-obj.Width.Value / 2, 0, obj.Height.Value)
vec_up_center = FreeCAD.Vector(0, 0, obj.SurfaceSlope * obj.Width.Value / 200 + obj.Height.Value)
vec_up_right = FreeCAD.Vector(obj.Width.Value / 2, 0, obj.Height.Value)
vec_down_left = FreeCAD.Vector(-(obj.Width.Value / 2 + obj.Height.Value / SurfaceDrainSlope), 0, 0)
vec_down_right = FreeCAD.Vector((obj.Width.Value / 2 + obj.Height.Value / SurfaceDrainSlope), 0, 0)
vec_sand_left = FreeCAD.Vector(-(obj.Width.Value / 2 + obj.Height.Value * (1 / SurfaceDrainSlope + SubbaseDrainSlope)), 0, - obj.Subbase.Value)
vec_sand_right = FreeCAD.Vector((obj.Width.Value / 2 + obj.Height.Value * (1 / SurfaceDrainSlope + SubbaseDrainSlope)), 0, - obj.Subbase.Value)
edge1 = Part.makeLine(vec_down_left, vec_down_right)
edge2 = Part.makeLine(vec_down_right, vec_up_right)
edge3 = Part.makeLine(vec_up_right, vec_up_center)
edge4 = Part.makeLine(vec_up_center, vec_up_left)
edge5 = Part.makeLine(vec_up_left, vec_down_left)
edge6 = Part.makeLine(vec_sand_left, vec_sand_right)
edge7 = Part.makeLine(vec_sand_left, vec_down_left)
edge8 = Part.makeLine(vec_sand_right, vec_down_right)
p = Part.Wire([edge1, edge2, edge3, edge4, edge5])
profiles.append(p)
p = Part.Wire([edge6, edge8, edge1, edge7])
profiles.append(p)
shapes = self.makeSolids(obj, profiles, w, (vec_down_right + vec_down_left) / 2)
angle = 30
height = FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax - obj.Height.Value
offset = height / math.tan(math.radians(angle))
'''cutProfile = Part.makePolygon([vec_sand_left, vec_sand_right, vec_sand_right + FreeCAD.Vector(offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax),
vec_sand_left + FreeCAD.Vector(-offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax), vec_sand_left])
height = obj.Height.Value - FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin
offset = height / math.tan(math.radians(angle))
fillProfile = Part.makePolygon([vec_sand_left, vec_sand_right, vec_sand_right + FreeCAD.Vector(offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin),
vec_sand_left + FreeCAD.Vector(-offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin), vec_sand_left])
cutshapes, fillshapes = self.makeSolids(obj, [cutProfile, fillProfile], w, (vec_up_right + vec_up_left) / 2)
cuts = self.calculateCut(obj, cutshapes)
fills = self.calculateFill(obj, fillshapes)
if cuts:
for cut in cuts:
Part.show(cut, "RoadCut")
if fills:
for fill in fills:
Part.show(fill, "RoadFill")'''
obj.Shape = Part.makeCompound(shapes)
def makeSolids(self, obj, profiles, w, origen):
import Draft
import DraftGeomUtils
shapes = []
for p in profiles:
if hasattr(p, "CenterOfMass"):
c = p.CenterOfMass
else:
c = p.BoundBox.Center
c = origen
delta = w.Vertexes[0].Point - c
p.translate(delta)
if Draft.getType(obj.Base) == "BezCurve":
v1 = obj.Base.Placement.multVec(obj.Base.Points[1]) - w.Vertexes[0].Point
else:
v1 = w.Vertexes[1].Point - w.Vertexes[0].Point
v2 = DraftGeomUtils.getNormal(p)
rot = FreeCAD.Rotation(v2, v1)
#p.rotate(w.Vertexes[0].Point, rot.Axis, math.degrees(rot.Angle))
ang = rot.toEuler()[0]
p.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), ang)
if p.Faces:
for f in p.Faces:
sh = w.makePipeShell([f.OuterWire], True, False, 2)
for shw in f.Wires:
if shw.hashCode() != f.OuterWire.hashCode():
sh2 = w.makePipeShell([shw], True, False, 2)
sh = sh.cut(sh2)
shapes.append(sh)
elif p.Wires:
for pw in p.Wires:
sh = w.makePipeShell([pw], True, False, 2)
shapes.append(sh)
return shapes
def calculateFill(self, obj, solid):
import BOPTools.SplitAPI as splitter
common = solid.common(FreeCAD.ActiveDocument.Site.Terrain.Shape)
if common.Area > 0:
sp = splitter.slice(solid, [common, ], "Split")
common.Placement.Base.z += 1
solids = []
for sol in sp.Solids:
common1 = sol.common(common)
if common1.Area > 0:
solids.append(sol)
if len(solids) > 0:
return solids
return None
def calculateCut(self, obj, solid):
import BOPTools.SplitAPI as splitter
common = solid.common(FreeCAD.ActiveDocument.Site.Terrain.Shape)
if common.Area > 0:
sp = splitter.slice(solid, [common, ], "Split")
shells = []
commoncopy = common.copy()
commoncopy.Placement.Base.z -= 1
for sol in sp.Solids:
common1 = sol.common(commoncopy)
if common1.Area > 0:
shell = sol.Shells[0]
shell = shell.cut(common)
shells.append(shell)
if len(shells) > 0:
return shells
return None
def makeLoft(self, profile):
"""Genera el sólido 3D de la carretera por extrusión de secciones."""
wire = self._get_alignment_wire(obj)
if not wire:
return
total_len = wire.Length
obj.TotalLength = total_len
interval = obj.StationInterval.Value
if interval <= 0:
interval = 20000
n_stations = max(2, int(total_len / interval) + 1)
obj.NumberOfStations = n_stations
# Generar el sólido mediante barrido de secciones
shapes = []
cut_volume = 0
fill_volume = 0
for i in range(n_stations):
param = i / (n_stations - 1)
try:
pt = wire.valueAt(wire.getParameterByLength(param * total_len))
tangent = wire.tangentAt(wire.getParameterByLength(param * total_len))
except Exception:
continue
sections = self._generate_cross_section(obj, pt, tangent)
# Barrer cada sección a lo largo del eje (versión simplificada)
for sec in sections:
try:
# Extrusión simple a lo largo del eje
# En una implementación completa: makePipeShell
shape = sec.extrude(FreeCAD.Vector(0, 0, 1))
if shape and not shape.isNull():
shapes.append(shape)
except Exception:
pass
if shapes:
try:
compound = Part.makeCompound(shapes)
obj.Shape = compound
# Cubicación contra el terreno
terrain = self._get_terrain(obj)
if terrain:
try:
common = compound.common(terrain.Shape)
if common and not common.isNull():
cut_volume = common.Volume
# Terraplén: volumen del sólido fuera del terreno
fill = compound.cut(terrain.Shape)
if fill and not fill.isNull():
fill_volume = fill.Volume
except Exception:
pass
obj.CutVolume = cut_volume
obj.FillVolume = fill_volume
except Exception:
pass
def _get_terrain(self, obj):
"""Obtiene el terreno desde el Site."""
try:
return FreeCAD.ActiveDocument.Site.Terrain
except Exception:
return None
def __getstate__(self):
return self.Type
def __setstate__(self, state):
if state:
self.Type = state
class _ViewProviderRoad(ArchComponent.ViewProviderComponent):
def __init__(self, vobj):
@@ -318,10 +301,12 @@ class _ViewProviderRoad(ArchComponent.ViewProviderComponent):
def getIcon(self):
return str(os.path.join(PVPlantResources.DirIcons, "road.svg"))
# ---------------------------------------------------------------------------
# TaskPanel para crear carretera interactivamente
# ---------------------------------------------------------------------------
class _RoadTaskPanel:
def __init__(self, obj=None):
if obj is None:
self.new = True
self.obj = makeRoad()
@@ -329,7 +314,8 @@ class _RoadTaskPanel:
self.new = False
self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantRoad.ui"))
self.form = FreeCADGui.PySideUic.loadUi(
os.path.join(PVPlantResources.__dir__, "PVPlantRoad.ui"))
def accept(self):
FreeCADGui.Control.closeDialog()
@@ -342,274 +328,40 @@ class _RoadTaskPanel:
return True
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import DraftVecUtils
import draftutils.utils as utils
import draftutils.gui_utils as gui_utils
import draftutils.todo as todo
import draftguitools.gui_base_original as gui_base_original
import draftguitools.gui_tool_utils as gui_tool_utils
from draftutils.messages import _msg
from draftutils.translate import translate
class _CommandRoad(gui_base_original.Creator):
"""Gui command for the Line tool."""
def __init__(self):
# super(_CommandRoad, self).__init__()
gui_base_original.Creator.__init__(self)
self.path = None
# ---------------------------------------------------------------------------
# Comando para dibujar carretera sobre un wire seleccionado
# ---------------------------------------------------------------------------
class _CommandRoad:
"""Comando para crear carretera seleccionando un wire + generando alignment."""
def GetResources(self):
"""Set icon, menu and tooltip."""
return {'Pixmap': str(os.path.join(DirIcons, "road.svg")),
'MenuText': QtCore.QT_TRANSLATE_NOOP("PVPlantRoad", "Road"),
'MenuText': QT_TRANSLATE_NOOP("PVPlantRoad", "Road"),
'Accel': "C, R",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("PVPlantRoad",
"Creates a Road object from setup dialog.")}
'ToolTip': QT_TRANSLATE_NOOP("PVPlantRoad",
"Crea una carretera con alineamiento profesional.")}
def Activated(self, name=translate("draft", "Line")):
"""Execute when the command is called."""
gui_base_original.Creator.Activated(self, name=translate("draft", "Line"))
self.obj = None # stores the temp shape
self.oldWP = None # stores the WP if we modify it
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
sel = FreeCADGui.Selection.getSelection()
done = False
self.existing = []
if len(sel) > 0:
print("Crear una carretera a lo largo de un trayecto")
# TODO: chequear que el objeto seleccionado sea un "wire"
wire = None
if sel:
import Draft
if Draft.getType(sel[0]) == "Wire":
self.path = sel[0]
done = True
wire = sel[0]
if not done:
self.ui.wireUi(name)
self.ui.setTitle("Road")
self.obj = self.doc.addObject("Part::Feature", self.featureName)
gui_utils.format_object(self.obj)
self.call = self.view.addEventCallback("SoEvent", self.action)
_msg(translate("draft", "Pick first point"))
def action(self, arg):
"""Handle the 3D scene events.
This is installed as an EventCallback in the Inventor view.
Parameters
----------
arg: dict
Dictionary with strings that indicates the type of event received
from the 3D view.
"""
if arg["Type"] == "SoKeyboardEvent" and arg["Key"] == "ESCAPE":
self.finish()
elif arg["Type"] == "SoLocation2Event":
self.point, ctrlPoint, self.info = gui_tool_utils.getPoint(self, arg)
gui_tool_utils.redraw3DView()
elif (arg["Type"] == "SoMouseButtonEvent"
and arg["State"] == "DOWN"
and arg["Button"] == "BUTTON1"):
if arg["Position"] == self.pos:
return self.finish(False, cont=True)
if (not self.node) and (not self.support):
gui_tool_utils.getSupport(arg)
self.point, ctrlPoint, self.info = gui_tool_utils.getPoint(self, arg)
if self.point:
self.point = FreeCAD.Vector(self.info["x"], self.info["y"], self.info["z"])
self.ui.redraw()
self.pos = arg["Position"]
self.node.append(self.point)
self.drawSegment(self.point)
if len(self.node) > 2:
# The wire is closed
if (self.point - self.node[0]).Length < utils.tolerance():
self.undolast()
if len(self.node) > 2:
self.finish(True, cont=True)
if wire:
# Crear alignment desde el wire seleccionado
alignment = make_alignment_from_wire(wire)
road = makeRoad(alignment=alignment)
FreeCAD.Console.PrintMessage(
f"Carretera creada desde '{wire.Label}'. "
f"Alineamiento: {alignment.Label}\n")
else:
self.finish(False, cont=True)
def finish(self, closed=False, cont=False):
"""Terminate the operation and close the polyline if asked.
Parameters
----------
closed: bool, optional
Close the line if `True`.
"""
self.removeTemporaryObject()
if self.oldWP:
App.DraftWorkingPlane = self.oldWP
if hasattr(Gui, "Snapper"):
Gui.Snapper.setGrid()
Gui.Snapper.restack()
self.oldWP = None
if len(self.node) > 1:
if False:
Gui.addModule("Draft")
# The command to run is built as a series of text strings
# to be committed through the `draftutils.todo.ToDo` class.
if (len(self.node) == 2
and utils.getParam("UsePartPrimitives", False)):
# Insert a Part::Primitive object
p1 = self.node[0]
p2 = self.node[-1]
_cmd = 'FreeCAD.ActiveDocument.'
_cmd += 'addObject("Part::Line", "Line")'
_cmd_list = ['line = ' + _cmd,
'line.X1 = ' + str(p1.x),
'line.Y1 = ' + str(p1.y),
'line.Z1 = ' + str(p1.z),
'line.X2 = ' + str(p2.x),
'line.Y2 = ' + str(p2.y),
'line.Z2 = ' + str(p2.z),
'Draft.autogroup(line)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Line"),
_cmd_list)
else:
# Insert a Draft line
rot, sup, pts, fil = self.getStrings()
_base = DraftVecUtils.toString(self.node[0])
_cmd = 'Draft.makeWire'
_cmd += '('
_cmd += 'points, '
_cmd += 'placement=pl, '
_cmd += 'closed=' + str(closed) + ', '
_cmd += 'face=' + fil + ', '
_cmd += 'support=' + sup
_cmd += ')'
_cmd_list = ['pl = FreeCAD.Placement()',
'pl.Rotation.Q = ' + rot,
'pl.Base = ' + _base,
'points = ' + pts,
'line = ' + _cmd,
'Draft.autogroup(line)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Wire"),
_cmd_list)
else:
import Draft
self.path = Draft.makeWire(self.node, closed=False, face=False)
# super(_CommandRoad, self).finish()
gui_base_original.Creator.finish(self)
if self.ui and self.ui.continueMode:
self.Activated()
self.makeRoad()
def makeRoad(self):
makeRoad(self.path)
def removeTemporaryObject(self):
"""Remove temporary object created."""
if self.obj:
try:
old = self.obj.Name
except ReferenceError:
# object already deleted, for some reason
pass
else:
todo.ToDo.delay(self.doc.removeObject, old)
self.obj = None
def undolast(self):
"""Undoes last line segment."""
import Part
if len(self.node) > 1:
self.node.pop()
# last = self.node[-1]
if self.obj.Shape.Edges:
edges = self.obj.Shape.Edges
if len(edges) > 1:
newshape = Part.makePolygon(self.node)
self.obj.Shape = newshape
else:
self.obj.ViewObject.hide()
# DNC: report on removal
# _msg(translate("draft", "Removing last point"))
_msg(translate("draft", "Pick next point"))
def drawSegment(self, point):
"""Draws new line segment."""
import Part
if self.planetrack and self.node:
self.planetrack.set(self.node[-1])
if len(self.node) == 1:
_msg(translate("draft", "Pick next point"))
elif len(self.node) == 2:
last = self.node[len(self.node) - 2]
newseg = Part.LineSegment(last, point).toShape()
self.obj.Shape = newseg
self.obj.ViewObject.Visibility = True
_msg(translate("draft", "Pick next point"))
else:
currentshape = self.obj.Shape.copy()
last = self.node[len(self.node) - 2]
if not DraftVecUtils.equals(last, point):
newseg = Part.LineSegment(last, point).toShape()
newshape = currentshape.fuse(newseg)
self.obj.Shape = newshape
_msg(translate("draft", "Pick next point"))
def wipe(self):
"""Remove all previous segments and starts from last point."""
if len(self.node) > 1:
# self.obj.Shape.nullify() # For some reason this fails
self.obj.ViewObject.Visibility = False
self.node = [self.node[-1]]
if self.planetrack:
self.planetrack.set(self.node[0])
_msg(translate("draft", "Pick next point"))
def orientWP(self):
"""Orient the working plane."""
import DraftGeomUtils
if hasattr(App, "DraftWorkingPlane"):
if len(self.node) > 1 and self.obj:
n = DraftGeomUtils.getNormal(self.obj.Shape)
if not n:
n = App.DraftWorkingPlane.axis
p = self.node[-1]
v = self.node[-2].sub(self.node[-1])
v = v.negative()
if not self.oldWP:
self.oldWP = App.DraftWorkingPlane.copy()
App.DraftWorkingPlane.alignToPointAndAxis(p, n, upvec=v)
if hasattr(Gui, "Snapper"):
Gui.Snapper.setGrid()
Gui.Snapper.restack()
if self.planetrack:
self.planetrack.set(self.node[-1])
def numericInput(self, numx, numy, numz):
"""Validate the entry fields in the user interface.
This function is called by the toolbar or taskpanel interface
when valid x, y, and z have been entered in the input fields.
"""
self.point = App.Vector(numx, numy, numz)
self.node.append(self.point)
self.drawSegment(self.point)
self.ui.setNextFocus()
FreeCAD.Console.PrintWarning(
"Selecciona un Wire (polilínea) para usarlo como eje de carretera.\n")
if FreeCAD.GuiUp:
+35 -41
View File
@@ -182,16 +182,14 @@ def makeSolarDiagram(longitude, latitude, scale=1, complete=False, tz=None):
import ladybug
from ladybug import location
from ladybug import sunpath
except:
# TODO - remove pysolar dependency
# FreeCAD.Console.PrintWarning("Ladybug module not found, using pysolar instead. Warning, this will be deprecated in the future\n")
except ImportError:
ladybug = False
try:
import pysolar
except:
except ImportError:
try:
import Pysolar as pysolar
except:
except ImportError:
FreeCAD.Console.PrintError("The pysolar module was not found. Unable to generate solar diagrams\n")
return None
else:
@@ -361,7 +359,7 @@ def makeWindRose(epwfile, scale=1, sectors=24):
try:
import ladybug
from ladybug import epw
except:
except ImportError:
FreeCAD.Console.PrintError("The ladybug module was not found. Unable to generate solar diagrams\n")
return None
if not epwfile:
@@ -667,23 +665,22 @@ class _PVPlantSite(ArchSite._Site):
self.computeAreas(obj)
def computeAreas(self, obj):
"""
Compute areas, perimeter and volumes.
Override to add custom logic after parent computation.
"""
ArchSite._Site.computeAreas(self, obj)
return
if not obj.Shape:
return
if obj.Shape.isNull():
if obj.Shape.isNull() or not obj.Shape.isValid() or not obj.Shape.Faces:
return
if not obj.Shape.isValid():
return
if not obj.Shape.Faces:
return
if not hasattr(obj, "Perimeter"): # check we have a latest version site
if not hasattr(obj, "Perimeter"):
return
if not obj.Terrain:
return
# compute area
# Compute projected area (horizontal projection of all near-horizontal faces)
fset = []
for f in obj.Shape.Faces:
if f.normalAt(0, 0).getAngle(FreeCAD.Vector(0, 0, 1)) < 1.5707:
@@ -694,13 +691,11 @@ class _PVPlantSite(ArchSite._Site):
for f in fset:
try:
pf = Part.Face(Part.Wire(Drawing.project(f, FreeCAD.Vector(0, 0, 1))[0].Edges))
except Part.OCCError:
# error in computing the area. Better set it to zero than show a wrong value
if obj.ProjectedArea.Value != 0:
print("Error computing areas for ", obj.Label)
obj.ProjectedArea = 0
else:
pset.append(pf)
except Part.OCCError:
if getattr(obj, 'ProjectedArea', None) and obj.ProjectedArea.Value != 0:
FreeCAD.Console.PrintWarning(f"Error computing projected area for {obj.Label}\n")
obj.ProjectedArea = 0
if pset:
self.flatarea = pset.pop()
for f in pset:
@@ -708,28 +703,27 @@ class _PVPlantSite(ArchSite._Site):
self.flatarea = self.flatarea.removeSplitter()
if obj.ProjectedArea.Value != self.flatarea.Area:
obj.ProjectedArea = self.flatarea.Area
# compute perimeter
# Compute perimeter (border edges only)
lut = {}
for e in obj.Shape.Edges:
lut.setdefault(e.hashCode(), []).append(e)
l = 0
for e in lut.values():
if len(e) == 1: # keep only border edges
l += e[0].Length
if l:
if obj.Perimeter.Value != l:
obj.Perimeter = l
# compute volumes
perimeter = sum(e[0].Length for e in lut.values() if len(e) == 1)
if perimeter and obj.Perimeter.Value != perimeter:
obj.Perimeter = perimeter
# Compute cut/fill volumes relative to terrain
try:
if obj.Terrain.Shape.Solids:
shapesolid = obj.Terrain.Shape.copy()
else:
shapesolid = obj.Terrain.Shape.extrude(obj.ExtrusionVector)
addvol = 0
subvol = 0
for sub in obj.Subtractions:
subvol += sub.Shape.common(shapesolid).Volume
for sub in obj.Additions:
addvol += sub.Shape.cut(shapesolid).Volume
except Exception:
return
subvol = sum(sub.Shape.common(shapesolid).Volume for sub in obj.Subtractions)
addvol = sum(sub.Shape.cut(shapesolid).Volume for sub in obj.Additions)
if obj.SubtractionVolume.Value != subvol:
obj.SubtractionVolume = subvol
if obj.AdditionVolume.Value != addvol:
@@ -771,12 +765,12 @@ class _PVPlantSite(ArchSite._Site):
import PVPlantImportGrid
x, y, zone_number, zone_letter = utm.from_latlon(lat, lon)
self.obj.UtmZone = zone_list[zone_number - 1]
zz = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz[0].z)
#self.obj.OriginOffset = FreeCAD.Vector(x * 1000, y * 1000, 0) #??
point = PVPlantImportGrid.getElevationFromOE([[lat, lon]])
self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z)
self.obj.Latitude = lat
self.obj.Longitude = lon
self.obj.Elevation = zz[0].z
self.obj.Elevation = point[0].z
class _ViewProviderSite(ArchSite._ViewProviderSite):
@@ -1056,7 +1050,7 @@ class _ViewProviderSite:
if hasattr(vobj.Object,"EPWFile") and vobj.Object.EPWFile:
try:
import ladybug
except:
except ImportError:
pass
else:
self.windrosenode = makeWindRose(vobj.Object.EPWFile,vobj.SolarDiagramScale)
+183 -63
View File
@@ -73,6 +73,42 @@ line_patterns = {
"Dot (.5x) ...............................": 0x5555,
"Dot (2x) . . . . . . . . . . .": 0x8888}
def open_xyz_mmap(archivo_path):
"""
Usa memory-mapping para archivos muy grandes (máxima velocidad)
"""
# Primera pasada: contar líneas válidas
total_puntos = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
float(partes[0]);
float(partes[1]);
float(partes[2])
total_puntos += 1
except:
continue
# Segunda pasada: cargar datos
puntos = np.empty((total_puntos, 3))
idx = 0
with open(archivo_path, 'r') as f:
for linea in f:
partes = linea.strip().split()
if len(partes) >= 3:
try:
x, y, z = float(partes[0]), float(partes[1]), float(partes[2])
puntos[idx] = [x, y, z]
idx += 1
except:
continue
return puntos
def makeTerrain(name="Terrain"):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain")
obj.Label = name
@@ -81,7 +117,6 @@ def makeTerrain(name="Terrain"):
FreeCAD.ActiveDocument.recompute()
return obj
class Terrain(ArchComponent.Component):
"A Shadow Terrain Obcject"
@@ -94,8 +129,14 @@ class Terrain(ArchComponent.Component):
# obj.IfcType = "Fence"
# obj.MoveWithHost = False
try:
self.site = PVPlantSite.get()
except Exception:
self.site = None
if self.site:
self.site.Terrain = obj
else:
FreeCAD.Console.PrintWarning('Terrain: No se encontró Site, algunas funciones DEM requerirán Site.\n')
obj.ViewObject.ShapeColor = (0.0000, 0.6667, 0.4980)
obj.ViewObject.LineColor = (0.0000, 0.6000, 0.4392)
@@ -157,11 +198,22 @@ class Terrain(ArchComponent.Component):
if prop == "InitialMesh":
obj.mesh = obj.InitialMesh.copy()
# Forzar actualización visual
obj.publishProperty("Mesh")
if prop == "mesh":
# La propiedad mesh cambió → forzar recompute para que updateData se dispare
pass
if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime
if obj.DEM and obj.CuttingBoundary:
from pathlib import Path
suffix = Path(obj.DEM).suffix
if suffix == '.asc':
'''
ASC format:
Parámetro Descripción Requisitos
NCOLS: Cantidad de columnas de celdas Entero mayor que 0.
NROWS: Cantidad de filas de celdas Entero mayor que 0.
@@ -197,7 +249,7 @@ class Terrain(ArchComponent.Component):
del templist
# create xy coordinates
offset = self.site.Origin
offset = self.site.Origin if self.site else FreeCAD.Vector(0, 0, 0)
x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
datavals = datavals * 1000 # Ajuste de altura
@@ -229,33 +281,97 @@ class Terrain(ArchComponent.Component):
stepx = math.ceil(nx / stepsize)
stepy = math.ceil(ny / stepsize)
mesh = Mesh.Mesh()
for indx in range(stepx):
inix = indx * stepsize - 1
finx = min([stepsize * (indx + 1), len(x)-1])
for indy in range(stepy):
iniy = indy * stepsize - 1
finy = min([stepsize * (indy + 1), len(y) - 1])
pts = []
for i in range(inix, finx):
for j in range(iniy, finy):
if datavals[j][i] != nodata_value:
# Malla completa primero como numpy y filtramos todo de una
from datetime import datetime
t_start = datetime.now()
# Crear grid completo de coordenadas
XX, YY = np.meshgrid(x, y)
ZZ = datavals.copy()
# Enmascarar nodata
mask_valida = ZZ != nodata_value
# Enmascarar cutting boundary si existe
if obj.CuttingBoundary:
if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True):
pts.append([x[i], y[j], datavals[j][i]])
else:
pts.append([x[i], y[j], datavals[j][i]])
if len(pts) > 3:
from FreeCAD import Base
shape = obj.CuttingBoundary.Shape
mask_boundary = np.zeros_like(ZZ, dtype=bool)
# Sampling: revisar solo puntos estratégicos para boundary grande
stride = max(1, min(nx, ny) // 200)
for i in range(0, ny, stride):
for j in range(0, nx, stride):
if mask_valida[i, j]:
if shape.isInside(FreeCAD.Vector(x[j], y[i], 0), 0, True):
mask_boundary[i, j] = True
mask_valida = mask_valida & mask_boundary
# Extraer puntos válidos como lista plana
pts_validos = np.column_stack([
XX[mask_valida].ravel(),
YY[mask_valida].ravel(),
ZZ[mask_valida].ravel()
])
del XX, YY, ZZ, mask_valida
# Triangulación completa de una vez (no por parches)
mesh = Mesh.Mesh()
if len(pts_validos) > 3:
# Si hay muchos puntos, triangulamos por parches para evitar OOM
patch_size = 50000
n_patches = max(1, math.ceil(len(pts_validos) / patch_size))
for p in range(n_patches):
patch = pts_validos[p * patch_size:(p + 1) * patch_size].tolist()
if len(patch) > 3:
try:
triangulated = Triangulation.Triangulate(pts)
triangulated = Triangulation.Triangulate(patch)
mesh.addMesh(triangulated)
except TypeError:
print(f"Error al procesar {len(pts)} puntos: {str(e)}")
except TypeError as e:
print(f"Patch {p}: error al procesar {len(patch)} puntos: {str(e)}")
except Exception as e:
print(f"Patch {p}: error inesperado: {str(e)}")
print(f'Terraín DEM: {len(pts_validos)} pts válidos, {n_patches} parches, {datetime.now()-t_start}')
del pts_validos
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
Mesh.show(mesh)
# Limpiar objetos mesh huérfanos previos si existen
for o in FreeCAD.ActiveDocument.Objects:
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
FreeCAD.ActiveDocument.removeObject(o.Name)
mesh_obj = Mesh.show(mesh)
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
elif suffix in ['.xyz']:
pts_array = open_xyz_mmap(obj.DEM)
if pts_array is not None and len(pts_array) > 3:
import MeshTools.Triangulation as Triangulation
import Mesh
if obj.CuttingBoundary:
mask = []
for pt in pts_array:
mask.append(obj.CuttingBoundary.Shape.isInside(
FreeCAD.Vector(pt[0], pt[1], 0), 0, True))
pts_array = pts_array[mask]
if len(pts_array) > 3:
from datetime import datetime
t0 = datetime.now()
pts_list = pts_array.tolist()
mesh = Triangulation.Triangulate(pts_list)
mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface()
obj.InitialMesh = mesh.copy()
# Limpiar objetos mesh huérfanos previos
for o in FreeCAD.ActiveDocument.Objects:
if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'):
FreeCAD.ActiveDocument.removeObject(o.Name)
mesh_obj = Mesh.show(mesh)
mesh_obj.Label = 'Terrain_mesh_' + obj.Label
print(f'XYZ import: {len(pts_array)} puntos en {datetime.now()-t0}')
if prop == "PointsGroup" or prop == "CuttingBoundary":
if obj.PointsGroup and obj.CuttingBoundary:
@@ -285,6 +401,11 @@ class Terrain(ArchComponent.Component):
if obj.DEM:
obj.DEM = None
obj.mesh = mesh
# Forzar actualización visual llamando a publishProperty
try:
obj.publishProperty("Mesh")
except:
pass
def execute(self, obj):
''''''
@@ -503,47 +624,47 @@ class ViewProviderTerrain:
offset.factor = -2.0
# Boundary features.
'''self.boundary_color = coin.SoBaseColor()
self.boundary_color = coin.SoBaseColor()
self.boundary_coords = coin.SoGeoCoordinate()
self.boundary_lines = coin.SoLineSet()
self.boundary_style = coin.SoDrawStyle()
self.boundary_style.style = coin.SoDrawStyle.LINES'''
self.boundary_style.style = coin.SoDrawStyle.LINES
# Boundary root.
'''boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
boundaries = coin.SoType.fromName('SoFCSelection').createInstance()
boundaries.style = 'EMISSIVE_DIFFUSE'
boundaries.addChild(self.boundary_color)
boundaries.addChild(self.boundary_style)
boundaries.addChild(self.boundary_coords)
boundaries.addChild(self.boundary_lines)'''
boundaries.addChild(self.boundary_lines)
# Major Contour features.
'''self.major_color = coin.SoBaseColor()
self.major_color = coin.SoBaseColor()
self.major_coords = coin.SoGeoCoordinate()
self.major_lines = coin.SoLineSet()
self.major_style = coin.SoDrawStyle()
self.major_style.style = coin.SoDrawStyle.LINES'''
self.major_style.style = coin.SoDrawStyle.LINES
# Major Contour root.
'''major_contours = coin.SoSeparator()
major_contours = coin.SoSeparator()
major_contours.addChild(self.major_color)
major_contours.addChild(self.major_style)
major_contours.addChild(self.major_coords)
major_contours.addChild(self.major_lines)'''
major_contours.addChild(self.major_lines)
# Minor Contour features.
'''self.minor_color = coin.SoBaseColor()
self.minor_color = coin.SoBaseColor()
self.minor_coords = coin.SoGeoCoordinate()
self.minor_lines = coin.SoLineSet()
self.minor_style = coin.SoDrawStyle()
self.minor_style.style = coin.SoDrawStyle.LINES'''
self.minor_style.style = coin.SoDrawStyle.LINES
# Minor Contour root.
'''minor_contours = coin.SoSeparator()
minor_contours = coin.SoSeparator()
minor_contours.addChild(self.minor_color)
minor_contours.addChild(self.minor_style)
minor_contours.addChild(self.minor_coords)
minor_contours.addChild(self.minor_lines)'''
minor_contours.addChild(self.minor_lines)
# Highlight for selection.
highlight = coin.SoType.fromName('SoFCSelection').createInstance()
@@ -552,7 +673,7 @@ class ViewProviderTerrain:
highlight.addChild(mat_binding)
highlight.addChild(self.geo_coords)
highlight.addChild(self.triangles)
#highlight.addChild(boundaries)
highlight.addChild(boundaries)
# Face root.
face = coin.SoSeparator()
@@ -565,19 +686,19 @@ class ViewProviderTerrain:
edge.addChild(self.edge_style)
edge.addChild(highlight)
# Surface root.
# Surface root - con contour lines visibles.
surface_root = coin.SoSeparator()
surface_root.addChild(face)
surface_root.addChild(offset)
surface_root.addChild(edge)
#surface_root.addChild(major_contours)
#surface_root.addChild(minor_contours)
surface_root.addChild(major_contours)
surface_root.addChild(minor_contours)
vobj.addDisplayMode(surface_root, "Surface")
# Boundary root.
#boundary_root = coin.SoSeparator()
#boundary_root.addChild(boundaries)
#vobj.addDisplayMode(boundary_root, "Boundary")
boundary_root = coin.SoSeparator()
boundary_root.addChild(boundaries)
vobj.addDisplayMode(boundary_root, "Boundary")
# Elevation/Shaded root.
'''shaded_root = coin.SoSeparator()
@@ -604,32 +725,33 @@ class ViewProviderTerrain:
self.onChanged(vobj, "ShapeColor")
self.onChanged(vobj, "LineColor")
self.onChanged(vobj, "LineWidth")
#self.onChanged(vobj, "BoundaryColor")
#self.onChanged(vobj, "BoundaryWidth")
#self.onChanged(vobj, "BoundaryPattern")
#self.onChanged(vobj, "PatternScale")
#self.onChanged(vobj, "MajorColor")
#self.onChanged(vobj, "MajorWidth")
#self.onChanged(vobj, "MinorColor")
#self.onChanged(vobj, "MinorWidth")
self.onChanged(vobj, "BoundaryColor")
self.onChanged(vobj, "BoundaryWidth")
self.onChanged(vobj, "BoundaryPattern")
self.onChanged(vobj, "PatternScale")
self.onChanged(vobj, "MajorColor")
self.onChanged(vobj, "MajorWidth")
self.onChanged(vobj, "MinorColor")
self.onChanged(vobj, "MinorWidth")
def updateData(self, obj, prop):
''' Update Object visuals when a data property changed. '''
# Set geosystem.
geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
try:
utm_zone = FreeCAD.ActiveDocument.Site.UtmZone
except:
utm_zone = "30"
geo_system = ["UTM", utm_zone, "FLAT"]
self.geo_coords.geoSystem.setValues(geo_system)
'''
self.boundary_coords.geoSystem.setValues(geo_system)
self.major_coords.geoSystem.setValues(geo_system)
self.minor_coords.geoSystem.setValues(geo_system)
'''
if prop == "Mesh":
if prop == "mesh" or prop == "Mesh":
if obj.mesh:
print("Mostrar mesh")
mesh = obj.mesh
try:
vertices = [tuple(v) for v in mesh.Topology[0]]
faces = []
for face in mesh.Topology[1]:
@@ -637,19 +759,16 @@ class ViewProviderTerrain:
faces.append(-1)
# Asignar a los nodos de visualización
self.geo_coords.point.values = vertices # <-- ¡Clave!
self.triangles.coordIndex.values = faces # <-- ¡Clave!
self.geo_coords.point.values = vertices
self.triangles.coordIndex.values = faces
except Exception as e:
FreeCAD.Console.PrintError(f"Error actualizando mesh visual: {e}\n")
def getDisplayModes(self, vobj):
''' Return a list of display modes. '''
modes = ["Surface", "Boundary"]
return modes
return ["Surface", "Boundary", "Flat Lines", "Wireframe"]
def getDefaultDisplayMode(self):
'''
Return the name of the default display mode.
'''
return "Surface"
def claimChildren(self):
@@ -692,3 +811,4 @@ class ViewProviderTerrain:
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Terrain', _CommandTerrain())'''
+7 -27
View File
@@ -450,35 +450,18 @@ class ContourTaskPanel():
starttime = datetime.now()
if self.land is None:
print("No hay objetos para procesar")
FreeCAD.Console.PrintWarning("No hay objetos para procesar\n")
return False
else:
minor = FreeCAD.Units.Quantity(self.inputMinorContourMargin.currentText()).Value
mayor = FreeCAD.Units.Quantity(self.inputMayorContourMargin.currentText()).Value
i = 2
if i == 0:
makeContours(self.land, minor, mayor, self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(), self.inputMayorContourThickness.value())
elif i == 1:
import multiprocessing
p = multiprocessing.Process(target=makeContours,
args=(self.land, minor, mayor,
makeContours(
self.land, minor, mayor,
self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(),
self.inputMayorContourThickness.value(), ))
p.start()
p.join()
else:
import threading
hilo = threading.Thread(target = makeContours,
args = (self.land, minor, mayor,
self.MinorColor, self.MayorColor,
self.inputMinorContourThickness.value(),
self.inputMayorContourThickness.value()))
hilo.daemon = True
hilo.start()
self.inputMayorContourThickness.value()
)
total_time = datetime.now() - starttime
print(" -- Tiempo tardado:", total_time)
@@ -569,7 +552,7 @@ class SlopeTaskPanel(_generalTaskPanel):
land.ViewObject.DiffuseColor = colorlist
# TODO: check this code:
elif obj.isDerivedFrom("Mesh::Feature"):
elif hasattr(land, 'Mesh') and land.isDerivedFrom("Mesh::Feature"):
fMesh = Mest2FemMesh(land)
import math
setColors = []
@@ -602,10 +585,7 @@ class SlopeTaskPanel(_generalTaskPanel):
print("Everything OK (", datetime.now() - starttime, ")")
def accept(self):
# self.getPointSlope()
import threading
hilo = threading.Thread(target=self.getPointSlope(self.ranges))
hilo.start()
self.getPointSlope(self.ranges)
return True
# Orientation Analisys: ---------------------------------------------------------------------------------
+7 -27
View File
@@ -54,30 +54,6 @@ class CommandPVPlantSite:
return
'''class CommandPVPlantGeoreferencing:
@staticmethod
def GetResources():
return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")),
'Accel': "G, R",
'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"),
'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")}
@staticmethod
def IsActive():
if FreeCAD.ActiveDocument:
return True
else:
return False
@staticmethod
def Activated():
import PVPlantGeoreferencing
taskd = PVPlantGeoreferencing.MapWindow()
#taskd.setParent(FreeCADGui.getMainWindow())
#taskd.setWindowFlags(QtCore.Qt.Window)
taskd.show()#exec_()'''
class CommandProjectSetup:
@staticmethod
def GetResources():
@@ -673,10 +649,12 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantTracker', PVPlantFrame.CommandTracker())
FreeCADGui.addCommand('RackType', CommandRackGroup())
import PVPlantFence
from Civil.Fence import PVPlantFence
FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup())
import docgenerator
FreeCADGui.addCommand('GenerateDocuments', docgenerator.generateDocuments())
projectlist = [ # "Reload",
"PVPlantSite",
"ProjectSetup",
@@ -712,4 +690,6 @@ pv_mechanical = [
]
objectlist = ['PVPlantTree',
'PVPlantFence',]
'PVPlantFenceGroup',
'GenerateDocuments',
]
+455 -42
View File
@@ -26,6 +26,9 @@ import PVPlantSite
import Utils.PVPlantUtils as utils
import MeshPart as mp
import pivy
from pivy import coin
if FreeCAD.GuiUp:
import FreeCADGui
from DraftTools import translate
@@ -69,6 +72,7 @@ class _Area:
''' Initialize the Area object '''
self.Type = None
self.obj = None
self.setProperties(obj)
def setProperties(self, obj):
pl = obj.PropertiesList
@@ -101,18 +105,18 @@ class _Area:
def __setstate__(self, state):
pass
def execute(self, obj):
''' Execute the area object '''
pass
class _ViewProviderArea:
def __init__(self, vobj):
self.Object = vobj.Object
vobj.Proxy = self
def attach(self, vobj):
'''
Create Object visuals in 3D view.
'''
self.Object = vobj.Object
return
''' Create Object visuals in 3D view. '''
self.ViewObject = vobj
def getIcon(self):
'''
@@ -120,6 +124,7 @@ class _ViewProviderArea:
'''
return str(os.path.join(DirIcons, "area.svg"))
'''
def claimChildren(self):
"""
@@ -159,17 +164,10 @@ class _ViewProviderArea:
pass
def __getstate__(self):
"""
Save variables to file.
"""
return None
def __setstate__(self, state):
"""
Get variables from file.
"""
return None
pass
''' Frame Area '''
@@ -311,17 +309,14 @@ class ViewProviderFrameArea(_ViewProviderArea):
''' offsets '''
def makeOffsetArea(base = None, val=None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "OffsetArea")
OffsetArea(obj)
obj.Base = base
ViewProviderOffsetArea(obj.ViewObject)
if val:
obj.Distance = val
obj.OffsetDistance = val
offsets = None
try:
offsetsgroup = FreeCAD.ActiveDocument.Offsets
except:
@@ -334,11 +329,13 @@ def makeOffsetArea(base = None, val=None):
class OffsetArea(_Area):
def __init__(self, obj):
_Area.__init__(self, obj)
self.setProperties(obj)
'''_Area.__init__(self, obj)
self.setProperties(obj)'''
super().__init__(obj) # Llama al constructor de _Area
def setProperties(self, obj):
_Area.setProperties(self, obj)
super().setProperties(obj) # Propiedades de la clase base
pl = obj.PropertiesList
if not ("OffsetDistance" in pl):
obj.addProperty("App::PropertyDistance",
@@ -354,24 +351,28 @@ class OffsetArea(_Area):
self.setProperties(obj)
def execute(self, obj):
import Utils.PVPlantUtils as utils
# Comprobar dependencias críticas
if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape:
return
if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain:
return
base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0, 0, 1)
wire = utils.getProjected(base, vec)
wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
tmp = mp.projectShapeOnMesh(wire, land, vec)
sections = mp.projectShapeOnMesh(wire, land, vec)
print(" javi ", sections)
pts = []
for section in tmp:
for section in sections:
pts.extend(section)
# Crear forma solo si hay resultados
if len(pts)>0:
obj.Shape = Part.makePolygon(pts)
def __getstate__(self):
return None
def __setstate__(self, state):
pass
else:
obj.Shape = Part.Shape() # Forma vacía si falla
class ViewProviderOffsetArea(_ViewProviderArea):
@@ -382,14 +383,12 @@ class ViewProviderOffsetArea(_ViewProviderArea):
def claimChildren(self):
""" Provides object grouping """
children = []
if self.Object.Base:
children.append(self.Object.Base)
if self.ViewObject and self.ViewObject.Object.Base:
children.append(self.ViewObject.Object.Base)
return children
''' Forbidden Area: '''
def makeProhibitedArea(base = None):
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ExclusionArea")
ProhibitedArea(obj)
@@ -420,29 +419,443 @@ class ProhibitedArea(OffsetArea):
"""Method run when the document is restored."""
self.setProperties(obj)
def execute(self, obj):
# Comprobar dependencias
if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape:
return
if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain:
return
base = obj.Base.Shape
land = PVPlantSite.get().Terrain.Mesh
vec = FreeCAD.Vector(0, 0, 1)
# 1. Crear wire original
original_wire = utils.getProjected(base, vec)
sections_original = mp.projectShapeOnMesh(original_wire, land, vec)
# 2. Crear wire offset
offset_wire = original_wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True)
sections_offset = mp.projectShapeOnMesh(offset_wire, land, vec)
# Crear formas compuestas
def make_polygon(sections):
if not sections:
return Part.Shape()
pts = []
for section in sections:
pts.extend(section)
return Part.makePolygon(pts)
compounds = []
if sections_original:
compounds.append(make_polygon(sections_original))
if sections_offset:
compounds.append(make_polygon(sections_offset))
if compounds:
obj.Shape = Part.makeCompound(compounds)
else:
obj.Shape = Part.Shape()
# Actualizar colores en la vista
"""if FreeCAD.GuiUp and obj.ViewObject:
obj.ViewObject.Proxy.updateVisual()"""
class ViewProviderForbiddenArea_old:
def __init__(self, vobj):
vobj.Proxy = self
self.setProperties(vobj)
def setProperties(self, vobj):
# Propiedades de color
if not hasattr(vobj, "OriginalColor"):
vobj.addProperty("App::PropertyColor",
"OriginalColor",
"ObjectStyle",
"Color for original wire")
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
if not hasattr(vobj, "OffsetColor"):
vobj.addProperty("App::PropertyColor",
"OffsetColor",
"ObjectStyle",
"Color for offset wire")
vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo
# Propiedades de grosor
if not hasattr(vobj, "OriginalWidth"):
vobj.addProperty("App::PropertyFloat",
"OriginalWidth",
"ObjectStyle",
"Line width for original wire")
vobj.OriginalWidth = 4.0
if not hasattr(vobj, "OffsetWidth"):
vobj.addProperty("App::PropertyFloat",
"OffsetWidth",
"ObjectStyle",
"Line width for offset wire")
vobj.OffsetWidth = 4.0
# Deshabilitar el color por defecto
vobj.setPropertyStatus("LineColor", "Hidden")
vobj.setPropertyStatus("PointColor", "Hidden")
vobj.setPropertyStatus("ShapeAppearance", "Hidden")
def attach(self, vobj):
self.ViewObject = vobj
self.Object = vobj.Object
# Crear la estructura de escena Coin3D
self.root = coin.SoGroup()
# Switch para habilitar/deshabilitar la selección
self.switch = coin.SoSwitch()
self.switch.whichChild = coin.SO_SWITCH_ALL
# Separador para el wire original
self.original_sep = coin.SoSeparator()
self.original_color = coin.SoBaseColor()
self.original_coords = coin.SoCoordinate3()
self.original_line_set = coin.SoLineSet()
self.original_draw_style = coin.SoDrawStyle()
# Separador para el wire offset
self.offset_sep = coin.SoSeparator()
self.offset_color = coin.SoBaseColor()
self.offset_coords = coin.SoCoordinate3()
self.offset_line_set = coin.SoLineSet()
self.offset_draw_style = coin.SoDrawStyle()
# Construir la jerarquía de escena
self.original_sep.addChild(self.original_color)
self.original_sep.addChild(self.original_draw_style)
self.original_sep.addChild(self.original_coords)
self.original_sep.addChild(self.original_line_set)
self.offset_sep.addChild(self.offset_color)
self.offset_sep.addChild(self.offset_draw_style)
self.offset_sep.addChild(self.offset_coords)
self.offset_sep.addChild(self.offset_line_set)
self.switch.addChild(self.original_sep)
self.switch.addChild(self.offset_sep)
self.root.addChild(self.switch)
vobj.addDisplayMode(self.root, "Wireframe")
# Inicializar estilos de dibujo
self.original_draw_style.style = coin.SoDrawStyle.LINES
self.offset_draw_style.style = coin.SoDrawStyle.LINES
# Actualizar visualización inicial
if hasattr(self.Object, 'Shape'):
self.updateData(self.Object, "Shape")
self.updateVisual()
def updateData(self, obj, prop):
if prop == "Shape" and obj.Shape and not obj.Shape.isNull():
self.updateGeometry()
def updateGeometry(self):
"""Actualiza la geometría en la escena 3D"""
if not hasattr(self, 'Object') or not self.Object.Shape or self.Object.Shape.isNull():
return
# Limpiar coordenadas existentes
self.original_coords.point.deleteValues(0)
self.offset_coords.point.deleteValues(0)
# Obtener los sub-shapes
subshapes = []
if hasattr(self.Object.Shape, 'SubShapes') and self.Object.Shape.SubShapes:
subshapes = self.Object.Shape.SubShapes
elif hasattr(self.Object.Shape, 'ChildShapes') and self.Object.Shape.ChildShapes:
subshapes = self.Object.Shape.ChildShapes
# Procesar wire original (primer sub-shape)
if len(subshapes) > 0:
self.processShape(subshapes[0], self.original_coords, self.original_line_set)
# Procesar wire offset (segundo sub-shape)
if len(subshapes) > 1:
self.processShape(subshapes[1], self.offset_coords, self.offset_line_set)
# Actualizar colores y grosores
self.updateVisual()
def processShape(self, shape, coords_node, lineset_node):
"""Procesa una forma y la añade al nodo de coordenadas"""
if not shape or shape.isNull():
return
points = []
line_indices = []
current_index = 0
# Obtener todos los edges de la forma
edges = []
if hasattr(shape, 'Edges'):
edges = shape.Edges
elif hasattr(shape, 'ChildShapes'):
for child in shape.ChildShapes:
if hasattr(child, 'Edges'):
edges.extend(child.Edges)
for edge in edges:
try:
# Discretizar la curva para obtener puntos
vertices = edge.discretize(Number=50)
for i, vertex in enumerate(vertices):
points.append([vertex.x, vertex.y, vertex.z])
line_indices.append(current_index)
current_index += 1
# Añadir -1 para indicar fin de línea
line_indices.append(-1)
except Exception as e:
print(f"Error processing edge: {e}")
continue
# Configurar coordenadas y líneas
if points:
coords_node.point.setValues(0, len(points), points)
lineset_node.numVertices.deleteValues(0)
lineset_node.numVertices.setValues(0, len(line_indices), line_indices)
def updateVisual(self):
"""Actualiza colores y grosores según las propiedades"""
if not hasattr(self, 'ViewObject') or not self.ViewObject:
return
vobj = self.ViewObject
try:
# Configurar wire original
if hasattr(vobj, "OriginalColor"):
original_color = vobj.OriginalColor
self.original_color.rgb.setValue(original_color[0], original_color[1], original_color[2])
if hasattr(vobj, "OriginalWidth"):
self.original_draw_style.lineWidth = vobj.OriginalWidth
# Configurar wire offset
if hasattr(vobj, "OffsetColor"):
offset_color = vobj.OffsetColor
self.offset_color.rgb.setValue(offset_color[0], offset_color[1], offset_color[2])
if hasattr(vobj, "OffsetWidth"):
self.offset_draw_style.lineWidth = vobj.OffsetWidth
except Exception as e:
print(f"Error updating visual: {e}")
def onChanged(self, vobj, prop):
"""Maneja cambios en propiedades"""
if prop in ["OriginalColor", "OffsetColor", "OriginalWidth", "OffsetWidth"]:
self.updateVisual()
def getDisplayModes(self, obj):
return ["Wireframe"]
def getDefaultDisplayMode(self):
return "Wireframe"
def setDisplayMode(self, mode):
return mode
def claimChildren(self):
"""Proporciona agrupamiento de objetos"""
children = []
if hasattr(self, 'Object') and self.Object and hasattr(self.Object, "Base"):
children.append(self.Object.Base)
return children
def getIcon(self):
'''Return object treeview icon'''
return str(os.path.join(DirIcons, "area_forbidden.svg"))
def onDocumentRestored(self, vobj):
"""Método ejecutado cuando el documento es restaurado"""
self.ViewObject = vobj
self.Object = vobj.Object
self.setProperties(vobj)
self.attach(vobj)
def __getstate__(self):
return None
def __setstate__(self, state):
pass
return None
class ViewProviderForbiddenArea(_ViewProviderArea):
class ViewProviderForbiddenArea:
def __init__(self, vobj):
vobj.Proxy = self
self.ViewObject = vobj
# Inicializar propiedades PRIMERO
self.setProperties(vobj)
# Configurar colores iniciales
self.updateColors(vobj)
def setProperties(self, vobj):
if not hasattr(vobj, "OriginalColor"):
vobj.addProperty("App::PropertyColor",
"OriginalColor",
"Display",
"Color for original wire")
vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo
if not hasattr(vobj, "OffsetColor"):
vobj.addProperty("App::PropertyColor",
"OffsetColor",
"Display",
"Color for offset wire")
vobj.OffsetColor = (1.0, 0.5, 0.0) # Naranja
def updateColors(self, vobj):
"""Actualiza los colores desde las propiedades"""
try:
if hasattr(vobj, "OriginalColor"):
self.original_color.rgb.setValue(*vobj.OriginalColor)
else:
self.original_color.rgb.setValue(1.0, 0.0, 0.0)
if hasattr(vobj, "OffsetColor"):
self.offset_color.rgb.setValue(*vobj.OffsetColor)
else:
self.offset_color.rgb.setValue(1.0, 0.5, 0.0)
except Exception as e:
print(f"Error en updateColors: {e}")
def onDocumentRestored(self, vobj):
self.setProperties(vobj)
# No llamar a __init__ de nuevo, solo actualizar propiedades
self.updateColors(vobj)
def getIcon(self):
''' Return object treeview icon '''
return str(os.path.join(DirIcons, "area_forbidden.svg"))
def attach(self, vobj):
self.ViewObject = vobj
# Inicializar nodos Coin3D
self.root = coin.SoGroup()
self.original_coords = coin.SoCoordinate3()
self.offset_coords = coin.SoCoordinate3()
self.original_color = coin.SoBaseColor()
self.offset_color = coin.SoBaseColor()
self.original_lineset = coin.SoLineSet()
self.offset_lineset = coin.SoLineSet()
# Añadir un nodo de dibujo para establecer el estilo de línea
self.draw_style = coin.SoDrawStyle()
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = 3.0
# Construir la escena
self.root.addChild(self.draw_style)
# Grupo para el polígono original
original_group = coin.SoGroup()
original_group.addChild(self.original_color)
original_group.addChild(self.original_coords)
original_group.addChild(self.original_lineset)
# Grupo para el polígono offset
offset_group = coin.SoGroup()
offset_group.addChild(self.offset_color)
offset_group.addChild(self.offset_coords)
offset_group.addChild(self.offset_lineset)
self.root.addChild(original_group)
self.root.addChild(offset_group)
vobj.addDisplayMode(self.root, "Standard")
# Asegurar que la visibilidad esté activada
vobj.Visibility = True
def updateData(self, obj, prop):
if prop == "Shape":
self.updateVisual(obj)
def updateVisual(self, obj):
"""Actualiza la representación visual basada en la forma del objeto"""
if not hasattr(obj, 'Shape') or not obj.Shape or obj.Shape.isNull():
return
try:
# Obtener todos los bordes de la forma compuesta
all_edges = obj.Shape.Edges
# Separar bordes por polígono (asumimos que el primer polígono es el original)
# Esto es una simplificación - podrías necesitar una lógica más sofisticada
if len(all_edges) >= 2:
# Polígono original - primer conjunto de bordes
original_edges = [all_edges[0]]
original_points = []
for edge in original_edges:
for vertex in edge.Vertexes:
original_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
# Polígono offset - segundo conjunto de bordes
offset_edges = [all_edges[1]]
offset_points = []
for edge in offset_edges:
for vertex in edge.Vertexes:
offset_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z))
# Asignar puntos a los nodos Coordinate3
if original_points:
self.original_coords.point.setValues(0, len(original_points), original_points)
self.original_lineset.numVertices.setValue(len(original_points))
if offset_points:
self.offset_coords.point.setValues(0, len(offset_points), offset_points)
self.offset_lineset.numVertices.setValue(len(offset_points))
# Actualizar colores
if hasattr(obj, 'ViewObject') and obj.ViewObject:
self.updateColors(obj.ViewObject)
except Exception as e:
print(f"Error en updateVisual: {e}")
def onChanged(self, vobj, prop):
if prop in ["OriginalColor", "OffsetColor"]:
self.updateColors(vobj)
elif prop == "Visibility" and vobj.Visibility:
# Cuando la visibilidad cambia a True, actualizar visual
self.updateVisual(vobj.Object)
def getDisplayModes(self, obj):
return ["Standard"]
def getDefaultDisplayMode(self):
return "Standard"
def setDisplayMode(self, mode):
return mode
def claimChildren(self):
""" Provides object grouping """
children = []
if self.Object.Base:
children.append(self.Object.Base)
if hasattr(self, 'ViewObject') and self.ViewObject and hasattr(self.ViewObject.Object, 'Base'):
children.append(self.ViewObject.Object.Base)
return children
def dumps(self):
return None
def loads(self, state):
return None
''' PV Area: '''
def makePVSubplant():
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant")
PVSubplant(obj)
+1 -1
View File
@@ -1,6 +1,6 @@
import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets
from PySide import QtWidgets
import os
if FreeCAD.GuiUp:
+61 -61
View File
@@ -39,6 +39,16 @@
<property name="spacing">
<number>9</number>
</property>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Maximum west-east slope:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QWidget" name="widget_2" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -46,17 +56,20 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Frame coloring:</string>
<string>South facing</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@@ -66,71 +79,20 @@
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Maximum west-east slope:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="editWETL">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> º</string>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="value">
<double>8.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="editSFTL">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string> º</string>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="value">
<double>2.800000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QWidget" name="widget_2" native="true"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>South facing</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<string>Frame coloring:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="editNFTL">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> º</string>
@@ -149,7 +111,45 @@
<string>North Facing</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="editSFTL">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> º</string>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="value">
<double>2.800000000000000</double>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="editWETL">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="suffix">
<string> º</string>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="value">
<double>8.000000000000000</double>
</property>
</widget>
</item>
-43
View File
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<polygon style="fill:#FFB74F;" points="432.106,250.534 432.106,470.021 296.578,470.021 296.578,336.975 221.399,336.975
221.399,470.021 79.894,470.021 79.894,250.534 256,115.075 "/>
<path style="fill:#FF7D3C;" d="M439.485,183.135V90.306h-74.167v35.772L256,41.979L0,238.92l53.633,69.712L256,152.959
l202.367,155.672L512,238.92L439.485,183.135z"/>
<polygon style="fill:#FF9A00;" points="432.106,250.534 432.106,470.021 296.578,470.021 296.578,336.975 256,336.975 256,115.075
"/>
<polygon style="fill:#FF4E19;" points="512,238.92 458.367,308.632 256,152.959 256,41.979 365.318,126.078 365.318,90.306
439.485,90.306 439.485,183.135 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-45
View File
@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
enable-background="new 0 0 511.771 511.771"
height="512"
viewBox="0 0 511.771 511.771"
width="512"
version="1.1"
sodipodi:docname="stringsetup.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<defs
id="defs11" />
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.4707031"
inkscape:cx="256"
inkscape:cy="256"
inkscape:window-width="2160"
inkscape:window-height="1361"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<g
id="g6"
transform="matrix(0.99955273,0,0,0.99955273,-8.003632e-5,1.12e-6)">
<g
id="g4">
<path
d="m 496.659,312.107 -47.061,-36.8 c 0.597,-5.675 1.109,-12.309 1.109,-19.328 0,-7.019 -0.491,-13.653 -1.109,-19.328 l 47.104,-36.821 c 8.747,-6.912 11.136,-19.179 5.568,-29.397 L 453.331,85.76 C 448.104,76.203 436.648,71.296 425.022,75.584 L 369.491,97.877 C 358.846,90.197 347.688,83.712 336.147,78.528 L 327.699,19.627 C 326.312,8.448 316.584,0 305.086,0 h -98.133 c -11.499,0 -21.205,8.448 -22.571,19.456 l -8.469,59.115 c -11.179,5.035 -22.165,11.435 -33.28,19.349 L 86.953,75.563 C 76.52,71.531 64.04,76.053 58.856,85.568 L 9.854,170.347 c -5.781,9.771 -3.392,22.464 5.547,29.547 l 47.061,36.8 c -0.747,7.189 -1.109,13.44 -1.109,19.307 0,5.867 0.363,12.117 1.109,19.328 L 15.358,312.15 c -8.747,6.933 -11.115,19.2 -5.547,29.397 l 48.939,84.672 c 5.227,9.536 16.576,14.485 28.309,10.176 l 55.531,-22.293 c 10.624,7.659 21.781,14.144 33.323,19.349 l 8.448,58.88 C 185.747,503.552 195.454,512 206.974,512 h 98.133 c 11.499,0 21.227,-8.448 22.592,-19.456 l 8.469,-59.093 c 11.179,-5.056 22.144,-11.435 33.28,-19.371 l 55.68,22.357 c 2.688,1.045 5.483,1.579 8.363,1.579 8.277,0 15.893,-4.523 19.733,-11.563 l 49.152,-85.12 c 5.462,-9.984 3.072,-22.25 -5.717,-29.226 z m -240.64,29.226 c -47.061,0 -85.333,-38.272 -85.333,-85.333 0,-47.061 38.272,-85.333 85.333,-85.333 47.061,0 85.333,38.272 85.333,85.333 0,47.061 -38.272,85.333 -85.333,85.333 z"
id="path2" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

-132
View File
@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="trench.svg"
id="svg66"
version="1.1"
width="512pt"
viewBox="0 0 512 512"
height="512pt">
<metadata
id="metadata72">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs70" />
<sodipodi:namedview
inkscape:current-layer="svg66"
inkscape:window-maximized="1"
inkscape:window-y="-9"
inkscape:window-x="-9"
inkscape:cy="341.33333"
inkscape:cx="341.33333"
inkscape:zoom="1.0766602"
showgrid="false"
id="namedview68"
inkscape:window-height="1361"
inkscape:window-width="2160"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<path
id="path2"
fill="#ffb655"
d="m359.78125 71.285156v288.496094h-207.5625v-288.496094h-144.71875v433.214844h497v-433.214844zm0 0" />
<path
id="path4"
fill="#a4e276"
d="m7.5 15.5c18.089844 0 18.089844-8 36.183594-8 18.089844 0 18.089844 8 36.179687 8 18.089844 0 18.089844-8 36.175781-8 18.089844 0 18.089844 8 36.179688 8v55.785156h-144.71875zm0 0" />
<path
id="path6"
fill="#a4e276"
d="m359.78125 15.5c18.089844 0 18.089844-8 36.183594-8 18.089844 0 18.089844 8 36.179687 8 18.089844 0 18.089844-8 36.175781-8 18.089844 0 18.089844 8 36.179688 8v55.785156h-144.71875zm0 0" />
<path
id="path16"
fill="#ff7956"
d="m359.78125 71.285156h30v288.496094h-30zm0 0" />
<path
id="path18"
fill="#ff7956"
d="m7.5 71.285156h30v433.214844h-30zm0 0" />
<path
id="path20"
fill="#64c37d"
d="m58.683594 10.179688c-3.6875-1.476563-7.984375-2.679688-15-2.679688-18.09375 0-18.09375 8-36.183594 8v55.785156h30v-55.785156c11.070312 0 15.367188-2.996094 21.183594-5.320312zm0 0" />
<path
id="path22"
fill="#64c37d"
d="m410.964844 10.179688c-3.6875-1.476563-7.984375-2.679688-15-2.679688-18.09375 0-18.09375 8-36.183594 8v55.785156h30v-55.785156c11.070312 0 15.367188-2.996094 21.183594-5.320312zm0 0" />
<path
id="path28"
d="m144.71875 91.289062v275.992188h67.402344v-15h-52.402344v-260.992188zm0 0" />
<path
id="path30"
d="m299.871094 367.28125h67.410156v-275.992188h-15v260.992188h-52.410156zm0 0" />
<path
id="path32"
d="m497 497h-482v-405.714844h-15v420.714844h512v-420.714844h-15zm0 0" />
<path
id="path34"
d="m159.71875 8h-7.5c-7.460938 0-10.8125-1.480469-15.054688-3.359375-4.917968-2.175781-10.492187-4.640625-21.125-4.640625-10.628906 0-16.203124 2.464844-21.121093 4.640625-4.242188 1.878906-7.59375 3.359375-15.054688 3.359375-7.460937 0-10.8125-1.480469-15.058593-3.359375-4.917969-2.175781-10.492188-4.640625-21.121094-4.640625-10.632813 0-16.207032 2.464844-21.125 4.640625-4.246094 1.878906-7.597656 3.359375-15.058594 3.359375h-7.5v70.785156h159.71875zm-15 55.785156h-129.71875v-41.257812c6.054688-.820313 10.015625-2.574219 13.625-4.167969 4.242188-1.878906 7.597656-3.359375 15.058594-3.359375 7.460937 0 10.8125 1.480469 15.054687 3.359375 4.917969 2.175781 10.496094 4.640625 21.125 4.640625 10.628907 0 16.203125-2.464844 21.121094-4.640625 4.246094-1.878906 7.597656-3.359375 15.054687-3.359375 7.460938 0 10.8125 1.480469 15.058594 3.359375 3.605469 1.59375 7.570313 3.347656 13.621094 4.167969zm0 0" />
<path
id="path36"
d="m504.5 8c-7.460938 0-10.8125-1.480469-15.054688-3.359375-4.917968-2.175781-10.496093-4.640625-21.125-4.640625-10.628906 0-16.203124 2.464844-21.121093 4.640625-4.242188 1.878906-7.59375 3.359375-15.054688 3.359375-7.460937 0-10.8125-1.480469-15.058593-3.359375-4.917969-2.175781-10.492188-4.640625-21.121094-4.640625-10.632813 0-16.207032 2.464844-21.125 4.640625-4.246094 1.878906-7.597656 3.359375-15.058594 3.359375h-7.5v70.785156h159.71875v-70.785156zm-7.5 55.785156h-129.71875v-41.257812c6.054688-.820313 10.015625-2.574219 13.625-4.167969 4.242188-1.878906 7.597656-3.359375 15.058594-3.359375 7.460937 0 10.8125 1.480469 15.054687 3.359375 4.917969 2.175781 10.496094 4.640625 21.125 4.640625 10.628907 0 16.203125-2.464844 21.121094-4.640625 4.246094-1.878906 7.597656-3.359375 15.058594-3.359375 7.457031 0 10.8125 1.480469 15.054687 3.359375 3.609375 1.59375 7.570313 3.347656 13.621094 4.167969zm0 0" />
<path
id="path40"
d="m131.007812 423.90625h15v15h-15zm0 0" />
<path
id="path42"
d="m348.007812 456.929688h15v15h-15zm0 0" />
<path
id="path44"
d="m386.429688 430.050781h15v15h-15zm0 0" />
<path
id="path46"
d="m423.140625 260.738281h15v15h-15zm0 0" />
<path
id="path48"
d="m84.722656 403.050781h15v15h-15zm0 0" />
<path
id="path50"
d="m384.929688 294.355469h15v15h-15zm0 0" />
<path
id="path52"
d="m50.222656 130.597656h15v15h-15zm0 0" />
<path
id="path54"
d="m92.222656 98.785156h15v15h-15zm0 0" />
<path
id="path56"
d="m430.640625 313.023438h15v15h-15zm0 0" />
<path
id="path58"
d="m57.722656 439.050781h15v15h-15zm0 0" />
<g
id="g64"
fill="#fff">
<path
id="path60"
d="m487 318.523438h-15v-172.238282h15zm0-182.238282h-15v-15h15zm0-25h-15v-15h15zm0 0" />
<path
id="path62"
d="m486.5 53.785156h-25.71875v-15h25.71875zm-35.71875 0h-15v-15h15zm-25 0h-15v-15h15zm0 0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

+7 -4
View File
@@ -4,12 +4,15 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
<!-- 1. Core Leaflet library -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw.css">
<script src="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw-src.js"></script>
<!-- 2. Leaflet.draw Plugin (MUST be loaded AFTER Leaflet) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<!-- 3. Other plugins -->
<script src="https://unpkg.com/togeojson@0.16.0"></script>
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
+233
View File
@@ -0,0 +1,233 @@
# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * Alignment - Alineamiento horizontal y vertical *
# * *
# * Define el eje de la carretera mediante: *
# * - Alineamiento horizontal: polilínea + curvas circulares *
# * - Alineamiento vertical: rasante con pendientes y curvas verticales*
# * - Estaciones (progresivas) *
# * *
# ***********************************************************************
import FreeCAD
import Part
import math
import numpy as np
def make_alignment_from_wire(wire, name="Alignment"):
"""Crea un objeto Alignment a partir de un wire de FreeCAD."""
if FreeCAD.ActiveDocument is None:
return None
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
Alignment(obj)
_ViewProviderAlignment(obj.ViewObject)
obj.SourceWire = wire
obj.Label = name
FreeCAD.ActiveDocument.recompute()
return obj
class Alignment:
"""
Alineamiento horizontal + vertical de una carretera.
Propiedades principales:
SourceWire : Polilínea base del eje
Stations : Progresivas (lista de distancias)
StationInterval : Intervalo entre estaciones
TotalLength : Longitud total del eje
HorizontalCurves : Curvas circulares (radio, longitud, parámetros)
VerticalPVI : Puntos de intersección vertical (PVI) para la rasante
"""
def __init__(self, obj):
obj.Proxy = self
self.setProperties(obj)
self._cached_chainage = None
self._cached_station_points = None
self._cached_tangents = None
def setProperties(self, obj):
pl = obj.PropertiesList
if "SourceWire" not in pl:
obj.addProperty("App::PropertyLink",
"SourceWire", "Alignment",
"Polilínea base del eje")
if "Stations" not in pl:
obj.addProperty("App::PropertyFloatList",
"Stations", "Alignment",
"Estaciones (progresivas) en mm")
if "StationInterval" not in pl:
obj.addProperty("App::PropertyLength",
"StationInterval", "Alignment",
"Intervalo entre estaciones").StationInterval = 20000
if "NumberOfStations" not in pl:
obj.addProperty("App::PropertyInteger",
"NumberOfStations", "Alignment",
"Número de estaciones")
obj.setEditorMode("NumberOfStations", 1)
if "TotalLength" not in pl:
obj.addProperty("App::PropertyLength",
"TotalLength", "Alignment",
"Longitud total del eje")
obj.setEditorMode("TotalLength", 1)
if "HorizontalCurveRadii" not in pl:
obj.addProperty("App::PropertyFloatList",
"HorizontalCurveRadii", "Alignment",
"Radios de curva en cada vértice (0 = recta)")
if "ShowStations" not in pl:
obj.addProperty("App::PropertyBool",
"ShowStations", "Alignment",
"Mostrar marcas de estación en 3D").ShowStations = False
def onDocumentRestored(self, obj):
self.setProperties(obj)
def execute(self, obj):
"""Calcula estaciones y geometría del alignment."""
if not obj.SourceWire or not obj.SourceWire.Shape:
return
wire = obj.SourceWire.Shape
if wire.isNull():
return
total_len = wire.Length
obj.TotalLength = total_len
if total_len <= 0:
return
interval = obj.StationInterval.Value
if interval <= 0:
interval = 20000
n_stations = max(2, int(total_len / interval) + 1)
obj.NumberOfStations = n_stations
stations = np.linspace(0, total_len, n_stations).tolist()
obj.Stations = stations
# Calcular radios de curva en cada vértice del wire
self._compute_curve_radii(obj, wire)
# Cache
self._cached_chainage = stations
self._cached_station_points = None
self._cached_tangents = None
def _compute_curve_radii(self, obj, wire):
"""Calcula el radio de curvatura en cada vértice de la polilínea."""
vertices = wire.Vertexes
n = len(vertices)
if n < 3:
obj.HorizontalCurveRadii = []
return
radii = []
for i in range(1, n - 1):
p0 = vertices[i - 1].Point
p1 = vertices[i].Point
p2 = vertices[i + 1].Point
v1 = p1 - p0
v2 = p2 - p1
cross = FreeCAD.Vector(0, 0, 1).dot(v1.cross(v2))
if abs(cross) < 1.0: # casi colineal
radii.append(0.0)
else:
# Radio aproximado = |v1| * |v2| / |v1 x v2|
r = v1.Length * v2.Length / abs(cross)
radii.append(round(r, 0))
obj.HorizontalCurveRadii = radii
def get_station_point(self, obj, distance):
"""Devuelve el punto 3D en el eje a una progresiva dada (mm)."""
if not obj.SourceWire:
return None
try:
wire = obj.SourceWire.Shape
param = wire.getParameterByLength(distance / obj.TotalLength)
return wire.valueAt(param)
except Exception:
return None
def get_tangent_at(self, obj, distance):
"""Devuelve el vector tangente en una progresiva (normalizado)."""
if not obj.SourceWire:
return None
try:
wire = obj.SourceWire.Shape
param = wire.getParameterByLength(distance / obj.TotalLength)
t = wire.tangentAt(param)
if t.Length > 0:
t.normalize()
return t
except Exception:
return None
def get_perpendicular_at(self, obj, distance):
"""Devuelve el vector perpendicular (horizontal) en una progresiva."""
t = self.get_tangent_at(obj, distance)
if t is None:
return None
perp = FreeCAD.Vector(-t.y, t.x, 0)
perp.normalize()
return perp
def get_station_data(self, obj):
"""
Devuelve arrays de (puntos, tangentes, perpendiculares) para todas las estaciones.
Returns:
tuple: (points, tangents, perps) listas de FreeCAD.Vector
"""
stations = obj.Stations
if not stations:
return [], [], []
points = []
tangents = []
perps = []
for s in stations:
pt = self.get_station_point(obj, s)
tg = self.get_tangent_at(obj, s)
pp = self.get_perpendicular_at(obj, s)
if pt and tg and pp:
points.append(pt)
tangents.append(tg)
perps.append(pp)
return points, tangents, perps
def __getstate__(self):
return None
def __setstate__(self, state):
return None
class _ViewProviderAlignment:
def __init__(self, vobj):
vobj.Proxy = self
def getIcon(self):
return ""
def __getstate__(self):
return None
def __setstate__(self, state):
return None
+7
View File
@@ -0,0 +1,7 @@
# Road Module - PVPlant Workbench
# Sistema de carreteras con alineamiento profesional
from .Alignment import make_alignment_from_wire, Alignment
from .CrossSection import CrossSectionBuilder
from .Road import make_road, Road
from .CutFill import calculate_cut_fill
+1 -1
View File
@@ -61,7 +61,7 @@ class SelObserver:
def onAceptClick(self):
''' '''
from PVPlantPlacement import moveFrameHead
from Civil.PVPlantPlacementCalc import moveFrameHead
moveFrameHead(self.obj, head=self.ui.comboHead.currentIndex(),
dist=self.ui.editDist.value())
self.setUI(self.obj)
+8 -2
View File
@@ -224,14 +224,20 @@ def getProjected(shape, direction=FreeCAD.Vector(0, 0, 1)): # Based on Draft / s
return ow
def findObjects(classtype):
'''def findObjects(classtype):
objects = FreeCAD.ActiveDocument.Objects
objlist = list()
for object in objects:
if hasattr(object, "Proxy"):
if object.Proxy.Type == classtype:
objlist.append(object)
return objlist
return objlist'''
def findObjects(classtype):
return [obj for obj in FreeCAD.ActiveDocument.Objects
if hasattr(obj, "Proxy")
and hasattr(obj.Proxy, "Type")
and obj.Proxy.Type == classtype]
def getClosePoints(sh1, angle):
'''
-348
View File
@@ -1,348 +0,0 @@
# -*- coding: utf-8 -*-
__title__ = "Freehand BSpline"
__author__ = "Christophe Grellier (Chris_G)"
__license__ = "LGPL 2.1"
__doc__ = "Creates an freehand BSpline curve"
__usage__ = """*** Interpolation curve control keys :
a - Select all / Deselect
i - Insert point in selected segments
t - Set / unset tangent (view direction)
p - Align selected objects
s - Snap points on shape / Unsnap
l - Set/unset a linear interpolation
x,y,z - Axis constraints during grab
q - Apply changes and quit editing"""
import os
import FreeCAD
import FreeCADGui
import Part
from . import ICONPATH
from . import _utils
from . import profile_editor
TOOL_ICON = os.path.join(ICONPATH, 'editableSpline.svg')
# debug = _utils.debug
debug = _utils.doNothing
def check_pivy():
try:
profile_editor.MarkerOnShape([FreeCAD.Vector()])
return True
except Exception as exc:
FreeCAD.Console.PrintWarning(str(exc) + "\nPivy interaction library failure\n")
return False
def midpoint(e):
p = e.FirstParameter + 0.5 * (e.LastParameter - e.FirstParameter)
return e.valueAt(p)
class GordonProfileFP:
"""Creates an editable interpolation curve"""
def __init__(self, obj, s, d, t):
"""Add the properties"""
obj.addProperty("App::PropertyLinkSubList", "Support", "Profile", "Constraint shapes").Support = s
obj.addProperty("App::PropertyFloatConstraint", "Parametrization", "Profile", "Parametrization factor")
obj.addProperty("App::PropertyFloat", "Tolerance", "Profile", "Tolerance").Tolerance = 1e-5
obj.addProperty("App::PropertyBool", "Periodic", "Profile", "Periodic curve").Periodic = False
obj.addProperty("App::PropertyVectorList", "Data", "Profile", "Data list").Data = d
obj.addProperty("App::PropertyVectorList", "Tangents", "Profile", "Tangents list")
obj.addProperty("App::PropertyBoolList", "Flags", "Profile", "Tangent flags")
obj.addProperty("App::PropertyIntegerList", "DataType", "Profile", "Types of interpolated points").DataType = t
obj.addProperty("App::PropertyBoolList", "LinearSegments", "Profile", "Linear segment flags")
obj.Parametrization = (1.0, 0.0, 1.0, 0.05)
obj.Proxy = self
def get_shapes(self, fp):
if hasattr(fp, 'Support'):
sl = list()
for ob, names in fp.Support:
for name in names:
if "Vertex" in name:
n = eval(name.lstrip("Vertex"))
if len(ob.Shape.Vertexes) >= n:
sl.append(ob.Shape.Vertexes[n - 1])
elif ("Point" in name):
sl.append(Part.Vertex(ob.Shape.Point))
elif ("Edge" in name):
n = eval(name.lstrip("Edge"))
if len(ob.Shape.Edges) >= n:
sl.append(ob.Shape.Edges[n - 1])
elif ("Face" in name):
n = eval(name.lstrip("Face"))
if len(ob.Shape.Faces) >= n:
sl.append(ob.Shape.Faces[n - 1])
return sl
def get_points(self, fp, stretch=True):
touched = False
shapes = self.get_shapes(fp)
if not len(fp.Data) == len(fp.DataType):
FreeCAD.Console.PrintError("Gordon Profile : Data and DataType mismatch\n")
return(None)
pts = list()
shape_idx = 0
for i in range(len(fp.Data)):
if fp.DataType[i] == 0: # Free point
pts.append(fp.Data[i])
elif (fp.DataType[i] == 1):
if (shape_idx < len(shapes)): # project on shape
d, p, i = Part.Vertex(fp.Data[i]).distToShape(shapes[shape_idx])
if d > fp.Tolerance:
touched = True
pts.append(p[0][1]) # shapes[shape_idx].valueAt(fp.Data[i].x))
shape_idx += 1
else:
pts.append(fp.Data[i])
if stretch and touched:
params = [0]
knots = [0]
moves = [pts[0] - fp.Data[0]]
lsum = 0
mults = [2]
for i in range(1, len(pts)):
lsum += fp.Data[i - 1].distanceToPoint(fp.Data[i])
params.append(lsum)
if fp.DataType[i] == 1:
knots.append(lsum)
moves.append(pts[i] - fp.Data[i])
mults.insert(1, 1)
mults[-1] = 2
if len(moves) < 2:
return(pts)
# FreeCAD.Console.PrintMessage("%s\n%s\n%s\n"%(moves,mults,knots))
curve = Part.BSplineCurve()
curve.buildFromPolesMultsKnots(moves, mults, knots, False, 1)
for i in range(1, len(pts)):
if fp.DataType[i] == 0:
# FreeCAD.Console.PrintMessage("Stretch %s #%d: %s to %s\n"%(fp.Label,i,pts[i],curve.value(params[i])))
pts[i] += curve.value(params[i])
if touched:
return pts
else:
return False
def execute(self, obj):
try:
o = FreeCADGui.ActiveDocument.getInEdit().Object
if o == obj:
return
except:
FreeCAD.Console.PrintWarning("execute is disabled during editing\n")
pts = self.get_points(obj)
if pts:
if len(pts) < 2:
FreeCAD.Console.PrintError("{} : Not enough points\n".format(obj.Label))
return False
else:
obj.Data = pts
else:
pts = obj.Data
tans = [FreeCAD.Vector()] * len(pts)
flags = [False] * len(pts)
for i in range(len(obj.Tangents)):
tans[i] = obj.Tangents[i]
for i in range(len(obj.Flags)):
flags[i] = obj.Flags[i]
# if not (len(obj.LinearSegments) == len(pts)-1):
# FreeCAD.Console.PrintError("%s : Points and LinearSegments mismatch\n"%obj.Label)
if len(obj.LinearSegments) > 0:
for i, b in enumerate(obj.LinearSegments):
if b:
tans[i] = pts[i + 1] - pts[i]
tans[i + 1] = tans[i]
flags[i] = True
flags[i + 1] = True
params = profile_editor.parameterization(pts, obj.Parametrization, obj.Periodic)
curve = Part.BSplineCurve()
if len(pts) == 2:
curve.buildFromPoles(pts)
elif obj.Periodic and pts[0].distanceToPoint(pts[-1]) < 1e-7:
curve.interpolate(Points=pts[:-1], Parameters=params, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Tangents=tans[:-1], TangentFlags=flags[:-1])
else:
curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Tangents=tans, TangentFlags=flags)
obj.Shape = curve.toShape()
def onChanged(self, fp, prop):
if prop in ("Support", "Data", "DataType", "Periodic"):
# FreeCAD.Console.PrintMessage("%s : %s changed\n"%(fp.Label,prop))
if (len(fp.Data) == len(fp.DataType)) and (sum(fp.DataType) == len(fp.Support)):
new_pts = self.get_points(fp, True)
if new_pts:
fp.Data = new_pts
if prop == "Parametrization":
self.execute(fp)
def onDocumentRestored(self, fp):
fp.setEditorMode("Data", 2)
fp.setEditorMode("DataType", 2)
class GordonProfileVP:
def __init__(self, vobj):
vobj.Proxy = self
self.select_state = True
self.active = False
def getIcon(self):
return TOOL_ICON
def attach(self, vobj):
self.Object = vobj.Object
self.active = False
self.select_state = vobj.Selectable
self.ip = None
def setEdit(self, vobj, mode=0):
if mode == 0 and check_pivy():
if vobj.Selectable:
self.select_state = True
vobj.Selectable = False
pts = list()
sl = list()
for ob, names in self.Object.Support:
for name in names:
sl.append((ob, (name,)))
shape_idx = 0
for i in range(len(self.Object.Data)):
p = self.Object.Data[i]
t = self.Object.DataType[i]
if t == 0:
pts.append(profile_editor.MarkerOnShape([p]))
elif t == 1:
pts.append(profile_editor.MarkerOnShape([p], sl[shape_idx]))
shape_idx += 1
for i in range(len(pts)): # p,t,f in zip(pts, self.Object.Tangents, self.Object.Flags):
if i < min(len(self.Object.Flags), len(self.Object.Tangents)):
if self.Object.Flags[i]:
pts[i].tangent = self.Object.Tangents[i]
self.ip = profile_editor.InterpoCurveEditor(pts, self.Object)
self.ip.periodic = self.Object.Periodic
self.ip.param_factor = self.Object.Parametrization
for i in range(min(len(self.Object.LinearSegments), len(self.ip.lines))):
self.ip.lines[i].tangent = self.Object.LinearSegments[i]
self.ip.lines[i].updateLine()
self.active = True
return True
return False
def unsetEdit(self, vobj, mode=0):
if isinstance(self.ip, profile_editor.InterpoCurveEditor) and check_pivy():
pts = list()
typ = list()
tans = list()
flags = list()
# original_links = self.Object.Support
new_links = list()
for p in self.ip.points:
if isinstance(p, profile_editor.MarkerOnShape):
pt = p.points[0]
pts.append(FreeCAD.Vector(pt[0], pt[1], pt[2]))
if p.sublink:
new_links.append(p.sublink)
typ.append(1)
else:
typ.append(0)
if p.tangent:
tans.append(p.tangent)
flags.append(True)
else:
tans.append(FreeCAD.Vector())
flags.append(False)
self.Object.Tangents = tans
self.Object.Flags = flags
self.Object.LinearSegments = [li.linear for li in self.ip.lines]
self.Object.DataType = typ
self.Object.Data = pts
self.Object.Support = new_links
vobj.Selectable = self.select_state
self.ip.quit()
self.ip = None
self.active = False
self.Object.Document.recompute()
return True
def doubleClicked(self, vobj):
if not hasattr(self, 'active'):
self.active = False
if not self.active:
self.active = True
# self.setEdit(vobj)
vobj.Document.setEdit(vobj)
else:
vobj.Document.resetEdit()
self.active = False
return True
def __getstate__(self):
return {"name": self.Object.Name}
def __setstate__(self, state):
self.Object = FreeCAD.ActiveDocument.getObject(state["name"])
return None
class GordonProfileCommand:
"""Creates a editable interpolation curve"""
def makeFeature(self, sub, pts, typ):
fp = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Freehand BSpline")
GordonProfileFP(fp, sub, pts, typ)
GordonProfileVP(fp.ViewObject)
FreeCAD.Console.PrintMessage(__usage__)
FreeCAD.ActiveDocument.recompute()
FreeCADGui.SendMsgToActiveView("ViewFit")
fp.ViewObject.Document.setEdit(fp.ViewObject)
def Activated(self):
s = FreeCADGui.Selection.getSelectionEx()
try:
ordered = FreeCADGui.activeWorkbench().Selection
if ordered:
s = ordered
except AttributeError:
pass
sub = list()
pts = list()
for obj in s:
if obj.HasSubObjects:
# FreeCAD.Console.PrintMessage("object has subobjects %s\n"%str(obj.SubElementNames))
for n in obj.SubElementNames:
sub.append((obj.Object, [n]))
for p in obj.PickedPoints:
pts.append(p)
if len(pts) == 0:
pts = [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(5, 0, 0), FreeCAD.Vector(10, 0, 0)]
typ = [0, 0, 0]
elif len(pts) == 1:
pts.append(pts[0] + FreeCAD.Vector(5, 0, 0))
pts.append(pts[0] + FreeCAD.Vector(10, 0, 0))
typ = [1, 0, 0]
else:
typ = [1] * len(pts)
self.makeFeature(sub, pts, typ)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
def GetResources(self):
return {'Pixmap': TOOL_ICON,
'MenuText': __title__,
'ToolTip': "{}<br><br><b>Usage :</b><br>{}".format(__doc__, "<br>".join(__usage__.splitlines()))}
FreeCADGui.addCommand('gordon_profile', GordonProfileCommand())
-533
View File
@@ -1,533 +0,0 @@
from pivy import coin
#from pivy.utils import getPointOnScreen
def getPointOnScreen(render_manager, screen_pos, normal="camera", point=None):
"""get coordinates from pixel position"""
pCam = render_manager.getCamera()
vol = pCam.getViewVolume()
point = point or coin.SbVec3f(0, 0, 0)
if normal == "camera":
plane = vol.getPlane(10)
normal = plane.getNormal()
elif normal == "x":
normal = SbVec3f(1, 0, 0)
elif normal == "y":
normal = SbVec3f(0, 1, 0)
elif normal == "z":
normal = SbVec3f(0, 0, 1)
normal.normalize()
x, y = screen_pos
vp = render_manager.getViewportRegion()
size = vp.getViewportSize()
dX, dY = size
fRatio = vp.getViewportAspectRatio()
pX = float(x) / float(vp.getViewportSizePixels()[0])
pY = float(y) / float(vp.getViewportSizePixels()[1])
if (fRatio > 1.0):
pX = (pX - 0.5 * dX) * fRatio + 0.5 * dX
elif (fRatio < 1.0):
pY = (pY - 0.5 * dY) / fRatio + 0.5 * dY
plane = coin.SbPlane(normal, point)
line = coin.SbLine(*vol.projectPointToLine(coin.SbVec2f(pX,pY)))
pt = plane.intersect(line)
return(pt)
COLORS = {
"black": (0., 0., 0.),
"white": (1., 1., 1.),
"grey": (.5, .5, .5),
"red": (1., 0., 0.),
"green": (0., 1., 0.),
"blue": (0., 0., 1.),
"yellow": (1., 1., 0.),
"cyan": (0., 1., 1.),
"magenta":(1., 0., 1.)
}
class Object3D(coin.SoSeparator):
std_col = "black"
ovr_col = "red"
sel_col = "yellow"
non_col = "grey"
def __init__(self, points, dynamic=False):
super(Object3D, self).__init__()
self.data = coin.SoCoordinate3()
self.color = coin.SoMaterial()
self.set_color()
self += [self.color, self.data]
self.start_pos = None
self.dynamic = dynamic
# callback function lists
self.on_drag = []
self.on_drag_release = []
self.on_drag_start = []
self._delete = False
self._tmp_points = None
self.enabled = True
self.points = points
def set_disabled(self):
self.color.diffuseColor = COLORS[self.non_col]
self.enabled = False
def set_enabled(self):
self.color.diffuseColor = COLORS[self.std_col]
self.enabled = True
def set_color(self, col=None):
self.std_col = col or self.std_col
self.color.diffuseColor = COLORS[self.std_col]
@property
def points(self):
return self.data.point.getValues()
@points.setter
def points(self, points):
self.data.point.setValue(0, 0, 0)
self.data.point.setValues(0, len(points), points)
def set_mouse_over(self):
if self.enabled:
self.color.diffuseColor = COLORS[self.ovr_col]
def unset_mouse_over(self):
if self.enabled:
self.color.diffuseColor = COLORS[self.std_col]
def select(self):
if self.enabled:
self.color.diffuseColor = COLORS[self.sel_col]
def unselect(self):
if self.enabled:
self.color.diffuseColor = COLORS[self.std_col]
def drag(self, mouse_coords, fact=1.):
if self.enabled:
pts = self.points
for i, pt in enumerate(pts):
pt[0] = mouse_coords[0] * fact + self._tmp_points[i][0]
pt[1] = mouse_coords[1] * fact + self._tmp_points[i][1]
pt[2] = mouse_coords[2] * fact + self._tmp_points[i][2]
self.points = pts
for foo in self.on_drag:
foo()
def drag_release(self):
if self.enabled:
for foo in self.on_drag_release:
foo()
def drag_start(self):
self._tmp_points = self.points
if self.enabled:
for foo in self.on_drag_start:
foo()
@property
def drag_objects(self):
if self.enabled:
return [self]
def delete(self):
if self.enabled and not self._delete:
self._delete = True
def check_dependency(self):
pass
class Marker(Object3D):
def __init__(self, points, dynamic=False):
super(Marker, self).__init__(points, dynamic)
self.marker = coin.SoMarkerSet()
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
self.addChild(self.marker)
class Line(Object3D):
def __init__(self, points, dynamic=False):
super(Line, self).__init__(points, dynamic)
self.drawstyle = coin.SoDrawStyle()
self.line = coin.SoLineSet()
self.addChild(self.drawstyle)
self.addChild(self.line)
class Point(Object3D):
def __init__(self, points, dynamic=False):
super(Point, self).__init__(points, dynamic)
self.drawstyle = coin.SoDrawStyle()
self.point = coin.SoPointSet()
self.addChild(self.drawstyle)
self.addChild(self.point)
class Polygon(Object3D):
def __init__(self, points, dynamic=False):
super(Polygon, self).__init__(points, dynamic)
self.polygon = coin.SoFaceSet()
self.addChild(self.polygon)
class Arrow(Line):
def __init__(self, points, dynamic=False, arrow_size=0.04, length=2):
super(Arrow, self).__init__(points, dynamic)
self.arrow_sep = coin.SoSeparator()
self.arrow_rot = coin.SoRotation()
self.arrow_scale = coin.SoScale()
self.arrow_translate = coin.SoTranslation()
self.arrow_scale.scaleFactor.setValue(arrow_size, arrow_size, arrow_size)
self.cone = coin.SoCone()
arrow_length = coin.SoScale()
arrow_length.scaleFactor = (1, length, 1)
arrow_origin = coin.SoTranslation()
arrow_origin.translation = (0, -1, 0)
self.arrow_sep += [self.arrow_translate, self.arrow_rot, self.arrow_scale]
self.arrow_sep += [arrow_length, arrow_origin, self.cone]
self += [self.arrow_sep]
self.set_arrow_direction()
def set_arrow_direction(self):
pts = np.array(self.points)
self.arrow_translate.translation = tuple(pts[-1])
direction = pts[-1] - pts[-2]
direction /= np.linalg.norm(direction)
_rot = coin.SbRotation()
_rot.setValue(coin.SbVec3f(0, 1, 0), coin.SbVec3f(*direction))
self.arrow_rot.rotation.setValue(_rot)
class InteractionSeparator(coin.SoSeparator):
pick_radius = 20
ctrl_keys = {"grab": "g",
"abort_grab": u"\uff1b",
"select_all": "a",
"delete": u"\uffff",
"axis_x": "x",
"axis_y": "y",
"axis_z": "z"}
def __init__(self, render_manager):
super(InteractionSeparator, self).__init__()
self.render_manager = render_manager
self.objects = coin.SoSeparator()
self.dynamic_objects = []
self.static_objects = []
self.over_object = None
self.selected_objects = []
self.drag_objects = []
self.on_drag = []
self.on_drag_release = []
self.on_drag_start = []
self._direction = None
self.events = coin.SoEventCallback()
self += self.events, self.objects
def register(self):
self._highlightCB = self.events.addEventCallback(
coin.SoLocation2Event.getClassTypeId(), self.highlightCB)
self._selectCB = self.events.addEventCallback(
coin.SoMouseButtonEvent.getClassTypeId(), self.selectCB)
self._grabCB = self.events.addEventCallback(
coin.SoMouseButtonEvent.getClassTypeId(), self.grabCB)
self._deleteCB = self.events.addEventCallback(
coin.SoKeyboardEvent.getClassTypeId(), self.deleteCB)
self._selectAllCB = self.events.addEventCallback(
coin.SoKeyboardEvent.getClassTypeId(), self.selectAllCB)
def unregister(self):
self.events.removeEventCallback(
coin.SoLocation2Event.getClassTypeId(), self._highlightCB)
self.events.removeEventCallback(
coin.SoMouseButtonEvent.getClassTypeId(), self._selectCB)
self.events.removeEventCallback(
coin.SoMouseButtonEvent.getClassTypeId(), self._grabCB)
self.events.removeEventCallback(
coin.SoKeyboardEvent.getClassTypeId(), self._deleteCB)
self.events.removeEventCallback(
coin.SoKeyboardEvent.getClassTypeId(), self._selectAllCB)
def addChild(self, child):
if hasattr(child, "dynamic"):
self.objects.addChild(child)
if child.dynamic:
self.dynamic_objects.append(child)
else:
self.static_objects.append(child)
else:
super(InteractionSeparator, self).addChild(child)
#-----------------------HIGHLIGHTING-----------------------#
# a SoLocation2Event calling a function which sends rays #
# int the scene. This will return the object the mouse is #
# currently hoovering. #
def highlightObject(self, obj):
if self.over_object:
self.over_object.unset_mouse_over()
self.over_object = obj
if self.over_object:
self.over_object.set_mouse_over()
self.colorSelected()
def highlightCB(self, attr, event_callback):
event = event_callback.getEvent()
pos = event.getPosition()
obj = self.sendRay(pos)
self.highlightObject(obj)
def sendRay(self, mouse_pos):
"""sends a ray through the scene and return the nearest entity"""
ray_pick = coin.SoRayPickAction(self.render_manager.getViewportRegion())
ray_pick.setPoint(coin.SbVec2s(*mouse_pos))
ray_pick.setRadius(InteractionSeparator.pick_radius)
ray_pick.setPickAll(True)
ray_pick.apply(self.render_manager.getSceneGraph())
picked_point = ray_pick.getPickedPointList()
return self.objByID(picked_point)
def objByID(self, picked_point):
for point in picked_point:
path = point.getPath()
length = path.getLength()
point = path.getNode(length - 2)
for o in self.dynamic_objects:
if point == o:
return(o)
# Code below was not working with python 2.7 (pb with getNodeId ?)
#point = list(filter(
#lambda ctrl: ctrl.getNodeId() == point.getNodeId(),
#self.dynamic_objects))
#if point != []:
#return point[0]
return None
#------------------------SELECTION------------------------#
def selectObject(self, obj, multi=False):
if not multi:
for o in self.selected_objects:
o.unselect()
self.selected_objects = []
if obj:
if obj in self.selected_objects:
self.selected_objects.remove(obj)
else:
self.selected_objects.append(obj)
self.colorSelected()
self.selectionChanged()
def selectCB(self, attr, event_callback):
event = event_callback.getEvent()
if (event.getState() == coin.SoMouseButtonEvent.DOWN and
event.getButton() == event.BUTTON1):
pos = event.getPosition()
obj = self.sendRay(pos)
self.selectObject(obj, event.wasCtrlDown())
def select_all_cb(self, event_callback):
event = event_callback.getEvent()
if (event.getKey() == ord(InteractionSeparator.ctrl_keys["select_all"])):
if event.getState() == event.DOWN:
if self.selected_objects:
for o in self.selected_objects:
o.unselect()
self.selected_objects = []
else:
for obj in self.objects:
if obj.dynamic:
self.selected_objects.append(obj)
self.ColorSelected()
self.selection_changed()
def deselect_all(self):
if self.selected_objects:
for o in self.selected_objects:
o.unselect()
self.selected_objects = []
def colorSelected(self):
for obj in self.selected_objects:
obj.select()
def selectionChanged(self):
pass
def selectAllCB(self, attr, event_callback):
event = event_callback.getEvent()
if (event.getKey() == ord(InteractionSeparator.ctrl_keys["select_all"])):
if event.getState() == event.DOWN:
if self.selected_objects:
for o in self.selected_objects:
o.unselect()
self.selected_objects = []
else:
for obj in self.dynamic_objects:
if obj.dynamic:
self.selected_objects.append(obj)
self.colorSelected()
self.selectionChanged()
#------------------------INTERACTION------------------------#
def cursor_pos(self, event):
pos = event.getPosition()
# print(list(getPointOnScreen1(self.render_manager, pos)))
return getPointOnScreen(self.render_manager, pos)
def constrained_vector(self, vector):
if self._direction is None:
return vector
if self._direction == InteractionSeparator.ctrl_keys["axis_x"]:
return [vector[0], 0, 0]
elif self._direction == InteractionSeparator.ctrl_keys["axis_y"]:
return [0, vector[1], 0]
elif self._direction == InteractionSeparator.ctrl_keys["axis_z"]:
return [0, 0, vector[2]]
def grabCB(self, attr, event_callback):
# press grab key to move an entity
event = event_callback.getEvent()
# get all drag objects, every selected object can add some drag objects
# but the eventhandler is not allowed to call the drag twice on an object
#if event.getKey() == ord(InteractionSeparator.ctrl_keys["grab"]):
if (event.getState() == coin.SoMouseButtonEvent.DOWN and
event.getButton() == event.BUTTON1):
pos = event.getPosition()
obj = self.sendRay(pos)
if obj:
#if not obj in self.selected_objects:
#self.selectObject(obj, event.wasCtrlDown())
self.drag_objects = set()
for i in self.selected_objects:
for j in i.drag_objects:
self.drag_objects.add(j)
# check if something is selected
if self.drag_objects:
# first delete the selection_cb, and higlight_cb
self.unregister()
# now add a callback that calls the dragfunction of the selected entities
self.start_pos = self.cursor_pos(event)
self._dragCB = self.events.addEventCallback(
coin.SoEvent.getClassTypeId(), self.dragCB)
for obj in self.drag_objects:
obj.drag_start()
for foo in self.on_drag_start:
foo()
def dragCB(self, attr, event_callback, force=False):
event = event_callback.getEvent()
b = ""
s = ""
if type(event) == coin.SoMouseButtonEvent:
if event.getButton() == coin.SoMouseButtonEvent.BUTTON1:
b = "mb1"
elif event.getButton() == coin.SoMouseButtonEvent.BUTTON2:
b = "mb2"
if event.getState() == coin.SoMouseButtonEvent.UP:
s = "up"
elif event.getState() == coin.SoMouseButtonEvent.DOWN:
s = "down"
import FreeCAD
FreeCAD.Console.PrintMessage("{} {}\n".format(b,s))
if ((type(event) == coin.SoMouseButtonEvent and
event.getState() == coin.SoMouseButtonEvent.UP
and event.getButton() == coin.SoMouseButtonEvent.BUTTON1) or
force):
self.register()
if self._dragCB:
self.events.removeEventCallback(
coin.SoEvent.getClassTypeId(), self._dragCB)
self._direction = None
self._dragCB = None
self.start_pos = None
for obj in self.drag_objects:
obj.drag_release()
for foo in self.on_drag_release:
foo()
self.drag_objects = []
elif (type(event) == coin.SoKeyboardEvent and
event.getState() == coin.SoMouseButtonEvent.DOWN):
if event.getKey() == InteractionSeparator.ctrl_keys["abort_grab"]: # esc
for obj in self.drag_objects:
obj.drag([0, 0, 0], 1) # set back to zero
self.dragCB(attr, event_callback, force=True)
return
try:
key = chr(event.getKey())
except ValueError:
# there is no character for this value
key = "_"
if key in [InteractionSeparator.ctrl_keys["axis_x"],
InteractionSeparator.ctrl_keys["axis_y"],
InteractionSeparator.ctrl_keys["axis_z"]] and key != self._direction:
self._direction = key
else:
self._direction = None
diff = self.cursor_pos(event) - self.start_pos
diff = self.constrained_vector(diff)
for obj in self.drag_objects:
obj.drag(diff, 1)
for foo in self.on_drag:
foo()
elif type(event) == coin.SoLocation2Event:
fact = 0.1 if event.wasShiftDown() else 1.
diff = self.cursor_pos(event) - self.start_pos
diff = self.constrained_vector(diff)
for obj in self.drag_objects:
obj.drag(diff, fact)
for foo in self.on_drag:
foo()
def deleteCB(self, attr, event_callback):
event = event_callback.getEvent()
# get all drag objects, every selected object can add some drag objects
# but the eventhandler is not allowed to call the drag twice on an object
if event.getKey() == ord(InteractionSeparator.ctrl_keys["delete"]) and (event.getState() == 1):
self.removeSelected()
def removeSelected(self):
temp = []
for i in self.selected_objects:
i.delete()
for i in self.dynamic_objects + self.static_objects:
i.check_dependency() #dependency length max = 1
for i in self.dynamic_objects + self.static_objects:
if i._delete:
temp.append(i)
self.selected_objects = []
self.over_object = None
self.selectionChanged()
for i in temp:
if i in self.dynamic_objects:
self.dynamic_objects.remove(i)
else:
self.static_objects.remove(i)
self.objects.removeChild(i)
del(i)
self.selectionChanged()
def removeAllChildren(self):
for i in self.dynamic_objects:
i.delete()
self.dynamic_objects = []
self.static_objects = []
self.selected_objects = []
self.over_object = None
super(InteractionSeparator, self).removeAllChildren()
-501
View File
@@ -1,501 +0,0 @@
# from curve workbench
import FreeCAD
import FreeCADGui
import Part
import PySide.QtCore as QtCore
import PySide.QtGui as QtGui
from pivy import coin
from Utils import graphics
def parameterization(points, a, closed):
"""Computes a knot Sequence for a set of points
fac (0-1) : parameterization factor
fac=0 -> Uniform / fac=0.5 -> Centripetal / fac=1.0 -> Chord-Length"""
pts = points.copy()
if closed and pts[0].distanceToPoint(pts[-1]) > 1e-7: # we need to add the first point as the end point
pts.append(pts[0])
params = [0]
for i in range(1, len(pts)):
p = pts[i] - pts[i - 1]
if isinstance(p, FreeCAD.Vector):
le = p.Length
else:
le = p.length()
pl = pow(le, a)
params.append(params[-1] + pl)
return params
class ConnectionMarker(graphics.Marker):
def __init__(self, points):
super(ConnectionMarker, self).__init__(points, True)
class MarkerOnShape(graphics.Marker):
def __init__(self, points, sh=None):
super(MarkerOnShape, self).__init__(points, True)
self._shape = None
self._sublink = None
self._tangent = None
self._translate = coin.SoTranslation()
self._text_font = coin.SoFont()
self._text_font.name = "Arial:Bold"
self._text_font.size = 13.0
self._text = coin.SoText2()
self._text_switch = coin.SoSwitch()
self._text_switch.addChild(self._translate)
self._text_switch.addChild(self._text_font)
self._text_switch.addChild(self._text)
self.on_drag_start.append(self.add_text)
self.on_drag_release.append(self.remove_text)
self.addChild(self._text_switch)
if isinstance(sh, Part.Shape):
self.snap_shape = sh
elif isinstance(sh, (tuple, list)):
self.sublink = sh
def subshape_from_sublink(self, o):
name = o[1][0]
print(name, " selected")
if 'Vertex' in name:
n = eval(name.lstrip('Vertex'))
return o[0].Shape.Vertexes[n - 1]
elif 'Edge' in name:
n = eval(name.lstrip('Edge'))
return o[0].Shape.Edges[n - 1]
elif 'Face' in name:
n = eval(name.lstrip('Face'))
return o[0].Shape.Faces[n - 1]
def add_text(self):
self._text_switch.whichChild = coin.SO_SWITCH_ALL
self.on_drag.append(self.update_text)
def remove_text(self):
self._text_switch.whichChild = coin.SO_SWITCH_NONE
self.on_drag.remove(self.update_text)
def update_text(self):
p = self.points[0]
coords = ['{: 9.3f}'.format(p[0]), '{: 9.3f}'.format(p[1]), '{: 9.3f}'.format(p[2])]
self._translate.translation = p
self._text.string.setValues(0, 3, coords)
@property
def tangent(self):
return self._tangent
@tangent.setter
def tangent(self, t):
if isinstance(t, FreeCAD.Vector):
if t.Length > 1e-7:
self._tangent = t
self._tangent.normalize()
self.marker.markerIndex = coin.SoMarkerSet.DIAMOND_FILLED_9_9
else:
self._tangent = None
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
else:
self._tangent = None
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
@property
def snap_shape(self):
return self._shape
@snap_shape.setter
def snap_shape(self, sh):
if isinstance(sh, Part.Shape):
self._shape = sh
else:
self._shape = None
self.alter_color()
@property
def sublink(self):
return self._sublink
@sublink.setter
def sublink(self, sl):
if isinstance(sl, (tuple, list)) and not (sl == self._sublink):
self._shape = self.subshape_from_sublink(sl)
self._sublink = sl
else:
self._shape = None
self._sublink = None
self.alter_color()
def alter_color(self):
if isinstance(self._shape, Part.Vertex):
self.set_color("white")
elif isinstance(self._shape, Part.Edge):
self.set_color("cyan")
elif isinstance(self._shape, Part.Face):
self.set_color("magenta")
else:
self.set_color("black")
def __repr__(self):
return "MarkerOnShape({})".format(self._shape)
def drag(self, mouse_coords, fact=1.):
if self.enabled:
pts = self.points
for i, p in enumerate(pts):
p[0] = mouse_coords[0] * fact + self._tmp_points[i][0]
p[1] = mouse_coords[1] * fact + self._tmp_points[i][1]
p[2] = mouse_coords[2] * fact + self._tmp_points[i][2]
if self._shape:
v = Part.Vertex(p[0], p[1], p[2])
proj = v.distToShape(self._shape)[1][0][1]
# FreeCAD.Console.PrintMessage("%s -> %s\n"%(p.getValue(), proj))
p[0] = proj.x
p[1] = proj.y
p[2] = proj.z
self.points = pts
for foo in self.on_drag:
foo()
class ConnectionPolygon(graphics.Polygon):
std_col = "green"
def __init__(self, markers):
super(ConnectionPolygon, self).__init__(
sum([m.points for m in markers], []), True)
self.markers = markers
for m in self.markers:
m.on_drag.append(self.updatePolygon)
def updatePolygon(self):
self.points = sum([m.points for m in self.markers], [])
@property
def drag_objects(self):
return self.markers
def check_dependency(self):
if any([m._delete for m in self.markers]):
self.delete()
class ConnectionLine(graphics.Line):
def __init__(self, markers):
super(ConnectionLine, self).__init__(
sum([m.points for m in markers], []), True)
self.markers = markers
self._linear = False
for m in self.markers:
m.on_drag.append(self.updateLine)
def updateLine(self):
self.points = sum([m.points for m in self.markers], [])
if self._linear:
p1 = self.markers[0].points[0]
p2 = self.markers[-1].points[0]
t = p2 - p1
tan = FreeCAD.Vector(t[0], t[1], t[2])
for m in self.markers:
m.tangent = tan
@property
def linear(self):
return self._linear
@linear.setter
def linear(self, b):
self._linear = bool(b)
@property
def drag_objects(self):
return self.markers
def check_dependency(self):
if any([m._delete for m in self.markers]):
self.delete()
class Edit(object):
def __init__(self, points=[], obj=None):
self.points = list()
self.lines = list()
self.obj = obj
self.root_inserted = False
self.root = None
self.editing = None
# event callbacks
self.selection_callback = None
self._keyPressedCB = None
self._mouseMovedCB = None
self._mousePressedCB = None
for p in points:
if isinstance(p, FreeCAD.Vector):
self.points.append(MarkerOnShape([p]))
elif isinstance(p, (tuple, list)):
self.points.append(MarkerOnShape([p[0]], p[1]))
elif isinstance(p, (MarkerOnShape, ConnectionMarker)):
self.points.append(p)
else:
FreeCAD.Console.PrintError("InterpoCurveEditor : bad input")
# Setup coin objects
if self.obj:
self.guidoc = self.obj.ViewObject.Document
else:
if not FreeCADGui.ActiveDocument:
FreeCAD.newDocument("New")
self.guidoc = FreeCADGui.ActiveDocument
self.view = self.guidoc.ActiveView
self.rm = self.view.getViewer().getSoRenderManager()
self.sg = self.view.getSceneGraph()
self.setupInteractionSeparator()
# Callbacks
#self.unregister_editing_callbacks()
#self.register_editing_callbacks()
def setupInteractionSeparator(self):
if self.root_inserted:
self.sg.removeChild(self.root)
self.root = graphics.InteractionSeparator(self.rm)
self.root.setName("InteractionSeparator")
self.root.pick_radius = 40
# Populate root node
self.root += self.points
self.build_lines()
self.root += self.lines
# set FreeCAD color scheme
for o in self.points + self.lines:
o.ovr_col = "yellow"
o.sel_col = "green"
self.root.register()
self.sg.addChild(self.root)
self.root_inserted = True
self.root.selected_objects = list()
def build_lines(self):
for i in range(len(self.points) - 1):
line = ConnectionLine([self.points[i], self.points[i + 1]])
line.set_color("blue")
self.lines.append(line)
# -------------------------------------------------------------------------
# SCENE EVENTS CALLBACKS
# -------------------------------------------------------------------------
def register_editing_callbacks(self):
""" Register editing callbacks (former action function) """
if self._keyPressedCB is None:
self._keyPressedCB = self.root.events.addEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self.keyPressed)
if self._mousePressedCB is None:
self._mousePressedCB = self.root.events.addEventCallback(coin.SoMouseButtonEvent.getClassTypeId(), self.mousePressed)
if self._mouseMovedCB is None:
self._mouseMovedCB = self.root.events.addEventCallback(coin.SoLocation2Event.getClassTypeId(), self.mouseMoved)
def unregister_editing_callbacks(self):
""" Remove callbacks used during editing if they exist """
if self._keyPressedCB:
self.root.events.removeEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self._keyPressedCB)
self._keyPressedCB = None
if self._mousePressedCB:
self.root.events.removeEventCallback(coin.SoMouseButtonEvent.getClassTypeId(), self._mousePressedCB)
self._mousePressedCB = None
if self._mouseMovedCB:
self.root.events.removeEventCallback(coin.SoLocation2Event.getClassTypeId(), self._mouseMovedCB)
self._mouseMovedCB = None
# -------------------------------------------------------------------------
# SCENE EVENT HANDLERS
# -------------------------------------------------------------------------
def keyPressed(self, attr, event_callback):
event = event_callback.getEvent()
if event.getState() == event.UP:
#FreeCAD.Console.PrintMessage("Key pressed : %s\n"%event.getKey())
if event.getKey() == ord("i"):
self.subdivide()
elif event.getKey() == ord("q"):# or event.getKey() == ord(65307):
if self.obj:
self.obj.ViewObject.Proxy.doubleClicked(self.obj.ViewObject)
else:
self.quit()
elif event.getKey() == ord("s"):
sel = FreeCADGui.Selection.getSelectionEx()
tup = None
if len(sel) == 1:
tup = (sel[0].Object, sel[0].SubElementNames)
for i in range(len(self.root.selected_objects)):
if isinstance(self.root.selected_objects[i], MarkerOnShape):
self.root.selected_objects[i].sublink = tup
#FreeCAD.Console.PrintMessage("Snapped to {}\n".format(str(self.root.selected_objects[i].sublink)))
self.root.selected_objects[i].drag_start()
self.root.selected_objects[i].drag((0, 0, 0.))
self.root.selected_objects[i].drag_release()
elif (event.getKey() == 65535) or (event.getKey() == 65288): # Suppr or Backspace
# FreeCAD.Console.PrintMessage("Some objects have been deleted\n")
pts = list()
for o in self.root.dynamic_objects:
if isinstance(o, MarkerOnShape):
pts.append(o)
self.points = pts
self.setupInteractionSeparator()
def mousePressed(self, attr, event_callback):
""" Mouse button event handler, calls: startEditing, endEditing, addPoint, delPoint """
event = event_callback.getEvent()
if (event.getState() == coin.SoMouseButtonEvent.DOWN) and (event.getButton() == event.BUTTON1): # left click
if not event.wasAltDown():
''' do something '''
if self.editing is None:
''' do something'''
else:
self.endEditing(self.obj, self.editing)
elif event.wasAltDown(): # left click with ctrl down
self.display_tracker_menu(event)
elif (event.getState() == coin.SoMouseButtonEvent.DOWN) and (event.getButton() == event.BUTTON2): # right click
self.display_tracker_menu(event)
def mouseMoved(self, attr, event_callback):
""" Execute as callback for mouse movement. Update tracker position and update preview ghost. """
event = event_callback.getEvent()
pos = event.getPosition()
'''
if self.editing is not None:
self.updateTrackerAndGhost(event)
else:
# look for a node in mouse position and highlight it
pos = event.getPosition()
node = self.getEditNode(pos)
ep = self.getEditNodeIndex(node)
if ep is not None:
if self.overNode is not None:
self.overNode.setColor(COLORS["default"])
self.trackers[str(node.objectName.getValue())][ep].setColor(COLORS["red"])
self.overNode = self.trackers[str(node.objectName.getValue())][ep]
print("show menu")
# self.display_tracker_menu(event)
else:
if self.overNode is not None:
self.overNode.setColor(COLORS["default"])
self.overNode = None
'''
def endEditing(self, obj, nodeIndex=None, v=None):
self.editing = None
# ------------------------------------------------------------------------
# DRAFT EDIT Context menu
# ------------------------------------------------------------------------
def display_tracker_menu(self, event):
self.tracker_menu = QtGui.QMenu()
self.event = event
actions = None
actions = ["add point"]
'''
if self.overNode:
# if user is over a node
doc = self.overNode.get_doc_name()
obj = App.getDocument(doc).getObject(self.overNode.get_obj_name())
ep = self.overNode.get_subelement_index()
obj_gui_tools = self.get_obj_gui_tools(obj)
if obj_gui_tools:
actions = obj_gui_tools.get_edit_point_context_menu(obj, ep)
else:
# try if user is over an edited object
pos = self.event.getPosition()
obj = self.get_selected_obj_at_position(pos)
if utils.get_type(obj) in ["Line", "Wire", "BSpline", "BezCurve"]:
actions = ["add point"]
elif utils.get_type(obj) in ["Circle"] and obj.FirstAngle != obj.LastAngle:
actions = ["invert arc"]
if actions is None:
return
'''
for a in actions:
self.tracker_menu.addAction(a)
self.tracker_menu.popup(FreeCADGui.getMainWindow().cursor().pos())
QtCore.QObject.connect(self.tracker_menu,
QtCore.SIGNAL("triggered(QAction *)"),
self.evaluate_menu_action)
def evaluate_menu_action(self, labelname):
action_label = str(labelname.text())
doc = None
obj = None
idx = None
if action_label == "add point":
self.addPoint(self.event)
del self.event
# -------------------------------------------------------------------------
# EDIT functions
# -------------------------------------------------------------------------
def addPoint(self, event):
''' add point to the end '''
pos = event.getPosition()
pts = self.points.copy()
new_select = list()
point = FreeCAD.Vector(pos)
mark = MarkerOnShape([point])
pts.append(mark)
new_select.append(mark)
self.points = pts
self.setupInteractionSeparator()
self.root.selected_objects = new_select
def subdivide(self):
# get selected lines and subdivide them
pts = list()
new_select = list()
for o in self.lines:
#FreeCAD.Console.PrintMessage("object %s\n"%str(o))
if isinstance(o, ConnectionLine):
pts.append(o.markers[0])
if o in self.root.selected_objects:
#idx = self.lines.index(o)
#FreeCAD.Console.PrintMessage("Subdividing line #{}\n".format(idx))
p1 = o.markers[0].points[0]
p2 = o.markers[1].points[0]
midpar = (FreeCAD.Vector(p1) + FreeCAD.Vector(p2)) / 2.0
mark = MarkerOnShape([midpar])
pts.append(mark)
new_select.append(mark)
pts.append(self.points[-1])
self.points = pts
self.setupInteractionSeparator()
self.root.selected_objects = new_select
return True
def quit(self):
self.unregister_editing_callbacks()
self.root.unregister()
self.sg.removeChild(self.root)
self.root_inserted = False
-533
View File
@@ -1,533 +0,0 @@
import FreeCAD
import FreeCADGui
import Part
from freecad.Curves import graphics
from pivy import coin
# from graphics import COLORS
# FreeCAD.Console.PrintMessage("Using local Pivy.graphics library\n")
def parameterization(points, a, closed):
"""Computes a knot Sequence for a set of points
fac (0-1) : parameterization factor
fac=0 -> Uniform / fac=0.5 -> Centripetal / fac=1.0 -> Chord-Length"""
pts = points.copy()
if closed and pts[0].distanceToPoint(pts[-1]) > 1e-7: # we need to add the first point as the end point
pts.append(pts[0])
params = [0]
for i in range(1, len(pts)):
p = pts[i] - pts[i - 1]
if isinstance(p, FreeCAD.Vector):
le = p.Length
else:
le = p.length()
pl = pow(le, a)
params.append(params[-1] + pl)
return params
class ConnectionMarker(graphics.Marker):
def __init__(self, points):
super(ConnectionMarker, self).__init__(points, True)
class MarkerOnShape(graphics.Marker):
def __init__(self, points, sh=None):
super(MarkerOnShape, self).__init__(points, True)
self._shape = None
self._sublink = None
self._tangent = None
self._translate = coin.SoTranslation()
self._text_font = coin.SoFont()
self._text_font.name = "Arial:Bold"
self._text_font.size = 13.0
self._text = coin.SoText2()
self._text_switch = coin.SoSwitch()
self._text_switch.addChild(self._translate)
self._text_switch.addChild(self._text_font)
self._text_switch.addChild(self._text)
self.on_drag_start.append(self.add_text)
self.on_drag_release.append(self.remove_text)
self.addChild(self._text_switch)
if isinstance(sh, Part.Shape):
self.snap_shape = sh
elif isinstance(sh, (tuple, list)):
self.sublink = sh
def subshape_from_sublink(self, o):
name = o[1][0]
if 'Vertex' in name:
n = eval(name.lstrip('Vertex'))
return(o[0].Shape.Vertexes[n - 1])
elif 'Edge' in name:
n = eval(name.lstrip('Edge'))
return(o[0].Shape.Edges[n - 1])
elif 'Face' in name:
n = eval(name.lstrip('Face'))
return(o[0].Shape.Faces[n - 1])
def add_text(self):
self._text_switch.whichChild = coin.SO_SWITCH_ALL
self.on_drag.append(self.update_text)
def remove_text(self):
self._text_switch.whichChild = coin.SO_SWITCH_NONE
self.on_drag.remove(self.update_text)
def update_text(self):
p = self.points[0]
coords = ['{: 9.3f}'.format(p[0]), '{: 9.3f}'.format(p[1]), '{: 9.3f}'.format(p[2])]
self._translate.translation = p
self._text.string.setValues(0, 3, coords)
@property
def tangent(self):
return self._tangent
@tangent.setter
def tangent(self, t):
if isinstance(t, FreeCAD.Vector):
if t.Length > 1e-7:
self._tangent = t
self._tangent.normalize()
self.marker.markerIndex = coin.SoMarkerSet.DIAMOND_FILLED_9_9
else:
self._tangent = None
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
else:
self._tangent = None
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
@property
def snap_shape(self):
return self._shape
@snap_shape.setter
def snap_shape(self, sh):
if isinstance(sh, Part.Shape):
self._shape = sh
else:
self._shape = None
self.alter_color()
@property
def sublink(self):
return self._sublink
@sublink.setter
def sublink(self, sl):
if isinstance(sl, (tuple, list)) and not (sl == self._sublink):
self._shape = self.subshape_from_sublink(sl)
self._sublink = sl
else:
self._shape = None
self._sublink = None
self.alter_color()
def alter_color(self):
if isinstance(self._shape, Part.Vertex):
self.set_color("white")
elif isinstance(self._shape, Part.Edge):
self.set_color("cyan")
elif isinstance(self._shape, Part.Face):
self.set_color("magenta")
else:
self.set_color("black")
def __repr__(self):
return("MarkerOnShape({})".format(self._shape))
def drag(self, mouse_coords, fact=1.):
if self.enabled:
pts = self.points
for i, p in enumerate(pts):
p[0] = mouse_coords[0] * fact + self._tmp_points[i][0]
p[1] = mouse_coords[1] * fact + self._tmp_points[i][1]
p[2] = mouse_coords[2] * fact + self._tmp_points[i][2]
if self._shape:
v = Part.Vertex(p[0], p[1], p[2])
proj = v.distToShape(self._shape)[1][0][1]
# FreeCAD.Console.PrintMessage("%s -> %s\n"%(p.getValue(), proj))
p[0] = proj.x
p[1] = proj.y
p[2] = proj.z
self.points = pts
for foo in self.on_drag:
foo()
class ConnectionPolygon(graphics.Polygon):
std_col = "green"
def __init__(self, markers):
super(ConnectionPolygon, self).__init__(
sum([m.points for m in markers], []), True)
self.markers = markers
for m in self.markers:
m.on_drag.append(self.updatePolygon)
def updatePolygon(self):
self.points = sum([m.points for m in self.markers], [])
@property
def drag_objects(self):
return self.markers
def check_dependency(self):
if any([m._delete for m in self.markers]):
self.delete()
class ConnectionLine(graphics.Line):
def __init__(self, markers):
super(ConnectionLine, self).__init__(
sum([m.points for m in markers], []), True)
self.markers = markers
self._linear = False
for m in self.markers:
m.on_drag.append(self.updateLine)
def updateLine(self):
self.points = sum([m.points for m in self.markers], [])
if self._linear:
p1 = self.markers[0].points[0]
p2 = self.markers[-1].points[0]
t = p2 - p1
tan = FreeCAD.Vector(t[0], t[1], t[2])
for m in self.markers:
m.tangent = tan
@property
def linear(self):
return self._linear
@linear.setter
def linear(self, b):
self._linear = bool(b)
@property
def drag_objects(self):
return self.markers
def check_dependency(self):
if any([m._delete for m in self.markers]):
self.delete()
class InterpoCurveEditor(object):
"""Interpolation curve free-hand editor
my_editor = InterpoCurveEditor([points], obj)
obj is the FreeCAD object that will receive
the curve shape at the end of editing.
points can be :
- Vector (free point)
- (Vector, shape) (point on shape)"""
def __init__(self, points=[], fp=None):
self.points = list()
self.curve = Part.BSplineCurve()
self.fp = fp
self.root_inserted = False
self.periodic = False
self.param_factor = 1.0
# self.support = None # Not yet implemented
for p in points:
if isinstance(p, FreeCAD.Vector):
self.points.append(MarkerOnShape([p]))
elif isinstance(p, (tuple, list)):
self.points.append(MarkerOnShape([p[0]], p[1]))
elif isinstance(p, (MarkerOnShape, ConnectionMarker)):
self.points.append(p)
else:
FreeCAD.Console.PrintError("InterpoCurveEditor : bad input")
# Setup coin objects
if self.fp:
self.guidoc = self.fp.ViewObject.Document
else:
if not FreeCADGui.ActiveDocument:
FreeCAD.newDocument("New")
self.guidoc = FreeCADGui.ActiveDocument
self.view = self.guidoc.ActiveView
self.rm = self.view.getViewer().getSoRenderManager()
self.sg = self.view.getSceneGraph()
self.setup_InteractionSeparator()
self.update_curve()
def setup_InteractionSeparator(self):
if self.root_inserted:
self.sg.removeChild(self.root)
self.root = graphics.InteractionSeparator(self.rm)
self.root.setName("InteractionSeparator")
# self.root.ovr_col = "yellow"
# self.root.sel_col = "green"
self.root.pick_radius = 40
self.root.on_drag.append(self.update_curve)
# Keyboard callback
# self.events = coin.SoEventCallback()
self._controlCB = self.root.events.addEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self.controlCB)
# populate root node
# self.root.addChild(self.events)
self.root += self.points
self.build_lines()
self.root += self.lines
# set FreeCAD color scheme
for o in self.points + self.lines:
o.ovr_col = "yellow"
o.sel_col = "green"
self.root.register()
self.sg.addChild(self.root)
self.root_inserted = True
self.root.selected_objects = list()
def compute_tangents(self):
tans = list()
flags = list()
for i in range(len(self.points)):
if isinstance(self.points[i].snap_shape, Part.Face):
for vec in self.points[i].points:
u, v = self.points[i].snap_shape.Surface.parameter(FreeCAD.Vector(vec))
norm = self.points[i].snap_shape.normalAt(u, v)
cp = self.curve.parameter(FreeCAD.Vector(vec))
t = self.curve.tangent(cp)[0]
pl = Part.Plane(FreeCAD.Vector(), norm)
ci = Part.Geom2d.Circle2d()
ci.Radius = t.Length * 2
w = Part.Wire([ci.toShape(pl)])
f = Part.Face(w)
# proj = f.project([Part.Vertex(t)])
proj = Part.Vertex(t).distToShape(f)[1][0][1]
# pt = proj.Vertexes[0].Point
# FreeCAD.Console.PrintMessage("Projection %s -> %s\n"%(t, proj))
if proj.Length > 1e-7:
tans.append(proj)
flags.append(True)
else:
tans.append(FreeCAD.Vector(1, 0, 0))
flags.append(False)
elif self.points[i].tangent:
for j in range(len(self.points[i].points)):
tans.append(self.points[i].tangent)
flags.append(True)
else:
for j in range(len(self.points[i].points)):
tans.append(FreeCAD.Vector(0, 0, 0))
flags.append(False)
return(tans, flags)
def update_curve(self):
pts = list()
for p in self.points:
pts += p.points
# FreeCAD.Console.PrintMessage("pts :\n%s\n"%str(pts))
if len(pts) > 1:
fac = self.param_factor
if self.fp:
fac = self.fp.Parametrization
params = parameterization(pts, fac, self.periodic)
self.curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=self.periodic)
tans, flags = self.compute_tangents()
if any(flags):
if (len(tans) == len(pts)) and (len(flags) == len(pts)):
self.curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=self.periodic, Tangents=tans, TangentFlags=flags)
if self.fp:
self.fp.Shape = self.curve.toShape()
def build_lines(self):
self.lines = list()
for i in range(len(self.points) - 1):
line = ConnectionLine([self.points[i], self.points[i + 1]])
line.set_color("blue")
self.lines.append(line)
def controlCB(self, attr, event_callback):
event = event_callback.getEvent()
if event.getState() == event.UP:
# FreeCAD.Console.PrintMessage("Key pressed : %s\n"%event.getKey())
if event.getKey() == ord("i"):
self.subdivide()
elif event.getKey() == ord("p"):
self.set_planar()
elif event.getKey() == ord("t"):
self.set_tangents()
elif event.getKey() == ord("q"):
if self.fp:
self.fp.ViewObject.Proxy.doubleClicked(self.fp.ViewObject)
else:
self.quit()
elif event.getKey() == ord("s"):
sel = FreeCADGui.Selection.getSelectionEx()
tup = None
if len(sel) == 1:
tup = (sel[0].Object, sel[0].SubElementNames)
for i in range(len(self.root.selected_objects)):
if isinstance(self.root.selected_objects[i], MarkerOnShape):
self.root.selected_objects[i].sublink = tup
FreeCAD.Console.PrintMessage("Snapped to {}\n".format(str(self.root.selected_objects[i].sublink)))
self.root.selected_objects[i].drag_start()
self.root.selected_objects[i].drag((0, 0, 0.))
self.root.selected_objects[i].drag_release()
self.update_curve()
elif event.getKey() == ord("l"):
self.toggle_linear()
elif (event.getKey() == 65535) or (event.getKey() == 65288): # Suppr or Backspace
# FreeCAD.Console.PrintMessage("Some objects have been deleted\n")
pts = list()
for o in self.root.dynamic_objects:
if isinstance(o, MarkerOnShape):
pts.append(o)
self.points = pts
self.setup_InteractionSeparator()
self.update_curve()
def toggle_linear(self):
for o in self.root.selected_objects:
if isinstance(o, ConnectionLine):
o.linear = not o.linear
i = self.lines.index(o)
if i > 0:
self.lines[i - 1].linear = False
if i < len(self.lines) - 1:
self.lines[i + 1].linear = False
o.updateLine()
o.drag_start()
o.drag((0, 0, 0.00001))
o.drag_release()
self.update_curve()
def set_tangents(self):
# view_dir = FreeCAD.Vector(0, 0, 1)
view_dir = FreeCADGui.ActiveDocument.ActiveView.getViewDirection()
markers = list()
for o in self.root.selected_objects:
if isinstance(o, MarkerOnShape):
markers.append(o)
elif isinstance(o, ConnectionLine):
markers.extend(o.markers)
if len(markers) > 0:
for m in markers:
if m.tangent:
m.tangent = None
else:
i = self.points.index(m)
if i == 0:
m.tangent = -view_dir
else:
m.tangent = view_dir
self.update_curve()
def set_planar(self):
# view_dir = FreeCAD.Vector(0, 0, 1)
view_dir = FreeCADGui.ActiveDocument.ActiveView.getViewDirection()
markers = list()
for o in self.root.selected_objects:
if isinstance(o, MarkerOnShape):
markers.append(o)
elif isinstance(o, ConnectionLine):
markers.extend(o.markers)
if len(markers) > 2:
vec0 = markers[0].points[0]
vec1 = markers[-1].points[0]
p0 = FreeCAD.Vector(vec0[0], vec0[1], vec0[2])
p1 = FreeCAD.Vector(vec1[0], vec1[1], vec1[2])
pl = Part.Plane(p0, p1, p1 + view_dir)
for o in markers:
if isinstance(o.snap_shape, Part.Vertex):
FreeCAD.Console.PrintMessage("Snapped to Vertex\n")
elif isinstance(o.snap_shape, Part.Edge):
FreeCAD.Console.PrintMessage("Snapped to Edge\n")
c = o.snap_shape.Curve
pts = pl.intersect(c)[0]
new_pts = list()
for ip in o.points:
iv = FreeCAD.Vector(ip[0], ip[1], ip[2])
dmin = 1e50
new = None
for op in pts:
ov = FreeCAD.Vector(op.X, op.Y, op.Z)
if iv.distanceToPoint(ov) < dmin:
dmin = iv.distanceToPoint(ov)
new = ov
new_pts.append(new)
o.points = new_pts
elif isinstance(o.snap_shape, Part.Face):
FreeCAD.Console.PrintMessage("Snapped to Face\n")
s = o.snap_shape.Surface
cvs = pl.intersect(s)
new_pts = list()
for ip in o.points:
iv = Part.Vertex(FreeCAD.Vector(ip[0], ip[1], ip[2]))
dmin = 1e50
new = None
for c in cvs:
e = c.toShape()
d, pts, info = iv.distToShape(e)
if d < dmin:
dmin = d
new = pts[0][1]
new_pts.append(new)
o.points = new_pts
else:
FreeCAD.Console.PrintMessage("Not snapped\n")
new_pts = list()
for ip in o.points:
iv = FreeCAD.Vector(ip[0], ip[1], ip[2])
u, v = pl.parameter(iv)
new_pts.append(pl.value(u, v))
o.points = new_pts
for li in self.lines:
li.updateLine()
self.update_curve()
def subdivide(self):
# get selected lines and subdivide them
pts = list()
new_select = list()
for o in self.lines:
# FreeCAD.Console.PrintMessage("object %s\n"%str(o))
if isinstance(o, ConnectionLine):
pts.append(o.markers[0])
if o in self.root.selected_objects:
idx = self.lines.index(o)
FreeCAD.Console.PrintMessage("Subdividing line #{}\n".format(idx))
p1 = o.markers[0].points[0]
p2 = o.markers[1].points[0]
par1 = self.curve.parameter(FreeCAD.Vector(p1))
par2 = self.curve.parameter(FreeCAD.Vector(p2))
midpar = (par1 + par2) / 2.0
mark = MarkerOnShape([self.curve.value(midpar)])
pts.append(mark)
new_select.append(mark)
pts.append(self.points[-1])
self.points = pts
self.setup_InteractionSeparator()
self.root.selected_objects = new_select
self.update_curve()
return(True)
def quit(self):
self.root.events.removeEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self._controlCB)
self.root.unregister()
# self.root.removeAllChildren()
self.sg.removeChild(self.root)
self.root_inserted = False
def get_guide_params():
sel = FreeCADGui.Selection.getSelectionEx()
pts = list()
for s in sel:
pts.extend(list(zip(s.PickedPoints, s.SubObjects)))
return(pts)
def main():
obj = FreeCAD.ActiveDocument.addObject("Part::Spline", "profile")
tups = get_guide_params()
InterpoCurveEditor(tups, obj)
FreeCAD.ActiveDocument.recompute()
if __name__ == '__main__':
main()
+357
View File
@@ -0,0 +1,357 @@
# Script para FreeCAD - Procesador de Documentos Word con Carátula
import os
import glob
from PySide import QtWidgets, QtCore
from PySide.QtWidgets import (QFileDialog, QMessageBox, QProgressDialog,
QApplication, QVBoxLayout, QWidget, QPushButton,
QLabel, QTextEdit)
import FreeCAD
import FreeCADGui
import PVPlantResources
from PVPlantResources import DirIcons as DirIcons
try:
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
FreeCAD.Console.PrintError("Error: python-docx no está instalado. Instala con: pip install python-docx\n")
class DocumentProcessor(QtWidgets.QDialog):
def __init__(self, parent=None):
super(DocumentProcessor, self).__init__(parent)
self.caratula_path = ""
self.carpeta_path = ""
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Procesador de Documentos Word")
self.setMinimumWidth(600)
self.setMinimumHeight(500)
layout = QVBoxLayout()
# Título
title = QLabel("<h2>Procesador de Documentos Word</h2>")
title.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(title)
# Información
info_text = QLabel(
"Este script buscará recursivamente todos los archivos .docx en una carpeta,\n"
"insertará una carátula y aplicará formato estándar a todos los documentos."
)
info_text.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(info_text)
# Botón seleccionar carátula
self.btn_caratula = QPushButton("1. Seleccionar Carátula")
self.btn_caratula.clicked.connect(self.seleccionar_caratula)
layout.addWidget(self.btn_caratula)
self.label_caratula = QLabel("No se ha seleccionado carátula")
self.label_caratula.setWordWrap(True)
layout.addWidget(self.label_caratula)
# Botón seleccionar carpeta
self.btn_carpeta = QPushButton("2. Seleccionar Carpeta de Documentos")
self.btn_carpeta.clicked.connect(self.seleccionar_carpeta)
layout.addWidget(self.btn_carpeta)
self.label_carpeta = QLabel("No se ha seleccionado carpeta")
self.label_carpeta.setWordWrap(True)
layout.addWidget(self.label_carpeta)
# Botón procesar
self.btn_procesar = QPushButton("3. Procesar Documentos")
self.btn_procesar.clicked.connect(self.procesar_documentos)
self.btn_procesar.setEnabled(False)
layout.addWidget(self.btn_procesar)
# Área de log
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
layout.addWidget(self.log_area)
# Botón cerrar
self.btn_cerrar = QPushButton("Cerrar")
self.btn_cerrar.clicked.connect(self.close)
layout.addWidget(self.btn_cerrar)
self.setLayout(layout)
def log(self, mensaje):
"""Agrega un mensaje al área de log"""
self.log_area.append(mensaje)
QApplication.processEvents() # Para actualizar la UI
def seleccionar_caratula(self):
"""Abre un diálogo para seleccionar el archivo de carátula"""
archivo, _ = QFileDialog.getOpenFileName(
self,
"Seleccionar archivo de carátula",
"",
"Word documents (*.docx);;All files (*.*)"
)
if archivo and os.path.exists(archivo):
self.caratula_path = archivo
self.label_caratula.setText(f"Carátula: {os.path.basename(archivo)}")
self.verificar_estado()
self.log(f"✓ Carátula seleccionada: {archivo}")
def seleccionar_carpeta(self):
"""Abre un diálogo para seleccionar la carpeta de documentos"""
carpeta = QFileDialog.getExistingDirectory(
self,
"Seleccionar carpeta con documentos"
)
if carpeta:
self.carpeta_path = carpeta
self.label_carpeta.setText(f"Carpeta: {carpeta}")
self.verificar_estado()
self.log(f"✓ Carpeta seleccionada: {carpeta}")
def verificar_estado(self):
"""Habilita el botón procesar si ambos paths están seleccionados"""
if self.caratula_path and self.carpeta_path:
self.btn_procesar.setEnabled(True)
def buscar_docx_recursivamente(self, carpeta):
"""Busca recursivamente todos los archivos .docx en una carpeta"""
archivos_docx = []
patron = os.path.join(carpeta, "**", "*.docx")
for archivo in glob.glob(patron, recursive=True):
archivos_docx.append(archivo)
return archivos_docx
def aplicar_formato_estandar(self, doc):
"""Aplica formato estándar al documento"""
try:
# Configurar estilos por defecto
style = doc.styles['Normal']
font = style.font
font.name = 'Arial'
font.size = Pt(11)
font.color.rgb = RGBColor(0, 0, 0) # Negro
# Configurar encabezados
try:
heading_style = doc.styles['Heading 1']
heading_font = heading_style.font
heading_font.name = 'Arial'
heading_font.size = Pt(14)
heading_font.bold = True
heading_font.color.rgb = RGBColor(0, 51, 102) # Azul oscuro
except:
pass
except Exception as e:
self.log(f" ⚠ Advertencia en formato: {str(e)}")
def aplicar_formato_avanzado(self, doc):
"""Aplica formato más avanzado y personalizado"""
try:
# Configurar márgenes
sections = doc.sections
for section in sections:
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
# Configurar estilos de párrafo
for paragraph in doc.paragraphs:
paragraph.paragraph_format.space_after = Pt(6)
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.line_spacing = 1.15
# Alinear párrafos justificados
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
# Aplicar fuente específica a cada run
for run in paragraph.runs:
run.font.name = 'Arial'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Arial')
run.font.size = Pt(11)
except Exception as e:
self.log(f" ⚠ Advertencia en formato avanzado: {str(e)}")
def insertar_caratula_y_formatear(self, archivo_docx, archivo_caratula):
"""Inserta la carátula y aplica formato al documento"""
try:
# Abrir el documento de carátula
doc_caratula = Document(archivo_caratula)
# Abrir el documento destino
doc_destino = Document(archivo_docx)
# Crear un nuevo documento que contendrá la carátula + contenido original
nuevo_doc = Document()
# Copiar todo el contenido de la carátula
for elemento in doc_caratula.element.body:
print(elemento)
nuevo_doc.element.body.append(elemento)
# Agregar un salto de página después de la carátula
nuevo_doc.add_page_break()
# Copiar todo el contenido del documento original
for elemento in doc_destino.element.body:
nuevo_doc.element.body.append(elemento)
# Aplicar formatos
self.aplicar_formato_estandar(nuevo_doc)
self.aplicar_formato_avanzado(nuevo_doc)
# Guardar el documento (sobrescribir el original)
nombre_base = os.path.splitext(os.path.basename(archivo_docx))[0]
extension = os.path.splitext(archivo_docx)[1]
name = f"{nombre_base}{extension}"
nuevo_docx = os.path.join(self.output_carpeta, name)
nuevo_doc.save(nuevo_docx)
return True, ""
except Exception as e:
return False, str(e)
def procesar_documentos(self):
"""Función principal que orquesta todo el proceso"""
if not DOCX_AVAILABLE:
QMessageBox.critical(self, "Error",
"La biblioteca python-docx no está disponible.\n\n"
"Instala con: pip install python-docx")
return
# Verificar paths
if not os.path.exists(self.caratula_path):
QMessageBox.warning(self, "Error", "El archivo de carátula no existe.")
return
if not os.path.exists(self.carpeta_path):
QMessageBox.warning(self, "Error", "La carpeta de documentos no existe.")
return
self.log("\n=== INICIANDO PROCESAMIENTO ===")
self.log(f"Carátula: {self.caratula_path}")
self.log(f"Carpeta: {self.carpeta_path}")
directorio_padre = os.path.dirname(self.carpeta_path)
self.output_carpeta = os.path.join(directorio_padre, "03.Outputs")
os.makedirs(self.output_carpeta, exist_ok=True)
# Buscar archivos .docx
self.log("Buscando archivos .docx...")
archivos_docx = self.buscar_docx_recursivamente(self.carpeta_path)
if not archivos_docx:
self.log("No se encontraron archivos .docx en la carpeta seleccionada.")
QMessageBox.information(self, "Información",
"No se encontraron archivos .docx en la carpeta seleccionada.")
return
self.log(f"Se encontraron {len(archivos_docx)} archivos .docx")
# Crear diálogo de progreso
progress = QProgressDialog("Procesando documentos...", "Cancelar", 0, len(archivos_docx), self)
progress.setWindowTitle("Procesando")
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.show()
# Procesar cada archivo
exitosos = 0
fallidos = 0
errores_detallados = []
for i, archivo_docx in enumerate(archivos_docx):
if progress.wasCanceled():
self.log("Proceso cancelado por el usuario.")
break
progress.setValue(i)
progress.setLabelText(f"Procesando {i + 1}/{len(archivos_docx)}: {os.path.basename(archivo_docx)}")
QApplication.processEvents()
self.log(f"Procesando: {os.path.basename(archivo_docx)}")
success, error_msg = self.insertar_caratula_y_formatear(archivo_docx, self.caratula_path)
if success:
self.log(f" ✓ Completado")
exitosos += 1
else:
self.log(f" ✗ Error: {error_msg}")
fallidos += 1
errores_detallados.append(f"{os.path.basename(archivo_docx)}: {error_msg}")
progress.setValue(len(archivos_docx))
# Mostrar resumen
self.log("\n=== RESUMEN ===")
self.log(f"Documentos procesados exitosamente: {exitosos}")
self.log(f"Documentos con errores: {fallidos}")
self.log(f"Total procesados: {exitosos + fallidos}")
# Mostrar mensaje final
mensaje = (f"Procesamiento completado:\n"
f"✓ Exitosos: {exitosos}\n"
f"✗ Fallidos: {fallidos}\n"
f"Total: {len(archivos_docx)}")
if fallidos > 0:
mensaje += f"\n\nErrores encontrados:\n" + "\n".join(
errores_detallados[:5]) # Mostrar solo primeros 5 errores
if len(errores_detallados) > 5:
mensaje += f"\n... y {len(errores_detallados) - 5} más"
QMessageBox.information(self, "Proceso Completado", mensaje)
# Función para ejecutar desde FreeCAD
def run_document_processor():
"""Función principal para ejecutar el procesador desde FreeCAD"""
# Verificar si python-docx está disponible
if not DOCX_AVAILABLE:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Biblioteca python-docx no encontrada")
msg.setInformativeText(
"Para usar este script necesitas instalar python-docx:\n\n"
"1. Abre la consola de FreeCAD\n"
"2. Ejecuta: import subprocess, sys\n"
"3. Ejecuta: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-docx'])\n\n"
"O instala desde una terminal externa con: pip install python-docx"
)
msg.setWindowTitle("Dependencia faltante")
msg.exec_()
return
# Crear y mostrar la interfaz
dialog = DocumentProcessor(FreeCADGui.getMainWindow())
dialog.exec_()
class generateDocuments:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "house.svg")),
'MenuText': "DocumentGenerator",
'Accel': "D, G",
'ToolTip': "Creates a Building object from setup dialog."}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
run_document_processor()
+1 -1
View File
@@ -159,7 +159,7 @@ def calculate_incenter(facet):
"""Calcula el incentro usando la función nativa de FreeCAD"""
try:
return facet.InCircle[0] # (x, y, z)
except:
except (IndexError, AttributeError):
return None
+21 -6
View File
@@ -2,17 +2,32 @@
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>PVPlant</name>
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
<version>2025.07.06</version>
<date>2025.07.06</date>
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
<version>2026.02.12</version>
<date>2026.02.15</date>
<maintainer email="javier.branagutierrez@gmail.com">
Javier Braña
</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant</url>
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
<url type="repository" branch="main">
https://homehud.duckdns.org/javier/PVPlant
</url>
<url type="bugtracker">
https://homehud.duckdns.org/javier/PVPlant/issues
</url>
<url type="readme">
https://homehud.duckdns.org/javier/PVPlant/raw/branch/main/README.md
</url>
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>
<content>
<workbench>
<classname>RoadWorkbench</classname>
<classname>PVPlantWorkbench</classname>
<subdirectory>./</subdirectory>
</workbench>
</content>
+12 -2
View File
@@ -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
@@ -51,6 +55,10 @@ class _CommandReload:
import hydro.hydrological as hydro
import Importer.importOSM as iOSM
import docgenerator
importlib.reload(docgenerator)
importlib.reload(ProjectSetup)
importlib.reload(PVPlantPlacement)
importlib.reload(PVPlantImportGrid)
@@ -59,9 +67,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)
+4 -9
View File
@@ -2,23 +2,18 @@ numpy~=1.26.2
opencv-python~=4.8.1
matplotlib~=3.8.2
openpyxl~=3.1.2
utm~=0.7.0
PySide2~=5.15.8
requests~=2.31.0
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
pycollada~=0.7.2
shapely
rtree
pandas
+1 -1
View File
@@ -22,7 +22,7 @@
import FreeCAD, FreeCADGui
#from freecad.trails import ICONPATH
from PySide2.QtWidgets import QLabel
from PySide.QtWidgets import QLabel
import copy