This commit is contained in:
2025-04-14 10:05:32 +06:00
parent 0e4b6e7fa4
commit 1241ee97ba
15 changed files with 1209 additions and 239 deletions

View File

@@ -340,7 +340,44 @@ class Trench(ArchComponent.Component):
p2.z = 0 p2.z = 0
return p2.sub(p1) return p2.sub(p1)
def getsegments(wire): def getsegments(wire): #deepseek
"""Divide un wire en segmentos rectos basados en cambios de dirección (sin splitWiresByCurvature)"""
import Part
from math import degrees
segments = []
current_segment = []
angle_threshold = 1.0 # Grados para considerar cambio de dirección
def get_angle(v1, v2):
return degrees(v1.getAngle(v2))
edges = wire.Edges
for i in range(len(edges)):
if i == 0:
current_segment.append(edges[i])
continue
prev_edge = edges[i - 1]
curr_edge = edges[i]
# Vectores de dirección
v1 = prev_edge.tangentAt(prev_edge.FirstParameter)
v2 = curr_edge.tangentAt(curr_edge.FirstParameter)
angle = get_angle(v1, v2)
if angle > angle_threshold:
segments.append(Part.Wire(current_segment))
current_segment = [curr_edge]
else:
current_segment.append(curr_edge)
if current_segment:
segments.append(Part.Wire(current_segment))
return segments
def getsegments_old(wire):
import math import math
segments = [] segments = []
@@ -381,13 +418,6 @@ class Trench(ArchComponent.Component):
pts_plane.append(tmp) pts_plane.append(tmp)
path_plane = Part.makePolygon(pts_plane) path_plane = Part.makePolygon(pts_plane)
'''o1 = path_plane.makeOffset2D(d, 2, False, True, True)
o2 = path_plane.makeOffset2D(-d, 2, False, True, True)
points = calculateOffset(o1)
points.insert(0, points.pop(1))
points.reverse()
points2 = calculateOffset(o2)'''
points = self.calculateOffset(path_plane, d) points = self.calculateOffset(path_plane, d)
points2 = self.calculateOffset(path_plane, -d) points2 = self.calculateOffset(path_plane, -d)

View File

