Skip to content

Commit 2085dfa

Browse files
committed
[FEATURE] 3D identify tool working on 3D entities
Until now the tool only considered terrain. This commit adds support for 3D renderers created from vector layers, so it is possible to correctly identify polygons and linestrings in 3D.
1 parent f99b2db commit 2085dfa

16 files changed

+273
-9
lines changed

python/gui/auto_generated/qgsmaptoolidentify.sip.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ Call the right method depending on layer type
158158
bool identifyRasterLayer( QList<QgsMapToolIdentify::IdentifyResult> *results, QgsRasterLayer *layer, QgsPointXY point, const QgsRectangle &viewExtent, double mapUnitsPerPixel );
159159
bool identifyVectorLayer( QList<QgsMapToolIdentify::IdentifyResult> *results, QgsVectorLayer *layer, const QgsPointXY &point );
160160

161+
QMap< QString, QString > derivedAttributesForPoint( const QgsPoint &point );
162+
%Docstring
163+
Returns derived attributes map for a clicked point in map coordinates. May be 2D or 3D point.
164+
%End
165+
161166
};
162167

163168
QFlags<QgsMapToolIdentify::Type> operator|(QgsMapToolIdentify::Type f1, QFlags<QgsMapToolIdentify::Type> f2);

src/3d/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ SET(QGIS_3D_MOC_HDRS
5656
qgscameracontroller.h
5757
qgslayoutitem3dmap.h
5858
qgsoffscreen3dengine.h
59+
qgstessellatedpolygongeometry.h
5960
qgswindow3dengine.h
6061

6162
chunks/qgschunkedentity_p.h

src/3d/qgs3dmapscene.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
#include "qgsaabb.h"
3232
#include "qgsabstract3dengine.h"
33+
#include "qgs3dmapscenepickhandler.h"
3334
#include "qgs3dmapsettings.h"
3435
#include "qgs3dutils.h"
3536
#include "qgsabstract3drenderer.h"
@@ -38,6 +39,7 @@
3839
#include "qgschunknode_p.h"
3940
#include "qgsterrainentity_p.h"
4041
#include "qgsterraingenerator.h"
42+
#include "qgstessellatedpolygongeometry.h"
4143
#include "qgsvectorlayer.h"
4244
#include "qgsvectorlayer3drenderer.h"
4345

@@ -177,6 +179,37 @@ int Qgs3DMapScene::terrainPendingJobsCount() const
177179
return mTerrain ? mTerrain->pendingJobsCount() : 0;
178180
}
179181

182+
void Qgs3DMapScene::registerPickHandler( Qgs3DMapScenePickHandler *pickHandler )
183+
{
184+
if ( mPickHandlers.isEmpty() )
185+
{
186+
// we need to add object pickers
187+
for ( Qt3DCore::QEntity *entity : mLayerEntities.values() )
188+
{
189+
Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( entity );
190+
entity->addComponent( picker );
191+
connect( picker, &Qt3DRender::QObjectPicker::pressed, this, &Qgs3DMapScene::onLayerEntityPickEvent );
192+
}
193+
}
194+
195+
mPickHandlers.append( pickHandler );
196+
}
197+
198+
void Qgs3DMapScene::unregisterPickHandler( Qgs3DMapScenePickHandler *pickHandler )
199+
{
200+
mPickHandlers.removeOne( pickHandler );
201+
202+
if ( mPickHandlers.isEmpty() )
203+
{
204+
// we need to remove pickers
205+
for ( Qt3DCore::QEntity *entity : mLayerEntities.values() )
206+
{
207+
Qt3DRender::QObjectPicker *picker = entity->findChild<Qt3DRender::QObjectPicker *>();
208+
picker->deleteLater();
209+
}
210+
}
211+
}
212+
180213
QgsChunkedEntity::SceneState _sceneState( QgsCameraController *cameraController )
181214
{
182215
Qt3DRender::QCamera *camera = cameraController->camera();
@@ -365,6 +398,54 @@ void Qgs3DMapScene::onBackgroundColorChanged()
365398
mEngine->setClearColor( mMap.backgroundColor() );
366399
}
367400

