mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 15:10:22 -05:00
The label is moved from the anchor point towards the camera to be drawn in front of other geometry. The offset was derived from the zoom level, and was not related to the distance between the camera and the anchor point. In a 3D view the offset is usually larger than this distance, moving the label behind the camera. Labels outside the view frustum are silently discarded by the text renderer. Limit the offset to the smaller of half the distance to the anchor point and the distance to the near plane. The limit is derived from the anchor point and not from the near plane, as the label parts are part of the scene and are used to compute the clipping planes. Reject anchor point candidates behind the camera. Coordinates behind the camera are mirrored into the viewport by the perspective divide in cvf::Camera::project(), and could be selected as the coordinate closest to the label position.
496 lines
22 KiB
C++
496 lines
22 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 "RivPolylineGenerator.h"
|
|
|
|
#include "cafAppEnum.h"
|
|
#include "cafEffectGenerator.h"
|
|
|
|
#include "cvfCamera.h"
|
|
#include "cvfDrawableGeo.h"
|
|
#include "cvfDrawableText.h"
|
|
#include "cvfModelBasicList.h"
|
|
#include "cvfPart.h"
|
|
#include "cvfViewport.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <optional>
|
|
|
|
namespace caf
|
|
{
|
|
|
|
template <>
|
|
void caf::AppEnum<RivAnnotationTools::LabelPositionStrategy>::setUp()
|
|
{
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::LEFT, "LEFT", "Left" );
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::RIGHT, "RIGHT", "Right" );
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::LEFT_AND_RIGHT, "LEFT_AND_RIGHT", "Left and Right" );
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::COUNT_HINT, "COUNT_HINT", "Count Hint" );
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::ALL, "All", "All" );
|
|
addItem( RivAnnotationTools::LabelPositionStrategy::NONE, "None", "Disabled" );
|
|
// RivAnnotationTools::LabelPositionStrategy::UNKNOWN is not included, as this is enum is not supposed to be displayed in GUI
|
|
setDefault( RivAnnotationTools::LabelPositionStrategy::RIGHT );
|
|
}
|
|
|
|
} // End namespace caf
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
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;
|
|
};
|
|
|
|
} // 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 )
|
|
{
|
|
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;
|
|
}
|