From 46b56a800cfd6434d1d6c9ff6d1f409e6520d359 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Sun, 15 Mar 2026 17:50:28 +0100 Subject: [PATCH] #12560 Surface: Add export to GRI format with UI dialog and context menu integration CMake: Change mdspan link from PRIVATE to PUBLIC for surfio_lib --- .../SurfaceCommands/CMakeLists_files.cmake | 4 + .../RicExportSurfaceToGriFeature.cpp | 178 ++++++++++++++++++ .../RicExportSurfaceToGriFeature.h | 51 +++++ .../RicExportSurfaceToGriUi.cpp | 116 ++++++++++++ .../SurfaceCommands/RicExportSurfaceToGriUi.h | 61 ++++++ .../RimContextCommandBuilder.cpp | 14 -- .../Surfaces/RimGridCaseSurface.cpp | 10 + .../Surfaces/RimGridCaseSurface.h | 2 + .../Surfaces/RimRegularSurface.cpp | 57 ++++++ .../Surfaces/RimRegularSurface.h | 11 +- .../ProjectDataModel/Surfaces/RimSurface.cpp | 13 ++ .../ProjectDataModel/Surfaces/RimSurface.h | 2 + .../Surface/RigSurfaceResampler.cpp | 60 ++++++ .../Surface/RigSurfaceResampler.h | 9 + .../UnitTests/RifSurfio-Test.cpp | 46 +++++ .../UnitTests/RigSurfaceResampler-Test.cpp | 55 ++++++ ThirdParty/custom-surfio/CMakeLists.txt | 2 +- 17 files changed, 674 insertions(+), 17 deletions(-) create mode 100644 ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.cpp create mode 100644 ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.h create mode 100644 ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.cpp create mode 100644 ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.h diff --git a/ApplicationLibCode/Commands/SurfaceCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/SurfaceCommands/CMakeLists_files.cmake index 54ff27a274..21c01bbd24 100644 --- a/ApplicationLibCode/Commands/SurfaceCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/SurfaceCommands/CMakeLists_files.cmake @@ -5,6 +5,8 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RicNewGridCaseSurfaceFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicExportKLayerToPtlFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToTsurfFeature.h + ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToGriFeature.h + ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToGriUi.h ${CMAKE_CURRENT_LIST_DIR}/RicNewSurfaceCollectionFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicNewDepthSurfaceFeature.h ${CMAKE_CURRENT_LIST_DIR}/RicNewRegularSurfaceFeature.h @@ -17,6 +19,8 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RicNewGridCaseSurfaceFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicExportKLayerToPtlFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToTsurfFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToGriFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicExportSurfaceToGriUi.cpp ${CMAKE_CURRENT_LIST_DIR}/RicNewSurfaceCollectionFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicNewDepthSurfaceFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicNewRegularSurfaceFeature.cpp diff --git a/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.cpp b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.cpp new file mode 100644 index 0000000000..0e6470b685 --- /dev/null +++ b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.cpp @@ -0,0 +1,178 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RicExportSurfaceToGriFeature.h" + +#include "RiaApplication.h" +#include "RiaLogging.h" + +#include "RifSurfio.h" + +#include "Surface/RigSurface.h" +#include "Surface/RigSurfaceResampler.h" + +#include "RimRegularSurface.h" +#include "RimSurface.h" + +#include "RicExportSurfaceToGriUi.h" + +#include "cafPdmUiPropertyViewDialog.h" +#include "cafSelectionManagerTools.h" +#include "cafUtils.h" + +#include "cvfBoundingBox.h" + +#include +#include + +CAF_CMD_SOURCE_INIT( RicExportSurfaceToGriFeature, "RicExportSurfaceToGriFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// For a RimRegularSurface whose stored grid matches gridParams exactly, depth values are returned +/// directly. Otherwise the surface is resampled onto the grid via RigSurfaceResampler. +//-------------------------------------------------------------------------------------------------- +std::vector RicExportSurfaceToGriFeature::resampleToGrid( RimSurface* surf, const RigRegularSurfaceData& gridParams ) +{ + // Regular surface with matching grid: use stored depth values directly (lossless) + if ( auto* reg = dynamic_cast( surf ) ) + { + if ( reg->nx() == gridParams.nx && reg->ny() == gridParams.ny && reg->originX() == gridParams.originX && + reg->originY() == gridParams.originY && reg->incrementX() == gridParams.incrementX && + reg->incrementY() == gridParams.incrementY && reg->rotation() == gridParams.rotation ) + { + return reg->depthValues(); + } + } + + // Resample the surface onto the requested grid + RigSurface* rig = surf->surfaceData(); + if ( !rig || rig->vertices().empty() ) return {}; + + auto depthValues = RigSurfaceResampler::resampleToRegularGrid( rig, + gridParams.nx, + gridParams.ny, + gridParams.originX, + gridParams.originY, + gridParams.incrementX, + gridParams.incrementY, + gridParams.rotation ); + + // RigSurface stores Z as negative depth; IRAP format uses positive depth values + for ( auto& v : depthValues ) + if ( !std::isnan( v ) ) v = -v; + + return depthValues; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicExportSurfaceToGriFeature::exportSurfaces( const std::vector& surfaces ) +{ + if ( surfaces.empty() ) return; + + RiaApplication* app = RiaApplication::instance(); + const QString defaultDir = app->lastUsedDialogDirectoryWithFallbackToProjectFolder( "EXPORT_SURFACE" ); + + // Build default grid params + RicExportSurfaceToGriUi ui; + + if ( surfaces.size() == 1 ) + { + if ( auto* reg = dynamic_cast( surfaces[0] ) ) + { + ui.setDefaults( defaultDir, reg->nx(), reg->ny(), reg->originX(), reg->originY(), reg->incrementX(), reg->incrementY() ); + } + } + else + { + cvf::BoundingBox bb; + size_t totalVertexCount = 0; + for ( RimSurface* surf : surfaces ) + { + RigSurface* rig = surf->surfaceData(); + if ( !rig ) continue; + for ( const auto& v : rig->vertices() ) + bb.add( v ); + totalVertexCount += rig->vertices().size(); + } + + if ( !bb.isValid() || totalVertexCount == 0 ) return; + + const double areaApprox = bb.extent().x() * bb.extent().y(); + const double spacing = ( areaApprox > 0.0 ) ? std::sqrt( areaApprox / static_cast( totalVertexCount ) ) : 1.0; + const int nx = std::max( 2, static_cast( std::ceil( bb.extent().x() / spacing ) ) + 1 ); + const int ny = std::max( 2, static_cast( std::ceil( bb.extent().y() / spacing ) ) + 1 ); + ui.setDefaults( defaultDir, nx, ny, bb.min().x(), bb.min().y(), spacing, spacing ); + } + + caf::PdmUiPropertyViewDialog dialog( nullptr, &ui, "Export Surface to IRAP/GRI", "" ); + dialog.resize( QSize( 400, 300 ) ); + if ( dialog.exec() != QDialog::Accepted ) return; + + const QString exportDir = ui.exportFolder(); + if ( exportDir.isEmpty() ) return; + + app->setLastUsedDialogDirectory( "EXPORT_SURFACE", exportDir ); + + const RigRegularSurfaceData gridParams = ui.gridParams(); + const bool binary = ( ui.exportFormat() == RicExportSurfaceToGriUi::ExportFormat::GRI ); + const QString extension = binary ? ".gri" : ".irap"; + + for ( RimSurface* surf : surfaces ) + { + const QString fileName = + caf::Utils::constructFullFileName( exportDir, caf::Utils::makeValidFileBasename( surf->fullName() ), extension ); + + const auto depthValues = resampleToGrid( surf, gridParams ); + if ( depthValues.empty() ) continue; + + bool ok = binary ? RifSurfio::exportToGri( fileName.toStdString(), gridParams, depthValues ) + : RifSurfio::exportToIrap( fileName.toStdString(), gridParams, depthValues ); + + if ( ok ) + RiaLogging::info( QString( "Exported surface to: %1" ).arg( fileName ) ); + else + RiaLogging::error( QString( "Failed to export surface to: %1" ).arg( fileName ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RicExportSurfaceToGriFeature::isCommandEnabled() const +{ + return !caf::selectedObjectsByTypeStrict().empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicExportSurfaceToGriFeature::onActionTriggered( bool isChecked ) +{ + exportSurfaces( caf::selectedObjectsByTypeStrict() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicExportSurfaceToGriFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setIcon( QIcon( ":/ReservoirSurfaces16x16.png" ) ); + actionToSetup->setText( "Export Surface to IRAP/GRI..." ); +} diff --git a/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.h b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.h new file mode 100644 index 0000000000..af00868867 --- /dev/null +++ b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriFeature.h @@ -0,0 +1,51 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RifSurfio.h" + +#include "cafCmdFeature.h" + +#include +#include + +#include + +class RimSurface; + +//================================================================================================== +/// +//================================================================================================== +class RicExportSurfaceToGriFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +public: + // Resamples one surface onto the given regular grid and returns depth values + // in row-major order (index = j * nx + i). Returns an empty vector on failure. + static std::vector resampleToGrid( RimSurface* surf, const RigRegularSurfaceData& gridParams ); + + // Shows the export dialog, resamples and writes each surface. Format is chosen in the dialog. + static void exportSurfaces( const std::vector& surfaces ); + +protected: + bool isCommandEnabled() const override; + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.cpp b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.cpp new file mode 100644 index 0000000000..2e3082dd2f --- /dev/null +++ b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.cpp @@ -0,0 +1,116 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RicExportSurfaceToGriUi.h" + +#include "cafPdmUiFilePathEditor.h" + +#include + +namespace caf +{ +template <> +void AppEnum::setUp() +{ + addItem( RicExportSurfaceToGriUi::ExportFormat::GRI, "GRI", "IRAP Binary (.gri)" ); + addItem( RicExportSurfaceToGriUi::ExportFormat::IRAP, "IRAP", "IRAP Classic (.irap)" ); + setDefault( RicExportSurfaceToGriUi::ExportFormat::GRI ); +} +} // namespace caf + +CAF_PDM_SOURCE_INIT( RicExportSurfaceToGriUi, "RicExportSurfaceToGriUi" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RicExportSurfaceToGriUi::RicExportSurfaceToGriUi() +{ + CAF_PDM_InitObject( "Export Surface to Regular Grid" ); + + CAF_PDM_InitField( &m_exportFormat, "ExportFormat", caf::AppEnum( ExportFormat::GRI ), "Format" ); + + CAF_PDM_InitFieldNoDefault( &m_exportFolder, "ExportFolder", "Export Folder" ); + m_exportFolder.uiCapability()->setUiEditorTypeName( caf::PdmUiFilePathEditor::uiEditorTypeName() ); + + CAF_PDM_InitField( &m_nx, "Nx", 10, "Nx (columns)" ); + CAF_PDM_InitField( &m_ny, "Ny", 10, "Ny (rows)" ); + CAF_PDM_InitField( &m_originX, "OriginX", 0.0, "Origin X" ); + CAF_PDM_InitField( &m_originY, "OriginY", 0.0, "Origin Y" ); + CAF_PDM_InitField( &m_incrementX, "IncrementX", 1.0, "Increment X" ); + CAF_PDM_InitField( &m_incrementY, "IncrementY", 1.0, "Increment Y" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicExportSurfaceToGriUi::setDefaults( const QString& exportFolder, int nx, int ny, double originX, double originY, double incrementX, double incrementY ) +{ + m_exportFolder = exportFolder; + m_nx = nx; + m_ny = ny; + m_originX = std::round( originX ); + m_originY = std::round( originY ); + m_incrementX = std::round( incrementX ); + m_incrementY = std::round( incrementY ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigRegularSurfaceData RicExportSurfaceToGriUi::gridParams() const +{ + RigRegularSurfaceData p; + p.nx = m_nx; + p.ny = m_ny; + p.originX = m_originX; + p.originY = m_originY; + p.incrementX = m_incrementX; + p.incrementY = m_incrementY; + p.rotation = 0.0; + return p; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RicExportSurfaceToGriUi::exportFolder() const +{ + return m_exportFolder; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RicExportSurfaceToGriUi::ExportFormat RicExportSurfaceToGriUi::exportFormat() const +{ + return m_exportFormat(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicExportSurfaceToGriUi::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + if ( field == &m_exportFolder ) + { + if ( auto* attr = dynamic_cast( attribute ) ) + { + attr->m_selectDirectory = true; + } + } +} diff --git a/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.h b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.h new file mode 100644 index 0000000000..205da96b88 --- /dev/null +++ b/ApplicationLibCode/Commands/SurfaceCommands/RicExportSurfaceToGriUi.h @@ -0,0 +1,61 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RifSurfio.h" + +#include "cafAppEnum.h" +#include "cafPdmField.h" +#include "cafPdmObject.h" + +//================================================================================================== +/// PdmObject holding the parameters for resampling a surface onto a regular grid before export. +//================================================================================================== +class RicExportSurfaceToGriUi : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + enum class ExportFormat + { + GRI, + IRAP + }; + + RicExportSurfaceToGriUi(); + + void setDefaults( const QString& exportFolder, int nx, int ny, double originX, double originY, double incrementX, double incrementY ); + + RigRegularSurfaceData gridParams() const; + QString exportFolder() const; + ExportFormat exportFormat() const; + +protected: + void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + +private: + caf::PdmField> m_exportFormat; + caf::PdmField m_exportFolder; + caf::PdmField m_nx; + caf::PdmField m_ny; + caf::PdmField m_originX; + caf::PdmField m_originY; + caf::PdmField m_incrementX; + caf::PdmField m_incrementY; +}; diff --git a/ApplicationLibCode/ProjectDataModel/RimContextCommandBuilder.cpp b/ApplicationLibCode/ProjectDataModel/RimContextCommandBuilder.cpp index d2c3617738..02c8166140 100644 --- a/ApplicationLibCode/ProjectDataModel/RimContextCommandBuilder.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimContextCommandBuilder.cpp @@ -84,7 +84,6 @@ #include "RimGeoMechPropertyFilter.h" #include "RimGeoMechPropertyFilterCollection.h" #include "RimGeoMechView.h" -#include "RimGridCaseSurface.h" #include "RimGridCollection.h" #include "RimGridCrossPlot.h" #include "RimGridCrossPlotCollection.h" @@ -135,7 +134,6 @@ #include "RimSummaryTable.h" #include "RimSummaryTableCollection.h" #include "RimSummaryTimeAxisProperties.h" -#include "RimSurface.h" #include "RimValveCollection.h" #include "RimValveTemplate.h" #include "RimValveTemplateCollection.h" @@ -930,18 +928,6 @@ caf::CmdFeatureMenuBuilder RimContextCommandBuilder::commandsFromSelection() { menuBuilder << "RicNewElasticPropertyScalingFeature"; } - else if ( dynamic_cast( firstUiItem ) ) - { - if ( dynamic_cast( firstUiItem ) ) - { - menuBuilder << "RicExportKLayerToPtlFeature"; - } - - menuBuilder << "RicExportSurfaceToTsurfFeature"; - menuBuilder << "Separator"; - menuBuilder << "RicCopySurfaceFeature"; - menuBuilder << "RicReloadSurfaceFeature"; - } else if ( dynamic_cast( firstUiItem ) ) { menuBuilder << "RicNewInlineSeismicSectionFeature"; diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.cpp b/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.cpp index beef6a80d5..d0f6d8f565 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.cpp +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.cpp @@ -29,6 +29,7 @@ #include "RimSurfaceCollection.h" #include "RimTools.h" +#include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiSliderEditor.h" @@ -596,3 +597,12 @@ QString RimGridCaseSurface::fullName() const retval += QString::number( m_oneBasedSliceIndex ); return retval; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCaseSurface::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const +{ + menuBuilder << "RicExportKLayerToPtlFeature"; + RimSurface::appendMenuItems( menuBuilder ); +} diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.h b/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.h index 039e27e32e..1268e81e3f 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.h +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimGridCaseSurface.h @@ -46,6 +46,8 @@ public: bool exportStructSurfaceFromGridCase( std::vector* vertices, std::vector>* structGridVertexIndices ); + void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + protected: QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.cpp b/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.cpp index 166e118bdc..716f16703d 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.cpp +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.cpp @@ -420,3 +420,60 @@ int RimRegularSurface::ny() const { return m_ny; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRegularSurface::originX() const +{ + return m_originX; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRegularSurface::originY() const +{ + return m_originY; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRegularSurface::incrementX() const +{ + return m_incrementX; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRegularSurface::incrementY() const +{ + return m_incrementY; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRegularSurface::rotation() const +{ + return m_rotation; +} + +//-------------------------------------------------------------------------------------------------- +/// Returns the depth values for all grid points in row-major order (index = j * nx + i). +/// Uses the active depth property if set, otherwise returns the fixed depth value. +//-------------------------------------------------------------------------------------------------- +std::vector RimRegularSurface::depthValues() const +{ + const int size = m_nx * m_ny; + + if ( m_depthProperty() != internal::fixedDepth() && !m_depthProperty().isEmpty() ) + { + auto it = m_properties.find( m_depthProperty() ); + if ( it != m_properties.end() ) return it->second; + } + + return std::vector( size, static_cast( m_depth() ) ); +} diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.h b/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.h index 12cd7a478c..9025943d04 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.h +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimRegularSurface.h @@ -42,8 +42,15 @@ public: void setProperty( const QString& key, const std::vector& values ); void setPropertyAsDepth( const QString& key ); - int nx() const; - int ny() const; + int nx() const; + int ny() const; + double originX() const; + double originY() const; + double incrementX() const; + double incrementY() const; + double rotation() const; + + std::vector depthValues() const; protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.cpp b/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.cpp index f915c7b821..15736b61fb 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.cpp +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.cpp @@ -28,6 +28,7 @@ #include "RigStatisticsMath.h" #include "Surface/RigSurface.h" +#include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiDoubleSliderEditor.h" @@ -380,3 +381,15 @@ cvf::BoundingBox RimSurface::boundingBoxInDomainCoords() const return boundingBox; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSurface::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const +{ + menuBuilder << "RicExportSurfaceToGriFeature"; + menuBuilder << "RicExportSurfaceToTsurfFeature"; + menuBuilder << "Separator"; + menuBuilder << "RicCopySurfaceFeature"; + menuBuilder << "RicReloadSurfaceFeature"; +} diff --git a/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.h b/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.h index 4ab981b315..2809087159 100644 --- a/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.h +++ b/ApplicationLibCode/ProjectDataModel/Surfaces/RimSurface.h @@ -81,6 +81,8 @@ public: virtual bool isMeshLinesEnabledDefault() const; + void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + protected: void setSurfaceData( RigSurface* surface ); diff --git a/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.cpp b/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.cpp index 6d95531c1f..5161e9bb6b 100644 --- a/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.cpp +++ b/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.cpp @@ -24,7 +24,67 @@ #include "cvfBoundingBox.h" #include "cvfObject.h" +#include #include +#include + +//-------------------------------------------------------------------------------------------------- +/// Resample the Z values of the given surface onto a regular grid. +/// Returns NaN for grid points outside the mesh. Values are in row-major order: index = j * nx + i. +//-------------------------------------------------------------------------------------------------- +std::vector RigSurfaceResampler::resampleToRegularGrid( RigSurface* surface, + int nx, + int ny, + double originX, + double originY, + double incrementX, + double incrementY, + double rotationDegrees ) +{ + if ( !surface || nx <= 0 || ny <= 0 ) return {}; + + surface->ensureIntersectionSearchTreeIsBuilt(); + + const double angle = rotationDegrees * std::numbers::pi / 180.0; + const double zHigh = 1.0e6; + const double zLow = -1.0e6; + + std::vector depthValues( nx * ny, std::numeric_limits::quiet_NaN() ); + + for ( int j = 0; j < ny; ++j ) + { + for ( int i = 0; i < nx; ++i ) + { + double x, y; + if ( i == 0 && j == 0 ) + { + x = originX; + y = originY; + } + else + { + const double xdist = incrementX * i; + const double ydist = incrementY * j; + const double dist = std::sqrt( xdist * xdist + ydist * ydist ); + const double beta = std::acos( xdist / dist ); + const double gamma = angle + beta; + x = originX + dist * std::cos( gamma ); + y = originY + dist * std::sin( gamma ); + } + + const cvf::Vec3d pointAbove( x, y, zHigh ); + const cvf::Vec3d pointBelow( x, y, zLow ); + + cvf::Vec3d intersectionPoint; + if ( computeIntersectionWithLine( surface, pointAbove, pointBelow, intersectionPoint ) ) + { + depthValues[j * nx + i] = static_cast( intersectionPoint.z() ); + } + } + } + + return depthValues; +} //-------------------------------------------------------------------------------------------------- /// Create a surface with same XY coordinates as targetSurface. Evaluate Z value at [X,Y] in the source surface. Search in XY plane to find diff --git a/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.h b/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.h index a29aa1cb09..875528e771 100644 --- a/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.h +++ b/ApplicationLibCode/ReservoirDataModel/Surface/RigSurfaceResampler.h @@ -32,6 +32,15 @@ public: static bool computeIntersectionWithLine( RigSurface* surface, const cvf::Vec3d& p1, const cvf::Vec3d& p2, cvf::Vec3d& intersectionPoint ); static bool findClosestPointOnSurface( RigSurface* surface, const cvf::Vec3d& p1, const cvf::Vec3d& p2, cvf::Vec3d& intersectionPoint ); + static std::vector resampleToRegularGrid( RigSurface* surface, + int nx, + int ny, + double originX, + double originY, + double incrementX, + double incrementY, + double rotationDegrees ); + static std::vector computeResampledPolyline( const std::vector& polyline, double resamplingDistance ); static std::vector> computeResampledPolylineWithSegmentInfo( const std::vector& polyline, double resamplingDistance ); diff --git a/ApplicationLibCode/UnitTests/RifSurfio-Test.cpp b/ApplicationLibCode/UnitTests/RifSurfio-Test.cpp index 511f26a9dd..8952e0d034 100644 --- a/ApplicationLibCode/UnitTests/RifSurfio-Test.cpp +++ b/ApplicationLibCode/UnitTests/RifSurfio-Test.cpp @@ -2,6 +2,7 @@ #include "RiaTestDataDirectory.h" +#include #include #include @@ -38,3 +39,48 @@ TEST( RifSurfioTest, ImportFromGriFile ) EXPECT_DOUBLE_EQ( regularSurface.rotation, 30.0 ); EXPECT_EQ( regularSurface.ny * regularSurface.nx, values.size() ); } + +//-------------------------------------------------------------------------------------------------- +/// Export a small synthetic grid to a temporary GRI file, re-import it and verify the round-trip +/// preserves all header parameters and depth values. +//-------------------------------------------------------------------------------------------------- +TEST( RifSurfioTest, ExportAndReimportGriRoundTrip ) +{ + RigRegularSurfaceData params; + params.nx = 4; + params.ny = 3; + params.originX = 100.0; + params.originY = 200.0; + params.incrementX = 25.0; + params.incrementY = 25.0; + params.rotation = 0.0; + + // Fill with linearly increasing depth values + std::vector depthValues( params.nx * params.ny ); + for ( int j = 0; j < params.ny; ++j ) + for ( int i = 0; i < params.nx; ++i ) + depthValues[j * params.nx + i] = static_cast( 1000.0 + j * 10.0 + i ); + + std::filesystem::path tmpFile = std::filesystem::temp_directory_path() / "resinsight_test_export.gri"; + bool ok = RifSurfio::exportToGri( tmpFile.string(), params, depthValues ); + ASSERT_TRUE( ok ); + + auto imported = RifSurfio::importSurfaceData( tmpFile.string() ); + ASSERT_TRUE( imported.has_value() ); + + const auto& [surf, vals] = imported.value(); + EXPECT_EQ( surf.nx, params.nx ); + EXPECT_EQ( surf.ny, params.ny ); + EXPECT_DOUBLE_EQ( surf.originX, params.originX ); + EXPECT_DOUBLE_EQ( surf.originY, params.originY ); + EXPECT_DOUBLE_EQ( surf.incrementX, params.incrementX ); + EXPECT_DOUBLE_EQ( surf.incrementY, params.incrementY ); + ASSERT_EQ( vals.size(), depthValues.size() ); + + for ( size_t idx = 0; idx < depthValues.size(); ++idx ) + { + EXPECT_FLOAT_EQ( vals[idx], depthValues[idx] ) << "Mismatch at index " << idx; + } + + std::filesystem::remove( tmpFile ); +} diff --git a/ApplicationLibCode/UnitTests/RigSurfaceResampler-Test.cpp b/ApplicationLibCode/UnitTests/RigSurfaceResampler-Test.cpp index 40460fcb10..73ce5c34be 100644 --- a/ApplicationLibCode/UnitTests/RigSurfaceResampler-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigSurfaceResampler-Test.cpp @@ -18,10 +18,13 @@ #include "gtest/gtest.h" +#include "Surface/RigSurface.h" #include "Surface/RigSurfaceResampler.h" #include "cvfObject.h" +#include + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -80,3 +83,55 @@ TEST( RigSurfaceResamplerTests, flatGeometryLargerSource ) ASSERT_EQ( r3.y(), 1.0 ); ASSERT_EQ( r3.z(), 2.0 ); } + +//-------------------------------------------------------------------------------------------------- +/// A flat surface at z=-100 spanning [0,4]x[0,4]. +/// Resample onto a 3x3 grid with origin (1,1) and increment 1. +/// All 9 sample points lie inside the mesh, so all values should be -100. +//-------------------------------------------------------------------------------------------------- +TEST( RigSurfaceResamplerTests, resampleToRegularGrid_flatSurface ) +{ + // Two triangles covering [0,4] x [0,4] at z = -100 + const double z = -100.0; + std::vector vertices = { cvf::Vec3d( 0.0, 0.0, z ), + cvf::Vec3d( 4.0, 0.0, z ), + cvf::Vec3d( 4.0, 4.0, z ), + cvf::Vec3d( 0.0, 4.0, z ) }; + std::vector indices = { 0, 1, 2, 0, 2, 3 }; + + cvf::ref surface = cvf::make_ref(); + surface->setTriangleData( indices, vertices ); + + // 3x3 grid at origin (1,1), increment 1 → points at x={1,2,3}, y={1,2,3}; all inside mesh + auto values = RigSurfaceResampler::resampleToRegularGrid( surface.p(), 3, 3, 1.0, 1.0, 1.0, 1.0, 0.0 ); + + ASSERT_EQ( values.size(), 9u ); + for ( auto v : values ) + { + EXPECT_FALSE( std::isnan( v ) ); + EXPECT_FLOAT_EQ( v, static_cast( z ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// Grid points outside the mesh extent should be NaN. +//-------------------------------------------------------------------------------------------------- +TEST( RigSurfaceResamplerTests, resampleToRegularGrid_outsidePointsAreNaN ) +{ + // Single triangle in the first quadrant + const double z = -50.0; + std::vector vertices = { cvf::Vec3d( 0.0, 0.0, z ), cvf::Vec3d( 10.0, 0.0, z ), cvf::Vec3d( 0.0, 10.0, z ) }; + std::vector indices = { 0, 1, 2 }; + + cvf::ref surface = cvf::make_ref(); + surface->setTriangleData( indices, vertices ); + + // 3x3 grid with origin at (8,8) — all points are outside the triangle + auto values = RigSurfaceResampler::resampleToRegularGrid( surface.p(), 3, 3, 8.0, 8.0, 1.0, 1.0, 0.0 ); + + ASSERT_EQ( values.size(), 9u ); + for ( auto v : values ) + { + EXPECT_TRUE( std::isnan( v ) ); + } +} diff --git a/ThirdParty/custom-surfio/CMakeLists.txt b/ThirdParty/custom-surfio/CMakeLists.txt index 4aa985c371..70e6776b31 100644 --- a/ThirdParty/custom-surfio/CMakeLists.txt +++ b/ThirdParty/custom-surfio/CMakeLists.txt @@ -32,5 +32,5 @@ target_include_directories( PRIVATE ../mio ) -target_link_libraries(surfio_lib PRIVATE mdspan) +target_link_libraries(surfio_lib PUBLIC mdspan)