///////////////////////////////////////////////////////////////////////////////// // // 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 "RimIjkIntersection.h" #include "RigMainGrid.h" #include "Rim3dView.h" #include "RimEclipseView.h" #include "RivIjkIntersectionPartMgr.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmUiSliderEditor.h" #include "cafPdmUiTreeOrdering.h" #include namespace caf { template <> void AppEnum::setUp() { addItem( RimIjkIntersection::GridAxis::AXIS_I, "AXIS_I", "I" ); addItem( RimIjkIntersection::GridAxis::AXIS_J, "AXIS_J", "J" ); addItem( RimIjkIntersection::GridAxis::AXIS_K, "AXIS_K", "K" ); setDefault( RimIjkIntersection::GridAxis::AXIS_I ); } } // namespace caf CAF_PDM_SOURCE_INIT( RimIjkIntersection, "IjkIntersection" ); //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimIjkIntersection::RimIjkIntersection() { CAF_PDM_InitObject( "Intersection I/J/K", ":/IntersectionBox16x16.png" ); CAF_PDM_InitField( &m_name, "UserDescription", QString( "Intersection I/J/K" ), "Name" ); CAF_PDM_InitFieldNoDefault( &m_axis, "Axis", "Axis" ); CAF_PDM_InitField( &m_useNegativeFace, "UseNegativeFace", false, "Use Back Pillar Face" ); CAF_PDM_InitField( &m_fixedIndex, "FixedIndex", 1, "Index" ); m_fixedIndex.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); CAF_PDM_InitField( &m_iMin, "IMin", 1, "Min" ); CAF_PDM_InitField( &m_iMax, "IMax", 1, "Max" ); CAF_PDM_InitField( &m_jMin, "JMin", 1, "Min" ); CAF_PDM_InitField( &m_jMax, "JMax", 1, "Max" ); CAF_PDM_InitField( &m_kMin, "KMin", 1, "Min" ); CAF_PDM_InitField( &m_kMax, "KMax", 1, "Max" ); for ( caf::PdmField* field : { &m_iMin, &m_iMax, &m_jMin, &m_jMax, &m_kMin, &m_kMax } ) { field->uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); } setDeletable( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimIjkIntersection::~RimIjkIntersection() { } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- caf::PdmFieldHandle* RimIjkIntersection::userDescriptionField() { return &m_name; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimIjkIntersection::name() const { return m_name(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setName( const QString& newName ) { m_name = newName; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimIjkIntersection::GridAxis RimIjkIntersection::axis() const { return m_axis(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- bool RimIjkIntersection::useNegativeFace() const { return m_useNegativeFace(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RimIjkIntersection::fixedIndex() const { return m_fixedIndex() - 1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RigBoundingBoxIjk RimIjkIntersection::ijkRange() const { // Guard against non-positive values from hand-edited project files before converting to unsigned auto zeroBased = []( int oneBasedValue ) { return static_cast( std::max( 1, oneBasedValue ) - 1 ); }; caf::VecIjk0 min( zeroBased( m_iMin() ), zeroBased( m_jMin() ), zeroBased( m_kMin() ) ); caf::VecIjk0 max( zeroBased( m_iMax() ), zeroBased( m_jMax() ), zeroBased( m_kMax() ) ); return RigBoundingBoxIjk( min, max ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setAxis( GridAxis axis ) { m_axis = axis; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setUseNegativeFace( bool useNegativeFace ) { m_useNegativeFace = useNegativeFace; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setFixedIndex( int fixedIndex ) { m_fixedIndex = fixedIndex + 1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setIjkRange( const RigBoundingBoxIjk& range ) { m_iMin = static_cast( range.min().i() ) + 1; m_iMax = static_cast( range.max().i() ) + 1; m_jMin = static_cast( range.min().j() ) + 1; m_jMax = static_cast( range.max().j() ) + 1; m_kMin = static_cast( range.min().k() ) + 1; m_kMax = static_cast( range.max().k() ) + 1; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::setToDefaultValues() { RigMainGrid* grid = mainGrid(); if ( !grid ) return; int ni = static_cast( grid->cellCountI() ); int nj = static_cast( grid->cellCountJ() ); int nk = static_cast( grid->cellCountK() ); m_axis = GridAxis::AXIS_I; m_iMin = 1; m_iMax = ni; m_jMin = 1; m_jMax = nj; m_kMin = 1; m_kMax = nk; m_fixedIndex = ni / 2 + 1; } //-------------------------------------------------------------------------------------------------- /// The I and J axes give a vertical curtain. A K slice is horizontal, and would require the surface /// to be contoured instead of projected along a vertical ray. //-------------------------------------------------------------------------------------------------- bool RimIjkIntersection::supportsSurfaceIntersectionCurves() const { return m_axis() != GridAxis::AXIS_K; } //-------------------------------------------------------------------------------------------------- /// Walk the cells of the index plane along the varying axis and pick the cell face corners. Each /// pillar runs from the top of the first k layer to the bottom of the last k layer, so a tilted /// pillar gives a curve that lies on the curtain rather than on a vertical plane through it. //-------------------------------------------------------------------------------------------------- RimIntersectionCurtain RimIjkIntersection::surfaceCurtain() const { if ( !supportsSurfaceIntersectionCurves() ) return {}; RigMainGrid* grid = mainGrid(); if ( !grid ) return {}; const auto cellRange = clampedCellRange( grid ); if ( !cellRange ) return {}; const bool alongJ = m_axis() == GridAxis::AXIS_I; // Corner indices of the cell face, see the hex vertex numbering in StructGridInterface::cellFaceVertexIndices(). // The first corner is at the start of the varying axis, the second at the end of the same axis. Corners 0-3 are // the low k side of the cell, and adding 4 gives the corner on the same pillar at the high k side. const int cornersPerKSide = 4; int startCorner = 0; int endCorner = 0; if ( alongJ ) { startCorner = m_useNegativeFace() ? 0 : 1; endCorner = m_useNegativeFace() ? 3 : 2; } else { startCorner = m_useNegativeFace() ? 0 : 3; endCorner = m_useNegativeFace() ? 1 : 2; } // The fixed axis is collapsed to a single index by clampedCellRange() const size_t fixedI = cellRange->min().i(); const size_t fixedJ = cellRange->min().j(); const size_t firstK = cellRange->min().k(); const size_t lastK = cellRange->max().k(); const size_t varyingMin = alongJ ? cellRange->min().j() : cellRange->min().i(); const size_t varyingMax = alongJ ? cellRange->max().j() : cellRange->max().i(); RimIntersectionCurtain curtain; // The pillar at one position is spanned between the first and the last cell of the k range. The // top of the pillar is used as the trace, so the curve is resampled along the top of the curtain. auto appendPillar = [&]( size_t i, size_t j, int corner ) { const size_t topCellIndex = grid->cellIndexFromIJK( i, j, firstK ); const size_t bottomCellIndex = grid->cellIndexFromIJK( i, j, lastK ); if ( topCellIndex == cvf::UNDEFINED_SIZE_T || bottomCellIndex == cvf::UNDEFINED_SIZE_T ) return; if ( grid->cell( topCellIndex ).isInvalid() || grid->cell( bottomCellIndex ).isInvalid() ) return; const auto topCorners = grid->cellCornerVertices( topCellIndex ); const auto bottomCorners = grid->cellCornerVertices( bottomCellIndex ); curtain.trace.push_back( topCorners[corner] ); curtain.pillars.emplace_back( topCorners[corner], bottomCorners[corner + cornersPerKSide] ); }; for ( size_t varying = varyingMin; varying <= varyingMax; ++varying ) { const size_t i = alongJ ? fixedI : varying; const size_t j = alongJ ? varying : fixedJ; appendPillar( i, j, startCorner ); if ( varying == varyingMax ) appendPillar( i, j, endCorner ); } return curtain; } //-------------------------------------------------------------------------------------------------- /// The index range of the visible cells, with the fixed axis collapsed to the fixed index //-------------------------------------------------------------------------------------------------- std::optional> RimIjkIntersection::clampedCellRange( const RigMainGrid* grid ) const { if ( !grid ) return {}; const size_t ni = grid->cellCountI(); const size_t nj = grid->cellCountJ(); const size_t nk = grid->cellCountK(); if ( ni == 0 || nj == 0 || nk == 0 ) return {}; const auto range = ijkRange(); caf::VecIjk0 min = range.min(); caf::VecIjk0 max = range.max(); const size_t fixed = static_cast( std::max( 0, fixedIndex() ) ); switch ( m_axis() ) { case GridAxis::AXIS_I: min.x() = max.x() = std::min( fixed, ni - 1 ); break; case GridAxis::AXIS_J: min.y() = max.y() = std::min( fixed, nj - 1 ); break; case GridAxis::AXIS_K: default: min.z() = max.z() = std::min( fixed, nk - 1 ); break; } const RigBoundingBoxIjk gridBounds( caf::VecIjk0::ZERO, caf::VecIjk0( ni - 1, nj - 1, nk - 1 ) ); return RigBoundingBoxIjk( min, max ).clamp( gridBounds ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RivIjkIntersectionPartMgr* RimIjkIntersection::intersectionPartMgr() { if ( m_intersectionPartMgr.isNull() ) m_intersectionPartMgr = new RivIjkIntersectionPartMgr( this ); return m_intersectionPartMgr.p(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::clearGeometry() { m_intersectionPartMgr = nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- const RivIntersectionGeometryGeneratorInterface* RimIjkIntersection::intersectionGeometryGenerator() const { if ( m_intersectionPartMgr.notNull() ) return m_intersectionPartMgr->intersectionGeometryGenerator(); return nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RigMainGrid* RimIjkIntersection::mainGrid() const { auto eclipseView = firstAncestorOrThisOfType(); if ( eclipseView ) return eclipseView->mainGrid(); return nullptr; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- int RimIjkIntersection::axisCellCount() const { RigMainGrid* grid = mainGrid(); if ( !grid ) return 0; switch ( m_axis() ) { case GridAxis::AXIS_I: return static_cast( grid->cellCountI() ); case GridAxis::AXIS_J: return static_cast( grid->cellCountJ() ); case GridAxis::AXIS_K: return static_cast( grid->cellCountK() ); } return 0; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { uiOrdering.add( &m_name ); { caf::PdmUiGroup* group = uiOrdering.addNewGroup( "Options" ); group->add( &m_axis ); group->add( &m_useNegativeFace ); group->add( &m_showInactiveCells ); QString axisLabel = caf::AppEnum::uiText( m_axis() ); m_fixedIndex.uiCapability()->setUiName( axisLabel + " Index" ); group->add( &m_fixedIndex ); } RigMainGrid* grid = mainGrid(); int ni = grid ? static_cast( grid->cellCountI() ) : 0; int nj = grid ? static_cast( grid->cellCountJ() ) : 0; int nk = grid ? static_cast( grid->cellCountK() ) : 0; if ( m_axis() != GridAxis::AXIS_I ) { caf::PdmUiGroup* group = uiOrdering.addNewGroup( QString( "I Range [1 .. %1]" ).arg( std::max( 1, ni ) ) ); group->add( &m_iMin ); group->add( &m_iMax ); } if ( m_axis() != GridAxis::AXIS_J ) { caf::PdmUiGroup* group = uiOrdering.addNewGroup( QString( "J Range [1 .. %1]" ).arg( std::max( 1, nj ) ) ); group->add( &m_jMin ); group->add( &m_jMax ); } if ( m_axis() != GridAxis::AXIS_K ) { caf::PdmUiGroup* group = uiOrdering.addNewGroup( QString( "K Range [1 .. %1]" ).arg( std::max( 1, nk ) ) ); group->add( &m_kMin ); group->add( &m_kMax ); } defineSeparateDataSourceUi( uiConfigName, uiOrdering ); if ( grid ) { auto setRange = []( caf::PdmFieldHandle& field, int maximum ) { caf::PdmUiSliderEditor::setRange( field.uiCapability(), 1, std::max( 1, maximum ) ); }; setRange( m_fixedIndex, axisCellCount() ); setRange( m_iMin, ni ); setRange( m_iMax, ni ); setRange( m_jMin, nj ); setRange( m_jMax, nj ); setRange( m_kMin, nk ); setRange( m_kMax, nk ); } else { for ( auto* field : { &m_fixedIndex, &m_iMin, &m_iMax, &m_jMin, &m_jMax, &m_kMin, &m_kMax } ) { caf::PdmUiSliderEditor::clearRange( field->uiCapability() ); } } uiOrdering.skipRemainingFields( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { appendCommonMenuItems( menuBuilder ); menuBuilder << "RicCopyIntersectionsToAllViewsInCaseFeature"; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName ) { appendSurfaceIntersectionsToTreeOrdering( uiTreeOrdering ); uiTreeOrdering.skipRemainingChildren( true ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { RigMainGrid* grid = mainGrid(); if ( grid ) { int ni = static_cast( grid->cellCountI() ); int nj = static_cast( grid->cellCountJ() ); int nk = static_cast( grid->cellCountK() ); auto clamp = []( caf::PdmField& f, int hi ) { f = std::max( 1, std::min( f(), hi ) ); }; clamp( m_iMin, ni ); clamp( m_iMax, ni ); clamp( m_jMin, nj ); clamp( m_jMax, nj ); clamp( m_kMin, nk ); clamp( m_kMax, nk ); clamp( m_fixedIndex, std::max( 1, axisCellCount() ) ); // Keep each min/max pair ordered by limiting the changed field to the other one if ( changedField == &m_iMin ) m_iMin = std::min( m_iMin(), m_iMax() ); if ( changedField == &m_iMax ) m_iMax = std::max( m_iMin(), m_iMax() ); if ( changedField == &m_jMin ) m_jMin = std::min( m_jMin(), m_jMax() ); if ( changedField == &m_jMax ) m_jMax = std::max( m_jMin(), m_jMax() ); if ( changedField == &m_kMin ) m_kMin = std::min( m_kMin(), m_kMax() ); if ( changedField == &m_kMax ) m_kMax = std::max( m_kMin(), m_kMax() ); } if ( changedField == &m_axis ) { updateConnectedEditors(); } if ( changedField != &m_name ) { rebuildGeometryAndScheduleCreateDisplayModel(); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimIjkIntersection::rebuildGeometryAndScheduleCreateDisplayModel() { m_intersectionPartMgr = nullptr; auto rimView = firstAncestorOrThisOfType(); if ( rimView ) { rimView->scheduleCreateDisplayModelAndRedraw(); } }