From 1241ee97bad9e8750440366719461b67a354e45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Mon, 14 Apr 2025 10:05:32 +0600 Subject: [PATCH] algo --- PVPlantTrench.py => Civil/PVPlantTrench.py | 46 ++- PVPlantTrench.ui => Civil/PVPlantTrench.ui | 0 Importer/importOSM.py | 411 ++++++++++++++++++--- Importer/module_inserter.py | 111 ++++++ InitGui.py | 6 +- PVPlantEarthWorks.py | 87 ++--- PVPlantImportGrid.py | 12 +- PVPlantPad.py | 2 +- PVPlantPlacement.py | 28 +- PVPlantTerrain.py | 85 +++-- PVPlantTools.py | 29 +- Project/GenerateExternalDocument.py | 92 +++++ Project/utils/renamer.py | 136 ++++--- hydro/hydrological.py | 389 +++++++++++++++++++ reload.py | 14 +- 15 files changed, 1209 insertions(+), 239 deletions(-) rename PVPlantTrench.py => Civil/PVPlantTrench.py (96%) rename PVPlantTrench.ui => Civil/PVPlantTrench.ui (100%) create mode 100644 Importer/module_inserter.py create mode 100644 Project/GenerateExternalDocument.py create mode 100644 hydro/hydrological.py diff --git a/PVPlantTrench.py b/Civil/PVPlantTrench.py similarity index 96% rename from PVPlantTrench.py rename to Civil/PVPlantTrench.py index f6d8a45..f329426 100644 --- a/PVPlantTrench.py +++ b/Civil/PVPlantTrench.py @@ -340,7 +340,44 @@ class Trench(ArchComponent.Component): p2.z = 0 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 segments = [] @@ -381,13 +418,6 @@ class Trench(ArchComponent.Component): pts_plane.append(tmp) 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) points2 = self.calculateOffset(path_plane, -d) diff --git a/PVPlantTrench.ui b/Civil/PVPlantTrench.ui similarity index 100% rename from PVPlantTrench.ui rename to Civil/PVPlantTrench.ui diff --git a/Importer/importOSM.py b/Importer/importOSM.py index 00a56d5..eb742c4 100644 --- a/Importer/importOSM.py +++ b/Importer/importOSM.py @@ -1,4 +1,5 @@ import FreeCAD +import FreeCADGui import Part import Draft from xml.etree import ElementTree as ET @@ -15,9 +16,7 @@ scale = 1000.0 class OSMImporter: def __init__(self, origin): - self.Origin = origin - if origin is None: - self.Origin = FreeCAD.Vector(0, 0, 0) + self.Origin = origin if origin else FreeCAD.Vector(0, 0, 0) self.overpass_url = "https://overpass-api.de/api/interpreter" self.nodes = {} self.ways_data = defaultdict(dict) @@ -40,10 +39,11 @@ class OSMImporter: 'vegetation': (0.4, 0.8, 0.4), '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) - return (x, y, 0) * 1000 + return FreeCAD.Vector(x, y, .0) * scale - self.Origin def get_osm_data(self, bbox): query = f""" @@ -61,43 +61,36 @@ class OSMImporter: (._;>;); out body; """ - # Configurar contexto SSL seguro - ssl_context = ssl.create_default_context(cafile=certifi.where()) - - # Modificar tu código de descarga - response = urllib.request.urlopen( + req = urllib.request.Request( self.overpass_url, data=query.encode('utf-8'), - context=ssl_context, - timeout=30 + headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'}, + method='POST' ) - return response.read() + return urllib.request.urlopen(req, context=self.ssl_context, timeout=30).read() def create_layer(self, name): if not FreeCAD.ActiveDocument.getObject(name): - layer = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) - return layer + return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) return FreeCAD.ActiveDocument.getObject(name) def process_osm_data(self, osm_data): root = ET.fromstring(osm_data) - # Primera pasada: almacenar todos los nodos + # Almacenar nodos transformados for node in root.findall('node'): - '''self.nodes[node.attrib['id']] = ( - float(node.attrib['lon']), - float(node.attrib['lat']), - 0)''' - self.nodes[node.attrib['id']] = self.transformFromLatLon( + self.nodes[node.attrib['id']] = self.transform_from_latlon( float(node.attrib['lat']), float(node.attrib['lon']) ) - # Segunda pasada: procesar ways y relaciones + # Procesar ways for way in root.findall('way'): - tags = {tag.attrib['k']: tag.attrib['v'] for tag in way.findall('tag')} - nodes = [nd.attrib['ref'] for nd in way.findall('nd')] - self.ways_data[way.attrib['id']] = {'tags': tags, 'nodes': nodes} + way_id = way.attrib['id'] + self.ways_data[way_id] = { + '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_buildings() @@ -133,7 +126,7 @@ class OSMImporter: self.create_railway(nodes, transport_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.Label = f"Road_{road_type}" polyline.ViewObject.LineWidth = 2.0 @@ -143,7 +136,7 @@ class OSMImporter: layer.addObject(polyline) 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.Label = "Railway" rail_line.ViewObject.LineWidth = 1.5 @@ -152,7 +145,6 @@ class OSMImporter: def create_buildings(self): building_layer = self.create_layer("Buildings") - for way_id, data in self.ways_data.items(): if 'building' not in data['tags']: continue @@ -167,14 +159,14 @@ class OSMImporter: height = self.get_building_height(tags) # 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]: polygon_points.append(polygon_points[0]) try: polygon = Part.makePolygon(polygon_points) 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.Shape = extruded @@ -190,7 +182,8 @@ class OSMImporter: except Exception as 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 if 'height' in tags: try: @@ -205,44 +198,355 @@ class OSMImporter: return 5.0 # Altura por defecto 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(): tags = data['tags'] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] if 'power' in tags: - if tags['power'] == 'line': - self.create_power_line(nodes, power_layer) - elif tags['power'] == 'substation': - self.create_substation(nodes, power_layer) + feature_type = tags['power'] - 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 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") layer.addObject(pole) pole.Shape = cylinder pole.ViewObject.ShapeColor = self.feature_colors['power']['tower'] # 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.ViewObject.LineWidth = 3.0 cable.ViewObject.ShapeColor = self.feature_colors['power']['line'] layer.addObject(cable) - def create_substation(self, nodes, layer): - # Crear área de subestación - polygon_points = [FreeCAD.Vector(n[0], n[1], 0) * scale - self.Origin for n in nodes] - if len(polygon_points) > 2: - polygon = Part.makePolygon(polygon_points) - face = Part.Face(polygon) - substation = FreeCAD.ActiveDocument.addObject("Part::Feature", "Substation") - layer.addObject(substation) - substation.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.5) * scale - self.Origin ) - substation.ViewObject.ShapeColor = self.feature_colors['power']['substation'] + def create_substation(self, way_id, tags, nodes, layer): + """Crea subestaciones con todos los componentes detallados""" + try: + # 1. Parámetros base + voltage = self.parse_voltage(tags.get('voltage', '0')) + substation_type = tags.get('substation', 'distribution') + name = tags.get('name', f"Substation_{way_id}") + + if len(nodes) < 3: + 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): vegetation_layer = self.create_layer("Vegetation") @@ -251,7 +555,7 @@ class OSMImporter: for node_id, coords in self.nodes.items(): # Verificar si es un árbol # (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") vegetation_layer.addObject(tree) tree.Shape = cylinder @@ -262,7 +566,7 @@ class OSMImporter: if 'natural' in data['tags'] or 'landuse' in data['tags']: nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] 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) face = Part.Face(polygon) area = vegetation_layer.addObject("Part::Feature", "GreenArea") @@ -276,9 +580,10 @@ class OSMImporter: if 'natural' in data['tags'] and data['tags']['natural'] == 'water': nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] 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) face = Part.Face(polygon) 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'] + diff --git a/Importer/module_inserter.py b/Importer/module_inserter.py new file mode 100644 index 0000000..23934b7 --- /dev/null +++ b/Importer/module_inserter.py @@ -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) \ No newline at end of file diff --git a/InitGui.py b/InitGui.py index 26d1c2f..0e7e001 100644 --- a/InitGui.py +++ b/InitGui.py @@ -53,12 +53,12 @@ class PVPlantWorkbench(Workbench): from Export import ExporterCommands self.inportExportlist = ExporterCommands.Exportlist - - self.objectlist = [ + self.objectlist = PVPlantTools.objectlist + ''' [ "PVPlantTree", "PVPlantBuilding", "PVPlantFenceGroup", - ] + ]''' self.electricalList = ["PVPlantStringBox", "PVPlantCable", "PVPlanElectricalLine", diff --git a/PVPlantEarthWorks.py b/PVPlantEarthWorks.py index 1ffb063..5fc0dd9 100644 --- a/PVPlantEarthWorks.py +++ b/PVPlantEarthWorks.py @@ -5,6 +5,7 @@ import Part import ArchComponent from pivy import coin import numpy as np +import DraftGeomUtils if FreeCAD.GuiUp: import FreeCADGui, os @@ -516,6 +517,7 @@ class EarthWorksTaskPanel: return False FreeCAD.ActiveDocument.openTransaction("Calcular movimiento de tierras") + def calculateEarthWorks(line, extreme=False): pts = [] pts1 = [] @@ -576,33 +578,6 @@ class EarthWorksTaskPanel: elif ver == 1: from PVPlantPlacement import getCols 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 group in groups: first = group[0] @@ -709,44 +684,41 @@ class EarthWorksTaskPanel: import Mesh pro = utils.getProjected(sh) 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] - '''if pts[0] != pts[-1]: - pts.append(pts[0])''' land.trim(pts, 1) + tmp = [] for face in sh.Faces: wire = face.Wires[0].copy() pl = wire.Placement.Base 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") - #tmp.append(face.extrude(FreeCAD.Vector(0, 0, -10000))) - #Part.show(tmp[-1], "face-extrude") - sh = sh.extrude(FreeCAD.Vector(0, 0, -10000)) - sh = Part.Solid(sh) - Part.show(sh) - import MeshPart as mp - msh = mp.meshFromShape(Shape=sh) # , MaxLength=1) - # msh = msh.smooth("Laplace", 3) - Mesh.show(msh, "tool") - Mesh.show(land, "trim") - '''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)''' + for tri in triangles: + # Crear wire triangular + wire = Part.makePolygon([v.Point for v in tri] + [tri[0].Point]) + # Hacer offset (ahora es coplanar por ser triángulo) + 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)) + + final_tool = Part.makeCompound(tmp) + Part.show(final_tool, "tool") FreeCAD.ActiveDocument.commitTransaction() self.closeForm() @@ -964,4 +936,5 @@ def accept(): FreeCAD.ActiveDocument.commitTransaction() self.closeForm() - return True \ No newline at end of file + return True + diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py index 89f1c09..d7b68d4 100644 --- a/PVPlantImportGrid.py +++ b/PVPlantImportGrid.py @@ -100,9 +100,11 @@ def get_elevation_from_oe(coordinates): # v1 deepseek return points + def getElevationFromOE(coordinates): """Obtiene elevaciones de Open-Elevation API y devuelve vectores FreeCAD en coordenadas UTM.""" + import certifi from requests.exceptions import RequestException if len(coordinates) == 0: return None @@ -110,15 +112,15 @@ def getElevationFromOE(coordinates): from requests import get import utm - str="" + locations_str="" total = len(coordinates) - 1 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: - str += '|' - query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + str + locations_str += '|' + query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str try: - r = get(query, timeout=20, verify=False) + r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí except RequestException as e: points = [] for i, point in enumerate(coordinates): diff --git a/PVPlantPad.py b/PVPlantPad.py index 52a49db..b5cd579 100644 --- a/PVPlantPad.py +++ b/PVPlantPad.py @@ -323,7 +323,7 @@ class _PadTaskPanel: self.new = False 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): FreeCAD.ActiveDocument.openTransaction("Create Pad") diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 3005ac6..94fe41e 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -50,12 +50,9 @@ version = "0.1.0" def selectionFilter(sel, objtype): - print("type: ", objtype) fil = [] for obj in sel: if hasattr(obj, "Proxy"): - print("objeto:", obj.Proxy.__class__) - print(obj.Proxy.__class__ is objtype) if obj.Proxy.__class__ is objtype: fil.append(obj) return fil @@ -143,7 +140,7 @@ class _PVPlantPlacementTaskPanel: def calculateWorkingArea(self): self.Area = self.getProjected(self.PVArea.Shape) - tmp = FreeCAD.ActiveDocument.findObjects(Name="ProhibitedArea") + tmp = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") if len(tmp): ProhibitedAreas = list() for obj in tmp: @@ -189,7 +186,6 @@ class _PVPlantPlacementTaskPanel: def adjustToTerrain(self, coordinates): mode = 1 terrain = self.Terrain.Mesh - type = 0 def placeRegion(df): # TODO: new import MeshPart as mp @@ -210,11 +206,10 @@ class _PVPlantPlacementTaskPanel: pbot = FreeCAD.Vector(base) pbot.y -= yl line = Part.LineSegment(ptop, pbot).toShape() - if type == 0: # Mesh: - profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] - else: # Shape: + 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. 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() yy = list() @@ -285,7 +280,7 @@ class _PVPlantPlacementTaskPanel: placeRegion(df) return df - def placeonregion_old(frames): # old + """def placeonregion_old(frames): # old for colnum, col in enumerate(frames): groups = list() groups.append([col[0]]) @@ -381,7 +376,7 @@ class _PVPlantPlacementTaskPanel: rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec) pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) placements.append(pl) - return placements + return placements""" def isInside(self, frame, point): if self.Area.isInside(point, 10, True): @@ -456,6 +451,8 @@ class _PVPlantPlacementTaskPanel: if countcols == self.form.editColCount.value(): offsetcols += valcols countcols = 0 + print("/n/n") + print(cols) return self.adjustToTerrain(cols) def calculateNonAlignedArray(self): @@ -566,19 +563,20 @@ class _PVPlantPlacementTaskPanel: self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value FreeCAD.ActiveDocument.openTransaction("Create Placement") + # 1. Calculate working area: self.calculateWorkingArea() - + # 2. Calculate aligned array: if self.form.cbAlignFrames.isChecked(): dataframe = self.calculateAlignedArray() else: dataframe = self.calculateNonAlignedArray() + # 3. Adjust to terrain: + self.createFrameFromPoints(dataframe) + FreeCAD.ActiveDocument.commitTransaction() - # last step: ------------------------------ FreeCAD.ActiveDocument.RecomputesFrozen = False params.SetBool("AutoSaveEnabled", auto_save_enabled) - self.createFrameFromPoints(dataframe) - total_time = datetime.now() - starttime print(" -- Tiempo tardado:", total_time) FreeCADGui.Control.closeDialog() diff --git a/PVPlantTerrain.py b/PVPlantTerrain.py index a945ceb..8043e0f 100644 --- a/PVPlantTerrain.py +++ b/PVPlantTerrain.py @@ -120,12 +120,12 @@ class Terrain(ArchComponent.Component): "Surface", "Use a Point Group to generate the surface") - if not ("Mesh" in pl): + if not ("mesh" in pl): obj.addProperty("Mesh::PropertyMeshKernel", - "Mesh", + "mesh", "Surface", "Mesh") - obj.setEditorMode("Mesh", 1) + obj.setEditorMode("mesh", 1) if not ("InitialMesh" in pl): obj.addProperty("Mesh::PropertyMeshKernel", @@ -156,7 +156,7 @@ class Terrain(ArchComponent.Component): '''Do something when a property has changed''' if prop == "InitialMesh": - obj.Mesh = obj.InitialMesh.copy() + obj.mesh = obj.InitialMesh.copy() if prop == "DEM" or prop == "CuttingBoundary": from datetime import datetime @@ -197,11 +197,10 @@ class Terrain(ArchComponent.Component): del templist # create xy coordinates - import PVPlantSite - offset = PVPlantSite.get().Origin - x = 1000 * (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) - offset.x - y = 1000 * (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) - offset.y - datavals = 1000 * datavals # - offset.z + offset = self.site.Origin + 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 # remove points out of area # 1. coarse: @@ -210,7 +209,6 @@ class Terrain(ArchComponent.Component): inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0 tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x), x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0] - print(tmp) x_max = np.ndarray.max(tmp) x_min = np.ndarray.min(tmp) @@ -249,10 +247,10 @@ class Terrain(ArchComponent.Component): pts.append([x[i], y[j], datavals[j][i]]) if len(pts) > 3: try: - mesh.addMesh(Triangulation.Triangulate(pts)) - #Mesh.show(mesh) + triangulated = Triangulation.Triangulate(pts) + mesh.addMesh(triangulated) except TypeError: - print("error al procesar: {0} puntos".format(len(pts))) + print(f"Error al procesar {len(pts)} puntos: {str(e)}") mesh.removeDuplicatedPoints() mesh.removeFoldsOnSurface() @@ -284,12 +282,9 @@ class Terrain(ArchComponent.Component): import MeshTools.Triangulation as Triangulation mesh = Triangulation.Triangulate(Data) - '''shape = PVPlantCreateTerrainMesh.MeshToShape(mesh) - shape.Placement.move(nbase)''' - - obj.Shape = shape if obj.DEM: obj.DEM = None + obj.mesh = mesh def execute(self, obj): '''''' @@ -307,7 +302,6 @@ class ViewProviderTerrain: "A View Provider for the Pipe object" def __init__(self, vobj): - self.Object = vobj.Object self.boundary_color = None self.edge_style = None self.edge_color = None @@ -321,16 +315,16 @@ class ViewProviderTerrain: # Triangulation properties. pl = vobj.PropertiesList if not ("Transparency" in pl): - vobj.addProperty("App::PropertyIntegerConstraint", + '''vobj.addProperty("App::PropertyIntegerConstraint", "Transparency", "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): vobj.addProperty("App::PropertyColor", "ShapeColor", "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): vobj.addProperty("App::PropertyMaterial", @@ -413,18 +407,21 @@ class ViewProviderTerrain: "Set major contour line width").MinorWidth = (2.0, 1.0, 20.0, 1.0) vobj.Proxy = self + self.Object = vobj.Object + # Inicializar colores correctamente vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor def onDocumentRestored(self, vobj): self.setProperties(vobj) 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 hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"): color = vobj.getPropertyByName("ShapeColor") 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 if prop == "ShapeMaterial": @@ -555,7 +552,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() @@ -573,14 +570,14 @@ class ViewProviderTerrain: 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() @@ -599,8 +596,8 @@ class ViewProviderTerrain: # Wireframe root. wireframe_root = coin.SoSeparator() wireframe_root.addChild(edge) - wireframe_root.addChild(major_contours) - wireframe_root.addChild(minor_contours) + #wireframe_root.addChild(major_contours) + #wireframe_root.addChild(minor_contours) vobj.addDisplayMode(wireframe_root, "Wireframe") # Take features from properties. @@ -629,19 +626,19 @@ class ViewProviderTerrain: ''' if prop == "Mesh": - print("update terrain mesh") - mesh = obj.Mesh - copy_mesh = mesh.copy() - # copy_mesh.Placement.move(origin.Origin) + if obj.mesh: + print("Mostrar mesh") - triangles = [] - for i in copy_mesh.Topology[1]: - triangles.extend(list(i)) - triangles.append(-1) + mesh = obj.mesh + vertices = [tuple(v) for v in mesh.Topology[0]] + faces = [] + for face in mesh.Topology[1]: + faces.extend(face) + faces.append(-1) - self.geo_coords.point.values = copy_mesh.Topology[0] - self.triangles.coordIndex.values = triangles - del copy_mesh + # Asignar a los nodos de visualización + self.geo_coords.point.values = vertices # <-- ¡Clave! + self.triangles.coordIndex.values = faces # <-- ¡Clave! def getDisplayModes(self, vobj): ''' Return a list of display modes. ''' @@ -656,7 +653,9 @@ class ViewProviderTerrain: return "Surface" def claimChildren(self): - return [self.Object.CuttingBoundary, ] + if hasattr(self, "Object") and self.Object: + return [self.Object.CuttingBoundary, ] + return [] def getIcon(self): return str(os.path.join(DirIcons, "terrain.svg")) diff --git a/PVPlantTools.py b/PVPlantTools.py index 8975045..2078867 100644 --- a/PVPlantTools.py +++ b/PVPlantTools.py @@ -25,10 +25,8 @@ __title__ = "RebarCommands" __author__ = "Amritpal Singh" __url__ = "https://www.freecadweb.org" -from pathlib import Path - import FreeCADGui, FreeCAD -from PySide import QtGui, QtCore +from PySide import QtCore from PySide.QtCore import QT_TRANSLATE_NOOP from PVPlantResources import DirIcons as DirIcons import os @@ -165,7 +163,6 @@ class CommandDivideArea: @staticmethod def Activated(): - from Project.Area import PVPlantArea sel = FreeCADGui.Selection.getSelection()[0] @@ -210,7 +207,6 @@ class CommandFrameArea: @staticmethod def Activated(): - from Project.Area import PVPlantArea sel = FreeCADGui.Selection.getSelection() makeFramedArea(None, sel) @@ -443,7 +439,7 @@ class CommandTrench: # V1: @staticmethod def Activated(): """Execute when the command is called.""" - import PVPlantTrench + from Civil import PVPlantTrench sel = FreeCADGui.Selection.getSelection() done = False @@ -489,7 +485,7 @@ class CommandSemiAutomaticTrench: # V1: @staticmethod def Activated(): """Execute when the command is called.""" - import PVPlantTrench + from Civil import PVPlantTrench semi = PVPlantTrench.semiAutomaticTrench() @@ -510,8 +506,8 @@ class CommandCalculateEarthworks: @staticmethod def Activated(): - import PVPlantEarthworks - TaskPanel = PVPlantEarthworks.EarthWorksTaskPanel() + import PVPlantEarthWorks + TaskPanel = PVPlantEarthWorks.EarthWorksTaskPanel() FreeCADGui.Control.showDialog(TaskPanel) @@ -646,6 +642,15 @@ if FreeCAD.GuiUp: FreeCADGui.addCommand('PVPlantAdjustToTerrain', PVPlantPlacement.CommandAdjustToTerrain()) 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", "PVPlantSite", "ProjectSetup", @@ -668,7 +673,11 @@ projectlist = [ # "Reload", # "PVPlantFoundation" # "GraphTerrainProfile", # "Trace", + "Separator", + 'HydrologicalAnalysis', + 'newExternalDocument', ] + pv_list = [ # "RackType", # "PVPlantRackCheck", @@ -678,3 +687,5 @@ pv_list = [ "PVPlantConvertTo", # "PVArea" ] + +objectlist = ['PVPlantTree',] diff --git a/Project/GenerateExternalDocument.py b/Project/GenerateExternalDocument.py new file mode 100644 index 0000000..3511cb4 --- /dev/null +++ b/Project/GenerateExternalDocument.py @@ -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 \ No newline at end of file diff --git a/Project/utils/renamer.py b/Project/utils/renamer.py index 54fc454..784e848 100644 --- a/Project/utils/renamer.py +++ b/Project/utils/renamer.py @@ -21,6 +21,10 @@ # *********************************************************************** import FreeCAD +import FreeCADGui +from PySide import QtGui, QtCore +import datetime +import getpass if FreeCAD.GuiUp: import FreeCADGui, os @@ -43,51 +47,99 @@ except AttributeError: import PVPlantResources from PVPlantResources import DirIcons as DirIcons +class SafeDict(dict): + """Diccionario seguro para manejar placeholders no definidos""" -def rename(objects, mask, mode=0): - ''' - 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() + def __missing__(self, key): + return f'{{{key}}}' -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" def GetResources(self): diff --git a/hydro/hydrological.py b/hydro/hydrological.py new file mode 100644 index 0000000..b362b15 --- /dev/null +++ b/hydro/hydrological.py @@ -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()''' diff --git a/reload.py b/reload.py index 96d71e8..030da1f 100644 --- a/reload.py +++ b/reload.py @@ -26,16 +26,17 @@ class _CommandReload: PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \ PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \ PVPlantCreateTerrainMesh, \ - PVPlantFoundation, PVPlantTreeGenerator, PVPlantBuilding, PVPlantTrench, PVPlantEarthWorks, PVPlantPad, \ + PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \ PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \ GraphProfile - + from Civil import PVPlantTrench + from Vegetation import PVPlantTreeGenerator + from Mechanical.Frame import PVPlantFrame from Project.Area import PVPlantArea, PVPlantAreaUtils #from Importer import importDXF from Export import PVPlantBOQCivil, PVPlantBOQElectrical, PVPlantBOQMechanical, exportPVSyst, exportDXF from Utils import PVPlantUtils, PVPlantTrace, m_gui_edit, profile_editor, graphics - #from Lib import GoogleMapDownloader from Electrical.Cable import PVPlantCable, PVPlantElectricalLine from Electrical import Conduit @@ -47,6 +48,8 @@ class _CommandReload: import MeshTools.Triangulation as Triangulation from Project import ProjectSetup import importlib + import hydro.hydrological as hydro + import Importer.importOSM as iOSM importlib.reload(ProjectSetup) importlib.reload(PVPlantPlacement) @@ -98,6 +101,11 @@ class _CommandReload: importlib.reload(layoutToExcel) importlib.reload(Conduit) + importlib.reload(hydro) + importlib.reload(iOSM) + + import Project.GenerateExternalDocument as GED + importlib.reload(GED) #importlib.reload(GoogleMapDownloader) print("Reload modules...")