@@ -345,6 +345,7 @@ def show_add_object_menu(self):
345345
346346 addFeatureAction = menu .addAction ("Surface from model" )
347347 loadFeatureAction = menu .addAction ("Load from file" )
348+ addQgsLayerAction = menu .addAction ("Add from QGIS layer" )
348349
349350 buttonPosition = self .sender ().mapToGlobal (self .sender ().rect ().bottomLeft ())
350351 action = menu .exec_ (buttonPosition )
@@ -353,10 +354,122 @@ def show_add_object_menu(self):
353354 self .add_feature_from_geological_model ()
354355 elif action == loadFeatureAction :
355356 self .load_feature_from_file ()
356-
357+ elif action == addQgsLayerAction :
358+ self .add_object_from_qgis_layer ()
357359 def add_feature_from_geological_model (self ):
358360 # Logic to add a feature from the geological model
359361 print ("Adding feature from geological model" )
362+ def add_object_from_qgis_layer (self ):
363+ """Show a dialog to pick a QGIS point vector layer, convert it to a VTK/PyVista
364+ point cloud and copy numeric attributes as point scalars.
365+ """
366+ # Local imports so the module can still be imported when QGIS GUI isn't available
367+ try :
368+ from qgis .gui import QgsMapLayerComboBox
369+ from qgis .core import QgsMapLayerProxyModel , QgsWkbTypes
370+ except Exception as e :
371+ print ("QGIS GUI components are not available:" , e )
372+ return
373+
374+ try :
375+ from loopstructural .main .vectorLayerWrapper import qgsLayerToGeoDataFrame
376+ except Exception as e :
377+ print ("Could not import qgsLayerToGeoDataFrame:" , e )
378+ return
379+ from loopstructural .main .model_manager import AllSampler
380+
381+ from PyQt5 .QtWidgets import QDialog , QDialogButtonBox , QVBoxLayout , QMessageBox
382+ import numpy as np
383+ import pandas as pd
384+
385+ dialog = QDialog (self )
386+ dialog .setWindowTitle ("Add from QGIS layer" )
387+ layout = QVBoxLayout (dialog )
388+
389+ layout .addWidget (QLabel ("Select point layer:" ))
390+ layer_combo = QgsMapLayerComboBox (dialog )
391+ # Restrict to point layers only
392+ layer_combo .setFilters (QgsMapLayerProxyModel .PointLayer )
393+ layout .addWidget (layer_combo )
394+
395+ buttons = QDialogButtonBox (QDialogButtonBox .Ok | QDialogButtonBox .Cancel )
396+ buttons .accepted .connect (dialog .accept )
397+ buttons .rejected .connect (dialog .reject )
398+ layout .addWidget (buttons )
399+
400+ if dialog .exec_ () != QDialog .Accepted :
401+ return
402+
403+ layer = layer_combo .currentLayer ()
404+ if layer is None or not layer .isValid ():
405+ QMessageBox .warning (self , "Invalid layer" , "No valid layer selected." )
406+ return
407+
408+ # Basic geometry check - ensure the layer contains point geometry
409+ try :
410+ if layer .wkbType () != QgsWkbTypes .Point and QgsWkbTypes .geometryType (layer .wkbType ()) != QgsWkbTypes .PointGeometry :
411+ # Some QGIS versions use different enums; allow via proxy filter primarily
412+ # If the check fails, continue but warn
413+ print ("Selected layer does not appear to be a point layer. Proceeding anyway." )
414+ except Exception :
415+ # ignore strict checks - rely on conversion result
416+ pass
417+
418+ # Convert layer to a DataFrame (no DTM)
419+ gdf = qgsLayerToGeoDataFrame (layer )
420+ sampler = AllSampler ()
421+ # sample the points from the gdf with no DTM and include Z if present
422+ df = sampler (gdf ,None ,True )
423+ if df is None or df .empty :
424+ QMessageBox .warning (self , "No data" , "Selected layer contains no points." )
425+ return
426+
427+ # Ensure X,Y,Z columns present
428+ if not set (["X" , "Y" , "Z" ]).issubset (df .columns ):
429+ QMessageBox .warning (self , "Invalid data" , "Layer conversion did not produce X/Y/Z columns." )
430+ return
431+
432+ # Build points array
433+ try :
434+ pts = np .vstack ([df ["X" ].to_numpy (), df ["Y" ].to_numpy (), df ["Z" ].to_numpy ()]).T .astype (float )
435+ except Exception as e :
436+ QMessageBox .warning (self , "Error" , f"Failed to build point coordinates: { e } " )
437+ return
438+
439+ # Create PyVista point cloud / PolyData
440+ try :
441+ mesh = pv .PolyData (pts )
442+ except Exception as e :
443+ QMessageBox .warning (self , "Error" , f"Failed to create mesh: { e } " )
444+ return
445+
446+ # Add numeric attributes as point scalars
447+ for col in df .columns :
448+ if col in ("X" , "Y" , "Z" ):
449+ continue
450+ try :
451+ ser = pd .to_numeric (df [col ], errors = 'coerce' )
452+ if ser .isnull ().all ():
453+ # no numeric values present
454+ continue
455+ arr = ser .to_numpy ().astype (float )
456+ # Ensure length matches points
457+ if len (arr ) != mesh .n_points :
458+ # skip columns that don't match
459+ continue
460+ mesh .point_data [col ] = arr
461+ except Exception :
462+ # skip non-numeric or problematic fields
463+ continue
464+
465+ # Add to viewer
466+ if self .viewer and hasattr (self .viewer , 'add_mesh_object' ):
467+ try :
468+ self .viewer .add_mesh_object (mesh , name = layer .name ())
469+ except Exception as e :
470+ print ("Failed to add mesh to viewer:" , e )
471+ else :
472+ print ("Error: Viewer is not initialized or does not support adding meshes." )
360473
361474 def load_feature_from_file (self ):
362475 file_path , _ = QFileDialog .getOpenFileName (
0 commit comments