401+
void Qgs3DMapScene::onLayerEntityPickEvent( Qt3DRender::QPickEvent *event )
402+
{
403+
if ( event->button() != Qt3DRender::QPickEvent::LeftButton )
404+
return;
405+
406+
Qt3DRender::QPickTriangleEvent *triangleEvent = qobject_cast<Qt3DRender::QPickTriangleEvent *>( event );
407+
if ( !triangleEvent )
408+
return;
409+
410+
Qt3DRender::QObjectPicker *picker = qobject_cast<Qt3DRender::QObjectPicker *>( sender() );
411+
if ( !picker )
412+
return;
413+
414+
Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( picker->parent() );
415+
if ( !entity )
416+
return;
417+
418+
QgsMapLayer *layer = mLayerEntities.key( entity );
419+
if ( !layer )
420+
return;
421+
422+
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
423+
if ( !vlayer )
424+
return;
425+
426+
for ( Qgs3DMapScenePickHandler *pickHandler : qgis::as_const( mPickHandlers ) )
427+
{
428+
// go figure out feature ID from the triangle index
429+
QgsFeatureId fid = -1;
430+
for ( Qt3DRender::QGeometryRenderer *geomRenderer : entity->findChildren<Qt3DRender::QGeometryRenderer *>() )
431+
{
432+
// unfortunately we can't access which sub-entity triggered the pick event
433+
// so as a temporary workaround let's just ignore the entity with selection
434+
// and hope the event was the main entity (QTBUG-58206)
435+
if ( geomRenderer->objectName() != "main" )
436+
continue;
437+
438+
if ( QgsTessellatedPolygonGeometry *g = qobject_cast<QgsTessellatedPolygonGeometry *>( geomRenderer->geometry() ) )
439+
{
440+
fid = g->triangleIndexToFeatureId( triangleEvent->triangleIndex() );
441+
break;
442+
}
443+
}
444+
pickHandler->handlePickOnVectorLayer( vlayer, fid, event->worldIntersection() );
445+
}
446+
447+
}
448+
368449
void Qgs3DMapScene::onLayerRenderer3DChanged()
369450
{
370451
QgsMapLayer *layer = qobject_cast<QgsMapLayer *>( sender() );
@@ -425,6 +506,13 @@ void Qgs3DMapScene::addLayerEntity( QgsMapLayer *layer )
425506
{
426507
newEntity->setParent( this );
427508
mLayerEntities.insert( layer, newEntity );
509+
510+
if ( !mPickHandlers.isEmpty() )
511+
{
512+
Qt3DRender::QObjectPicker *picker = new Qt3DRender::QObjectPicker( newEntity );
513+
newEntity->addComponent( picker );
514+
connect( picker, &Qt3DRender::QObjectPicker::pressed, this, &Qgs3DMapScene::onLayerEntityPickEvent );
515+
}
428516
}
429517
}
430518

src/3d/qgs3dmapscene.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Qt3DRender
2424
{
2525
class QRenderSettings;
2626
class QCamera;
27+
class QPickEvent;
2728
}
2829

2930
namespace Qt3DLogic
@@ -39,6 +40,7 @@ namespace Qt3DExtras
3940
class QgsAbstract3DEngine;
4041
class QgsMapLayer;
4142
class QgsCameraController;
43+
class Qgs3DMapScenePickHandler;
4244
class Qgs3DMapSettings;
4345
class QgsTerrainEntity;
4446
class QgsChunkedEntity;
@@ -76,6 +78,11 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
7678
//! Returns the current state of the scene
7779
SceneState sceneState() const { return mSceneState; }
7880

81+
//! Registers an object that will get results of pick events on 3D entities. Does not take ownerhip of the pick handler. Adds object picker components to 3D entities.
82+
void registerPickHandler( Qgs3DMapScenePickHandler *pickHandler );
83+
//! Unregisters previously registered pick handler. Pick handler is not deleted. Also removes object picker components from 3D entities.
84+
void unregisterPickHandler( Qgs3DMapScenePickHandler *pickHandler );
85+
7986
signals:
8087
//! Emitted when the current terrain entity is replaced by a new one
8188
void terrainEntityChanged();
@@ -92,6 +99,7 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
9299
void onLayersChanged();
93100
void createTerrainDeferred();
94101
void onBackgroundColorChanged();
102+
void onLayerEntityPickEvent( Qt3DRender::QPickEvent *event );
95103

