From 0b13a8c5f1290f62ef8c26a1414e89dc7f17b10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Sat, 2 May 2026 22:47:58 +0200 Subject: [PATCH] Mejoras PVPlantTerrain: fix XYZ import, DEM rendimiento, ViewProvider boundary+contour, error handling --- PVPlantTerrain.py | 172 +++++++++++++++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 54 deletions(-) diff --git a/PVPlantTerrain.py b/PVPlantTerrain.py index 91f8bcc..3f2ff50 100644 --- a/PVPlantTerrain.py +++ b/PVPlantTerrain.py @@ -129,8 +129,14 @@ class Terrain(ArchComponent.Component): # obj.IfcType = "Fence" # obj.MoveWithHost = False - self.site = PVPlantSite.get() - self.site.Terrain = obj + try: + self.site = PVPlantSite.get() + except Exception: + self.site = None + if self.site: + self.site.Terrain = obj + else: + FreeCAD.Console.PrintWarning('Terrain: No se encontró Site, algunas funciones DEM requerirán Site.\n') obj.ViewObject.ShapeColor = (0.0000, 0.6667, 0.4980) obj.ViewObject.LineColor = (0.0000, 0.6000, 0.4392) @@ -237,7 +243,7 @@ class Terrain(ArchComponent.Component): del templist # create xy coordinates - offset = self.site.Origin + offset = self.site.Origin if self.site else FreeCAD.Vector(0, 0, 0) x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y datavals = datavals * 1000 # Ajuste de altura @@ -269,35 +275,90 @@ class Terrain(ArchComponent.Component): stepx = math.ceil(nx / stepsize) stepy = math.ceil(ny / stepsize) + # Malla completa primero como numpy y filtramos todo de una + from datetime import datetime + t_start = datetime.now() + + # Crear grid completo de coordenadas + XX, YY = np.meshgrid(x, y) + ZZ = datavals.copy() + + # Enmascarar nodata + mask_valida = ZZ != nodata_value + + # Enmascarar cutting boundary si existe + if obj.CuttingBoundary: + from FreeCAD import Base + shape = obj.CuttingBoundary.Shape + mask_boundary = np.zeros_like(ZZ, dtype=bool) + # Sampling: revisar solo puntos estratégicos para boundary grande + stride = max(1, min(nx, ny) // 200) + for i in range(0, ny, stride): + for j in range(0, nx, stride): + if mask_valida[i, j]: + if shape.isInside(FreeCAD.Vector(x[j], y[i], 0), 0, True): + mask_boundary[i, j] = True + mask_valida = mask_valida & mask_boundary + + # Extraer puntos válidos como lista plana + pts_validos = np.column_stack([ + XX[mask_valida].ravel(), + YY[mask_valida].ravel(), + ZZ[mask_valida].ravel() + ]) + + del XX, YY, ZZ, mask_valida + + # Triangulación completa de una vez (no por parches) mesh = Mesh.Mesh() - for indx in range(stepx): - inix = indx * stepsize - 1 - finx = min([stepsize * (indx + 1), len(x)-1]) - for indy in range(stepy): - iniy = indy * stepsize - 1 - finy = min([stepsize * (indy + 1), len(y) - 1]) - pts = [] - for i in range(inix, finx): - for j in range(iniy, finy): - if datavals[j][i] != nodata_value: - if obj.CuttingBoundary: - if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True): - pts.append([x[i], y[j], datavals[j][i]]) - else: - pts.append([x[i], y[j], datavals[j][i]]) - if len(pts) > 3: + if len(pts_validos) > 3: + # Si hay muchos puntos, triangulamos por parches para evitar OOM + patch_size = 50000 + n_patches = max(1, math.ceil(len(pts_validos) / patch_size)) + for p in range(n_patches): + patch = pts_validos[p * patch_size:(p + 1) * patch_size].tolist() + if len(patch) > 3: try: - triangulated = Triangulation.Triangulate(pts) + triangulated = Triangulation.Triangulate(patch) mesh.addMesh(triangulated) - except TypeError: - print(f"Error al procesar {len(pts)} puntos: {str(e)}") + except TypeError as e: + print(f"Patch {p}: error al procesar {len(patch)} puntos: {str(e)}") + except Exception as e: + print(f"Patch {p}: error inesperado: {str(e)}") + + print(f'Terraín DEM: {len(pts_validos)} pts válidos, {n_patches} parches, {datetime.now()-t_start}') + del pts_validos mesh.removeDuplicatedPoints() mesh.removeFoldsOnSurface() obj.InitialMesh = mesh.copy() - Mesh.show(mesh) + # Limpiar objetos mesh huérfanos previos si existen + for o in FreeCAD.ActiveDocument.Objects: + if o.TypeId == 'Mesh::Feature' and o.Label.startswith('Terrain_mesh_'): + FreeCAD.ActiveDocument.removeObject(o.Name) + mesh_obj = Mesh.show(mesh) + mesh_obj.Label = 'Terrain_mesh_' + obj.Label elif suffix in ['.xyz']: - data = open_xyz_mmap(obj.DEM) + pts_array = open_xyz_mmap(obj.DEM) + if pts_array is not None and len(pts_array) > 3: + import MeshTools.Triangulation as Triangulation + import Mesh + if obj.CuttingBoundary: + mask = [] + for pt in pts_array: + mask.append(obj.CuttingBoundary.Shape.isInside( + FreeCAD.Vector(pt[0], pt[1], 0), 0, True)) + pts_array = pts_array[mask] + if len(pts_array) > 3: + from datetime import datetime + t0 = datetime.now() + pts_list = pts_array.tolist() + mesh = Triangulation.Triangulate(pts_list) + mesh.removeDuplicatedPoints() + mesh.removeFoldsOnSurface() + obj.InitialMesh = mesh.copy() + Mesh.show(mesh) + print(f'XYZ import: {len(pts_array)} puntos en {datetime.now()-t0}') @@ -547,47 +608,47 @@ class ViewProviderTerrain: offset.factor = -2.0 # Boundary features. - '''self.boundary_color = coin.SoBaseColor() + self.boundary_color = coin.SoBaseColor() self.boundary_coords = coin.SoGeoCoordinate() self.boundary_lines = coin.SoLineSet() self.boundary_style = coin.SoDrawStyle() - self.boundary_style.style = coin.SoDrawStyle.LINES''' + self.boundary_style.style = coin.SoDrawStyle.LINES # Boundary root. - '''boundaries = coin.SoType.fromName('SoFCSelection').createInstance() + boundaries = coin.SoType.fromName('SoFCSelection').createInstance() boundaries.style = 'EMISSIVE_DIFFUSE' boundaries.addChild(self.boundary_color) boundaries.addChild(self.boundary_style) boundaries.addChild(self.boundary_coords) - boundaries.addChild(self.boundary_lines)''' + boundaries.addChild(self.boundary_lines) # Major Contour features. - '''self.major_color = coin.SoBaseColor() + self.major_color = coin.SoBaseColor() self.major_coords = coin.SoGeoCoordinate() self.major_lines = coin.SoLineSet() self.major_style = coin.SoDrawStyle() - self.major_style.style = coin.SoDrawStyle.LINES''' + self.major_style.style = coin.SoDrawStyle.LINES # Major Contour root. - '''major_contours = coin.SoSeparator() + major_contours = coin.SoSeparator() major_contours.addChild(self.major_color) major_contours.addChild(self.major_style) major_contours.addChild(self.major_coords) - major_contours.addChild(self.major_lines)''' + major_contours.addChild(self.major_lines) # Minor Contour features. - '''self.minor_color = coin.SoBaseColor() + self.minor_color = coin.SoBaseColor() self.minor_coords = coin.SoGeoCoordinate() self.minor_lines = coin.SoLineSet() self.minor_style = coin.SoDrawStyle() - self.minor_style.style = coin.SoDrawStyle.LINES''' + self.minor_style.style = coin.SoDrawStyle.LINES # Minor Contour root. - '''minor_contours = coin.SoSeparator() + minor_contours = coin.SoSeparator() minor_contours.addChild(self.minor_color) minor_contours.addChild(self.minor_style) minor_contours.addChild(self.minor_coords) - minor_contours.addChild(self.minor_lines)''' + minor_contours.addChild(self.minor_lines) # Highlight for selection. highlight = coin.SoType.fromName('SoFCSelection').createInstance() @@ -596,7 +657,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() @@ -609,19 +670,19 @@ class ViewProviderTerrain: edge.addChild(self.edge_style) edge.addChild(highlight) - # Surface root. + # Surface root - con contour lines visibles. surface_root = coin.SoSeparator() surface_root.addChild(face) surface_root.addChild(offset) surface_root.addChild(edge) - #surface_root.addChild(major_contours) - #surface_root.addChild(minor_contours) + surface_root.addChild(major_contours) + surface_root.addChild(minor_contours) vobj.addDisplayMode(surface_root, "Surface") # Boundary root. - #boundary_root = coin.SoSeparator() - #boundary_root.addChild(boundaries) - #vobj.addDisplayMode(boundary_root, "Boundary") + boundary_root = coin.SoSeparator() + boundary_root.addChild(boundaries) + vobj.addDisplayMode(boundary_root, "Boundary") # Elevation/Shaded root. '''shaded_root = coin.SoSeparator() @@ -648,26 +709,28 @@ class ViewProviderTerrain: self.onChanged(vobj, "ShapeColor") self.onChanged(vobj, "LineColor") self.onChanged(vobj, "LineWidth") - #self.onChanged(vobj, "BoundaryColor") - #self.onChanged(vobj, "BoundaryWidth") - #self.onChanged(vobj, "BoundaryPattern") - #self.onChanged(vobj, "PatternScale") - #self.onChanged(vobj, "MajorColor") - #self.onChanged(vobj, "MajorWidth") - #self.onChanged(vobj, "MinorColor") - #self.onChanged(vobj, "MinorWidth") + self.onChanged(vobj, "BoundaryColor") + self.onChanged(vobj, "BoundaryWidth") + self.onChanged(vobj, "BoundaryPattern") + self.onChanged(vobj, "PatternScale") + self.onChanged(vobj, "MajorColor") + self.onChanged(vobj, "MajorWidth") + self.onChanged(vobj, "MinorColor") + self.onChanged(vobj, "MinorWidth") def updateData(self, obj, prop): ''' Update Object visuals when a data property changed. ''' # Set geosystem. - geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"] + try: + utm_zone = FreeCAD.ActiveDocument.Site.UtmZone + except: + utm_zone = "30" + geo_system = ["UTM", utm_zone, "FLAT"] self.geo_coords.geoSystem.setValues(geo_system) - ''' self.boundary_coords.geoSystem.setValues(geo_system) self.major_coords.geoSystem.setValues(geo_system) self.minor_coords.geoSystem.setValues(geo_system) - ''' if prop == "Mesh": if obj.mesh: @@ -736,3 +799,4 @@ class ViewProviderTerrain: if FreeCAD.GuiUp: FreeCADGui.addCommand('Terrain', _CommandTerrain())''' +