@@ -1,4 +1,5 @@
import FreeCAD import FreeCAD
import FreeCADGui
import Part import Part
import Draft import Draft
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
@@ -15,9 +16,7 @@ scale = 1000.0
class OSMImporter: class OSMImporter:
def __init__(self, origin): def __init__(self, origin):
self.Origin = origin self.Origin = origin if origin else FreeCAD.Vector(0, 0, 0)
if origin is None:
self.Origin = FreeCAD.Vector(0, 0, 0)
self.overpass_url = "https://overpass-api.de/api/interpreter" self.overpass_url = "https://overpass-api.de/api/interpreter"
self.nodes = {} self.nodes = {}
self.ways_data = defaultdict(dict) self.ways_data = defaultdict(dict)
@@ -40,10 +39,11 @@ class OSMImporter:
'vegetation': (0.4, 0.8, 0.4), 'vegetation': (0.4, 0.8, 0.4),
'water': (0.4, 0.6, 1.0) 'water': (0.4, 0.6, 1.0)
} }
self.ssl_context = ssl.create_default_context(cafile=certifi.where())
def transformFromLatLon(self, lat, lon): def transform_from_latlon(self, lat, lon):
x, y, _, _ = utm.from_latlon(lat, lon) x, y, _, _ = utm.from_latlon(lat, lon)
return (x, y, 0) * 1000 return FreeCAD.Vector(x, y, .0) * scale - self.Origin
def get_osm_data(self, bbox): def get_osm_data(self, bbox):
query = f""" query = f"""
@@ -61,43 +61,36 @@ class OSMImporter:
(._;>;); (._;>;);
out body; out body;
""" """
# Configurar contexto SSL seguro req = urllib.request.Request(
ssl_context = ssl.create_default_context(cafile=certifi.where())
# Modificar tu código de descarga
response = urllib.request.urlopen(
self.overpass_url, self.overpass_url,
data=query.encode('utf-8'), data=query.encode('utf-8'),
context=ssl_context, headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'},
timeout=30 method='POST'
) )
return response.read() return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read()
def create_layer(self, name): def create_layer(self, name):
if not FreeCAD.ActiveDocument.getObject(name): if not FreeCAD.ActiveDocument.getObject(name):
layer = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
return layer
return FreeCAD.ActiveDocument.getObject(name) return FreeCAD.ActiveDocument.getObject(name)
def process_osm_data(self, osm_data): def process_osm_data(self, osm_data):
root = ET.fromstring(osm_data) root = ET.fromstring(osm_data)
# Primera pasada: almacenar todos los nodos # Almacenar nodos transformados
for node in root.findall('node'): for node in root.findall('node'):
'''self.nodes[node.attrib['id']] = ( self.nodes[node.attrib['id']] = self.transform_from_latlon(
float(node.attrib['lon']),
float(node.attrib['lat']),
0)'''
self.nodes[node.attrib['id']] = self.transformFromLatLon(
float(node.attrib['lat']), float(node.attrib['lat']),
float(node.attrib['lon']) float(node.attrib['lon'])
) )
# Segunda pasada: procesar ways y relaciones # Procesar ways
for way in root.findall('way'): for way in root.findall('way'):
tags = {tag.attrib['k']: tag.attrib['v'] for tag in way.findall('tag')} way_id = way.attrib['id']
nodes = [nd.attrib['ref'] for nd in way.findall('nd')] self.ways_data[way_id] = {
self.ways_data[way.attrib['id']] = {'tags': tags, 'nodes': nodes} 'tags': {tag.attrib['k']: tag.attrib['v'] for tag in way.findall('tag')},
'nodes': [nd.attrib['ref'] for nd in way.findall('nd')]
}
self.create_transportation() self.create_transportation()
self.create_buildings() self.create_buildings()
@@ -133,7 +126,7 @@ class OSMImporter:
self.create_railway(nodes, transport_layer) self.create_railway(nodes, transport_layer)
def create_road(self, nodes, width, road_type, layer): def create_road(self, nodes, width, road_type, layer):
points = [FreeCAD.Vector(n[0], n[1], .0) * scale - self.Origin for n in nodes] points = [n for n in nodes]
polyline = Draft.make_wire(points, closed=False, face=False) polyline = Draft.make_wire(points, closed=False, face=False)
polyline.Label = f"Road_{road_type}" polyline.Label = f"Road_{road_type}"
polyline.ViewObject.LineWidth = 2.0 polyline.ViewObject.LineWidth = 2.0
@@ -143,7 +136,7 @@ class OSMImporter:
layer.addObject(polyline) layer.addObject(polyline)
def create_railway(self, nodes, layer): def create_railway(self, nodes, layer):
points = [FreeCAD.Vector(n[0], n[1], .0) * scale - self.Origin for n in nodes] points = [n for n in nodes]
rail_line = Draft.make_wire(points, closed=False, face=False) rail_line = Draft.make_wire(points, closed=False, face=False)
rail_line.Label = "Railway" rail_line.Label = "Railway"
rail_line.ViewObject.LineWidth = 1.5 rail_line.ViewObject.LineWidth = 1.5
@@ -152,7 +145,6 @@ class OSMImporter:
def create_buildings(self): def create_buildings(self):
building_layer = self.create_layer("Buildings") building_layer = self.create_layer("Buildings")
for way_id, data in self.ways_data.items(): for way_id, data in self.ways_data.items():
if 'building' not in data['tags']: if 'building' not in data['tags']:
continue continue
@@ -167,14 +159,14 @@ class OSMImporter:
height = self.get_building_height(tags) height = self.get_building_height(tags)
# Crear polígono base # Crear polígono base
polygon_points = [FreeCAD.Vector(n[0], n[1], .0) * scale - self.Origin for n in nodes] polygon_points = [n for n in nodes]
if polygon_points[0] != polygon_points[-1]: if polygon_points[0] != polygon_points[-1]:
polygon_points.append(polygon_points[0]) polygon_points.append(polygon_points[0])
try: try:
polygon = Part.makePolygon(polygon_points) polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon) face = Part.Face(polygon)
extruded = face.extrude(FreeCAD.Vector(0, 0, height) * scale - self.Origin ) extruded = face.extrude(FreeCAD.Vector(0, 0, height))# * scale - self.Origin )
building = building_layer.addObject("Part::Feature", f"Building_{way_id}") building = building_layer.addObject("Part::Feature", f"Building_{way_id}")
building.Shape = extruded building.Shape = extruded
@@ -190,7 +182,8 @@ class OSMImporter:
except Exception as e: except Exception as e:
print(f"Error en edificio {way_id}: {str(e)}") print(f"Error en edificio {way_id}: {str(e)}")
def get_building_height(self, tags): @staticmethod
def get_building_height(tags):
# Lógica de cálculo de altura # Lógica de cálculo de altura
if 'height' in tags: if 'height' in tags:
try: try:
@@ -205,44 +198,355 @@ class OSMImporter:
return 5.0 # Altura por defecto return 5.0 # Altura por defecto
def create_power_infrastructure(self): def create_power_infrastructure(self):
power_layer = self.create_layer("Power") power_layer = self.create_layer("Power_Infrastructure")
for way_id, data in self.ways_data.items(): for way_id, data in self.ways_data.items():
tags = data['tags'] tags = data['tags']
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if 'power' in tags: if 'power' in tags:
if tags['power'] == 'line': feature_type = tags['power']
self.create_power_line(nodes, power_layer)
elif tags['power'] == 'substation':
self.create_substation(nodes, power_layer)
def create_power_line(self, nodes, layer): if feature_type == 'line':
self.create_power_line(
nodes=nodes,
tags=tags,
layer=power_layer
)
elif feature_type == 'substation':
self.create_substation(
way_id=way_id,
tags=tags,
nodes=nodes,
layer=power_layer
)
elif feature_type == 'tower':
self.create_power_tower(
position=nodes[0] if nodes else None,
tags=tags,
layer=power_layer
)
def create_power_line(self, nodes, tags, layer):
"""Crea líneas de transmisión eléctrica con propiedades técnicas"""
try:
# Configuración basada en tags
line_type = tags.get('line', 'overhead')
voltage = self.parse_voltage(tags.get('voltage', '0'))
cables = int(tags.get('cables', '3'))
material = tags.get('material', 'aluminum')
# Crear geometría
points = [FreeCAD.Vector(*n) for n in nodes]
if len(points) < 2:
return
wire = Draft.make_wire(points, closed=False, face=False)
wire.Label = f"Power_Line_{voltage}V"
# Propiedades visuales
wire.ViewObject.LineWidth = 1 + (voltage / 100000)
color = self.feature_colors['power']['line']
wire.ViewObject.ShapeColor = color
# Propiedades técnicas
wire.addProperty("App::PropertyFloat", "Voltage", "PowerLine", "Voltage in volts").Voltage = voltage
wire.addProperty("App::PropertyInteger", "Cables", "PowerLine", "Number of conductors").Cables = cables
wire.addProperty("App::PropertyString", "Material", "PowerLine", "Conductor material").Material = material
wire.addProperty("App::PropertyString", "Type", "PowerLine", "Line type").Type = line_type
layer.addObject(wire)
# Añadir torres si es overhead
if line_type == 'overhead':
distance_between_towers = 150 # metros por defecto
if 'distance_between_towers' in tags:
try:
distance_between_towers = float(tags['distance_between_towers'])
except:
pass
self.create_power_towers_along_line(
points=points,
voltage=voltage,
distance=distance_between_towers,
layer=layer
)
except Exception as e:
FreeCAD.Console.PrintError(f"Error creating power line: {str(e)}\n")
def create_power_towers_along_line(self, points, voltage, distance, layer):
"""Crea torres de transmisión a lo largo de la línea"""
total_length = 0
previous_point = None
for point in points:
if previous_point is not None:
segment_length = (point - previous_point).Length
num_towers = int(segment_length / distance)
if num_towers > 0:
step = segment_length / num_towers
direction = (point - previous_point).normalize()
for i in range(num_towers):
tower_pos = previous_point + (direction * (i * step))
self.create_power_tower(
position=tower_pos,
tags={'voltage': str(voltage)},
layer=layer
)
previous_point = point
def create_power_tower(self, position, tags, layer):
"""Crea una torre de transmisión individual"""
try:
voltage = self.parse_voltage(tags.get('voltage', '0'))
# Dimensiones basadas en voltaje
base_size = 2.0 + (voltage / 100000)
height = 25.0 + (voltage / 10000)
# Geometría de la torre
base = Part.makeBox(base_size, base_size, 3.0,
FreeCAD.Vector(position.x - base_size / 2, position.y - base_size / 2, 0))
mast = Part.makeCylinder(0.5, height, FreeCAD.Vector(position.x, position.y, 3.0))
# Unir componentes
tower = base.fuse(mast)
tower_obj = layer.addObject("Part::Feature", "Power_Tower")
tower_obj.Shape = tower
tower_obj.ViewObject.ShapeColor = self.feature_colors['power']['tower']
# Añadir propiedades
tower_obj.addProperty("App::PropertyFloat", "Voltage", "Technical", "Design voltage").Voltage = voltage
tower_obj.addProperty("App::PropertyFloat", "Height", "Technical", "Tower height").Height = height
# Añadir crossarms (crucetas)
for i in range(3):
crossarm_height = 3.0 + (i * 5.0)
crossarm = Part.makeBox(
4.0, 0.2, 0.2,
FreeCAD.Vector(position.x - 2.0, position.y - 0.1, crossarm_height)
)
tower_obj.Shape = tower_obj.Shape.fuse(crossarm)
except Exception as e:
FreeCAD.Console.PrintError(f"Error creating power tower: {str(e)}\n")
def create_power_line_simple(self, nodes, layer):
# Torres de alta tensión # Torres de alta tensión
for node in nodes: for node in nodes:
cylinder = Part.makeCylinder(1.0, 20.0, FreeCAD.Vector(node[0], node[1], 0) * scale - self.Origin ) cylinder = Part.makeCylinder(1.0, 20.0, FreeCAD.Vector(node[0], node[1], 0))# * scale - self.Origin )
pole = FreeCAD.ActiveDocument.addObject("Part::Feature", "PowerPole") pole = FreeCAD.ActiveDocument.addObject("Part::Feature", "PowerPole")
layer.addObject(pole) layer.addObject(pole)
pole.Shape = cylinder pole.Shape = cylinder
pole.ViewObject.ShapeColor = self.feature_colors['power']['tower'] pole.ViewObject.ShapeColor = self.feature_colors['power']['tower']
# Líneas eléctricas # Líneas eléctricas
points = [FreeCAD.Vector(n[0], n[1], .0) * scale - self.Origin for n in nodes] points = [n for n in nodes]
cable = Draft.make_wire(points, closed=False, face=False) cable = Draft.make_wire(points, closed=False, face=False)
cable.ViewObject.LineWidth = 3.0 cable.ViewObject.LineWidth = 3.0
cable.ViewObject.ShapeColor = self.feature_colors['power']['line'] cable.ViewObject.ShapeColor = self.feature_colors['power']['line']
layer.addObject(cable) layer.addObject(cable)
def create_substation(self, nodes, layer): def create_substation(self, way_id, tags, nodes, layer):
# Crear área de subestación """Crea subestaciones con todos los componentes detallados"""
polygon_points = [FreeCAD.Vector(n[0], n[1], 0) * scale - self.Origin for n in nodes] try:
if len(polygon_points) > 2: # 1. Parámetros base
polygon = Part.makePolygon(polygon_points) voltage = self.parse_voltage(tags.get('voltage', '0'))
face = Part.Face(polygon) substation_type = tags.get('substation', 'distribution')
substation = FreeCAD.ActiveDocument.addObject("Part::Feature", "Substation") name = tags.get('name', f"Substation_{way_id}")
layer.addObject(substation)
substation.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.5) * scale - self.Origin ) if len(nodes) < 3:
substation.ViewObject.ShapeColor = self.feature_colors['power']['substation'] FreeCAD.Console.PrintWarning(f"Subestación {way_id} ignorada: polígono inválido\n")
return
# 2. Geometría base
polygon_points = [n for n in nodes if isinstance(n, FreeCAD.Vector)]
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.Shape = base_extrude
base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
except Exception as e:
FreeCAD.Console.PrintError(f"Error base {way_id}: {str(e)}\n")
# 4. Cercado perimetral
if tags.get('fence', 'no') == 'yes':
try:
fence_offset = -0.8 # metros hacia adentro
fence_points = self.offset_polygon(polygon_points, fence_offset)
if len(fence_points) > 2:
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.Shape = fence_extrude
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error cerca {way_id}: {str(e)}\n")
# 5. Edificio principal
if tags.get('building', 'no') == 'yes':
try:
building_offset = -2.0 # metros hacia adentro
building_height = 4.5 + (voltage / 100000)
building_points = self.offset_polygon(polygon_points, building_offset)
if len(building_points) > 2:
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.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:
num_transformers = int(tags.get('transformers', 1))
for i in range(num_transformers):
transformer_pos = self.calculate_equipment_position(
polygon_points,
index=i,
total=num_transformers,
offset=3.0
)
transformer = self.create_transformer(
position=transformer_pos,
voltage=voltage,
tech_type=tags.get('substation:type', 'outdoor')
)
layer.addObject(transformer)
except Exception as e:
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:
try:
tower_pos = self.calculate_tower_position(polygon_points)
tower = self.create_circuit_breaker_tower(
position=tower_pos,
voltage=voltage
)
layer.addObject(tower)
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n")
# 8. Propiedades técnicas
substation_data = layer.addObject("App::FeaturePython", f"{name}_Data")
props = {
"Voltage": voltage,
"Type": substation_type,
"Components": ['Base'] + (['Fence'] if 'fence' in tags else []) +
(['Building'] if 'building' in tags else []) +
[f'Transformer_{n + 1}' for n in range(num_transformers)]
}
for prop, value in props.items():
if isinstance(value, list):
substation_data.addProperty("App::PropertyStringList", prop, "Technical").Components = value
else:
substation_data.addProperty(
"App::PropertyFloat" if isinstance(value, float) else "App::PropertyString",
prop, "Technical").setValue(value)
except Exception as e:
FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n")
def add_substation_fence(self, parent_obj, polygon_points):
"""Añade cerca perimetral"""
try:
offset_points = self.offset_polygon(polygon_points, -0.5)
fence = Part.makePolygon(offset_points).extrude(FreeCAD.Vector(0, 0, 2.5))
fence_obj = parent_obj.Document.addObject("Part::Feature", "Fence")
fence_obj.Shape = fence
fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4)
fence_obj.Placement.Base = parent_obj.Placement.Base
except Exception as e:
FreeCAD.Console.PrintWarning(f"Error en cerca: {str(e)}\n")
def parse_voltage(self, voltage_str):
# Convertir valores comunes de voltaje a numéricos
voltage_map = {
'low': 400,
'medium': 20000,
'high': 110000,
'extra high': 380000,
'ultra high': 765000
}
try:
return float(''.join(filter(str.isdigit, voltage_str.split()[0]))) * 1000
except:
return 20000 # Valor por defecto
def offset_polygon(self, points, offset):
"""Versión corregida sin error de sintaxis"""
if not points:
return []
sum_x = sum(p.x for p in points)
sum_y = sum(p.y for p in points)
centroid = FreeCAD.Vector(sum_x / len(points), sum_y / len(points), 0)
return [p + (centroid - p).normalize() * offset for p in points]
def create_transformer(self, position, voltage, technology):
# Crear transformador básico según características
height = 2.0 + (voltage / 100000)
radius = 0.5 + (voltage / 500000)
transformer = Part.makeCylinder(radius, height, FreeCAD.Vector(position))
transformer_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "Transformer")
transformer_obj.Shape = transformer
transformer_obj.ViewObject.ShapeColor = (0.1, 0.1, 0.5) if 'oil' in technology.lower() else (0.5, 0.5, 0.5)
return transformer_obj
def calculate_equipment_position(self, polygon_points, index, total, offset=0.0):
"""Calcula posición equidistante alrededor del perímetro con offset"""
perimeter = sum((p2 - p1).Length for p1, p2 in zip(polygon_points, polygon_points[1:]))
target_dist = (perimeter / total) * index
accumulated = 0.0
for i in range(len(polygon_points) - 1):
p1 = polygon_points[i]
p2 = polygon_points[i + 1]
segment_length = (p2 - p1).Length
if accumulated + segment_length >= target_dist:
direction = (p2 - p1).normalize()
return p1 + direction * (target_dist - accumulated) + direction.cross(FreeCAD.Vector(0, 0, 1)) * offset
accumulated += segment_length
return polygon_points[0]
def create_circuit_breaker_tower(self, position, voltage):
"""Crea torre de seccionamiento especializada"""
tower = Part.makeCompound([
Part.makeCylinder(0.8, 12, position), # Poste principal
Part.makeBox(3, 0.3, 0.3, position + FreeCAD.Vector(-1.5, -0.15, 12)), # Cruceta superior
Part.makeBox(2.5, 0.3, 0.3, position + FreeCAD.Vector(-1.25, -0.15, 9)) # Cruceta media
])
tower_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "CircuitBreakerTower")
tower_obj.Shape = tower
tower_obj.ViewObject.ShapeColor = (0.1, 0.1, 0.1)
tower_obj.addProperty("App::PropertyFloat", "Voltage", "Technical").Voltage = voltage
return tower_obj
def calculate_tower_position(self, polygon_points):
# Colocar torre en el punto medio del lado más largo
# (Implementación compleja que requeriría cálculo geométrico)
return FreeCAD.Vector(polygon_points[0].x, polygon_points[0].y, 0)
def create_vegetation(self): def create_vegetation(self):
vegetation_layer = self.create_layer("Vegetation") vegetation_layer = self.create_layer("Vegetation")
@@ -251,7 +555,7 @@ class OSMImporter:
for node_id, coords in self.nodes.items(): for node_id, coords in self.nodes.items():
# Verificar si es un árbol # Verificar si es un árbol
# (Necesitarías procesar los tags de los nodos, implementación simplificada) # (Necesitarías procesar los tags de los nodos, implementación simplificada)
cylinder = Part.makeCylinder(0.5, 5.0, FreeCAD.Vector(coords[0], coords[1], 0) * scale - self.Origin ) cylinder = Part.makeCylinder(0.5, 5.0, FreeCAD.Vector(coords[0], coords[1], 0))# * scale - self.Origin )
tree = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree") tree = FreeCAD.ActiveDocument.addObject("Part::Feature", "Tree")
vegetation_layer.addObject(tree) vegetation_layer.addObject(tree)
tree.Shape = cylinder tree.Shape = cylinder
@@ -262,7 +566,7 @@ class OSMImporter:
if 'natural' in data['tags'] or 'landuse' in data['tags']: if 'natural' in data['tags'] or 'landuse' in data['tags']:
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) > 2: if len(nodes) > 2:
polygon_points = [FreeCAD.Vector(n[0], n[1], 0) * scale - self.Origin for n in nodes] polygon_points = [n for n in nodes]
polygon = Part.makePolygon(polygon_points) polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon) face = Part.Face(polygon)
area = vegetation_layer.addObject("Part::Feature", "GreenArea") area = vegetation_layer.addObject("Part::Feature", "GreenArea")
@@ -276,9 +580,10 @@ class OSMImporter:
if 'natural' in data['tags'] and data['tags']['natural'] == 'water': if 'natural' in data['tags'] and data['tags']['natural'] == 'water':
nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes]
if len(nodes) > 2: if len(nodes) > 2:
polygon_points = [FreeCAD.Vector(n[0], n[1], 0) * scale - self.Origin for n in nodes] polygon_points = [n for n in nodes]
polygon = Part.makePolygon(polygon_points) polygon = Part.makePolygon(polygon_points)
face = Part.Face(polygon) face = Part.Face(polygon)
water = water_layer.addObject("Part::Feature", "WaterBody") water = water_layer.addObject("Part::Feature", "WaterBody")
water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1) * scale - self.Origin ) water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin )
water.ViewObject.ShapeColor = self.feature_colors['water'] water.ViewObject.ShapeColor = self.feature_colors['water']