96104
private:
97105
void addLayerEntity( QgsMapLayer *layer );
@@ -116,6 +124,8 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity
116124
QMap<QgsMapLayer *, Qt3DCore::QEntity *> mLayerEntities;
117125
bool mTerrainUpdateScheduled = false;
118126
SceneState mSceneState = Ready;
127+
//! List of currently registered pick handlers (used by identify tool)
128+
QList<Qgs3DMapScenePickHandler *> mPickHandlers;
119129
};
120130

121131
#endif // QGS3DMAPSCENE_H

src/3d/qgs3dmapscenepickhandler.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#ifndef QGS3DMAPSCENEPICKHANDLER_H
2+
#define QGS3DMAPSCENEPICKHANDLER_H
3+
4+
#include "qgsfeatureid.h"
5+
6+
class QVector3D;
7+
class QgsVectorLayer;
8+
9+
/**
10+
* \ingroup 3d
11+
* Abstract base class for handlers that process pick events from a 3D map scene.
12+
* 3D entities in map scene get QObjectPicker components assigned and mouse press events trigger virtual methods
13+
* or pick handlers.
14+
*
15+
* This is used for identify tool to be able to identify entities coming from 3D renderers assigned to map layers.
16+
*
17+
* \since QGIS 3.4
18+
*/
19+
class Qgs3DMapScenePickHandler
20+
{
21+
public:
22+
virtual ~Qgs3DMapScenePickHandler() {}
23+
24+
//! Called when user clicked a 3D entity belonging to a feature of a vector layer
25+
virtual void handlePickOnVectorLayer( QgsVectorLayer *vlayer, QgsFeatureId id, const QVector3D &worldIntersection ) = 0;
26+
};
27+
28+
#endif // QGS3DMAPSCENEPICKHANDLER_H

src/3d/qgstessellatedpolygongeometry.cpp

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,20 @@ QgsTessellatedPolygonGeometry::QgsTessellatedPolygonGeometry( QNode *parent )
5656
}
5757
}
5858

