diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp index d35b823615..c2ba2b2e27 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp @@ -42,6 +42,7 @@ #include "RimProject.h" #include "RigActiveCellInfo.h" +#include "RigBoundingBoxIjk.h" #include "RigEclipseCaseData.h" #include "RigEclipseCaseDataTools.h" #include "RigEclipseResultTools.h" @@ -65,6 +66,7 @@ #include "opm/input/eclipse/Deck/DeckRecord.hpp" #include "opm/input/eclipse/Parser/ParserKeyword.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/C.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/E.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/O.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/S.hpp" #include "opm/input/eclipse/Parser/ParserKeywords/W.hpp" @@ -342,6 +344,11 @@ std::expected RicExportEclipseSectorModelFeature::exportSimulatio return result; } + if ( auto result = replaceEqualsKeywordIndices( &eclipseCase, exportSettings, deckFile ); !result ) + { + return result; + } + if ( auto result = addBorderBoundaryConditions( &eclipseCase, exportSettings, deckFile ); !result ) { return result; @@ -566,6 +573,72 @@ std::expected RicExportEclipseSectorModelFeature::addBorderBounda return {}; } +std::expected RicExportEclipseSectorModelFeature::replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, + const RicExportEclipseSectorModelUi& exportSettings, + RifOpmFlowDeckFile& deckFile ) +{ + auto keywords = deckFile.keywords( false ); + + for ( const auto& keyword : keywords ) + { + // Handle EQUALS keyword specially - it has IJK coordinates that need transformation + if ( keyword == "EQUALS" ) + { + auto equalsKeywordsWithIndices = deckFile.findAllKeywordsWithIndices( keyword ); + if ( !equalsKeywordsWithIndices.empty() ) + { + RiaLogging::info( QString( "Processing %1 occurrence(s) of EQUALS keyword" ).arg( equalsKeywordsWithIndices.size() ) ); + + // Process in reverse order so indices remain valid after modifications + for ( auto it = equalsKeywordsWithIndices.rbegin(); it != equalsKeywordsWithIndices.rend(); ++it ) + { + const Opm::FileDeck::Index& index = it->first; + const Opm::DeckKeyword& kw = it->second; + + // Create new keyword with transformed coordinates + Opm::DeckKeyword transformedKeyword( kw.location(), kw.name() ); + + try + { + for ( size_t recordIdx = 0; recordIdx < kw.size(); ++recordIdx ) + { + const auto& record = kw.getRecord( recordIdx ); + auto result = processEqualsRecord( record, exportSettings ); + + if ( result ) + { + transformedKeyword.addRecord( std::move( result.value() ) ); + } + else + { + RiaLogging::warning( QString( "Failed to process EQUALS record: %1" ).arg( result.error() ) ); + } + } + + // Replace with transformed keyword + if ( transformedKeyword.size() > 0 ) + { + RiaLogging::info( QString( "Got %1 keywords." ).arg( transformedKeyword.size() ) ); + deckFile.replaceKeywordAtIndex( index, transformedKeyword ); + } + else + { + // If all records failed, remove the keyword + deckFile.replaceKeywordAtIndex( index, Opm::DeckKeyword( kw.location(), "SKIP" ) ); + } + } + catch ( std::exception& e ) + { + return std::unexpected( QString( "Exception processing EQUALS keyword: %1" ).arg( e.what() ) ); + } + } + } + } + } + + return {}; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -987,6 +1060,106 @@ std::expected return Opm::DeckRecord{ std::move( items ) }; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected + RicExportEclipseSectorModelFeature::processEqualsRecord( const Opm::DeckRecord& record, const RicExportEclipseSectorModelUi& exportSettings ) +{ + // EQUALS format: FIELD VALUE I1 I2 J1 J2 K1 K2 + // Items: 0=FIELD, 1=VALUE, 2=I1, 3=I2, 4=J1, 5=J2, 6=K1, 7=K2 + if ( record.size() < 8 ) + { + return std::unexpected( QString( "EQUALS record has insufficient items (expected at least 8, got %1)" ).arg( record.size() ) ); + } + + std::vector items; + + // Copy field name and value (first two items) + items.push_back( record.getItem( 0 ) ); + items.push_back( record.getItem( 1 ) ); + + // Transform IJK box coordinates (items 2-7: I1, I2, J1, J2, K1, K2) + // Note: EQUALS uses 1-based Eclipse coordinates + int origI1 = record.getItem( 2 ).get( 0 ) - 1; // Convert to 0-based + int origI2 = record.getItem( 3 ).get( 0 ) - 1; + int origJ1 = record.getItem( 4 ).get( 0 ) - 1; + int origJ2 = record.getItem( 5 ).get( 0 ) - 1; + int origK1 = record.getItem( 6 ).get( 0 ) - 1; + int origK2 = record.getItem( 7 ).get( 0 ) - 1; + + // Create bounding boxes + RigBoundingBoxIjk equalsBox( cvf::Vec3st( origI1, origJ1, origK1 ), cvf::Vec3st( origI2, origJ2, origK2 ) ); + RigBoundingBoxIjk sectorBox( exportSettings.min(), exportSettings.max() - cvf::Vec3st( 1, 1, 1 ) ); + + // Check if boxes overlap and get intersection + auto intersection = equalsBox.intersection( sectorBox ); + if ( !intersection ) + { + // No overlap with sector - skip this record + QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); + RiaLogging::info( QString( "EQUALS record for field '%1' [%2-%3, %4-%5, %6-%7] does not overlap with sector - skipping" ) + .arg( fieldName ) + .arg( origI1 + 1 ) + .arg( origI2 + 1 ) + .arg( origJ1 + 1 ) + .arg( origJ2 + 1 ) + .arg( origK1 + 1 ) + .arg( origK2 + 1 ) ); + return std::unexpected( "EQUALS record does not overlap with sector" ); + } + + // Get the clamped coordinates from the intersection + cvf::Vec3st corner1 = intersection->min(); + cvf::Vec3st corner2 = intersection->max(); + + // Log if clamping occurred (partial overlap) + if ( origI1 != static_cast( corner1.x() ) || origI2 != static_cast( corner2.x() ) || origJ1 != static_cast( corner1.y() ) || + origJ2 != static_cast( corner2.y() ) || origK1 != static_cast( corner1.z() ) || origK2 != static_cast( corner2.z() ) ) + { + QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); + RiaLogging::info( + QString( "EQUALS record for field '%1' partially overlaps sector, clamped from [%2-%3, %4-%5, %6-%7] to [%8-%9, %10-%11, " + "%12-%13]" ) + .arg( fieldName ) + .arg( origI1 + 1 ) + .arg( origI2 + 1 ) + .arg( origJ1 + 1 ) + .arg( origJ2 + 1 ) + .arg( origK1 + 1 ) + .arg( origK2 + 1 ) + .arg( corner1.x() + 1 ) + .arg( corner2.x() + 1 ) + .arg( corner1.y() + 1 ) + .arg( corner2.y() + 1 ) + .arg( corner1.z() + 1 ) + .arg( corner2.z() + 1 ) ); + } + + auto transformResult1 = transformIjkToSectorCoordinates( corner1, exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); + auto transformResult2 = transformIjkToSectorCoordinates( corner2, exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); + + if ( !transformResult1 ) + { + return std::unexpected( QString( "EQUALS I1,J1,K1 coordinate is out of sector bounds: %1" ).arg( transformResult1.error() ) ); + } + + if ( !transformResult2 ) + { + return std::unexpected( QString( "EQUALS I2,J2,K2 coordinate is out of sector bounds: %1" ).arg( transformResult2.error() ) ); + } + + using E = Opm::ParserKeywords::EQUALS; + items.push_back( RifOpmDeckTools::item( E::I1::itemName, static_cast( transformResult1->x() ) ) ); + items.push_back( RifOpmDeckTools::item( E::I2::itemName, static_cast( transformResult2->x() ) ) ); + items.push_back( RifOpmDeckTools::item( E::J1::itemName, static_cast( transformResult1->y() ) ) ); + items.push_back( RifOpmDeckTools::item( E::J2::itemName, static_cast( transformResult2->y() ) ) ); + items.push_back( RifOpmDeckTools::item( E::K1::itemName, static_cast( transformResult1->z() ) ) ); + items.push_back( RifOpmDeckTools::item( E::K2::itemName, static_cast( transformResult2->z() ) ) ); + + return Opm::DeckRecord{ std::move( items ) }; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h index 7e5a290b24..fb75450136 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h @@ -89,6 +89,10 @@ private: RifOpmFlowDeckFile& deckFile, int operNumRegion ); + static std::expected replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, + const RicExportEclipseSectorModelUi& exportSettings, + RifOpmFlowDeckFile& deckFile ); + static std::vector findIntersectingWells( RimEclipseCase* eclipseCase, const cvf::Vec3st& min, const cvf::Vec3st& max ); static std::expected transformIjkToSectorCoordinates( const cvf::Vec3st& originalIjk, @@ -108,4 +112,7 @@ private: const std::string& wellName, bool isWellNameRecord, const RicExportEclipseSectorModelUi& exportSettings ); + + static std::expected processEqualsRecord( const Opm::DeckRecord& record, + const RicExportEclipseSectorModelUi& exportSettings ); }; diff --git a/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake b/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake index 5f0e5f46c6..f332ee80e8 100644 --- a/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake +++ b/ApplicationLibCode/ReservoirDataModel/CMakeLists_files.cmake @@ -7,6 +7,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RigTypeSafeIndex.h ${CMAKE_CURRENT_LIST_DIR}/RigAllanDiagramData.h ${CMAKE_CURRENT_LIST_DIR}/RigBasicPlane.h + ${CMAKE_CURRENT_LIST_DIR}/RigBoundingBoxIjk.h ${CMAKE_CURRENT_LIST_DIR}/RigCaseCellResultCalculator.h ${CMAKE_CURRENT_LIST_DIR}/RigCaseCellResultsData.h ${CMAKE_CURRENT_LIST_DIR}/RigCaseRealizationParameters.h @@ -93,6 +94,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RigActiveCellInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/RigActiveCellLocalGrid.cpp ${CMAKE_CURRENT_LIST_DIR}/RigAllanDiagramData.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigBoundingBoxIjk.cpp ${CMAKE_CURRENT_LIST_DIR}/RigBasicPlane.cpp ${CMAKE_CURRENT_LIST_DIR}/RigCaseCellResultCalculator.cpp ${CMAKE_CURRENT_LIST_DIR}/RigCaseCellResultsData.cpp diff --git a/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.cpp b/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.cpp new file mode 100644 index 0000000000..ed21a4f389 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.cpp @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- 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 "RigBoundingBoxIjk.h" + +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigBoundingBoxIjk::RigBoundingBoxIjk() + : m_min( cvf::Vec3st::ZERO ) + , m_max( cvf::Vec3st::ZERO ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigBoundingBoxIjk::RigBoundingBoxIjk( const cvf::Vec3st& min, const cvf::Vec3st& max ) + : m_min( min ) + , m_max( max ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RigBoundingBoxIjk::isValid() const +{ + return m_min.x() <= m_max.x() && m_min.y() <= m_max.y() && m_min.z() <= m_max.z(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RigBoundingBoxIjk::overlaps( const RigBoundingBoxIjk& other ) const +{ + // Two boxes overlap if they overlap in all three dimensions + // No overlap if: this.max < other.min OR this.min > other.max (for any dimension) + bool overlapsI = m_max.x() >= other.m_min.x() && m_min.x() <= other.m_max.x(); + bool overlapsJ = m_max.y() >= other.m_min.y() && m_min.y() <= other.m_max.y(); + bool overlapsK = m_max.z() >= other.m_min.z() && m_min.z() <= other.m_max.z(); + + return overlapsI && overlapsJ && overlapsK; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RigBoundingBoxIjk::intersection( const RigBoundingBoxIjk& other ) const +{ + if ( !overlaps( other ) ) + { + return std::nullopt; + } + + // Compute intersection box + cvf::Vec3st intersectMin( std::max( m_min.x(), other.m_min.x() ), + std::max( m_min.y(), other.m_min.y() ), + std::max( m_min.z(), other.m_min.z() ) ); + + cvf::Vec3st intersectMax( std::min( m_max.x(), other.m_max.x() ), + std::min( m_max.y(), other.m_max.y() ), + std::min( m_max.z(), other.m_max.z() ) ); + + return RigBoundingBoxIjk( intersectMin, intersectMax ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RigBoundingBoxIjk::clamp( const RigBoundingBoxIjk& bounds ) const +{ + // Clamp is the same as intersection for bounding boxes + return intersection( bounds ); +} diff --git a/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.h b/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.h new file mode 100644 index 0000000000..552381f2c7 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/RigBoundingBoxIjk.h @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- 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 "cvfVector3.h" + +#include + +//================================================================================================== +/// +/// Value type representing an axis-aligned bounding box in IJK (integer) grid coordinates +/// +//================================================================================================== +class RigBoundingBoxIjk +{ +public: + RigBoundingBoxIjk(); + RigBoundingBoxIjk( const cvf::Vec3st& min, const cvf::Vec3st& max ); + + const cvf::Vec3st& min() const { return m_min; } + const cvf::Vec3st& max() const { return m_max; } + + bool isValid() const; + + // Check if this box overlaps with another box + bool overlaps( const RigBoundingBoxIjk& other ) const; + + // Compute intersection of this box with another box + // Returns std::nullopt if boxes don't overlap + std::optional intersection( const RigBoundingBoxIjk& other ) const; + + // Clamp this box to be within another box + // Returns std::nullopt if boxes don't overlap + std::optional clamp( const RigBoundingBoxIjk& bounds ) const; + +private: + cvf::Vec3st m_min; + cvf::Vec3st m_max; +}; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 9f40d7f323..10477a5eae 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -8,6 +8,7 @@ endif(MSVC) set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/cvfGeometryTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/Ert-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigBoundingBoxIjk-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifcCommandCore-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEclipseInputFileTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifOpmFlowDeckFile-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RigBoundingBoxIjk-Test.cpp b/ApplicationLibCode/UnitTests/RigBoundingBoxIjk-Test.cpp new file mode 100644 index 0000000000..a9a40a41db --- /dev/null +++ b/ApplicationLibCode/UnitTests/RigBoundingBoxIjk-Test.cpp @@ -0,0 +1,181 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- 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 "gtest/gtest.h" + +#include "RigBoundingBoxIjk.h" + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, BasicConstruction ) +{ + // Default constructor + RigBoundingBoxIjk defaultBox; + EXPECT_TRUE( defaultBox.isValid() ); // Min and max are both (0,0,0) + EXPECT_EQ( cvf::Vec3st::ZERO, defaultBox.min() ); + EXPECT_EQ( cvf::Vec3st::ZERO, defaultBox.max() ); + + // Constructor with min and max + RigBoundingBoxIjk box( cvf::Vec3st( 1, 2, 3 ), cvf::Vec3st( 5, 6, 7 ) ); + EXPECT_TRUE( box.isValid() ); + EXPECT_EQ( cvf::Vec3st( 1, 2, 3 ), box.min() ); + EXPECT_EQ( cvf::Vec3st( 5, 6, 7 ), box.max() ); + + // Invalid box (min > max) + RigBoundingBoxIjk invalidBox( cvf::Vec3st( 10, 10, 10 ), cvf::Vec3st( 5, 5, 5 ) ); + EXPECT_FALSE( invalidBox.isValid() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, IsValid ) +{ + // Valid boxes + EXPECT_TRUE( RigBoundingBoxIjk( cvf::Vec3st( 0, 0, 0 ), cvf::Vec3st( 10, 10, 10 ) ).isValid() ); + EXPECT_TRUE( RigBoundingBoxIjk( cvf::Vec3st( 5, 5, 5 ), cvf::Vec3st( 5, 5, 5 ) ).isValid() ); // Single point + + // Invalid boxes (min > max in at least one dimension) + EXPECT_FALSE( RigBoundingBoxIjk( cvf::Vec3st( 10, 0, 0 ), cvf::Vec3st( 5, 10, 10 ) ).isValid() ); // X invalid + EXPECT_FALSE( RigBoundingBoxIjk( cvf::Vec3st( 0, 10, 0 ), cvf::Vec3st( 10, 5, 10 ) ).isValid() ); // Y invalid + EXPECT_FALSE( RigBoundingBoxIjk( cvf::Vec3st( 0, 0, 10 ), cvf::Vec3st( 10, 10, 5 ) ).isValid() ); // Z invalid + EXPECT_FALSE( RigBoundingBoxIjk( cvf::Vec3st( 10, 10, 10 ), cvf::Vec3st( 5, 5, 5 ) ).isValid() ); // All invalid +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, OverlapDetection ) +{ + RigBoundingBoxIjk box1( cvf::Vec3st( 0, 0, 0 ), cvf::Vec3st( 10, 10, 10 ) ); + + // Same box overlaps with itself + EXPECT_TRUE( box1.overlaps( box1 ) ); + + // Completely inside + RigBoundingBoxIjk box2( cvf::Vec3st( 2, 2, 2 ), cvf::Vec3st( 8, 8, 8 ) ); + EXPECT_TRUE( box1.overlaps( box2 ) ); + EXPECT_TRUE( box2.overlaps( box1 ) ); // Symmetric + + // Partially overlapping + RigBoundingBoxIjk box3( cvf::Vec3st( 5, 5, 5 ), cvf::Vec3st( 15, 15, 15 ) ); + EXPECT_TRUE( box1.overlaps( box3 ) ); + EXPECT_TRUE( box3.overlaps( box1 ) ); + + // Touching at edge (inclusive bounds - should overlap) + RigBoundingBoxIjk box4( cvf::Vec3st( 10, 10, 10 ), cvf::Vec3st( 20, 20, 20 ) ); + EXPECT_TRUE( box1.overlaps( box4 ) ); + EXPECT_TRUE( box4.overlaps( box1 ) ); + + // Completely separate (no overlap) + RigBoundingBoxIjk box5( cvf::Vec3st( 20, 20, 20 ), cvf::Vec3st( 30, 30, 30 ) ); + EXPECT_FALSE( box1.overlaps( box5 ) ); + EXPECT_FALSE( box5.overlaps( box1 ) ); + + // Overlap in 2D but not 3D (separated in Z) + RigBoundingBoxIjk box6( cvf::Vec3st( 5, 5, 20 ), cvf::Vec3st( 15, 15, 30 ) ); + EXPECT_FALSE( box1.overlaps( box6 ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, Intersection ) +{ + RigBoundingBoxIjk box1( cvf::Vec3st( 0, 0, 0 ), cvf::Vec3st( 10, 10, 10 ) ); + + // No overlap returns nullopt + RigBoundingBoxIjk box2( cvf::Vec3st( 20, 20, 20 ), cvf::Vec3st( 30, 30, 30 ) ); + auto result1 = box1.intersection( box2 ); + EXPECT_FALSE( result1.has_value() ); + + // Partial overlap + RigBoundingBoxIjk box3( cvf::Vec3st( 5, 5, 5 ), cvf::Vec3st( 15, 15, 15 ) ); + auto result2 = box1.intersection( box3 ); + EXPECT_TRUE( result2.has_value() ); + EXPECT_EQ( cvf::Vec3st( 5, 5, 5 ), result2->min() ); + EXPECT_EQ( cvf::Vec3st( 10, 10, 10 ), result2->max() ); + + // Complete containment (box4 inside box1) + RigBoundingBoxIjk box4( cvf::Vec3st( 2, 2, 2 ), cvf::Vec3st( 8, 8, 8 ) ); + auto result3 = box1.intersection( box4 ); + EXPECT_TRUE( result3.has_value() ); + EXPECT_EQ( cvf::Vec3st( 2, 2, 2 ), result3->min() ); + EXPECT_EQ( cvf::Vec3st( 8, 8, 8 ), result3->max() ); + + // Same box + auto result4 = box1.intersection( box1 ); + EXPECT_TRUE( result4.has_value() ); + EXPECT_EQ( cvf::Vec3st( 0, 0, 0 ), result4->min() ); + EXPECT_EQ( cvf::Vec3st( 10, 10, 10 ), result4->max() ); + + // Single cell intersection + RigBoundingBoxIjk box5( cvf::Vec3st( 10, 10, 10 ), cvf::Vec3st( 20, 20, 20 ) ); + auto result5 = box1.intersection( box5 ); + EXPECT_TRUE( result5.has_value() ); + EXPECT_EQ( cvf::Vec3st( 10, 10, 10 ), result5->min() ); + EXPECT_EQ( cvf::Vec3st( 10, 10, 10 ), result5->max() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, Clamp ) +{ + RigBoundingBoxIjk bounds( cvf::Vec3st( 0, 0, 0 ), cvf::Vec3st( 10, 10, 10 ) ); + + // Box completely inside bounds + RigBoundingBoxIjk box1( cvf::Vec3st( 2, 2, 2 ), cvf::Vec3st( 8, 8, 8 ) ); + auto result1 = box1.clamp( bounds ); + EXPECT_TRUE( result1.has_value() ); + EXPECT_EQ( cvf::Vec3st( 2, 2, 2 ), result1->min() ); + EXPECT_EQ( cvf::Vec3st( 8, 8, 8 ), result1->max() ); + + // Box partially outside bounds - should be clamped + RigBoundingBoxIjk box2( cvf::Vec3st( 5, 5, 5 ), cvf::Vec3st( 15, 15, 15 ) ); + auto result2 = box2.clamp( bounds ); + EXPECT_TRUE( result2.has_value() ); + EXPECT_EQ( cvf::Vec3st( 5, 5, 5 ), result2->min() ); + EXPECT_EQ( cvf::Vec3st( 10, 10, 10 ), result2->max() ); + + // Box completely outside bounds + RigBoundingBoxIjk box3( cvf::Vec3st( 20, 20, 20 ), cvf::Vec3st( 30, 30, 30 ) ); + auto result3 = box3.clamp( bounds ); + EXPECT_FALSE( result3.has_value() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RigBoundingBoxIjk, ClampBehavesLikeIntersection ) +{ + // Verify that clamp() and intersection() behave identically + RigBoundingBoxIjk bounds( cvf::Vec3st( 0, 0, 0 ), cvf::Vec3st( 10, 10, 10 ) ); + RigBoundingBoxIjk box( cvf::Vec3st( 5, 5, 5 ), cvf::Vec3st( 15, 15, 15 ) ); + + auto clampResult = box.clamp( bounds ); + auto intersectionResult = box.intersection( bounds ); + + EXPECT_EQ( clampResult.has_value(), intersectionResult.has_value() ); + if ( clampResult.has_value() && intersectionResult.has_value() ) + { + EXPECT_EQ( clampResult->min(), intersectionResult->min() ); + EXPECT_EQ( clampResult->max(), intersectionResult->max() ); + } +}