111
Importer/module_inserter.py Normal file
View File

@@ -0,0 +1,111 @@
import os
import csv
from PySide import QtGui, QtCore
class SelectorDialog(QtGui.QDialog):
def __init__(self, csv_path, title, parent=None):
super(SelectorDialog, self).__init__(parent)
self.setWindowTitle(title)
self.csv_path = csv_path
self.data = []
self.brand_filter = ""
self.model_filter = ""
# Cargar datos del CSV
self.load_csv_data()
# Crear widgets
self.create_widgets()
self.create_layout()
self.create_connections()
def load_csv_data(self):
"""Carga los datos desde el archivo CSV"""
if os.path.exists(self.csv_path):
with open(self.csv_path, 'r') as f:
reader = csv.DictReader(f, delimiter=';')
self.data = [row for row in reader]
def get_unique_brands(self):
"""Obtiene marcas únicas"""
return list(set(row['Marca'] for row in self.data))
def get_models_by_brand(self, brand):
"""Filtra modelos por marca"""
return [row['Modelo'] for row in self.data if row['Marca'] == brand]
def create_widgets(self):
self.lbl_brand = QtGui.QLabel("Marca:")
self.cb_brand = QtGui.QComboBox()
self.cb_brand.addItems(self.get_unique_brands())
self.lbl_model = QtGui.QLabel("Modelo:")
self.cb_model = QtGui.QComboBox()
self.update_model_combo()
self.btn_accept = QtGui.QPushButton("Aceptar")
self.btn_cancel = QtGui.QPushButton("Cancelar")
def create_layout(self):
layout = QtGui.QVBoxLayout()
form_layout = QtGui.QFormLayout()
form_layout.addRow(self.lbl_brand, self.cb_brand)
form_layout.addRow(self.lbl_model, self.cb_model)
button_layout = QtGui.QHBoxLayout()
button_layout.addWidget(self.btn_accept)
button_layout.addWidget(self.btn_cancel)
layout.addLayout(form_layout)
layout.addLayout(button_layout)
self.setLayout(layout)
def create_connections(self):
self.cb_brand.currentIndexChanged.connect(self.update_model_combo)
self.btn_accept.clicked.connect(self.accept)
self.btn_cancel.clicked.connect(self.reject)
def update_model_combo(self):
brand = self.cb_brand.currentText()
models = self.get_models_by_brand(brand)
self.cb_model.clear()
self.cb_model.addItems(models)
def get_selected_item(self):
brand = self.cb_brand.currentText()
model = self.cb_model.currentText()
for row in self.data:
if row['Marca'] == brand and row['Modelo'] == model:
return row
return None
def select_modulo():
csv_path = "/ruta/a/tu/databases/modulos.csv" # Ajusta esta ruta
dialog = SelectorDialog(csv_path, "Seleccionar Módulo")
if dialog.exec_():
selected = dialog.get_selected_item()
print("Módulo seleccionado:", selected) # Aquí puedes agregar la lógica de importación
def select_inversor():
csv_path = "/ruta/a/tu/databases/inversores.csv" # Ajusta esta ruta
dialog = SelectorDialog(csv_path, "Seleccionar Inversor")
if dialog.exec_():
selected = dialog.get_selected_item()
print("Inversor seleccionado:", selected) # Aquí puedes agregar la lógica de importación
# Crear una barra de herramientas para acceder fácilmente
toolbar = QtGui.QToolBar()
select_modulo_action = QtGui.QAction("Seleccionar Módulo", toolbar)
select_modulo_action.triggered.connect(select_modulo)
toolbar.addAction(select_modulo_action)
select_inversor_action = QtGui.QAction("Seleccionar Inversor", toolbar)
select_inversor_action.triggered.connect(select_inversor)
toolbar.addAction(select_inversor_action)
# Agregar la barra de herramientas a FreeCAD
Gui.addToolBar(toolbar)