59-
void QgsTessellatedPolygonGeometry::setPolygons( const QList<QgsPolygon *> &polygons, const QgsPointXY &origin, float extrusionHeight, const QList<float> &extrusionHeightPerPolygon )
59+
void QgsTessellatedPolygonGeometry::setPolygons( const QList<QgsPolygon *> &polygons, const QList<QgsFeatureId> &featureIds, const QgsPointXY &origin, float extrusionHeight, const QList<float> &extrusionHeightPerPolygon )
6060
{
61+
Q_ASSERT( polygons.count() == featureIds.count() );
62+
mTriangleIndexStartingIndices.reserve( polygons.count() );
63+
mTriangleIndexFids.reserve( polygons.count() );
64+
6165
QgsTessellator tessellator( origin.x(), origin.y(), mWithNormals, mInvertNormals, mAddBackFaces );
6266
for ( int i = 0; i < polygons.count(); ++i )
6367
{
68+
Q_ASSERT( tessellator.dataVerticesCount() % 3 == 0 );
69+
uint startingTriangleIndex = static_cast<uint>( tessellator.dataVerticesCount() / 3 );
70+
mTriangleIndexStartingIndices.append( startingTriangleIndex );
71+
mTriangleIndexFids.append( featureIds[i] );
72+
6473
QgsPolygon *polygon = polygons.at( i );
6574
float extr = extrusionHeightPerPolygon.isEmpty() ? extrusionHeight : extrusionHeightPerPolygon.at( i );
6675
tessellator.addPolygon( *polygon, extr );
@@ -76,3 +85,38 @@ void QgsTessellatedPolygonGeometry::setPolygons( const QList<QgsPolygon *> &poly
7685
if ( mNormalAttribute )
7786
mNormalAttribute->setCount( nVerts );
7887
}
88+
89+
90+
// run binary search on a sorted array, return index i where data[i] <= x < data[i+1]
91+
static int binary_search( uint v, const uint *data, int count )
92+
{
93+
int idx0 = 0;
94+
int idx1 = count - 1;
95+
96+
if ( v < data[0] )
97+
return -1; // not in the array
98+
99+
while ( idx0 != idx1 )
100+
{
101+
int idxPivot = ( idx0 + idx1 ) / 2;
102+
uint pivot = data[idxPivot];
103+
if ( pivot <= v )
104+
{
105+
if ( data[idxPivot + 1] > v )
106+
return idxPivot; // we're done!
107+
else // continue searching values greater than the pivot
108+
idx0 = idxPivot;
109+
}
110+
else // continue searching values lower than the pivot
111+
idx1 = idxPivot;
112+
}
113+
return idx0;
114+
}
115+
116+
117+
QgsFeatureId QgsTessellatedPolygonGeometry::triangleIndexToFeatureId( uint triangleIndex )
118+
{
119+
int i = binary_search( triangleIndex, mTriangleIndexStartingIndices.constData(), mTriangleIndexStartingIndices.count() );
120+
Q_ASSERT( i != -1 );
121+
return mTriangleIndexFids[i];
122+
}

src/3d/qgstessellatedpolygongeometry.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#ifndef QGSTESSELLATEDPOLYGONGEOMETRY_H
1717
#define QGSTESSELLATEDPOLYGONGEOMETRY_H
1818

19+
#include "qgsfeatureid.h"
1920
#include "qgspolygon.h"
2021

2122
#include <Qt3DRender/QGeometry>
@@ -36,6 +37,7 @@ namespace Qt3DRender
3637
*/
3738
class QgsTessellatedPolygonGeometry : public Qt3DRender::QGeometry
3839
{
40+
Q_OBJECT
3941
public:
4042
//! Constructor
4143
QgsTessellatedPolygonGeometry( QNode *parent = nullptr );
@@ -58,14 +60,20 @@ class QgsTessellatedPolygonGeometry : public Qt3DRender::QGeometry
5860
void setAddBackFaces( bool add ) { mAddBackFaces = add; }
5961

6062
//! Initializes vertex buffer from given polygons. Takes ownership of passed polygon geometries
61-
void setPolygons( const QList<QgsPolygon *> &polygons, const QgsPointXY &origin, float extrusionHeight, const QList<float> &extrusionHeightPerPolygon = QList<float>() );
63+
void setPolygons( const QList<QgsPolygon *> &polygons, const QList<QgsFeatureId> &featureIds, const QgsPointXY &origin, float extrusionHeight, const QList<float> &extrusionHeightPerPolygon = QList<float>() );
64+
65+
//! Returns ID of the feature to which given triangle index belongs (used for picking)
66+
QgsFeatureId triangleIndexToFeatureId( uint triangleIndex );
6267

6368
private:
6469

6570
Qt3DRender::QAttribute *mPositionAttribute = nullptr;
6671
Qt3DRender::QAttribute *mNormalAttribute = nullptr;
6772
Qt3DRender::QBuffer *mVertexBuffer = nullptr;
6873

74+
QVector<QgsFeatureId> mTriangleIndexFids;
75+
QVector<uint> mTriangleIndexStartingIndices;
76+
6977
bool mWithNormals = true;
7078
bool mInvertNormals = false;
7179
bool mAddBackFaces = false;

src/3d/symbols/qgsline3dsymbol_p.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ void QgsLine3DSymbolEntity::addEntityForNotSelectedLines( const Qgs3DMapSettings
8585

8686
// build the entity
8787
QgsLine3DSymbolEntityNode *entity = new QgsLine3DSymbolEntityNode( map, layer, symbol, req );
88+
entity->findChild<Qt3DRender::QGeometryRenderer *>()->setObjectName( "main" ); // temporary measure to distinguish between "selected" and "main"
8889
entity->addComponent( mat );
8990
entity->setParent( this );
9091
}
@@ -106,6 +107,7 @@ Qt3DRender::QGeometryRenderer *QgsLine3DSymbolEntityNode::renderer( const Qgs3DM
106107
double mitreLimit = 0;
107108

108109
QList<QgsPolygon *> polygons;
110+
QList<QgsFeatureId> fids;
109111
QgsFeature f;
110112
QgsFeatureIterator fi = layer->getFeatures( request );
111113
while ( fi.nextFeature( f ) )
@@ -129,6 +131,7 @@ Qt3DRender::QGeometryRenderer *QgsLine3DSymbolEntityNode::renderer( const Qgs3DM
129131
QgsPolygon *polyBuffered = static_cast<QgsPolygon *>( buffered );
130132
Qgs3DUtils::clampAltitudes( polyBuffered, symbol.altitudeClamping(), symbol.altitudeBinding(), symbol.height(), map );
131133
polygons.append( polyBuffered );
134+
fids.append( f.id() );
132135
}
133136
else if ( QgsWkbTypes::flatType( buffered->wkbType() ) == QgsWkbTypes::MultiPolygon )
134137
{
@@ -140,13 +143,14 @@ Qt3DRender::QGeometryRenderer *QgsLine3DSymbolEntityNode::renderer( const Qgs3DM
140143
QgsPolygon *polyBuffered = static_cast<QgsPolygon *>( partBuffered )->clone(); // need to clone individual geometry parts
141144
Qgs3DUtils::clampAltitudes( polyBuffered, symbol.altitudeClamping(), symbol.altitudeBinding(), symbol.height(), map );
142145
polygons.append( polyBuffered );
146+
fids.append( f.id() );
143147
}
144148
delete buffered;
145149
}
146150
}
147151

148152
mGeometry = new QgsTessellatedPolygonGeometry;
149-
mGeometry->setPolygons( polygons, origin, symbol.extrusionHeight() );
153+
mGeometry->setPolygons( polygons, fids, origin, symbol.extrusionHeight() );
150154

151155
Qt3DRender::QGeometryRenderer *renderer = new Qt3DRender::QGeometryRenderer;
152156
renderer->setGeometry( mGeometry );

src/3d/symbols/qgspolygon3dsymbol_p.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ void QgsPolygon3DSymbolEntity::addEntityForNotSelectedPolygons( const Qgs3DMapSe
101101

102102
// build the entity
103103
QgsPolygon3DSymbolEntityNode *entity = new QgsPolygon3DSymbolEntityNode( map, layer, symbol, req );
104+
entity->findChild<Qt3DRender::QGeometryRenderer *>()->setObjectName( "main" ); // temporary measure to distinguish between "selected" and "main"
104105
entity->addComponent( mat );
105106
entity->addComponent( tform );
106107
entity->setParent( this );
@@ -141,6 +142,7 @@ Qt3DRender::QGeometryRenderer *QgsPolygon3DSymbolEntityNode::renderer( const Qgs
141142
{
142143
QgsPointXY origin( map.origin().x(), map.origin().y() );
143144
QList<QgsPolygon *> polygons;
145+
QList<QgsFeatureId> fids;
144146
QList<float> extrusionHeightPerPolygon; // will stay empty if not needed per polygon
145147

146148
QgsExpressionContext ctx( _expressionContext3D() );
@@ -178,6 +180,7 @@ Qt3DRender::QGeometryRenderer *QgsPolygon3DSymbolEntityNode::renderer( const Qgs
178180
QgsPolygon *polyClone = poly->clone();
179181
Qgs3DUtils::clampAltitudes( polyClone, symbol.altitudeClamping(), symbol.altitudeBinding(), height, map );
180182
polygons.append( polyClone );
183+
fids.append( f.id() );
181184
if ( hasDDExtrusion )
182185
extrusionHeightPerPolygon.append( extrusionHeight );
183186
}
@@ -190,6 +193,7 @@ Qt3DRender::QGeometryRenderer *QgsPolygon3DSymbolEntityNode::renderer( const Qgs
190193
QgsPolygon *polyClone = static_cast< const QgsPolygon *>( g2 )->clone();
191194
Qgs3DUtils::clampAltitudes( polyClone, symbol.altitudeClamping(), symbol.altitudeBinding(), height, map );
192195
polygons.append( polyClone );
196+
fids.append( f.id() );
193197
if ( hasDDExtrusion )
194198
extrusionHeightPerPolygon.append( extrusionHeight );
195199
}
@@ -201,7 +205,7 @@ Qt3DRender::QGeometryRenderer *QgsPolygon3DSymbolEntityNode::renderer( const Qgs
201205
mGeometry = new QgsTessellatedPolygonGeometry;
202206
mGeometry->setInvertNormals( symbol.invertNormals() );
203207
mGeometry->setAddBackFaces( symbol.addBackFaces() );
204-
mGeometry->setPolygons( polygons, origin, symbol.extrusionHeight(), extrusionHeightPerPolygon );
208+
mGeometry->setPolygons( polygons, fids, origin, symbol.extrusionHeight(), extrusionHeightPerPolygon );
205209

206210
Qt3DRender::QGeometryRenderer *renderer = new Qt3DRender::QGeometryRenderer;
207211
renderer->setGeometry( mGeometry );

0 commit comments

Comments
 (0)