Files
ResInsight/ApplicationLibCode/ModelVisualization/RivAnnotationTools.cpp
T
Magne Sjaastad 10afb45933 Show measured depth labels on the well path pipe surface
Labels are anchored at the pipe surface point closest to the camera and offset perpendicular to the pipe on screen. Also account for well paths starting at non-zero measured depth.
2026-11-10 15:38:08 +01:00

595 lines
27 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2023 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RivAnnotationTools.h"
#include "RiaColorTools.h"
#include "RiaFontCache.h"
#include "RivAnnotationSourceInfo.h"
#include "RivObjectSourceInfo.h"
#include "RivPartPriority.h"
#include "RivPipeSurfaceLabelSourceInfo.h"
#include "RivPolylineGenerator.h"
#include "cafEffectGenerator.h"
#include "cvfCamera.h"
#include "cvfDrawableGeo.h"
#include "cvfDrawableText.h"
#include "cvfModelBasicList.h"
#include "cvfPart.h"
#include "cvfRenderStateDepth.h"
#include "cvfRenderStatePoint.h"
#include "cvfViewport.h"
#include <algorithm>
#include <cmath>
#include <optional>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RivAnnotationTools::RivAnnotationTools()
: m_overrideStrategy( RivAnnotationTools::LabelPositionStrategy::UNDEFINED )
, m_labelCountHint( 5 )
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RivAnnotationTools::setOverrideLabelPositionStrategy( LabelPositionStrategy strategy )
{
// By default, each annotation object has a label position strategy. Use this method to override the default.
m_overrideStrategy = strategy;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RivAnnotationTools::setCountHint( int countHint )
{
m_labelCountHint = countHint;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::Part> RivAnnotationTools::createPartFromPolyline( const cvf::Color3f& color, const std::vector<cvf::Vec3d>& polyLine )
{
cvf::ref<cvf::DrawableGeo> drawableGeo = RivPolylineGenerator::createLineAlongPolylineDrawable( polyLine );
if ( drawableGeo.isNull() ) return nullptr;
cvf::ref<cvf::Part> part = new cvf::Part;
part->setDrawable( drawableGeo.p() );
caf::MeshEffectGenerator colorEffgen( color );
cvf::ref<cvf::Effect> eff = colorEffgen.generateCachedEffect();
part->setEffect( eff.p() );
part->setPriority( RivPartPriority::PartType::MeshLines );
return part;
}
namespace
{
struct LabelTextAndPosition
{
std::string label;
cvf::Vec3d labelPosition;
cvf::Vec3d lineAnchorPosition;
};
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
auto computeScalingFactorFromZoom = []( const cvf::Camera* camera ) -> double
{
double scalingFactor = 1.0;
if ( camera )
{
auto viewPort = camera->viewport();
cvf::Vec3d vpCorner1( 0, viewPort->height(), 0 );
cvf::Vec3d vpCorner2( viewPort->width(), 0, 0 );
bool unprojOk = true;
cvf::Vec3d corner1Display, corner2Display, e1;
unprojOk &= camera->unproject( vpCorner1, &corner1Display );
unprojOk &= camera->unproject( vpCorner2, &corner2Display );
if ( unprojOk )
{
scalingFactor = 10 * ( corner1Display - corner2Display ).length();
}
}
return scalingFactor;
};
//--------------------------------------------------------------------------------------------------
/// cvf::Camera::project() divides by the homogeneous w-coordinate, and coordinates behind the camera are mirrored into the viewport. Reject
/// these coordinates before screen space coordinates are compared.
//--------------------------------------------------------------------------------------------------
auto isInFrontOfNearPlane = []( const cvf::Camera* camera, const cvf::Vec3d& displayCoord ) -> bool
{
const double distanceAlongViewDir = ( displayCoord - camera->position() ) * camera->direction();
return distanceAlongViewDir > camera->nearPlane();
};
//--------------------------------------------------------------------------------------------------
/// Move the label towards the camera to make sure the label is drawn in front of other geometry. The scaling factor is derived from the
/// zoom level, and is not related to the distance between the camera and the anchor point. If the label is moved past the near plane, the
/// label is silently discarded by the text renderer. Limit the offset to keep the label inside the view frustum.
//--------------------------------------------------------------------------------------------------
auto computeLabelPosition = []( const cvf::Camera* camera, const cvf::Vec3d& anchorPosition, const double anchorLineScalingFactor ) -> cvf::Vec3d
{
const double distanceAlongViewDir = ( anchorPosition - camera->position() ) * camera->direction();
// Move the label at most half the way towards the camera. The label parts are part of the scene, and are used to compute the near and
// far clipping planes. Deriving the limit from the anchor point and not from the near plane avoids a feedback loop between the label
// positions and the clipping planes.
const double maxOffsetFromAnchorPoint = 0.5 * distanceAlongViewDir;
// If the near plane is close to the anchor point, the limit above is not sufficient to keep the label inside the frustum
const double maxOffsetFromNearPlane = distanceAlongViewDir - 1.1 * camera->nearPlane();
const double offset = std::max( 0.0, std::min( { anchorLineScalingFactor, maxOffsetFromAnchorPoint, maxOffsetFromNearPlane } ) );
const cvf::Vec3d directionPointToCam = ( camera->position() - anchorPosition ).getNormalized();
return anchorPosition + directionPointToCam * offset;
};
//--------------------------------------------------------------------------------------------------
/// Project candidate coordinates to screen space, and compare with the normalized viewport position. Create a label item for the closest
/// coordinate.
//--------------------------------------------------------------------------------------------------
auto createLabelForClosestCoordinate = []( const cvf::Camera* camera,
const RivAnnotationSourceInfo* annotationObject,
const double viewportWidth,
const double viewportHeight,
const double normalizedXPosition,
const double anchorLineScalingFactor ) -> std::optional<LabelTextAndPosition>
{
if ( !camera || !annotationObject ) return std::nullopt;
cvf::Vec3d anchorPosition;
double smallestDistance = std::numeric_limits<double>::max();
// Prefer coordinates inside the viewport, as a label attached to a coordinate outside the viewport can be positioned outside the
// visible area
cvf::Vec3d anchorPositionInViewport;
double smallestDistanceInViewport = std::numeric_limits<double>::max();
for ( const auto& displayCoord : annotationObject->anchorPointsInDisplayCoords() )
{
if ( !isInFrontOfNearPlane( camera, displayCoord ) ) continue;
cvf::Vec3d screenCoord;
if ( !camera->project( displayCoord, &screenCoord ) ) continue;
double horizontalDistance = std::fabs( normalizedXPosition * viewportWidth - screenCoord.x() );
if ( horizontalDistance < smallestDistance )
{
smallestDistance = horizontalDistance;
anchorPosition = displayCoord;
}
if ( screenCoord.y() > 0 && screenCoord.y() < viewportHeight && horizontalDistance < smallestDistanceInViewport )
{
smallestDistanceInViewport = horizontalDistance;
anchorPositionInViewport = displayCoord;
}
}
if ( smallestDistanceInViewport != std::numeric_limits<double>::max() )
{
smallestDistance = smallestDistanceInViewport;
anchorPosition = anchorPositionInViewport;
}
if ( smallestDistance == std::numeric_limits<double>::max() ) return std::nullopt;
cvf::Vec3d labelPosition = computeLabelPosition( camera, anchorPosition, anchorLineScalingFactor );
const double maxScreenSpaceAdjustment = viewportWidth * 0.05;
if ( smallestDistance < maxScreenSpaceAdjustment )
{
// Establish a fixed horizontal anchor point for the label in screen coordinates. Achieved through conversion to screen coordinates,
// adjusting the x-coordinate, and then reverting to display coordinates.
cvf::Vec3d screenCoord;
if ( camera->project( labelPosition, &screenCoord ) )
{
screenCoord.x() = normalizedXPosition * viewportWidth;
camera->unproject( screenCoord, &labelPosition );
}
}
LabelTextAndPosition info = { annotationObject->text(), anchorPosition, labelPosition };
return info;
};
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
auto createMultipleLabels = []( const cvf::Camera* camera,
const RivAnnotationSourceInfo* annotationObject,
const double viewportWidth,
const double viewportHeight,
const double anchorLineScalingFactor ) -> std::vector<LabelTextAndPosition>
{
if ( !annotationObject || annotationObject->texts().empty() ) return {};
std::vector<LabelTextAndPosition> labelInfo;
std::vector<cvf::Vec3d> labelCoords;
std::vector<std::string> labelTexts;
const auto candidateCoords = annotationObject->anchorPointsInDisplayCoords();
const auto candidateLabels = annotationObject->texts();
if ( candidateCoords.size() == candidateLabels.size() )
{
for ( size_t i = 0; i < annotationObject->anchorPointsInDisplayCoords().size(); i++ )
{
const auto& displayCoord = candidateCoords[i];
if ( !isInFrontOfNearPlane( camera, displayCoord ) ) continue;
cvf::Vec3d screenCoord;
if ( !camera->project( displayCoord, &screenCoord ) ) continue;
if ( screenCoord.x() > 0 && screenCoord.x() < viewportWidth && screenCoord.y() > 0 && screenCoord.y() < viewportHeight )
{
const auto& text = candidateLabels[i];
labelCoords.push_back( displayCoord );
labelTexts.push_back( text );
}
}
for ( size_t i = 0; i < labelCoords.size(); i++ )
{
const cvf::Vec3d lineAnchorPosition = labelCoords[i];
const cvf::Vec3d labelPosition = computeLabelPosition( camera, lineAnchorPosition, anchorLineScalingFactor );
labelInfo.push_back( { labelTexts[i], lineAnchorPosition, labelPosition } );
}
}
return labelInfo;
};
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void addPipeSurfaceLabels( const RivPipeSurfaceLabelSourceInfo* labelSource, const cvf::Camera* camera, cvf::ModelBasicList* model )
{
if ( !labelSource || !camera || !model || labelSource->labels().empty() ) return;
// Distance in pixels between the anchor point on the pipe surface and the label
const double labelOffsetInPixels = 20.0;
const auto backgroundColor = labelSource->backgroundColor();
cvf::ref<cvf::DrawableText> drawableText = new cvf::DrawableText;
drawableText->setFont( labelSource->font() );
drawableText->setCheckPosVisible( false );
// Disable depth testing to make sure the labels are visible, as the well path can have geometry with a larger radius
// than the pipe, e.g. perforation intervals and casing
drawableText->setUseDepthBuffer( false );
drawableText->setDrawBorder( true );
drawableText->setDrawBackground( true );
drawableText->setBackgroundColor( backgroundColor );
drawableText->setBorderColor( RiaColorTools::computeOffsetColor( backgroundColor, 0.5f ) );
drawableText->setVerticalAlignment( cvf::TextDrawer::CENTER );
drawableText->setTextColor( RiaColorTools::contrastColor( backgroundColor ) );
std::vector<cvf::Vec3d> anchorPoints;
std::vector<std::vector<cvf::Vec3d>> anchorLines;
for ( const auto& label : labelSource->labels() )
{
const auto anchor = labelSource->anchorDisplayCoord( label, camera );
if ( !isInFrontOfNearPlane( camera, anchor ) ) continue;
cvf::Vec3d screenAnchor;
if ( !camera->project( anchor, &screenAnchor ) ) continue;
cvf::Vec2d screenTangent( 0.0, 0.0 );
cvf::Vec3d screenTangentEnd;
if ( isInFrontOfNearPlane( camera, anchor + label.tangent ) && camera->project( anchor + label.tangent, &screenTangentEnd ) )
{
screenTangent = { screenTangentEnd.x() - screenAnchor.x(), screenTangentEnd.y() - screenAnchor.y() };
}
const auto screenDirection = RivPipeSurfaceLabelSourceInfo::screenOffsetDirection( screenTangent );
// The text is drawn to the right of the label position, and vertically centered. Shift the text to avoid
// overlap between the text and the anchor line.
cvf::Vec2d textShift( 0.0, 0.0 );
if ( labelSource->font() )
{
const auto textExtent = labelSource->font()->textExtent( cvf::String( label.text ) );
textShift = { -0.5 * textExtent.x() * ( 1.0 - screenDirection.x() ), 0.5 * textExtent.y() * screenDirection.y() };
}
const cvf::Vec3d screenLineEnd( screenAnchor.x() + screenDirection.x() * labelOffsetInPixels,
screenAnchor.y() + screenDirection.y() * labelOffsetInPixels,
screenAnchor.z() );
const cvf::Vec3d screenLabel( screenLineEnd.x() + textShift.x(), screenLineEnd.y() + textShift.y(), screenAnchor.z() );
cvf::Vec3d lineEnd;
cvf::Vec3d labelPosition;
if ( !camera->unproject( screenLineEnd, &lineEnd ) || !camera->unproject( screenLabel, &labelPosition ) ) continue;
anchorPoints.push_back( anchor );
anchorLines.push_back( { anchor, lineEnd } );
drawableText->addText( cvf::String( label.text ), cvf::Vec3f( labelPosition ) );
}
if ( anchorPoints.empty() ) return;
// Shared by anchor lines and points. The point size render state has no effect on the lines.
caf::MeshEffectGenerator effectGenerator( labelSource->anchorColor() );
cvf::ref<cvf::Effect> anchorEffect = effectGenerator.generateUnCachedEffect();
{
cvf::ref<cvf::RenderStateDepth> depth = new cvf::RenderStateDepth;
depth->enableDepthTest( false );
anchorEffect->setRenderState( depth.p() );
cvf::ref<cvf::RenderStatePoint> pointRenderState = new cvf::RenderStatePoint( cvf::RenderStatePoint::FIXED_SIZE );
pointRenderState->setSize( 5.0f );
anchorEffect->setRenderState( pointRenderState.p() );
}
if ( auto lineGeo = RivPolylineGenerator::createLineAlongPolylineDrawable( anchorLines ); lineGeo.notNull() )
{
cvf::ref<cvf::Part> part = new cvf::Part;
part->setName( "RivAnnotationTools: pipe surface label anchor lines" );
part->setDrawable( lineGeo.p() );
part->setEffect( anchorEffect.p() );
part->setPriority( RivPartPriority::PartType::Highlight );
model->addPart( part.p() );
}
if ( auto pointGeo = RivPolylineGenerator::createPointsFromPolylineDrawable( anchorPoints ); pointGeo.notNull() )
{
cvf::ref<cvf::Part> part = new cvf::Part;
part->setName( "RivAnnotationTools: pipe surface label anchor points" );
part->setDrawable( pointGeo.p() );
part->setEffect( anchorEffect.p() );
part->setPriority( RivPartPriority::PartType::Highlight );
model->addPart( part.p() );
}
auto part = RivAnnotationTools::createPart( drawableText.p() );
part->setName( "RivAnnotationTools: pipe surface labels" );
model->addPart( part.p() );
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RivAnnotationTools::addAnnotationLabels( const cvf::Collection<cvf::Part>& partCollection,
const cvf::Camera* camera,
cvf::ModelBasicList* model,
bool computeScalingFactor )
{
if ( !camera || !model ) return;
// The scaling factor is computed using the camera, and this does not work for the flat intersection view
const double anchorLineScalingFactor = computeScalingFactor ? computeScalingFactorFromZoom( camera ) : 1.0;
for ( auto p : partCollection )
{
if ( auto pipeLabels = dynamic_cast<RivPipeSurfaceLabelSourceInfo*>( p->sourceInfo() ) )
{
addPipeSurfaceLabels( pipeLabels, camera, model );
continue;
}
auto annotationObject = dynamic_cast<RivAnnotationSourceInfo*>( p->sourceInfo() );
if ( annotationObject )
{
std::vector<LabelTextAndPosition> labels;
const auto viewportWidth = camera->viewport()->width();
const auto viewportHeight = camera->viewport()->height();
if ( !annotationObject->texts().empty() )
{
labels = createMultipleLabels( camera, annotationObject, viewportWidth, viewportHeight, anchorLineScalingFactor );
}
else
{
auto strategy = annotationObject->labelPositionStrategyHint();
if ( m_overrideStrategy != LabelPositionStrategy::UNDEFINED )
{
// Can override annotation object strategy defined in Rim3dView::updateScreenSpaceModel()
strategy = m_overrideStrategy;
}
if ( strategy == LabelPositionStrategy::RIGHT || strategy == LabelPositionStrategy::LEFT_AND_RIGHT )
{
// Close to the right edge of the visible screen area
const auto normalizedXPosition = 0.9;
auto labelCandidate = createLabelForClosestCoordinate( camera,
annotationObject,
viewportWidth,
viewportHeight,
normalizedXPosition,
anchorLineScalingFactor );
if ( labelCandidate.has_value() ) labels.push_back( labelCandidate.value() );
}
if ( strategy == LabelPositionStrategy::LEFT || strategy == LabelPositionStrategy::LEFT_AND_RIGHT )
{
// Close to the left edge of the visible screen area
const auto normalizedXPosition = 0.1;
auto labelCandidate = createLabelForClosestCoordinate( camera,
annotationObject,
viewportWidth,
viewportHeight,
normalizedXPosition,
anchorLineScalingFactor );
if ( labelCandidate.has_value() ) labels.push_back( labelCandidate.value() );
}
if ( strategy == LabelPositionStrategy::COUNT_HINT || strategy == LabelPositionStrategy::ALL )
{
std::vector<cvf::Vec3d> visibleCoords;
for ( const auto& v : annotationObject->anchorPointsInDisplayCoords() )
{
if ( !isInFrontOfNearPlane( camera, v ) ) continue;
cvf::Vec3d screenCoord;
if ( !camera->project( v, &screenCoord ) ) continue;
if ( screenCoord.x() > 0 && screenCoord.x() < viewportWidth && screenCoord.y() > 0 && screenCoord.y() < viewportHeight )
visibleCoords.push_back( v );
}
size_t stride = 1;
if ( strategy == LabelPositionStrategy::COUNT_HINT )
{
stride = std::max( size_t( 1 ), visibleCoords.size() / std::max( 1, m_labelCountHint - 1 ) );
}
for ( size_t i = 0; i < visibleCoords.size(); i += stride )
{
size_t adjustedIndex = std::min( i, visibleCoords.size() - 1 );
const cvf::Vec3d lineAnchorPosition = visibleCoords[adjustedIndex];
const cvf::Vec3d labelPosition = computeLabelPosition( camera, lineAnchorPosition, anchorLineScalingFactor );
labels.push_back( { annotationObject->text(), lineAnchorPosition, labelPosition } );
}
}
}
for ( const auto& [labelText, lineAnchorPosition, labelPosition] : labels )
{
{
// Line part
std::vector<cvf::Vec3d> points = { lineAnchorPosition, labelPosition };
auto anchorLineColor = cvf::Color3f::BLACK;
auto part = RivAnnotationTools::createPartFromPolyline( anchorLineColor, points );
if ( part.notNull() )
{
part->setName( "AnnotationObjectAnchorPoints" );
model->addPart( part.p() );
}
}
{
// Text part
auto backgroundColor = annotationObject->showColor() ? annotationObject->color() : cvf::Color3f::LIGHT_GRAY;
auto textColor = RiaColorTools::contrastColor( backgroundColor );
auto fontSize = 10;
auto font = RiaFontCache::getFont( fontSize );
auto drawableText = createDrawableText( font.p(), textColor, backgroundColor, labelText, cvf::Vec3f( labelPosition ) );
auto part = createPart( drawableText.p() );
part->setName( "RivAnnotationTools: " + labelText );
model->addPart( part.p() );
}
}
}
}
model->updateBoundingBoxesRecursive();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::DrawableText> RivAnnotationTools::createDrawableText( cvf::Font* font,
const cvf::Color3f& textColor,
const cvf::Color3f& backgroundColor,
const std::string& text,
const cvf::Vec3f& position )
{
auto drawableText = new cvf::DrawableText;
drawableText->setFont( font );
drawableText->setCheckPosVisible( false );
drawableText->setUseDepthBuffer( true );
drawableText->setDrawBorder( true );
drawableText->setDrawBackground( true );
drawableText->setVerticalAlignment( cvf::TextDrawer::BASELINE );
drawableText->setBackgroundColor( backgroundColor );
drawableText->setBorderColor( RiaColorTools::computeOffsetColor( backgroundColor, 0.5f ) );
drawableText->setTextColor( textColor );
drawableText->addText( cvf::String( text ), position );
return drawableText;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::DrawableText> RivAnnotationTools::createDrawableTextNoBackground( cvf::Font* font,
const cvf::Color3f& textColor,
const std::string& text,
const cvf::Vec3f& position )
{
cvf::ref<cvf::DrawableText> drawableText = new cvf::DrawableText;
drawableText->setFont( font );
drawableText->setCheckPosVisible( false );
drawableText->setDrawBorder( false );
drawableText->setDrawBackground( false );
drawableText->setVerticalAlignment( cvf::TextDrawer::CENTER );
drawableText->setTextColor( textColor );
drawableText->addText( cvf::String( text ), position );
return drawableText;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
cvf::ref<cvf::Part> RivAnnotationTools::createPart( cvf::DrawableText* drawableText )
{
auto part = new cvf::Part;
part->setDrawable( drawableText );
auto eff = new cvf::Effect();
part->setEffect( eff );
part->setPriority( RivPartPriority::PartType::Text );
return part;
}