View File

@@ -53,12 +53,12 @@ class PVPlantWorkbench(Workbench):
from Export import ExporterCommands from Export import ExporterCommands
self.inportExportlist = ExporterCommands.Exportlist self.inportExportlist = ExporterCommands.Exportlist
self.objectlist = PVPlantTools.objectlist
self.objectlist = [ ''' [
"PVPlantTree", "PVPlantTree",
"PVPlantBuilding", "PVPlantBuilding",
"PVPlantFenceGroup", "PVPlantFenceGroup",
] ]'''
self.electricalList = ["PVPlantStringBox", self.electricalList = ["PVPlantStringBox",
"PVPlantCable", "PVPlantCable",
"PVPlanElectricalLine", "PVPlanElectricalLine",

View File

@@ -5,6 +5,7 @@ import Part
import ArchComponent import ArchComponent
from pivy import coin from pivy import coin
import numpy as np import numpy as np
import DraftGeomUtils
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui, os import FreeCADGui, os
@@ -516,6 +517,7 @@ class EarthWorksTaskPanel:
return False return False
FreeCAD.ActiveDocument.openTransaction("Calcular movimiento de tierras") FreeCAD.ActiveDocument.openTransaction("Calcular movimiento de tierras")
def calculateEarthWorks(line, extreme=False): def calculateEarthWorks(line, extreme=False):
pts = [] pts = []
pts1 = [] pts1 = []
@@ -576,33 +578,6 @@ class EarthWorksTaskPanel:
elif ver == 1: elif ver == 1:
from PVPlantPlacement import getCols from PVPlantPlacement import getCols
columns = getCols(frames) columns = getCols(frames)
'''colelements = set()
rowelements = set()
for groups in columns:
for group in groups:
for frame in group:
colelements.add(frame.Placement.Base.x)
rowelements.add(frame.Placement.Base.y)
colelements = sorted(colelements)
rowelements = sorted(rowelements, reverse=True)
print("Cols: ", len(colelements), " - ", colelements)
print("Rows: ", len(rowelements), " - ", rowelements)
a = []
colnum = len(colelements)
for r in range(len(rowelements)):
a.append([None] * colnum)
mat = np.array(a, dtype=object)
for groups in columns:
for group in groups:
for frame in group:
colidx = colelements.index(frame.Placement.Base.x)
rowidx = rowelements.index(frame.Placement.Base.y)
mat[rowidx][colidx] = frame
print(mat)
return'''
for groups in columns: for groups in columns:
for group in groups: for group in groups:
first = group[0] first = group[0]
@@ -709,44 +684,41 @@ class EarthWorksTaskPanel:
import Mesh import Mesh
pro = utils.getProjected(sh) pro = utils.getProjected(sh)
pro = utils.simplifyWire(pro) pro = utils.simplifyWire(pro)
#pro = pro.makeOffset2D(20000, 2, False, False, True)
Part.show(sh, "loft")
Part.show(pro, "pro")
pts = [ver.Point for ver in pro.Vertexes] pts = [ver.Point for ver in pro.Vertexes]
'''if pts[0] != pts[-1]:
pts.append(pts[0])'''
land.trim(pts, 1) land.trim(pts, 1)
tmp = [] tmp = []
for face in sh.Faces: for face in sh.Faces:
wire = face.Wires[0].copy() wire = face.Wires[0].copy()
pl = wire.Placement.Base pl = wire.Placement.Base
wire.Placement.Base = wire.Placement.Base - pl wire.Placement.Base = wire.Placement.Base - pl
wire = wire.scale(2)
wire.Placement.Base = wire.Placement.Base + pl
#wire = wire.makeOffset2D(10000, 0, False, False, True)
wire.Placement.Base.z = wire.Placement.Base.z - 10000
face1 = Part.makeLoft([face.Wires[0], wire], True, True, False)
if DraftGeomUtils.isPlanar(wire):
# Caso simple
wire = wire.makeOffset2D(10000, 0, False, False, True)
wire.Placement.Base.z = wire.Placement.Base.z - 10000
wire = wire.makeFillet(1, wire.Edges)
tmp.append(Part.makeLoft([face.Wires[0], wire], True, True, False))
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]]
]
Part.show(face1, "tool") for tri in triangles:
#tmp.append(face.extrude(FreeCAD.Vector(0, 0, -10000))) # Crear wire triangular
#Part.show(tmp[-1], "face-extrude") wire = Part.makePolygon([v.Point for v in tri] + [tri[0].Point])
sh = sh.extrude(FreeCAD.Vector(0, 0, -10000)) # Hacer offset (ahora es coplanar por ser triángulo)
sh = Part.Solid(sh) wire = wire.makeOffset2D(10000, 0, False, False, True)
Part.show(sh) wire.Placement.Base.z = wire.Placement.Base.z - 10000
import MeshPart as mp wire = wire.makeFillet(1, wire.Edges)
msh = mp.meshFromShape(Shape=sh) # , MaxLength=1) tmp.append(Part.makeLoft([face.Wires[0], wire], True, True, False))
# msh = msh.smooth("Laplace", 3)
Mesh.show(msh, "tool") final_tool = Part.makeCompound(tmp)
Mesh.show(land, "trim") Part.show(final_tool, "tool")
'''inner = msh.inner(land)
Mesh.show(inner)
outer = msh.inner(land)
Mesh.show(outer)'''
'''intersec = land.section(msh, MinDist=0.01)
import Draft
for sec in intersec:
Draft.makeWire(sec)'''
FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.commitTransaction()
self.closeForm() self.closeForm()
@@ -964,4 +936,5 @@ def accept():
FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.commitTransaction()
self.closeForm() self.closeForm()
return True return True

View File

@@ -100,9 +100,11 @@ def get_elevation_from_oe(coordinates): # v1 deepseek
return points return points
def getElevationFromOE(coordinates): def getElevationFromOE(coordinates):
"""Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.""" """Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM."""
import certifi
from requests.exceptions import RequestException from requests.exceptions import RequestException
if len(coordinates) == 0: if len(coordinates) == 0:
return None return None
@@ -110,15 +112,15 @@ def getElevationFromOE(coordinates):
from requests import get from requests import get
import utm import utm
str="" locations_str=""
total = len(coordinates) - 1 total = len(coordinates) - 1
for i, point in enumerate(coordinates): for i, point in enumerate(coordinates):
str += '{:.6f},{:.6f}'.format(point[0], point[1]) locations_str += '{:.6f},{:.6f}'.format(point[0], point[1])
if i != total: if i != total:
str += '|' locations_str += '|'
query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + str query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str
try: try:
r = get(query, timeout=20, verify=False) r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí
except RequestException as e: except RequestException as e:
points = [] points = []
for i, point in enumerate(coordinates): for i, point in enumerate(coordinates):

View File

@@ -323,7 +323,7 @@ class _PadTaskPanel:
self.new = False self.new = False
self.obj = obj self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantTrench.ui")) self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "Civil/PVPlantTrench.ui"))
def accept(self): def accept(self):
FreeCAD.ActiveDocument.openTransaction("Create Pad") FreeCAD.ActiveDocument.openTransaction("Create Pad")

View File

@@ -50,12 +50,9 @@ version = "0.1.0"
def selectionFilter(sel, objtype): def selectionFilter(sel, objtype):
print("type: ", objtype)
fil = [] fil = []
for obj in sel: for obj in sel:
if hasattr(obj, "Proxy"): if hasattr(obj, "Proxy"):
print("objeto:", obj.Proxy.__class__)
print(obj.Proxy.__class__ is objtype)
if obj.Proxy.__class__ is objtype: if obj.Proxy.__class__ is objtype:
fil.append(obj) fil.append(obj)
return fil return fil
@@ -143,7 +140,7 @@ class _PVPlantPlacementTaskPanel:
def calculateWorkingArea(self): def calculateWorkingArea(self):
self.Area = self.getProjected(self.PVArea.Shape) self.Area = self.getProjected(self.PVArea.Shape)
tmp = FreeCAD.ActiveDocument.findObjects(Name="ProhibitedArea") tmp = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
if len(tmp): if len(tmp):
ProhibitedAreas = list() ProhibitedAreas = list()
for obj in tmp: for obj in tmp:
@@ -189,7 +186,6 @@ class _PVPlantPlacementTaskPanel:
def adjustToTerrain(self, coordinates): def adjustToTerrain(self, coordinates):
mode = 1 mode = 1
terrain = self.Terrain.Mesh terrain = self.Terrain.Mesh
type = 0
def placeRegion(df): # TODO: new def placeRegion(df): # TODO: new
import MeshPart as mp import MeshPart as mp
@@ -210,11 +206,10 @@ class _PVPlantPlacementTaskPanel:
pbot = FreeCAD.Vector(base) pbot = FreeCAD.Vector(base)
pbot.y -= yl pbot.y -= yl
line = Part.LineSegment(ptop, pbot).toShape() line = Part.LineSegment(ptop, pbot).toShape()
if type == 0: # Mesh: profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0]
profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo.
else: # Shape:
tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1))
profilepoints = [ver.Point for ver in tmp.Vertexes] profilepoints = [ver.Point for ver in tmp.Vertexes]'''
xx = list() xx = list()
yy = list() yy = list()
@@ -285,7 +280,7 @@ class _PVPlantPlacementTaskPanel:
placeRegion(df) placeRegion(df)
return df return df
def placeonregion_old(frames): # old """def placeonregion_old(frames): # old
for colnum, col in enumerate(frames): for colnum, col in enumerate(frames):
groups = list() groups = list()
groups.append([col[0]]) groups.append([col[0]])
@@ -381,7 +376,7 @@ class _PVPlantPlacementTaskPanel:
rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec) rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec)
pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
placements.append(pl) placements.append(pl)
return placements return placements"""
def isInside(self, frame, point): def isInside(self, frame, point):
if self.Area.isInside(point, 10, True): if self.Area.isInside(point, 10, True):
@@ -456,6 +451,8 @@ class _PVPlantPlacementTaskPanel:
if countcols == self.form.editColCount.value(): if countcols == self.form.editColCount.value():
offsetcols += valcols offsetcols += valcols
countcols = 0 countcols = 0
print("/n/n")
print(cols)
return self.adjustToTerrain(cols) return self.adjustToTerrain(cols)
def calculateNonAlignedArray(self): def calculateNonAlignedArray(self):
@@ -566,19 +563,20 @@ class _PVPlantPlacementTaskPanel:
self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
FreeCAD.ActiveDocument.openTransaction("Create Placement") FreeCAD.ActiveDocument.openTransaction("Create Placement")
# 1. Calculate working area:
self.calculateWorkingArea() self.calculateWorkingArea()
# 2. Calculate aligned array:
if self.form.cbAlignFrames.isChecked(): if self.form.cbAlignFrames.isChecked():
dataframe = self.calculateAlignedArray() dataframe = self.calculateAlignedArray()
else: else:
dataframe = self.calculateNonAlignedArray() dataframe = self.calculateNonAlignedArray()
# 3. Adjust to terrain:
self.createFrameFromPoints(dataframe)
FreeCAD.ActiveDocument.commitTransaction()
# last step: ------------------------------
FreeCAD.ActiveDocument.RecomputesFrozen = False FreeCAD.ActiveDocument.RecomputesFrozen = False
params.SetBool("AutoSaveEnabled", auto_save_enabled) params.SetBool("AutoSaveEnabled", auto_save_enabled)
self.createFrameFromPoints(dataframe)
total_time = datetime.now() - starttime total_time = datetime.now() - starttime
print(" -- Tiempo tardado:", total_time) print(" -- Tiempo tardado:", total_time)
FreeCADGui.Control.closeDialog() FreeCADGui.Control.closeDialog()

View File

@@ -120,12 +120,12 @@ class Terrain(ArchComponent.Component):
"Surface", "Surface",
"Use a Point Group to generate the surface") "Use a Point Group to generate the surface")
if not ("Mesh" in pl): if not ("mesh" in pl):
obj.addProperty("Mesh::PropertyMeshKernel", obj.addProperty("Mesh::PropertyMeshKernel",
"Mesh", "mesh",
"Surface", "Surface",
"Mesh") "Mesh")
obj.setEditorMode("Mesh", 1) obj.setEditorMode("mesh", 1)
if not ("InitialMesh" in pl): if not ("InitialMesh" in pl):
obj.addProperty("Mesh::PropertyMeshKernel", obj.addProperty("Mesh::PropertyMeshKernel",
@@ -156,7 +156,7 @@ class Terrain(ArchComponent.Component):
'''Do something when a property has changed''' '''Do something when a property has changed'''
if prop == "InitialMesh": if prop == "InitialMesh":
obj.Mesh = obj.InitialMesh.copy() obj.mesh = obj.InitialMesh.copy()
if prop == "DEM" or prop == "CuttingBoundary": if prop == "DEM" or prop == "CuttingBoundary":
from datetime import datetime from datetime import datetime
@@ -197,11 +197,10 @@ class Terrain(ArchComponent.Component):
del templist del templist
# create xy coordinates # create xy coordinates
import PVPlantSite offset = self.site.Origin
offset = PVPlantSite.get().Origin x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x
x = 1000 * (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) - offset.x y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y
y = 1000 * (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) - offset.y datavals = datavals * 1000 # Ajuste de altura
datavals = 1000 * datavals # - offset.z
# remove points out of area # remove points out of area
# 1. coarse: # 1. coarse:
@@ -210,7 +209,6 @@ class Terrain(ArchComponent.Component):
inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0 inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0
tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x), tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x),
x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0] x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0]
print(tmp)
x_max = np.ndarray.max(tmp) x_max = np.ndarray.max(tmp)
x_min = np.ndarray.min(tmp) x_min = np.ndarray.min(tmp)
@@ -249,10 +247,10 @@ class Terrain(ArchComponent.Component):
pts.append([x[i], y[j], datavals[j][i]]) pts.append([x[i], y[j], datavals[j][i]])
if len(pts) > 3: if len(pts) > 3:
try: try:
mesh.addMesh(Triangulation.Triangulate(pts)) triangulated = Triangulation.Triangulate(pts)
#Mesh.show(mesh) mesh.addMesh(triangulated)
except TypeError: except TypeError:
print("error al procesar: {0} puntos".format(len(pts))) print(f"Error al procesar {len(pts)} puntos: {str(e)}")
mesh.removeDuplicatedPoints() mesh.removeDuplicatedPoints()
mesh.removeFoldsOnSurface() mesh.removeFoldsOnSurface()
@@ -284,12 +282,9 @@ class Terrain(ArchComponent.Component):
import MeshTools.Triangulation as Triangulation import MeshTools.Triangulation as Triangulation
mesh = Triangulation.Triangulate(Data) mesh = Triangulation.Triangulate(Data)
'''shape = PVPlantCreateTerrainMesh.MeshToShape(mesh)
shape.Placement.move(nbase)'''
obj.Shape = shape
if obj.DEM: if obj.DEM:
obj.DEM = None obj.DEM = None
obj.mesh = mesh
def execute(self, obj): def execute(self, obj):
'''''' ''''''
@@ -307,7 +302,6 @@ class ViewProviderTerrain:
"A View Provider for the Pipe object" "A View Provider for the Pipe object"
def __init__(self, vobj): def __init__(self, vobj):
self.Object = vobj.Object
self.boundary_color = None self.boundary_color = None
self.edge_style = None self.edge_style = None
self.edge_color = None self.edge_color = None
@@ -321,16 +315,16 @@ class ViewProviderTerrain:
# Triangulation properties. # Triangulation properties.
pl = vobj.PropertiesList pl = vobj.PropertiesList
if not ("Transparency" in pl): if not ("Transparency" in pl):
vobj.addProperty("App::PropertyIntegerConstraint", '''vobj.addProperty("App::PropertyIntegerConstraint",
"Transparency", "Transparency",
"Surface Style", "Surface Style",
"Set triangle face transparency").Transparency = (50, 0, 100, 1) "Set triangle face transparency").Transparency = (50, 0, 100, 1)'''
if not ("ShapeColor" in pl): if not ("ShapeColor" in pl):
vobj.addProperty("App::PropertyColor", vobj.addProperty("App::PropertyColor",
"ShapeColor", "ShapeColor",
"Surface Style", "Surface Style",
"Set triangle face color").ShapeColor = (r, g, b, vobj.Transparency / 100) "Set triangle face color").ShapeColor = (0.0, 0.667, 0.49, vobj.Transparency / 100)
if not ("ShapeMaterial" in pl): if not ("ShapeMaterial" in pl):
vobj.addProperty("App::PropertyMaterial", vobj.addProperty("App::PropertyMaterial",
@@ -413,18 +407,21 @@ class ViewProviderTerrain:
"Set major contour line width").MinorWidth = (2.0, 1.0, 20.0, 1.0) "Set major contour line width").MinorWidth = (2.0, 1.0, 20.0, 1.0)
vobj.Proxy = self vobj.Proxy = self
self.Object = vobj.Object
# Inicializar colores correctamente
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
def onDocumentRestored(self, vobj): def onDocumentRestored(self, vobj):
self.setProperties(vobj) self.setProperties(vobj)
def onChanged(self, vobj, prop): def onChanged(self, vobj, prop):
''' Update Object visuals when a view property changed. ''' """ Update Object visuals when a view property changed. """
if prop == "ShapeColor" or prop == "Transparency": if prop == "ShapeColor" or prop == "Transparency":
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"): if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
color = vobj.getPropertyByName("ShapeColor") color = vobj.getPropertyByName("ShapeColor")
transparency = vobj.getPropertyByName("Transparency") transparency = vobj.getPropertyByName("Transparency")
color = (color[0], color[1], color[2], transparency / 100) color = (color[0], color[1], color[2], 50 / 100)
vobj.ShapeMaterial.DiffuseColor = color vobj.ShapeMaterial.DiffuseColor = color
if prop == "ShapeMaterial": if prop == "ShapeMaterial":
@@ -555,7 +552,7 @@ class ViewProviderTerrain:
highlight.addChild(mat_binding) highlight.addChild(mat_binding)
highlight.addChild(self.geo_coords) highlight.addChild(self.geo_coords)
highlight.addChild(self.triangles) highlight.addChild(self.triangles)
highlight.addChild(boundaries) #highlight.addChild(boundaries)
# Face root. # Face root.
face = coin.SoSeparator() face = coin.SoSeparator()
@@ -573,14 +570,14 @@ class ViewProviderTerrain:
surface_root.addChild(face) surface_root.addChild(face)
surface_root.addChild(offset) surface_root.addChild(offset)
surface_root.addChild(edge) surface_root.addChild(edge)
surface_root.addChild(major_contours) #surface_root.addChild(major_contours)
surface_root.addChild(minor_contours) #surface_root.addChild(minor_contours)
vobj.addDisplayMode(surface_root, "Surface") vobj.addDisplayMode(surface_root, "Surface")
# Boundary root. # Boundary root.
boundary_root = coin.SoSeparator() #boundary_root = coin.SoSeparator()
boundary_root.addChild(boundaries) #boundary_root.addChild(boundaries)
vobj.addDisplayMode(boundary_root, "Boundary") #vobj.addDisplayMode(boundary_root, "Boundary")
# Elevation/Shaded root. # Elevation/Shaded root.
'''shaded_root = coin.SoSeparator() '''shaded_root = coin.SoSeparator()
@@ -599,8 +596,8 @@ class ViewProviderTerrain:
# Wireframe root. # Wireframe root.
wireframe_root = coin.SoSeparator() wireframe_root = coin.SoSeparator()
wireframe_root.addChild(edge) wireframe_root.addChild(edge)
wireframe_root.addChild(major_contours) #wireframe_root.addChild(major_contours)
wireframe_root.addChild(minor_contours) #wireframe_root.addChild(minor_contours)
vobj.addDisplayMode(wireframe_root, "Wireframe") vobj.addDisplayMode(wireframe_root, "Wireframe")
# Take features from properties. # Take features from properties.
@@ -629,19 +626,19 @@ class ViewProviderTerrain:
''' '''
if prop == "Mesh": if prop == "Mesh":
print("update terrain mesh") if obj.mesh:
mesh = obj.Mesh print("Mostrar mesh")
copy_mesh = mesh.copy()
# copy_mesh.Placement.move(origin.Origin)
triangles = [] mesh = obj.mesh
for i in copy_mesh.Topology[1]: vertices = [tuple(v) for v in mesh.Topology[0]]
triangles.extend(list(i)) faces = []
triangles.append(-1) for face in mesh.Topology[1]:
faces.extend(face)
faces.append(-1)
self.geo_coords.point.values = copy_mesh.Topology[0] # Asignar a los nodos de visualización
self.triangles.coordIndex.values = triangles self.geo_coords.point.values = vertices # <-- ¡Clave!
del copy_mesh self.triangles.coordIndex.values = faces # <-- ¡Clave!
def getDisplayModes(self, vobj): def getDisplayModes(self, vobj):
''' Return a list of display modes. ''' ''' Return a list of display modes. '''
@@ -656,7 +653,9 @@ class ViewProviderTerrain:
return "Surface" return "Surface"
def claimChildren(self): def claimChildren(self):
return [self.Object.CuttingBoundary, ] if hasattr(self, "Object") and self.Object:
return [self.Object.CuttingBoundary, ]
return []
def getIcon(self): def getIcon(self):
return str(os.path.join(DirIcons, "terrain.svg")) return str(os.path.join(DirIcons, "terrain.svg"))

View File

@@ -25,10 +25,8 @@ __title__ = "RebarCommands"
__author__ = "Amritpal Singh" __author__ = "Amritpal Singh"
__url__ = "https://www.freecadweb.org" __url__ = "https://www.freecadweb.org"
from pathlib import Path
import FreeCADGui, FreeCAD import FreeCADGui, FreeCAD
from PySide import QtGui, QtCore from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP from PySide.QtCore import QT_TRANSLATE_NOOP
from PVPlantResources import DirIcons as DirIcons from PVPlantResources import DirIcons as DirIcons
import os import os
@@ -165,7 +163,6 @@ class CommandDivideArea:
@staticmethod @staticmethod
def Activated(): def Activated():
from Project.Area import PVPlantArea
sel = FreeCADGui.Selection.getSelection()[0] sel = FreeCADGui.Selection.getSelection()[0]
@@ -210,7 +207,6 @@ class CommandFrameArea:
@staticmethod @staticmethod
def Activated(): def Activated():
from Project.Area import PVPlantArea
sel = FreeCADGui.Selection.getSelection() sel = FreeCADGui.Selection.getSelection()
makeFramedArea(None, sel) makeFramedArea(None, sel)
@@ -443,7 +439,7 @@ class CommandTrench: # V1:
@staticmethod @staticmethod
def Activated(): def Activated():
"""Execute when the command is called.""" """Execute when the command is called."""
import PVPlantTrench from Civil import PVPlantTrench
sel = FreeCADGui.Selection.getSelection() sel = FreeCADGui.Selection.getSelection()
done = False done = False
@@ -489,7 +485,7 @@ class CommandSemiAutomaticTrench: # V1:
@staticmethod @staticmethod
def Activated(): def Activated():
"""Execute when the command is called.""" """Execute when the command is called."""
import PVPlantTrench from Civil import PVPlantTrench
semi = PVPlantTrench.semiAutomaticTrench() semi = PVPlantTrench.semiAutomaticTrench()
@@ -510,8 +506,8 @@ class CommandCalculateEarthworks:
@staticmethod @staticmethod
def Activated(): def Activated():
import PVPlantEarthworks import PVPlantEarthWorks
TaskPanel = PVPlantEarthworks.EarthWorksTaskPanel() TaskPanel = PVPlantEarthWorks.EarthWorksTaskPanel()
FreeCADGui.Control.showDialog(TaskPanel) FreeCADGui.Control.showDialog(TaskPanel)
@@ -646,6 +642,15 @@ if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantAdjustToTerrain', PVPlantPlacement.CommandAdjustToTerrain()) FreeCADGui.addCommand('PVPlantAdjustToTerrain', PVPlantPlacement.CommandAdjustToTerrain())
FreeCADGui.addCommand('PVPlantConvertTo', PVPlantPlacement.CommandConvert()) FreeCADGui.addCommand('PVPlantConvertTo', PVPlantPlacement.CommandConvert())
import hydro.hydrological as hydro
FreeCADGui.addCommand('HydrologicalAnalysis', hydro.CommandHydrologicalAnalysis())
import Vegetation.PVPlantTreeGenerator as TreeGenerator
FreeCADGui.addCommand('PVPlantTree', TreeGenerator.CommandTree())
import Project.GenerateExternalDocument as GED
FreeCADGui.addCommand('newExternalDocument', GED.CommandGenerateExternalDocument())
projectlist = [ # "Reload", projectlist = [ # "Reload",
"PVPlantSite", "PVPlantSite",
"ProjectSetup", "ProjectSetup",
@@ -668,7 +673,11 @@ projectlist = [ # "Reload",
# "PVPlantFoundation" # "PVPlantFoundation"
# "GraphTerrainProfile", # "GraphTerrainProfile",
# "Trace", # "Trace",
"Separator",
'HydrologicalAnalysis',
'newExternalDocument',
] ]
pv_list = [ pv_list = [
# "RackType", # "RackType",
# "PVPlantRackCheck", # "PVPlantRackCheck",
@@ -678,3 +687,5 @@ pv_list = [
"PVPlantConvertTo", "PVPlantConvertTo",
# "PVArea" # "PVArea"
] ]
objectlist = ['PVPlantTree',]

View File

@@ -0,0 +1,92 @@
import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets
import os
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui, QtWidgets
from PySide.QtCore import QT_TRANSLATE_NOOP
import os
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
__title__ = "PVPlant Export to DXF"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
from PVPlantResources import DirIcons as DirIcons
def copy_object_with_reference():
try:
# Verificar selección
selected = FreeCADGui.Selection.getSelection()
if len(selected) != 1:
QtWidgets.QMessageBox.critical(None, "Error", "Selecciona exactamente un objeto")
return
original_doc = FreeCAD.ActiveDocument
original_obj = selected[0]
original_center = original_obj.Shape.BoundBox.Center
# Crear nuevo documento
new_doc = FreeCAD.newDocument(f"{original_doc.Name} - {original_obj.Label}")
# Copiar objeto al nuevo documento
new_obj = new_doc.copyObject(original_obj, True)
new_obj.Label = f"Linked_{original_obj.Label}"
new_obj.Placement.Base = original_obj.Placement.Base - original_center
# Guardar el documenton nuevp
path = os.path.dirname(FreeCAD.ActiveDocument.FileName)
new_doc.saveAs(os.path.join(path, new_doc.Name))
# Mantener posición original en el nuevo documento
# new_obj.Placement = original_obj.Placement
# Crear referencia (App::Link) en el documento original
link = original_doc.addObject("App::Link", f"Link_{new_obj.Label}")
link.LinkedObject = new_obj
# Mantener posición original del objeto
link.Placement = original_obj.Placement
# Actualizar vistas
original_doc.recompute()
new_doc.recompute()
# Regresar al documento original
FreeCAD.setActiveDocument(original_doc.Name)
#QtWidgets.QMessageBox.information(None, "Éxito", "Operación completada correctamente")
except Exception as e:
QtWidgets.QMessageBox.critical(None, "Error", f"Error: {str(e)}")
# Ejecutar la función
class CommandGenerateExternalDocument:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "dxf.svg")),
'Accel': "P, E",
'MenuText': "Export to DXF",
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Export choosed layers to dxf")}
def Activated(self):
''' '''
copy_object_with_reference()
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False

View File

@@ -21,6 +21,10 @@
# *********************************************************************** # ***********************************************************************
import FreeCAD import FreeCAD
import FreeCADGui
from PySide import QtGui, QtCore
import datetime
import getpass
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
import FreeCADGui, os import FreeCADGui, os
@@ -43,51 +47,99 @@ except AttributeError:
import PVPlantResources import PVPlantResources
from PVPlantResources import DirIcons as DirIcons from PVPlantResources import DirIcons as DirIcons
class SafeDict(dict):
"""Diccionario seguro para manejar placeholders no definidos"""
def rename(objects, mask, mode=0): def __missing__(self, key):
''' return f'{{{key}}}'
mode = 0/1/2/3
0: izquierda a derecha - arriba a abajo
1: arriba a abajo - izquierda a derecha
'''
# sort:
tmp = sorted(objects, key=lambda x: (x.Placement.Base.x,
x.Placement.Base.y))
for idx, obj in tmp:
obj.Name = name
class renamerTaskPanel:
def __init__(self, obj=None):
self.obj = obj
# -------------------------------------------------------------------------------------------------------------
# Module widget form
# -------------------------------------------------------------------------------------------------------------
self.formRack = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantFrame.ui")
self.formRack.widgetTracker.setVisible(False)
self.formRack.comboFrameType.currentIndexChanged.connect(self.selectionchange)
self.formPiling = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantRackFixedPiling.ui")
self.formPiling.editBreadthwaysNumOfPost.valueChanged.connect(self.editBreadthwaysNumOfPostChange)
self.formPiling.editAlongNumOfPost.valueChanged.connect(self.editAlongNumOfPostChange)
self.form = [self.formRack, self.formPiling]
def accept(self):
self.closeForm()
return True
def reject(self):
self.closeForm()
return False
def closeForm(self):
FreeCADGui.Control.closeDialog()
class _CommandRenamer: class RenameDialog(QtGui.QDialog):
def __init__(self):
super(RenameDialog, self).__init__()
self.setupUI()
def setupUI(self):
self.setWindowTitle("Renombrar objetos con plantilla")
self.setMinimumWidth(400)
layout = QtGui.QVBoxLayout(self)
# Campo para la plantilla
layout.addWidget(QtGui.QLabel("Plantilla de nombre:"))
self.template_input = QtGui.QLineEdit()
self.template_input.setPlaceholderText("Ej: {label}_mod_{index:03d}_{date:%Y%m%d}")
layout.addWidget(self.template_input)
# Info de placeholders
info = QtGui.QLabel(
"Placeholders disponibles:\n"
"{index} - Número en orden\n"
"{label} - Nombre actual del objeto\n"
"{name} - Nombre interno\n"
"{date} - Fecha actual\n"
"{time} - Hora actual\n"
"{user} - Usuario del sistema\n"
"{datetime} - Fecha y hora completa\n"
"Formatos: {date:%Y/%m/%d}, {index:03d}, etc."
)
layout.addWidget(info)
# Botones
btn_box = QtGui.QDialogButtonBox()
btn_box.addButton(QtGui.QDialogButtonBox.Apply)
btn_box.addButton(QtGui.QDialogButtonBox.Close)
btn_box.clicked.connect(self.on_button_click)
layout.addWidget(btn_box)
def on_button_click(self, button):
if button == btn_box.button(QtGui.QDialogButtonBox.Apply):
self.rename_objects()
else:
self.close()
def rename_objects(self):
template = self.template_input.text()
if not template:
QtGui.QMessageBox.warning(self, "Error", "¡La plantilla no puede estar vacía!")
return
selected_objects = FreeCADGui.Selection.getSelection()
if not selected_objects:
QtGui.QMessageBox.warning(self, "Error", "¡No hay objetos seleccionados!")
return
now = datetime.datetime.now()
user_name = getpass.getuser()
errors = []
for idx, obj in enumerate(selected_objects, 1):
try:
placeholders = SafeDict({
'index': idx,
'label': obj.Label,
'name': obj.Name,
'date': now.date(),
'time': now.time(),
'datetime': now,
'user': user_name
})
new_name = template.format_map(placeholders)
obj.Label = new_name
except Exception as e:
errors.append(f"{obj.Name}: {str(e)}")
FreeCAD.ActiveDocument.recompute()
if errors:
error_msg = "\n".join(errors)
QtGui.QMessageBox.critical(self, "Errores", f"Error(es) encontrado(s):\n{error_msg}")
else:
QtGui.QMessageBox.information(self, "Éxito", "¡Objetos renombrados correctamente!")
class CommandRenamer:
"the Arch Building command definition" "the Arch Building command definition"
def GetResources(self): def GetResources(self):

389
hydro/hydrological.py Normal file
View File

@@ -0,0 +1,389 @@
import FreeCAD
import FreeCADGui
import Mesh
import Part
import numpy as np
import random
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Pool, cpu_count
from collections import deque
import os
from PVPlantResources import DirIcons as DirIcons
def mesh_to_numpy(mesh_obj):
"""Convierte la malla a arrays de NumPy con validación robusta"""
mesh = mesh_obj.Mesh
# Convertir vértices a array NumPy (shape: Nx3)
vertices = np.array([(v.x, v.y, v.z) for v in mesh.Points], dtype=np.float32)
# Convertir facetas a array NumPy (shape: Mx3)
facets = np.array( [f.PointIndices for f in mesh.Facets], dtype=np.uint32)
# Verificar integridad de índices
max_index = len(mesh.Points) - 1
if facets.size > 0 and (facets > max_index).any():
raise ValueError("Índices de vértices fuera de rango")
return vertices, facets
def build_adjacency_matrix(facets):
"""Construye matriz de adyacencia con conversión segura de tipos"""
edges = {}
adjacency = [[] for _ in range(len(facets))]
for idx, facet in enumerate(facets):
if len(facet) != 3:
continue
v0, v1, v2 = facet
for edge in [(v0, v1), (v1, v2), (v2, v0)]:
sorted_edge = tuple(sorted(edge))
if sorted_edge not in edges:
edges[sorted_edge] = []
edges[sorted_edge].append(idx)
# Procesar solo aristas con 2 facetas
for edge, facet_indices in edges.items():
if len(facet_indices) == 2:
f1, f2 = facet_indices
adjacency[f1].append(f2)
adjacency[f2].append(f1)
return adjacency
def calculate_incenters_parallel(vertices, facets):
"""Cálculo paralelizado de incentros usando NumPy"""
v0 = vertices[facets[:, 0]]
v1 = vertices[facets[:, 1]]
v2 = vertices[facets[:, 2]]
a = np.linalg.norm(v1 - v2, axis=1)
b = np.linalg.norm(v0 - v2, axis=1)
c = np.linalg.norm(v0 - v1, axis=1)
perimeters = a + b + c
return (a[:, None] * v0 + b[:, None] * v1 + c[:, None] * v2) / perimeters[:, None]
def find_basins_parallel(args):
"""Función paralelizable para procesamiento de cuencas"""
chunk, adjacency, elevations = args
basins = []
visited = np.zeros(len(elevations), dtype=bool)
for seed in chunk:
if visited[seed]:
continue
queue = deque([seed])
basin = []
min_elev = elevations[seed]
while queue:
current = queue.popleft()
if visited[current]:
continue
visited[current] = True
basin.append(current)
neighbors = [n for n in adjacency[current] if elevations[n] >= min_elev]
queue.extend(neighbors)
if len(basin) > 0:
basins.append(basin)
return basins
def find_hydrological_basins(mesh_obj, min_area=100):
"""Identificación de cuencas optimizada"""
FreeCAD.Console.PrintMessage(f" -- vertices y facets: ")
FreeCADGui.updateGui()
vertices, facets = mesh_to_numpy(mesh_obj)
FreeCAD.Console.PrintMessage(f" -- Adjacency: ")
FreeCADGui.updateGui()
adjacency = build_adjacency_matrix(facets)
FreeCAD.Console.PrintMessage(f" -- Elevations: ")
FreeCADGui.updateGui()
elevations = calculate_incenters_parallel(vertices, facets)[:, 2]
# Dividir trabajo en chunks
chunk_size = len(facets) // (cpu_count() * 2)
chunks = [
(chunk_range, adjacency, elevations) # Empaqueta los 3 argumentos
for chunk_range in [
range(i, min(i + chunk_size, len(facets)))
for i in range(0, len(facets), chunk_size)
]
]
# Procesamiento paralelo
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
results = list(executor.map(find_basins_parallel, chunks))
# Combinar resultados
all_basins = [b for sublist in results for b in sublist]
# Filtrar por área mínima
valid_basins = []
for basin in all_basins:
area = sum(triangle_area(vertices[facets[i]]) for i in basin)
if area >= min_area:
valid_basins.append({'facets': basin, 'area': area})
return valid_basins
def triangle_area(vertices):
"""Cálculo rápido de área con producto cruz"""
return 0.5 * np.linalg.norm(
np.cross(vertices[1] - vertices[0], vertices[2] - vertices[0])
)
def validate_facet(facet):
"""Valida que la faceta sea un triángulo válido"""
return hasattr(facet, 'Points') and len(facet.Points) == 3
def calculate_incenter(facet):
"""Calcula el incentro usando la función nativa de FreeCAD"""
try:
return facet.InCircle[0] # (x, y, z)
except:
return None
def build_adjacency(mesh):
"""Construye matriz de adyacencia eficiente en memoria"""
edges = {}
adjacency = [[] for _ in mesh.Facets]
for idx, facet in enumerate(mesh.Facets):
if not validate_facet(facet):
continue
pts = facet.Points
for edge in [(min(pts[0], pts[1]), max(pts[0], pts[1])),
(min(pts[1], pts[2]), max(pts[1], pts[2])),
(min(pts[2], pts[0]), max(pts[2], pts[0]))]:
if edge in edges:
neighbor = edges[edge]
adjacency[idx].append(neighbor)
adjacency[neighbor].append(idx)
del edges[edge] # Liberar memoria
else:
edges[edge] = idx
return adjacency
def find_hydrological_basins_old(mesh_obj, min_area=100):
"""Identificación de cuencas con validación de datos"""
mesh = mesh_obj.Mesh
adjacency = build_adjacency(mesh)
basin_map = {}
current_basin = 0
for seed in range(len(mesh.Facets)):
if seed in basin_map or not validate_facet(mesh.Facets[seed]):
continue
queue = deque([seed])
basin_area = 0.0
basin_facets = []
while queue:
facet_idx = queue.popleft()
if facet_idx in basin_map:
continue
facet = mesh.Facets[facet_idx]
in_center = calculate_incenter(facet)
if not in_center:
continue
# Verificar mínimo local
is_sink = True
for neighbor in adjacency[facet_idx]:
if neighbor >= len(mesh.Facets) or not validate_facet(mesh.Facets[neighbor]):
continue
n_center = calculate_incenter(mesh.Facets[neighbor])
if n_center and n_center[2] < in_center[2]:
is_sink = False
break
if is_sink:
basin_map[facet_idx] = current_basin
basin_facets.append(facet_idx)
basin_area += facet.Area
# Expansión controlada
for neighbor in adjacency[facet_idx]:
if neighbor not in basin_map:
queue.append(neighbor)
if basin_area >= min_area:
yield {
'facets': basin_facets,
'area': basin_area,
'depth': calculate_basin_depth(mesh, basin_facets)
}
current_basin += 1
def calculate_basin_depth(mesh, basin_facets):
"""Calcula la profundidad máxima de la cuenca"""
min_z = float('inf')
max_z = -float('inf')
for idx in basin_facets:
center = calculate_incenter(mesh.Facets[idx])
if center:
min_z = min(min_z, center[2])
max_z = max(max_z, center[2])
return max_z - min_z if max_z != min_z else 0.0
def simulate_water_flow(mesh_obj, basins, rainfall=1.0):
""" Simulación de flujo con prevención de bucles infinitos """
mesh = mesh_obj.Mesh
adjacency = build_adjacency(mesh)
flow_paths = []
for basin in basins:
start_facets = basin['facets'][:2] # Muestra primeros 10 caminos
for start in start_facets:
path = []
visited = set()
current = start
while current is not None and current not in visited:
visited.add(current)
facet = mesh.Facets[current]
center = calculate_incenter(facet)
if not center:
break
path.append(FreeCAD.Vector(*center))
# Buscar vecino más bajo
next_facet = None
min_elev = float('inf')
for neighbor in adjacency[current]:
if neighbor >= len(mesh.Facets):
continue
n_center = calculate_incenter(mesh.Facets[neighbor])
if n_center and n_center[2] < min_elev:
min_elev = n_center[2]
next_facet = neighbor
current = next_facet if min_elev < center[2] else None
if len(path) > 1:
flow_paths.append(path)
return flow_paths
def colorize_mesh(mesh_obj, facet_indices, color):
"""Coloriza facetas específicas de forma compatible"""
mesh = mesh_obj.Mesh
# Crear nuevo objeto Mesh
colored_mesh = Mesh.Mesh()
colored_mesh.addMesh(mesh)
# Crear nuevo objeto en el documento
new_obj = FreeCAD.ActiveDocument.addObject("Mesh::Feature", "ColoredBasin")
new_obj.Mesh = colored_mesh
# Asignar colores a los vértices
vcolors = []
for idx in range(len(mesh.Points)):
vcolors.append((0.8, 0.8, 0.8)) # Color base
for facet_id in facet_indices:
facet = mesh.Facets[facet_id]
for vtx in facet.PointIndices:
vcolors[vtx] = color # Color de la cuenca
new_obj.ViewObject.PointColor = vcolors
new_obj.ViewObject.Lighting = "One side"
new_obj.ViewObject.Shading = "Flat Lines"
def create_polyline(points):
"""Crea un objeto Polyline en FreeCAD"""
if len(points) < 2:
return
poly = Part.makePolygon(points)
obj = FreeCAD.ActiveDocument.addObject("Part::Feature", "FlowPath")
obj.Shape = poly
obj.ViewObject.LineWidth = 2.0
obj.ViewObject.LineColor = (0.0, 0.0, 1.0)
class CommandHydrologicalAnalysis:
def GetResources(self):
return {'Pixmap': str(os.path.join(DirIcons, "drop.jpg")),
'MenuText': "Hidrological analysis",
'Accel': "H, A",
'ToolTip': "Hidrological analysis"}
def IsActive(self):
return True
def Activated(self):
# User input parameters (example values)
os.environ['OMP_NUM_THREADS'] = str(cpu_count())
os.environ['MKL_NUM_THREADS'] = str(cpu_count())
os.environ["FREECAD_NO_FORK"] = "1" # Desactiva el fork en sistemas Unix
#try:
# Parámetros de usuario
min_basin_area = 100 # m²
rainfall_intensity = 1.0
# Validar selección
mesh_obj = FreeCADGui.Selection.getSelection()[0]
if not mesh_obj.isDerivedFrom("Mesh::Feature"):
raise ValueError("Selecciona un objeto de malla")
# Procesamiento principal
FreeCAD.Console.PrintMessage(f"buscar basins: ")
FreeCADGui.updateGui()
basins = list(find_hydrological_basins(mesh_obj, min_basin_area))
FreeCAD.Console.PrintMessage(f" - Cuencas identificadas: {len(basins)}\n")
'''FreeCAD.Console.PrintMessage(f"simulate_water_flow: ")
FreeCADGui.updateGui()
flow_paths = simulate_water_flow(mesh_obj, basins, rainfall_intensity)
FreeCAD.Console.PrintMessage(f" - Trayectorias de flujo generadas: {len(flow_paths)}\n")
FreeCADGui.updateGui()'''
# Visualización
for basin in basins:
color = (random.random(), random.random(), random.random())
colorize_mesh(mesh_obj, basin['facets'], color)
'''for path in flow_paths:
create_polyline(path)'''
FreeCAD.ActiveDocument.recompute()
'''except Exception as e:
FreeCAD.Console.PrintError(f"Error: {str(e)}\n")
finally:
# Limpieza de memoria
import gc
gc.collect()'''

View File

@@ -26,16 +26,17 @@ class _CommandReload:
PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \ PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \
PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \ PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \
PVPlantCreateTerrainMesh, \ PVPlantCreateTerrainMesh, \
PVPlantFoundation, PVPlantTreeGenerator, PVPlantBuilding, PVPlantTrench, PVPlantEarthWorks, PVPlantPad, \ PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \
PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \ PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \
GraphProfile GraphProfile
from Civil import PVPlantTrench
from Vegetation import PVPlantTreeGenerator
from Mechanical.Frame import PVPlantFrame from Mechanical.Frame import PVPlantFrame
from Project.Area import PVPlantArea, PVPlantAreaUtils from Project.Area import PVPlantArea, PVPlantAreaUtils
#from Importer import importDXF #from Importer import importDXF
from Export import PVPlantBOQCivil, PVPlantBOQElectrical, PVPlantBOQMechanical, exportPVSyst, exportDXF from Export import PVPlantBOQCivil, PVPlantBOQElectrical, PVPlantBOQMechanical, exportPVSyst, exportDXF
from Utils import PVPlantUtils, PVPlantTrace, m_gui_edit, profile_editor, graphics from Utils import PVPlantUtils, PVPlantTrace, m_gui_edit, profile_editor, graphics
#from Lib import GoogleMapDownloader
from Electrical.Cable import PVPlantCable, PVPlantElectricalLine from Electrical.Cable import PVPlantCable, PVPlantElectricalLine
from Electrical import Conduit from Electrical import Conduit
@@ -47,6 +48,8 @@ class _CommandReload:
import MeshTools.Triangulation as Triangulation import MeshTools.Triangulation as Triangulation
from Project import ProjectSetup from Project import ProjectSetup
import importlib import importlib
import hydro.hydrological as hydro
import Importer.importOSM as iOSM
importlib.reload(ProjectSetup) importlib.reload(ProjectSetup)
importlib.reload(PVPlantPlacement) importlib.reload(PVPlantPlacement)
@@ -98,6 +101,11 @@ class _CommandReload:
importlib.reload(layoutToExcel) importlib.reload(layoutToExcel)
importlib.reload(Conduit) importlib.reload(Conduit)
importlib.reload(hydro)
importlib.reload(iOSM)
import Project.GenerateExternalDocument as GED
importlib.reload(GED)
#importlib.reload(GoogleMapDownloader) #importlib.reload(GoogleMapDownloader)
print("Reload modules...") print("Reload modules...")