Compare commits

...

93 Commits

Author SHA1 Message Date
Jorge Gustavo Rocha
bc7ecb7d25
Merge 5f93eb8162e26b6c33167716856b12a138da361d into 2e07d9829f38ea12661050415d09e28675c961bd 2025-10-01 11:46:36 +01:00
Nyall Dawson
2e07d9829f Fix build 2025-10-01 18:00:57 +10:00
Nyall Dawson
7d31cc1dab Run code_cleanup script 2025-10-01 18:00:57 +10:00
qgis-bot
e28d12d058 auto sipify 🍺 2025-10-01 05:54:45 +00:00
Denis Rouzaud
0f1c0de0f8
Merge pull request #63214 from nyalldawson/annotation3d
Support rendering annotation layer markers as 3d billboards
2025-10-01 07:52:11 +02:00
qgis-bot
82d7ab9b62 auto sipify 🍺 2025-10-01 03:49:17 +00:00
Nyall Dawson
8bc1f639bb Update tests 2025-10-01 13:46:41 +10:00
Nyall Dawson
928bdd8cc4 Fix tooltip formatting for console actions
The tooltip text should be in bold, not the shortcut
2025-10-01 13:46:41 +10:00
Nyall Dawson
1db5d1d246 Add common actions for run script, run selection
Allows these shortcuts to be customised
2025-10-01 13:46:41 +10:00
Nyall Dawson
1e3167082c Use correct shortcut in context menu for console script editor
And add reformat code action to menu
2025-10-01 13:46:41 +10:00
Nyall Dawson
c6accd8517 Fix more misleading names
These objects are actions, not buttons
2025-10-01 13:46:41 +10:00
Nyall Dawson
41962a0a24 Use correct user-set shortcuts for console script editor code actions
And rename member variable for correctness
2025-10-01 13:46:41 +10:00
Nyall Dawson
0fc8b37487 Set shortcut for script editor toggle comment action 2025-10-01 13:46:41 +10:00
Nyall Dawson
1c53ab415b Use registered shortcuts for code editor comment/reformat actions 2025-10-01 13:46:41 +10:00
Nyall Dawson
579a818ce7 Add sequenceForCommonAction 2025-10-01 13:46:41 +10:00
Nyall Dawson
5ed5f51121 [api] Add framework to handle common actions in shortcuts manager
Common actions allow for shortcuts to be registered for actions
which do not yet exist, or which are not associated with a single
global QAction object attached to at the application level. For
example, code editor actions which will be created as children
of individual code editor widgets, but which should have
shortcuts available for user configuration via the shortcuts
manager dialog.
2025-10-01 13:46:41 +10:00
Nyall Dawson
62fbbe6392 Refactor to extract utility function 2025-10-01 13:46:41 +10:00
Nyall Dawson
ac1260d159 Handle shortcut manager sections which don't have trailing "/" 2025-10-01 13:46:41 +10:00
Nyall Dawson
d9a4c80aa9 Rename method to more generic name 2025-10-01 13:46:41 +10:00
Nyall Dawson
af0208c974
Refine z range logic 2025-10-01 12:47:45 +10:00
Nyall Dawson
a01770e5cb
[feature] Support rendering annotation layer markers as 3d billboards
This commit adds a new option for showing annotation layers in 3d maps.

From the annotation layer properties, 3d tab, users can set the layer
to render as 3d billboards. When activated, all MARKERS from the layer
will be shown as floating billboard symbols above the 3d map. (lines,
polygons, and text items from the layer are ignored).

Users have control over the terrain clamping and offset for the
billboards, and whether or not "callout" lines should be shown (vertical
lines joining the billboard to the corresponding location on the
terrain surface)
2025-10-01 10:56:50 +10:00
Nyall Dawson
d63653925c Fix 3d build 2025-10-01 10:54:09 +10:00
Jan Caha
85e00b755b do not prefil htmlmetadata on creation, it can be fetched later when the currentChanged is triggered on mOptStackedWidget 2025-10-01 10:32:00 +10:00
Jan Caha
206cf49e41 use single query to get the information 2025-10-01 10:32:00 +10:00
Jan Caha
5cf505c584 line breaks for html 2025-10-01 10:32:00 +10:00
Jan Caha
a92a318f99 fix condition 2025-10-01 10:32:00 +10:00
Jan Caha
22d6b83351 replace /n with </br> to fix html 2025-10-01 10:32:00 +10:00
Jan Caha
1f7f070325 fix error 2025-10-01 10:32:00 +10:00
Jan Caha
9cddecd8be fix error
Co-authored-by: Stefanos Natsis <uclaros@gmail.com>
2025-10-01 10:32:00 +10:00
Jan Caha
9b58c9d973 add table comment 2025-10-01 10:32:00 +10:00
Jan Caha
6df907a02b review comments 2025-10-01 10:32:00 +10:00
Jan Caha
7108812a7b fix version 2025-10-01 10:32:00 +10:00
Jan Caha
45f0e33759 remove unused variable 2025-10-01 10:32:00 +10:00
Jan Caha
3e06b3d151 fix 2025-10-01 10:32:00 +10:00
Jan Caha
8063554822 remove const 2025-10-01 10:32:00 +10:00
Jan Caha
fb9429fb20 simplify args use 2025-10-01 10:32:00 +10:00
Jan Caha
9d369ed282 translate strings 2025-10-01 10:32:00 +10:00
Jan Caha
621be995f6 rename function 2025-10-01 10:32:00 +10:00
Jan Caha
4d6f204063 add more htmlMetadata to the postgres provider 2025-10-01 10:32:00 +10:00
Jan Caha
36ec7fbcce move dumpVariantMap to qgspostgresutils 2025-10-01 10:32:00 +10:00
Germán Carrillo
88d443ac8f [tests] Add tests for setting extent in 'Export Map to Image/PDF' dialogs, both with normal behavior and with locking the scale activated 2025-10-01 10:20:18 +10:00
Germán Carrillo
656959f5f2 [app] Export Map to image/PDF: Add option to lock the scale while setting the extent
+ Currently, setting the extent using current map view, layer extent, or drawn on canvas buttons will always reset the scale to match canvas scale.
+ This commit makes it possible to use a lock button to keep the scale as is in the dialog when setting the extent by any of the aforementioned methods.
2025-10-01 10:20:18 +10:00
qgis-bot
0dbabdf1e1 auto sipify 🍺 2025-10-01 00:19:43 +00:00
Damiano Lombardi
7a36d05a37 Move enum DevToolsNodeRole to Qgis namespace 2025-10-01 10:17:00 +10:00
Damiano Lombardi
c0a3055b6e Explicitly forbid copy for QgsDevToolsModelGroup 2025-10-01 10:17:00 +10:00
Damiano Lombardi
d553c935c3 Move networklogger to gui 2025-10-01 10:17:00 +10:00
Nyall Dawson
143949770a Update src/gui/processing/qgsprocessingoutputdestinationwidget.cpp 2025-10-01 10:15:12 +10:00
Alexander Bruy
43ae0b6706 change default folder for Processing outputs to $HOME/processing to make
it more visible and accessible
2025-10-01 10:15:12 +10:00
Alexander Bruy
e6746493b9 provide default value when reading output folder setting to generate
destination path in Processing (fix #61965)
2025-10-01 10:15:12 +10:00
Nyall Dawson
5f320b4369 Blocklist fragile server tests on Ubuntu CI
These tests have started segfaulting randomly since upgrade from
24.10 to 25.04. It's a Ubuntu 25.04 specific issue, as the tests
have been run on the Fedora CI (and older Ubuntu) for years without
issue.

At this stage the cause is unknown, but the failures are just
noise and risk hiding real issues. Given that the Fedora CI is
still running these tests then we don't have any drop in
test coverage by blocking them from running on the Ubuntu builds.
2025-10-01 05:56:34 +10:00
Benoit D.-M. - oslandia
3c3a6069c7
Merge pull request #63173 from ptitjano/export-fix
[3D] fix terrain obj export
2025-09-30 07:40:17 +02:00
Nyall Dawson
bae5d99f67 Make layout scalebar "map units" choice more user-friendly
Explicitly list the actual map units in the item text,
eg "Map Units (meters)", so that users know exactly what unit
the scalebar is using.

Refs https://www.reddit.com/r/QGIS/comments/1nsgbhz/why_is_my_scale_bar_not_working/
2025-09-30 11:35:35 +10:00
Malik Blesius
e233e48826 [themes][ui] Fix color for selected tree widget items, when they are also disabled for dark themes 2025-09-30 00:57:55 +02:00
Julien Cabieces
f580afd7a6 fix(QObjectUniquePtr): delete currently pointed object when moving 2025-09-30 07:59:08 +10:00
github-actions[bot]
beb3e987f0 auto-fix pre-commit issues 2025-09-29 21:52:09 +00:00
Trex
a5f3530fae Follow-up to Fix #40120: Python console editor buttons now have shortcuts in tooltips. 2025-09-30 07:51:07 +10:00
Trexerx
65cfee669c Uppercase letter E in button tooltip
Co-authored-by: Andrea Giudiceandrea <andreaerdna@libero.it>
2025-09-30 07:51:07 +10:00
Trex
5743ee9c95 Follow-up to Fix #40120: Setting shortcut moved from console_editor.py to console.py 2025-09-30 07:51:07 +10:00
Trex
3d316b1bb1 Follow-up to Fix #40120: Setting shortcut moved from console_editor.py to console.py 2025-09-30 07:51:07 +10:00
Trex
652159f079 Fix #40120: Added Run Selected button to python console editor toolbar. 2025-09-30 07:51:07 +10:00
Trex
81e9b9ad47 Fix #40120: Added Run Selected button to python console editor toolbar. 2025-09-30 07:51:07 +10:00
Alexander Bruy
42feace4f9
Merge pull request #63298 from elpaso/bugfix-gh63277-fixed-interval-single-class
Symbol renderer: special case for fixed interval single class
2025-09-29 13:02:30 +01:00
Alexander Bruy
c6578b7986
Merge pull request #63279 from nirvn/vector_tile_style_url_fix
[vector tiles] Fix relative path handling of "url" sources on a different domain
2025-09-29 13:01:13 +01:00
Alexander Bruy
a71ed7cb77
Merge pull request #63240 from nyalldawson/aleixpol-work/apol/fix-gdalmerge-deprecation
processing: Call gdal_merge instead of gdal_merge.py
2025-09-29 13:00:33 +01:00
Alessandro Pasotti
b2ccee0990
Apply suggestion from @nyalldawson
Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>
2025-09-29 09:22:17 +02:00
Alessandro Pasotti
091e072c22
Apply suggestion from @nyalldawson
Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>
2025-09-29 09:22:08 +02:00
Alessandro Pasotti
85437bccd9
Apply suggestion from @nyalldawson
Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>
2025-09-29 09:21:56 +02:00
Alessandro Pasotti
5f9c6ffaff
Apply suggestion from @nyalldawson
Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>
2025-09-29 09:21:38 +02:00
Alexander Bruy
8525448b2a add tests 2025-09-29 14:05:03 +10:00
Alexander Bruy
4b5129f940 assign correct raster type to the paletted raster with alpha band (fix #61283) 2025-09-29 14:05:03 +10:00
Nyall Dawson
833aace899
Use gdal_merge.py for GDAL < 3.9 2025-09-29 10:59:13 +10:00
Nyall Dawson
e2a955eee2
Update tests 2025-09-29 10:56:00 +10:00
Aleix Pol
9ede69b396
processing: Call gdal_merge instead of gdal_merge.py
The latter is deprecated by gdal and it has a bogus shebang.
2025-09-29 10:55:59 +10:00
Nyall Dawson
127ff85935 Fix warnings 2025-09-29 10:51:43 +10:00
Nyall Dawson
bc9689d09f Fix internal spatialindex build 2025-09-29 10:51:43 +10:00
Nyall Dawson
bd0ebbfe7d Use internal spatialindex for Ubuntu CI 2025-09-29 10:51:43 +10:00
qgis-bot
bd320e3f29 auto sipify 🍺 2025-09-28 22:19:36 +00:00
Nyall Dawson
82136d0c6e Sipify 2025-09-29 08:16:55 +10:00
Nyall Dawson
15d89d273a Rework API 2025-09-29 08:16:55 +10:00
Nyall Dawson
131e12668c Fix typo 2025-09-29 08:16:55 +10:00
Nyall Dawson
e9ed10a2a5 Use Format_ARGB32_Premultiplied 2025-09-29 08:16:55 +10:00
Nyall Dawson
270b91524f Mark internal class as private 2025-09-29 08:16:55 +10:00
Nyall Dawson
97d77a9edf Introduce QgsTextureAtlasGenerator
Generates texture atlases by efficient packing of multiple
input rectangles/images.

Based on the rectPack2D library.
2025-09-29 08:16:55 +10:00
Jean Felder
29a7b983a8 testqgs3dexporter: Remove useless comments 2025-09-25 11:53:40 +02:00
Jean Felder
c70a30fa2b testqgs3dexporter: Activate test3DSceneExporterBig on CI 2025-09-25 11:53:40 +02:00
Jean Felder
f17100d1bb testqgs3dexporter: Add an export test on a scene with a flat terrain 2025-09-25 11:53:40 +02:00
Jean Felder
49f44ea494 testqgs3dexporter: Remove unused terrain_scene_export test
This test was never activated because the obj file was too big.
2025-09-25 11:53:39 +02:00
Jean Felder
505701993e tests(3d): Factor out the exporter tests
Move it to a dedicated file.
2025-09-25 11:53:39 +02:00
Alessandro Pasotti
2c7980b97d Also fix pretty-breaks when single class 2025-09-22 16:02:13 +02:00
Alessandro Pasotti
25444cca9f Symbol renderer: special case for fixed interval single class
Fix #63277
2025-09-22 15:42:46 +02:00
Jorge Gustavo Rocha
5f93eb8162 Fix #63260 2025-09-21 21:28:01 +01:00
Mathieu Pellerin
ff528b63cc [vector tiles] Fix relative path handling of "url" sources on a different domain 2025-09-20 19:39:37 +07:00
Jean Felder
f3354b205b qgs3dsceneexporter: Fix terrain export
In the different terrain cases, the generated tiles have an origin at
0 unlike other objects which have their origin correctly set. Fix the
issue by setting the correct origin. This is the same logic that is
applied in `Qgs3DMapScene::createTerrainDeferred` when a new terrain
entity is created.

Fixes: b0e038559dc8627ba5d8bfe0bd419634f9cfc3af
2025-09-19 15:37:58 +02:00
200 changed files with 52259 additions and 836 deletions

View File

@ -16,3 +16,8 @@ test_core_openclutils
# Relies on a broken/unreliable 3rd party service
test_core_layerdefinition
# Randomly segfault on Ubuntu 25.04, for unknown reasons
PyQgsServerApi
PyQgsServerWMS
PyQgsServerConfigCache

View File

@ -109,6 +109,7 @@ cmake \
-DWITH_QTSERIALPORT=ON \
-DWITH_PDF4QT=${WITH_PDF4QT} \
-DWITH_SFCGAL=${WITH_SFCGAL} \
-DWITH_INTERNAL_SPATIALINDEX=${WITH_INTERNAL_SPATIALINDEX} \
-DORACLE_INCLUDEDIR=/instantclient_21_16/sdk/include/ \
-DORACLE_LIBDIR=/instantclient_21_16/ \
-DDISABLE_DEPRECATED=ON \

View File

@ -144,17 +144,6 @@ RUN apt-get update \
libgdal-dev \
libproj-dev
# download spatialindex and compile it
RUN curl -L https://github.com/libspatialindex/libspatialindex/releases/download/2.0.0/spatialindex-src-2.0.0.tar.gz --output spatialindex-src-2.0.0.tar.gz \
&& mkdir spatialindex \
&& tar zxf spatialindex-src-2.0.0.tar.gz -C spatialindex --strip-components=1 \
&& rm -f spatialindex-src-2.0.0.tar.gz \
&& mkdir -p spatialindex/build \
&& cd spatialindex/build \
&& cmake -GNinja -DCMAKE_INSTALL_PREFIX=/usr/local .. \
&& ninja \
&& ninja install
RUN
FROM binary-for-oracle AS binary-only

View File

@ -56,6 +56,7 @@ jobs:
with-sfcgal: ON
with-compile-commands: ON
with-model-test: ON
with-internal-spatialindex: ON
with-pg-test: ON
with-mssql-test: ON
with-hana-test: ON
@ -80,6 +81,7 @@ jobs:
with-sfcgal: ON
with-compile-commands: OFF
with-model-test: OFF
with-internal-spatialindex: OFF
with-pg-test: OFF
with-mssql-test: OFF
with-hana-test: OFF
@ -180,6 +182,7 @@ jobs:
--env WITH_SFCGAL=${{ matrix.with-sfcgal }} \
--env WITH_COMPILE_COMMANDS=${{ matrix.with-compile-commands }} \
--env ENABLE_UNITY_BUILDS=${{ matrix.unity-builds }} \
--env WITH_INTERNAL_SPATIALINDEX=${{ matrix.with-internal-spatialindex }} \
--env WITH_MODEL_TEST=${{ matrix.with-model-test }} \
--env WITH_PG_TEST=${{ matrix.with-pg-test }} \
--env WITH_MSSQL_TEST=${{ matrix.with-mssql-test }} \

21
external/rectpack2D/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Patryk Czachurski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

217
external/rectpack2D/README.md vendored Normal file
View File

@ -0,0 +1,217 @@
<div align="center">
# rectpack2D
[![Linux & Windows Build](https://github.com/TeamHypersomnia/rectpack2D/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/TeamHypersomnia/rectpack2D/actions/workflows/cmake-multi-platform.yml)
**Used in [Assassin's Creed: Valhalla](https://www.youtube.com/watch?v=2KnjDL4DnwM&t=2382s)!**
**Used by [Skydio](https://pages.skydio.com/rs/784-TUF-591/images/Open%20Source%20Software%20Notice%20v0.2.html), one of the top drone manufacturers!**
**[2 scientific references](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=teamhypersomnia&btnG=)!**
</div>
A header-only 2D rectangle packing library written in modern C++.
This is a refactored and **highly optimized** version of the [original library](https://github.com/TeamHypersomnia/rectpack2D/tree/legacy).
It was originally developed for the needs of [Hypersomnia](https://github.com/TeamHypersomnia/Hypersomnia), a free and open-source multiplayer shooter.
![7](https://user-images.githubusercontent.com/3588717/42707552-d8b1c65e-86da-11e8-9412-54c580bd2696.jpg)
## Table of contents
- [Benchmarks](#benchmarks)
- [Usage](#usage)
- [Building the example](#building-the-example)
* [Windows](#windows)
* [Linux](#linux)
- [Algorithm](#algorithm)
* [Insertion algorithm](#insertion-algorithm)
* [Additional heuristics](#additional-heuristics)
## Benchmarks
Tests were conducted on a ``Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz``.
The binary was built with ``clang 6.0.0``, using an -03 switch.
### Arbitrary game sprites: 582 subjects.
**Runtime: 0.8 ms**
**Wasted pixels: 10982 (0.24% - equivalent of a 105 x 105 square)**
Output (1896 x 2382):
![1](images/atlas_small.png)
In color:
(black is wasted space)
![2](images/atlas_small_color.png)
### Arbitrary game sprites + Japanese glyphs: 3264 subjects.
**Runtime: 4 ms**
**Wasted pixels: 15538 (0.31% - equivalent of a 125 x 125 square)**
Output (2116 x 2382):
![3](images/atlas_big.png)
In color:
(black is wasted space)
![4](images/atlas_big_color.png)
### Japanese glyphs + some GUI sprites: 3122 subjects.
**Runtime: 3.5 - 7 ms**
**Wasted pixels: 9288 (1.23% - equivalent of a 96 x 96 square)**
Output (866 x 871):
![5](images/atlas_tiny.png)
In color:
(black is wasted space)
![6](images/atlas_tiny_color.png)
## Usage
This is a header-only library.
Just include the ``src/finders_interface.h`` and you should be good to go.
For an example use, see ``example/main.cpp``.
## Building the example
### Windows
From the repository's folder, run:
```bash
mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" ..
````
Then just build the generated ``.sln`` file using the newest Visual Studio Preview.
### Linux
From the repository's folder, run:
```bash
cmake/build_example.sh Release gcc g++
cd build/current
ninja run
````
Or, if you want to use clang, run:
```bash
cmake/build_example.sh Release clang clang++
cd build/current
ninja run
````
## Algorithm
### Insertion algorithm
The library started as an implementation of this algorithm:
http://blackpawn.com/texts/lightmaps/default.html
The current version somewhat derives from the concept described there -
however, it uses just a **vector of empty spaces, instead of a tree** - this turned out to be a performance breakthrough.
Given
```cpp
struct rect_xywh {
int x;
int y;
int w;
int h;
};
````
Let us create a vector and call it empty_spaces.
```cpp
std::vector<rect_xywh> empty_spaces;
````
Given a user-specified initial bin, which is a square of some size S, we initialize the first empty space.
```cpp
empty_spaces.push_back(rect_xywh(0, 0, S, S));
````
Now, we'd like to insert the first image rectangle.
To do this, we iterate the vector of empty spaces **backwards** and look for an empty space into which the image can fit.
For now, we only have the S x S square: let's save the index of this candidate empty space,
which is ``candidate_space_index = 0;``
If our image is strictly smaller than the candidate space, we have something like this:
![diag01](images/diag01.png)
The blue is our image rectangle.
We now calculate the gray rectangles labeled as "bigger split" and "smaller split",
and save them like this:
```cpp
// Erase the space that we've just inserted to, by swapping and popping.
empty_spaces[candidate_space_index] = empty_spaces.back();
empty_spaces.pop_back();
// Save the resultant splits
empty_spaces.push_back(bigger_split);
empty_spaces.push_back(smaller_split);
````
Notice that we push the smaller split *after* the bigger one.
This is fairly important, because later the candidate images will encounter the smaller splits first,
which will make better use of empty spaces overall.
#### Corner cases:
- If the image dimensions equal the dimensions of the candidate empty space (image fits exactly),
- we just delete the space and create no splits.
- If the image fits into the candidate empty space, but exactly one of the image dimensions equals the respective dimension of the candidate empty space (e.g. image = 20x40, candidate space = 30x40)
- we delete the space and create a single split. In this case a 10x40 space.
To see the complete, modular procedure for calculating the splits (along with the corner cases),
[see this source](src/rectpack2D/insert_and_split.h).
If the insertion fails, we also try the same procedure for a flipped image.
### Additional heuristics
Now we know how to insert individual images into a bin of a given initial size S.
1. However, what S should be passed to the algorithm so that the rectangles end up wasting the least amount of space?
- We perform a binary search.
- We start with the size specified by the library user. Typically, it would be the maximum texture size allowed on a particular GPU.
- If the packing was successful on the given bin size, decrease the size and try to pack again.
- If the packing has failed on the given bin size - because some rectangles could not be further inserted - increase the size and try to pack again.
- The search is aborted if we've successfully inserted into a bin and the dimensions of the next candidate would differ from the previous by less than ``discard_step``.
- This variable exists so that we may easily trade accuracy for a speedup. ``discard_step = 1`` yields the highest accuracy. ``discard_step = 128`` will yield worse packings, but will be a lot faster, etc.
- The search is performed first by decreasing the bin size by both width and height, keeping it in square shape.
- Then we do the same, but only decreasing width.
- Then we do the same, but only decreasing height.
- The last two were a breakthrough in packing tightness. It turns out important to consider non-square bins.
2. In what order should the rectangles be inserted so that they pack the tightest?
- By default, the library tries 5 decreasing orders:
- By area.
- By perimeter.
- By the bigger side.
- By width.
- By height.

286
external/rectpack2D/best_bin_finder.h vendored Normal file
View File

@ -0,0 +1,286 @@
#pragma once
#include <variant>
#include <cassert>
#include "rect_structs.h"
namespace rectpack2D {
enum class callback_result {
ABORT_PACKING,
CONTINUE_PACKING
};
template <class T>
auto& dereference(T& r) {
/*
This will allow us to pass orderings that consist of pointers,
as well as ones that are just plain objects in a vector.
*/
if constexpr(std::is_pointer_v<T>) {
return *r;
}
else {
return r;
}
};
/*
This function will do a binary search on viable bin sizes,
starting from the biggest one: starting_bin.
The search stops when the bin was successfully inserted into,
AND the bin size to be tried next differs in size from the last viable one by *less* then discard_step.
If we could not insert all input rectangles into a bin even as big as the starting_bin - the search fails.
In this case, we return the amount of space (total_area_type) inserted in total.
If we've found a viable bin that is smaller or equal to starting_bin, the search succeeds.
In this case, we return the viable bin (rect_wh).
*/
enum class bin_dimension {
BOTH,
WIDTH,
HEIGHT
};
template <class empty_spaces_type, class O>
std::variant<total_area_type, rect_wh> best_packing_for_ordering_impl(
empty_spaces_type& root,
O ordering,
const rect_wh starting_bin,
int discard_step,
const bin_dimension tried_dimension
) {
auto candidate_bin = starting_bin;
int tries_before_discarding = 0;
if (discard_step <= 0)
{
tries_before_discarding = -discard_step;
discard_step = 1;
}
//std::cout << "best_packing_for_ordering_impl dim: " << int(tried_dimension) << " w: " << starting_bin.w << " h: " << starting_bin.h << std::endl;
int starting_step = 0;
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w /= 2;
candidate_bin.h /= 2;
starting_step = candidate_bin.w / 2;
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w /= 2;
starting_step = candidate_bin.w / 2;
}
else {
candidate_bin.h /= 2;
starting_step = candidate_bin.h / 2;
}
for (int step = starting_step; ; step = std::max(1, step / 2)) {
//std::cout << "candidate: " << candidate_bin.w << "x" << candidate_bin.h << std::endl;
root.reset(candidate_bin);
int total_inserted_area = 0;
const bool all_inserted = [&]() {
for (const auto& r : ordering) {
const auto& rect = dereference(r).get_rect();
if (root.insert(rect.get_wh())) {
total_inserted_area += rect.area();
}
else {
return false;
}
}
return true;
}();
if (all_inserted) {
/* Attempt was successful. Try with a smaller bin. */
if (step <= discard_step) {
if (tries_before_discarding > 0)
{
tries_before_discarding--;
}
else
{
return candidate_bin;
}
}
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w -= step;
candidate_bin.h -= step;
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w -= step;
}
else {
candidate_bin.h -= step;
}
root.reset(candidate_bin);
}
else {
/* Attempt ended with failure. Try with a bigger bin. */
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w += step;
candidate_bin.h += step;
if (candidate_bin.area() > starting_bin.area()) {
return total_inserted_area;
}
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w += step;
if (candidate_bin.w > starting_bin.w) {
return total_inserted_area;
}
}
else {
candidate_bin.h += step;
if (candidate_bin.h > starting_bin.h) {
return total_inserted_area;
}
}
}
}
}
template <class empty_spaces_type, class O>
std::variant<total_area_type, rect_wh> best_packing_for_ordering(
empty_spaces_type& root,
O&& ordering,
const rect_wh starting_bin,
const int discard_step
) {
const auto try_pack = [&](
const bin_dimension tried_dimension,
const rect_wh starting_bin
) {
return best_packing_for_ordering_impl(
root,
std::forward<O>(ordering),
starting_bin,
discard_step,
tried_dimension
);
};
const auto best_result = try_pack(bin_dimension::BOTH, starting_bin);
if (const auto failed = std::get_if<total_area_type>(&best_result)) {
return *failed;
}
auto best_bin = std::get<rect_wh>(best_result);
auto trial = [&](const bin_dimension tried_dimension) {
const auto trial = try_pack(tried_dimension, best_bin);
if (const auto better = std::get_if<rect_wh>(&trial)) {
best_bin = *better;
}
};
trial(bin_dimension::WIDTH);
trial(bin_dimension::HEIGHT);
return best_bin;
}
/*
This function will try to find the best bin size among the ones generated by all provided rectangle orders.
Only the best order will have results written to.
The function reports which of the rectangles did and did not fit in the end.
*/
template <
class empty_spaces_type,
class OrderType,
class F,
class I
>
rect_wh find_best_packing_impl(F for_each_order, const I input) {
const auto max_bin = rect_wh(input.max_bin_side, input.max_bin_side);
OrderType* best_order = nullptr;
int best_total_inserted = -1;
auto best_bin = max_bin;
/*
The root node is re-used on the TLS.
It is always reset before any packing attempt.
*/
thread_local empty_spaces_type root = rect_wh();
root.flipping_mode = input.flipping_mode;
for_each_order ([&](OrderType& current_order) {
const auto packing = best_packing_for_ordering(
root,
current_order,
max_bin,
input.discard_step
);
if (const auto total_inserted = std::get_if<total_area_type>(&packing)) {
/*
Track which function inserts the most area in total,
just in case that all orders will fail to fit into the largest allowed bin.
*/
if (best_order == nullptr) {
if (*total_inserted > best_total_inserted) {
best_order = std::addressof(current_order);
best_total_inserted = *total_inserted;
}
}
}
else if (const auto result_bin = std::get_if<rect_wh>(&packing)) {
/* Save the function if it performed the best. */
if (result_bin->area() <= best_bin.area()) {
best_order = std::addressof(current_order);
best_bin = *result_bin;
}
}
});
{
assert(best_order != nullptr);
root.reset(best_bin);
for (auto& rr : *best_order) {
auto& rect = dereference(rr).get_rect();
if (const auto ret = root.insert(rect.get_wh())) {
rect = *ret;
if (callback_result::ABORT_PACKING == input.handle_successful_insertion(rect)) {
break;
}
}
else {
if (callback_result::ABORT_PACKING == input.handle_unsuccessful_insertion(rect)) {
break;
}
}
}
return root.get_rects_aabb();
}
}
}

View File

@ -0,0 +1,70 @@
#pragma once
#include <array>
#include <vector>
#include <type_traits>
#include "rect_structs.h"
namespace rectpack2D {
class default_empty_spaces {
std::vector<space_rect> empty_spaces;
public:
void remove(const int i) {
empty_spaces[i] = empty_spaces.back();
empty_spaces.pop_back();
}
bool add(const space_rect r) {
empty_spaces.emplace_back(r);
return true;
}
auto get_count() const {
return empty_spaces.size();
}
void reset() {
empty_spaces.clear();
}
const auto& get(const int i) {
return empty_spaces[i];
}
};
template <int MAX_SPACES>
class static_empty_spaces {
int count_spaces = 0;
std::array<space_rect, MAX_SPACES> empty_spaces;
public:
void remove(const int i) {
empty_spaces[i] = empty_spaces[count_spaces - 1];
--count_spaces;
}
bool add(const space_rect r) {
if (count_spaces < static_cast<int>(empty_spaces.size())) {
empty_spaces[count_spaces] = r;
++count_spaces;
return true;
}
return false;
}
auto get_count() const {
return count_spaces;
}
void reset() {
count_spaces = 0;
}
const auto& get(const int i) {
return empty_spaces[i];
}
};
}

149
external/rectpack2D/empty_spaces.h vendored Normal file
View File

@ -0,0 +1,149 @@
#pragma once
#include "insert_and_split.h"
namespace rectpack2D {
enum class flipping_option {
DISABLED,
ENABLED
};
class default_empty_spaces;
template <bool allow_flip, class empty_spaces_provider = default_empty_spaces>
class empty_spaces {
rect_wh current_aabb;
empty_spaces_provider spaces;
/* MSVC fix for non-conformant if constexpr implementation */
static auto make_output_rect(const int x, const int y, const int w, const int h) {
return rect_xywh(x, y, w, h);
}
static auto make_output_rect(const int x, const int y, const int w, const int h, const bool flipped) {
return rect_xywhf(x, y, w, h, flipped);
}
public:
using output_rect_type = std::conditional_t<allow_flip, rect_xywhf, rect_xywh>;
flipping_option flipping_mode = flipping_option::ENABLED;
empty_spaces(const rect_wh& r) {
reset(r);
}
void reset(const rect_wh& r) {
current_aabb = {};
spaces.reset();
spaces.add(rect_xywh(0, 0, r.w, r.h));
}
template <class F>
std::optional<output_rect_type> insert(const rect_wh image_rectangle, F report_candidate_empty_space) {
for (int i = static_cast<int>(spaces.get_count()) - 1; i >= 0; --i) {
const auto candidate_space = spaces.get(i);
report_candidate_empty_space(candidate_space);
auto accept_result = [this, i, image_rectangle, candidate_space](
const created_splits& splits,
const bool flipping_necessary
) -> std::optional<output_rect_type> {
spaces.remove(i);
for (int s = 0; s < splits.count; ++s) {
if (!spaces.add(splits.spaces[s])) {
return std::nullopt;
}
}
if constexpr(allow_flip) {
const auto result = make_output_rect(
candidate_space.x,
candidate_space.y,
image_rectangle.w,
image_rectangle.h,
flipping_necessary
);
current_aabb.expand_with(result);
return result;
}
else if constexpr(!allow_flip) {
(void)flipping_necessary;
const auto result = make_output_rect(
candidate_space.x,
candidate_space.y,
image_rectangle.w,
image_rectangle.h
);
current_aabb.expand_with(result);
return result;
}
};
auto try_to_insert = [&](const rect_wh& img) {
return rectpack2D::insert_and_split(img, candidate_space);
};
if constexpr(!allow_flip) {
if (const auto normal = try_to_insert(image_rectangle)) {
return accept_result(normal, false);
}
}
else {
if (flipping_mode == flipping_option::ENABLED) {
const auto normal = try_to_insert(image_rectangle);
const auto flipped = try_to_insert(rect_wh(image_rectangle).flip());
/*
If both were successful,
prefer the one that generated less remainder spaces.
*/
if (normal && flipped) {
if (flipped.better_than(normal)) {
/* Accept the flipped result if it producues less or "better" spaces. */
return accept_result(flipped, true);
}
return accept_result(normal, false);
}
if (normal) {
return accept_result(normal, false);
}
if (flipped) {
return accept_result(flipped, true);
}
}
else {
if (const auto normal = try_to_insert(image_rectangle)) {
return accept_result(normal, false);
}
}
}
}
return std::nullopt;
}
decltype(auto) insert(const rect_wh& image_rectangle) {
return insert(image_rectangle, [](auto&){ });
}
auto get_rects_aabb() const {
return current_aabb;
}
const auto& get_spaces() const {
return spaces;
}
};
}

154
external/rectpack2D/finders_interface.h vendored Normal file
View File

@ -0,0 +1,154 @@
#pragma once
#include <optional>
#include <vector>
#include <array>
#include <variant>
#include <algorithm>
#include "insert_and_split.h"
#include "empty_spaces.h"
#include "empty_space_allocators.h"
#include "best_bin_finder.h"
namespace rectpack2D {
template <class empty_spaces_type>
using output_rect_t = typename empty_spaces_type::output_rect_type;
template <class F, class G>
struct finder_input {
const int max_bin_side;
const int discard_step;
F handle_successful_insertion;
G handle_unsuccessful_insertion;
const flipping_option flipping_mode;
};
template <class F, class G>
auto make_finder_input(
const int max_bin_side,
const int discard_step,
F&& handle_successful_insertion,
G&& handle_unsuccessful_insertion,
const flipping_option flipping_mode
) {
return finder_input<F, G> {
max_bin_side,
discard_step,
std::forward<F>(handle_successful_insertion),
std::forward<G>(handle_unsuccessful_insertion),
flipping_mode
};
};
/*
Finds the best packing for the rectangles,
just in the order that they were passed.
*/
template <class empty_spaces_type, class Subjects, class F, class G>
rect_wh find_best_packing_dont_sort(
Subjects& subjects,
const finder_input<F, G>& input
) {
using order_type = std::remove_reference_t<decltype(subjects)>;
return find_best_packing_impl<empty_spaces_type, order_type>(
[&subjects](auto callback) { callback(subjects); },
input
);
}
/*
Finds the best packing for the rectangles.
Accepts a list of predicates able to compare two input rectangles.
The function will try to pack the rectangles in all orders generated by the predicates,
and will only write the x, y coordinates of the best packing found among the orders.
*/
template <class empty_spaces_type, class Subjects, class F, class G, class Comparator, class... Comparators>
rect_wh find_best_packing(
Subjects& subjects,
const finder_input<F, G>& input,
Comparator comparator,
Comparators... comparators
) {
using rect_type = output_rect_t<empty_spaces_type>;
using order_type = std::vector<rect_type*>;
constexpr auto count_orders = 1 + sizeof...(Comparators);
thread_local std::array<order_type, count_orders> orders;
{
/* order[0] will always exist since this overload requires at least one comparator */
auto& initial_pointers = orders[0];
initial_pointers.clear();
for (auto& s : subjects) {
auto& r = s.get_rect();
if (r.area() > 0) {
initial_pointers.emplace_back(std::addressof(r));
}
}
for (std::size_t i = 1; i < count_orders; ++i) {
orders[i] = initial_pointers;
}
}
std::size_t f = 0;
auto& orders_ref = orders;
auto make_order = [&f, &orders_ref](auto& predicate) {
std::sort(orders_ref[f].begin(), orders_ref[f].end(), predicate);
++f;
};
make_order(comparator);
(make_order(comparators), ...);
return find_best_packing_impl<empty_spaces_type, order_type>(
[&orders_ref](auto callback){ for (auto& o : orders_ref) { callback(o); } },
input
);
}
/*
Finds the best packing for the rectangles.
Provides a list of several sensible comparison predicates.
*/
template <class empty_spaces_type, class Subjects, class F, class G>
rect_wh find_best_packing(
Subjects& subjects,
const finder_input<F, G>& input
) {
using rect_type = output_rect_t<empty_spaces_type>;
return find_best_packing<empty_spaces_type>(
subjects,
input,
[](const rect_type* const a, const rect_type* const b) {
return a->area() > b->area();
},
[](const rect_type* const a, const rect_type* const b) {
return a->perimeter() > b->perimeter();
},
[](const rect_type* const a, const rect_type* const b) {
return std::max(a->w, a->h) > std::max(b->w, b->h);
},
[](const rect_type* const a, const rect_type* const b) {
return a->w > b->w;
},
[](const rect_type* const a, const rect_type* const b) {
return a->h > b->h;
}
);
}
}

135
external/rectpack2D/insert_and_split.h vendored Normal file
View File

@ -0,0 +1,135 @@
#pragma once
#include <array>
#include "rect_structs.h"
namespace rectpack2D {
struct created_splits {
int count = 0;
std::array<space_rect, 2> spaces;
static auto failed() {
created_splits result;
result.count = -1;
return result;
}
static auto none() {
return created_splits();
}
template <class... Args>
created_splits(Args&&... args) : spaces({ std::forward<Args>(args)... }) {
count = sizeof...(Args);
}
bool better_than(const created_splits& b) const {
return count < b.count;
}
explicit operator bool() const {
return count != -1;
}
};
inline created_splits insert_and_split(
const rect_wh& im, /* Image rectangle */
const space_rect& sp /* Space rectangle */
) {
const auto free_w = sp.w - im.w;
const auto free_h = sp.h - im.h;
if (free_w < 0 || free_h < 0) {
/*
Image is bigger than the candidate empty space.
We'll need to look further.
*/
return created_splits::failed();
}
if (free_w == 0 && free_h == 0) {
/*
If the image dimensions equal the dimensions of the candidate empty space (image fits exactly),
we will just delete the space and create no splits.
*/
return created_splits::none();
}
/*
If the image fits into the candidate empty space,
but exactly one of the image dimensions equals the respective dimension of the candidate empty space
(e.g. image = 20x40, candidate space = 30x40)
we delete the space and create a single split. In this case a 10x40 space.
*/
if (free_w > 0 && free_h == 0) {
auto r = sp;
r.x += im.w;
r.w -= im.w;
return created_splits(r);
}
if (free_w == 0 && free_h > 0) {
auto r = sp;
r.y += im.h;
r.h -= im.h;
return created_splits(r);
}
/*
Every other option has been exhausted,
so at this point the image must be *strictly* smaller than the empty space,
that is, it is smaller in both width and height.
Thus, free_w and free_h must be positive.
*/
/*
Decide which way to split.
Instead of having two normally-sized spaces,
it is better - though I have no proof of that - to have a one tiny space and a one huge space.
This creates better opportunity for insertion of future rectangles.
This is why, if we had more of width remaining than we had of height,
we split along the vertical axis,
and if we had more of height remaining than we had of width,
we split along the horizontal axis.
*/
if (free_w > free_h) {
const auto bigger_split = space_rect(
sp.x + im.w,
sp.y,
free_w,
sp.h
);
const auto lesser_split = space_rect(
sp.x,
sp.y + im.h,
im.w,
free_h
);
return created_splits(bigger_split, lesser_split);
}
const auto bigger_split = space_rect(
sp.x,
sp.y + im.h,
sp.w,
free_h
);
const auto lesser_split = space_rect(
sp.x + im.w,
sp.y,
free_w,
im.h
);
return created_splits(bigger_split, lesser_split);
}
}

90
external/rectpack2D/rect_structs.h vendored Normal file
View File

@ -0,0 +1,90 @@
#pragma once
#include <utility>
namespace rectpack2D {
using total_area_type = int;
struct rect_wh {
rect_wh() : w(0), h(0) {}
rect_wh(const int w, const int h) : w(w), h(h) {}
int w;
int h;
auto& flip() {
std::swap(w, h);
return *this;
}
int max_side() const {
return h > w ? h : w;
}
int min_side() const {
return h < w ? h : w;
}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
template <class R>
void expand_with(const R& r) {
w = std::max(w, r.x + r.w);
h = std::max(h, r.y + r.h);
}
};
struct rect_xywh {
int x;
int y;
int w;
int h;
rect_xywh() : x(0), y(0), w(0), h(0) {}
rect_xywh(const int x, const int y, const int w, const int h) : x(x), y(y), w(w), h(h) {}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
auto get_wh() const {
return rect_wh(w, h);
}
auto& get_rect() {
return *this;
}
const auto& get_rect() const {
return *this;
}
};
struct rect_xywhf {
int x;
int y;
int w;
int h;
bool flipped;
rect_xywhf() : x(0), y(0), w(0), h(0), flipped(false) {}
rect_xywhf(const int x, const int y, const int w, const int h, const bool flipped) : x(x), y(y), w(flipped ? h : w), h(flipped ? w : h), flipped(flipped) {}
rect_xywhf(const rect_xywh& b) : rect_xywhf(b.x, b.y, b.w, b.h, false) {}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
auto get_wh() const {
return rect_wh(w, h);
}
auto& get_rect() {
return *this;
}
const auto& get_rect() const {
return *this;
}
};
using space_rect = rect_xywh;
}

View File

@ -40,7 +40,7 @@ private:
public:
LeafQuery();
~LeafQuery() { }
~LeafQuery();
void getNextEntry( const SpatialIndex::IEntry& entry,
SpatialIndex::id_type& nextEntry,
bool& hasNext);

View File

@ -33,6 +33,8 @@ LeafQuery::LeafQuery()
}
LeafQuery::~LeafQuery() = default;
LeafQueryResult get_results(const SpatialIndex::INode* n)
{
LeafQueryResult result (n->getIdentifier());

View File

@ -47,7 +47,7 @@ static __thread struct
int code;
char message[LAST_ERROR_BUFFER_SIZE];
char method[LAST_ERROR_BUFFER_SIZE];
} last_error = {0};
} last_error = {0, "", ""};
#else
static std::stack<Error> errors;
#endif

View File

@ -548,7 +548,7 @@ bool SpatialIndex::MVRTree::MVRTree::isIndexValid()
//std::cerr << "Total accessible nodes: " << visitedEntries.size() << std::endl;
//std::cerr << "Degenerate nodes: " << degenerateEntries << std::endl;
(void)degenerateEntries;
return ret;
}

View File

@ -45,6 +45,7 @@ namespace Tools
while (! m_pool.empty())
{
SpatialIndex::MVRTree::Node* x = m_pool.top(); m_pool.pop();
(void)x;
}
}

View File

@ -6,6 +6,7 @@
%Include auto_generated/qgs3dtypes.sip
%Include auto_generated/qgs3dmapcanvas.sip
%Include auto_generated/qgsabstractvectorlayer3drenderer.sip
%Include auto_generated/qgsannotationlayer3drenderer.sip
%Include auto_generated/qgscameracontroller.sip
%Include auto_generated/qgscamerapose.sip
%Include auto_generated/qgslayoutitem3dmap.sip
@ -13,6 +14,7 @@
%Include auto_generated/qgstiledscenelayer3drenderer.sip
%Include auto_generated/qgsvectorlayer3drenderer.sip
%Include auto_generated/qgspointcloudlayer3drenderer.sip
%Include auto_generated/qgstextureatlasgenerator.sip
%Include auto_generated/lights/qgsdirectionallightsettings.sip
%Include auto_generated/lights/qgslightsource.sip
%Include auto_generated/lights/qgspointlightsettings.sip

View File

@ -0,0 +1,9 @@
# The following has been generated automatically from src/3d/qgsannotationlayer3drenderer.h
try:
QgsAnnotationLayer3DRendererMetadata.__overridden_methods__ = ['createRenderer']
except (NameError, AttributeError):
pass
try:
QgsAnnotationLayer3DRenderer.__overridden_methods__ = ['type', 'clone', 'writeXml', 'readXml', 'resolveReferences']
except (NameError, AttributeError):
pass

View File

@ -0,0 +1,6 @@
# The following has been generated automatically from src/3d/qgstextureatlasgenerator.h
try:
QgsTextureAtlasGenerator.createFromRects = staticmethod(QgsTextureAtlasGenerator.createFromRects)
QgsTextureAtlasGenerator.createFromImages = staticmethod(QgsTextureAtlasGenerator.createFromImages)
except (NameError, AttributeError):
pass

View File

@ -0,0 +1,154 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgsannotationlayer3drenderer.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
// this is needed for the "convert to subclass" code below to compile
%ModuleHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
class QgsAnnotationLayer3DRendererMetadata : Qgs3DRendererAbstractMetadata
{
%Docstring(signature="appended")
Metadata for annotation layer 3D renderer to allow creation of its
instances from XML.
.. warning::
This is not considered stable API, and may change in future QGIS releases. It is
exposed to the Python bindings as a tech preview only.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
public:
QgsAnnotationLayer3DRendererMetadata();
virtual QgsAbstract3DRenderer *createRenderer( QDomElement &elem, const QgsReadWriteContext &context ) /Factory/;
%Docstring
Creates an instance of a 3D renderer based on a DOM element with
renderer configuration
%End
};
class QgsAnnotationLayer3DRenderer : QgsAbstract3DRenderer
{
%Docstring(signature="appended")
3D renderers for annotation layers.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
%ConvertToSubClassCode
if ( dynamic_cast<QgsAnnotationLayer3DRenderer *>( sipCpp ) != nullptr )
sipType = sipType_QgsAnnotationLayer3DRenderer;
else
sipType = nullptr;
%End
public:
QgsAnnotationLayer3DRenderer();
void setLayer( QgsAnnotationLayer *layer );
%Docstring
Sets the annotation layer associated with the renderer.
.. seealso:: :py:func:`layer`
%End
QgsAnnotationLayer *layer() const;
%Docstring
Returns the annotation layer associated with the renderer.
.. seealso:: :py:func:`setLayer`
%End
virtual QString type() const;
virtual QgsAnnotationLayer3DRenderer *clone() const /Factory/;
virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const;
virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context );
virtual void resolveReferences( const QgsProject &project );
Qgis::AltitudeClamping altitudeClamping() const;
%Docstring
Returns the altitude clamping method, which determines the vertical
position of annotations.
.. seealso:: :py:func:`setAltitudeClamping`
%End
void setAltitudeClamping( Qgis::AltitudeClamping clamping );
%Docstring
Sets the altitude ``clamping`` method, which determines the vertical
position of annotations.
.. seealso:: :py:func:`altitudeClamping`
%End
double zOffset() const;
%Docstring
Returns the z offset, which is a fixed offset amount which should be
added to z values for the annotations.
.. seealso:: :py:func:`setZOffset`
%End
void setZOffset( double offset );
%Docstring
Sets the z ``offset``, which is a fixed offset amount which will be
added to z values for the annotations.
.. seealso:: :py:func:`zOffset`
%End
bool showCalloutLines() const;
%Docstring
Returns ``True`` if callout lines are shown, vertically joining the
annotations to the terrain.
.. seealso:: :py:func:`setShowCalloutLines`
%End
void setShowCalloutLines( bool show );
%Docstring
Sets whether callout lines are shown, vertically joining the annotations
to the terrain.
.. seealso:: :py:func:`showCalloutLines`
%End
private:
QgsAnnotationLayer3DRenderer( const QgsAnnotationLayer3DRenderer & );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgsannotationlayer3drenderer.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -0,0 +1,154 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgstextureatlasgenerator.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
class QgsTextureAtlas
{
%Docstring(signature="appended")
Encapsulates a texture atlas.
:py:class:`QgsTextureAtlas` contains the packed regions for aggregated
texture atlases, and optionally the packed texture map.
See :py:class:`QgsTextureAtlasGenerator` for a class which automatically
creates texture atlases.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgstextureatlasgenerator.h"
%End
public:
QgsTextureAtlas();
~QgsTextureAtlas();
QgsTextureAtlas( const QgsTextureAtlas &other );
bool isValid() const;
%Docstring
Returns ``True`` if the atlas is valid.
%End
QSize atlasSize() const;
%Docstring
Returns the total size required for the atlas, i.e. the total size for
the packed images and rectangles.
%End
QRect rect( int index ) const;
%Docstring
Returns the packed rectangle for the texture with the specified
``index``.
:raises IndexError: if no texture with the specified index exists.
%End
%MethodCode
const int count = sipCpp->count();
if ( a0 < 0 || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return sipConvertFromNewType( new QRect( sipCpp->rect( a0 ) ), sipType_QRect, Py_None );
}
%End
QImage renderAtlasTexture() const;
%Docstring
Renders the combined texture atlas, containing all source images.
.. note::
This may be a null image if the atlas was created with rectangles alone.
%End
QImage renderDebugTexture() const;
%Docstring
Renders a debug texture.
The debug texture renders all packed rectangles with a unique color, and
can be used to visualize the solution.
%End
int count() const;
%Docstring
Returns the number of textures in the atlas.
%End
int __len__() const;
%Docstring
Returns the number of textures in the atlas.
%End
%MethodCode
sipRes
= sipCpp->count();
%End
};
class QgsTextureAtlasGenerator
{
%Docstring(signature="appended")
Generates texture atlases by efficient packing of multiple input
rectangles/images.
:py:class:`QgsTextureAtlasGenerator` can be used to pack either images
or raw rectangles. The static :py:func:`~createFromRects` or
:py:func:`~createFromImages` methods should be called with the source
images or rectangles, which will return a :py:class:`QgsTextureAtlas`
containing the results.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgstextureatlasgenerator.h"
%End
public:
static QgsTextureAtlas createFromRects( const QVector< QRect > &rectangles, int maxSide = 1000 );
%Docstring
Creates a texture atlas for a set of ``rectangles``.
This method should be used when the generator is used to pack rectangle
shapes only. No image will be associated with the rectangle.
The ``maxSide`` argument specifies the maximum permitted side size for
the atlas. The calculated solution can only be less than or equal to
this size - if it cannot fit, then algorithm will gracefully fail and
return an invalid :py:class:`QgsTextureAtlas`.
.. seealso:: :py:func:`createFromImages`
%End
static QgsTextureAtlas createFromImages( const QVector< QImage > &images, int maxSide = 1000 );
%Docstring
Creates a texture atlas for a set of ``images``.
The ``maxSide`` argument specifies the maximum permitted side size for
the atlas. The calculated solution can only be less than or equal to
this size - if it cannot fit, then algorithm will gracefully fail and
return an invalid :py:class:`QgsTextureAtlas`.
.. seealso:: :py:func:`createFromRects`
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgstextureatlasgenerator.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -246,6 +246,22 @@ QgsAbstractVectorLayer3DRenderer.setTilingSettings: src/3d/qgsabstractvectorlaye
QgsAbstractVectorLayer3DRenderer.tilingSettings: src/3d/qgsabstractvectorlayer3drenderer.h#L89
QgsAbstractVectorLayer3DRenderer.writeXmlBaseProperties: src/3d/qgsabstractvectorlayer3drenderer.h#L97
QgsAbstractVectorLayer3DRenderer: src/3d/qgsabstractvectorlayer3drenderer.h#L76
QgsAnnotationLayer3DRenderer.altitudeClamping: src/3d/qgsannotationlayer3drenderer.h#L100
QgsAnnotationLayer3DRenderer.clone: src/3d/qgsannotationlayer3drenderer.h#L88
QgsAnnotationLayer3DRenderer.layer: src/3d/qgsannotationlayer3drenderer.h#L85
QgsAnnotationLayer3DRenderer.readXml: src/3d/qgsannotationlayer3drenderer.h#L92
QgsAnnotationLayer3DRenderer.resolveReferences: src/3d/qgsannotationlayer3drenderer.h#L93
QgsAnnotationLayer3DRenderer.setAltitudeClamping: src/3d/qgsannotationlayer3drenderer.h#L107
QgsAnnotationLayer3DRenderer.setLayer: src/3d/qgsannotationlayer3drenderer.h#L78
QgsAnnotationLayer3DRenderer.setShowCalloutLines: src/3d/qgsannotationlayer3drenderer.h#L135
QgsAnnotationLayer3DRenderer.setZOffset: src/3d/qgsannotationlayer3drenderer.h#L121
QgsAnnotationLayer3DRenderer.showCalloutLines: src/3d/qgsannotationlayer3drenderer.h#L128
QgsAnnotationLayer3DRenderer.type: src/3d/qgsannotationlayer3drenderer.h#L87
QgsAnnotationLayer3DRenderer.writeXml: src/3d/qgsannotationlayer3drenderer.h#L91
QgsAnnotationLayer3DRenderer.zOffset: src/3d/qgsannotationlayer3drenderer.h#L114
QgsAnnotationLayer3DRenderer: src/3d/qgsannotationlayer3drenderer.h#L59
QgsAnnotationLayer3DRendererMetadata.createRenderer: src/3d/qgsannotationlayer3drenderer.h#L50
QgsAnnotationLayer3DRendererMetadata: src/3d/qgsannotationlayer3drenderer.h#L44
QgsCameraController.cameraChanged: src/3d/qgscameracontroller.h#L360
QgsCameraController.cameraMovementSpeed: src/3d/qgscameracontroller.h#L89
QgsCameraController.cameraMovementSpeedChanged: src/3d/qgscameracontroller.h#L368
@ -770,6 +786,17 @@ QgsSingleColorPointCloud3DSymbol.singleColor: src/3d/symbols/qgspointcloud3dsymb
QgsSingleColorPointCloud3DSymbol.symbolType: src/3d/symbols/qgspointcloud3dsymbol.h#L202
QgsSingleColorPointCloud3DSymbol.writeXml: src/3d/symbols/qgspointcloud3dsymbol.h#L205
QgsSingleColorPointCloud3DSymbol: src/3d/symbols/qgspointcloud3dsymbol.h#L197
QgsTextureAtlas.__len__: src/3d/qgstextureatlasgenerator.h#L107
QgsTextureAtlas.atlasSize: src/3d/qgstextureatlasgenerator.h#L58
QgsTextureAtlas.count: src/3d/qgstextureatlasgenerator.h#L104
QgsTextureAtlas.isValid: src/3d/qgstextureatlasgenerator.h#L52
QgsTextureAtlas.rect: src/3d/qgstextureatlasgenerator.h#L71
QgsTextureAtlas.renderAtlasTexture: src/3d/qgstextureatlasgenerator.h#L91
QgsTextureAtlas.renderDebugTexture: src/3d/qgstextureatlasgenerator.h#L99
QgsTextureAtlas: src/3d/qgstextureatlasgenerator.h#L40
QgsTextureAtlasGenerator.createFromImages: src/3d/qgstextureatlasgenerator.h#L161
QgsTextureAtlasGenerator.createFromRects: src/3d/qgstextureatlasgenerator.h#L150
QgsTextureAtlasGenerator: src/3d/qgstextureatlasgenerator.h#L135
QgsTiledSceneLayer3DRenderer.clone: src/3d/qgstiledscenelayer3drenderer.h#L107
QgsTiledSceneLayer3DRenderer.layer: src/3d/qgstiledscenelayer3drenderer.h#L72
QgsTiledSceneLayer3DRenderer.maximumScreenError: src/3d/qgstiledscenelayer3drenderer.h#L81

View File

@ -262,7 +262,7 @@ if (WITH_3D)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/3d/project.py.in ${CMAKE_CURRENT_BINARY_DIR}/3d/project.py @ONLY)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/3d/pyproject.toml.temp" "${toml}")
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/3d/pyproject.toml.temp ${CMAKE_CURRENT_BINARY_DIR}/3d/pyproject.toml)
SET(SIP_CONCAT_PARTS 10) # 3D doesn't have enough .sip files to fill 15+ .cpp files
SET(SIP_CONCAT_PARTS 8) # 3D doesn't have enough .sip files to fill 15+ .cpp files
GENERATE_SIP_PYTHON_MODULE_CODE(qgis._3d_p 3d/3d.sip "${sip_files_3d}" cpp_files)
BUILD_SIP_PYTHON_MODULE(qgis._3d_p 3d/3d.sip ${cpp_files} "" qgis_core qgis_3d)
endif()

View File

@ -6,6 +6,7 @@
%Include auto_generated/qgs3dtypes.sip
%Include auto_generated/qgs3dmapcanvas.sip
%Include auto_generated/qgsabstractvectorlayer3drenderer.sip
%Include auto_generated/qgsannotationlayer3drenderer.sip
%Include auto_generated/qgscameracontroller.sip
%Include auto_generated/qgscamerapose.sip
%Include auto_generated/qgslayoutitem3dmap.sip
@ -13,6 +14,7 @@
%Include auto_generated/qgstiledscenelayer3drenderer.sip
%Include auto_generated/qgsvectorlayer3drenderer.sip
%Include auto_generated/qgspointcloudlayer3drenderer.sip
%Include auto_generated/qgstextureatlasgenerator.sip
%Include auto_generated/lights/qgsdirectionallightsettings.sip
%Include auto_generated/lights/qgslightsource.sip
%Include auto_generated/lights/qgspointlightsettings.sip

View File

@ -0,0 +1,9 @@
# The following has been generated automatically from src/3d/qgsannotationlayer3drenderer.h
try:
QgsAnnotationLayer3DRendererMetadata.__overridden_methods__ = ['createRenderer']
except (NameError, AttributeError):
pass
try:
QgsAnnotationLayer3DRenderer.__overridden_methods__ = ['type', 'clone', 'writeXml', 'readXml', 'resolveReferences']
except (NameError, AttributeError):
pass

View File

@ -0,0 +1,6 @@
# The following has been generated automatically from src/3d/qgstextureatlasgenerator.h
try:
QgsTextureAtlasGenerator.createFromRects = staticmethod(QgsTextureAtlasGenerator.createFromRects)
QgsTextureAtlasGenerator.createFromImages = staticmethod(QgsTextureAtlasGenerator.createFromImages)
except (NameError, AttributeError):
pass

View File

@ -0,0 +1,154 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgsannotationlayer3drenderer.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
// this is needed for the "convert to subclass" code below to compile
%ModuleHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
class QgsAnnotationLayer3DRendererMetadata : Qgs3DRendererAbstractMetadata
{
%Docstring(signature="appended")
Metadata for annotation layer 3D renderer to allow creation of its
instances from XML.
.. warning::
This is not considered stable API, and may change in future QGIS releases. It is
exposed to the Python bindings as a tech preview only.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
public:
QgsAnnotationLayer3DRendererMetadata();
virtual QgsAbstract3DRenderer *createRenderer( QDomElement &elem, const QgsReadWriteContext &context ) /Factory/;
%Docstring
Creates an instance of a 3D renderer based on a DOM element with
renderer configuration
%End
};
class QgsAnnotationLayer3DRenderer : QgsAbstract3DRenderer
{
%Docstring(signature="appended")
3D renderers for annotation layers.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgsannotationlayer3drenderer.h"
%End
%ConvertToSubClassCode
if ( dynamic_cast<QgsAnnotationLayer3DRenderer *>( sipCpp ) != nullptr )
sipType = sipType_QgsAnnotationLayer3DRenderer;
else
sipType = nullptr;
%End
public:
QgsAnnotationLayer3DRenderer();
void setLayer( QgsAnnotationLayer *layer );
%Docstring
Sets the annotation layer associated with the renderer.
.. seealso:: :py:func:`layer`
%End
QgsAnnotationLayer *layer() const;
%Docstring
Returns the annotation layer associated with the renderer.
.. seealso:: :py:func:`setLayer`
%End
virtual QString type() const;
virtual QgsAnnotationLayer3DRenderer *clone() const /Factory/;
virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const;
virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context );
virtual void resolveReferences( const QgsProject &project );
Qgis::AltitudeClamping altitudeClamping() const;
%Docstring
Returns the altitude clamping method, which determines the vertical
position of annotations.
.. seealso:: :py:func:`setAltitudeClamping`
%End
void setAltitudeClamping( Qgis::AltitudeClamping clamping );
%Docstring
Sets the altitude ``clamping`` method, which determines the vertical
position of annotations.
.. seealso:: :py:func:`altitudeClamping`
%End
double zOffset() const;
%Docstring
Returns the z offset, which is a fixed offset amount which should be
added to z values for the annotations.
.. seealso:: :py:func:`setZOffset`
%End
void setZOffset( double offset );
%Docstring
Sets the z ``offset``, which is a fixed offset amount which will be
added to z values for the annotations.
.. seealso:: :py:func:`zOffset`
%End
bool showCalloutLines() const;
%Docstring
Returns ``True`` if callout lines are shown, vertically joining the
annotations to the terrain.
.. seealso:: :py:func:`setShowCalloutLines`
%End
void setShowCalloutLines( bool show );
%Docstring
Sets whether callout lines are shown, vertically joining the annotations
to the terrain.
.. seealso:: :py:func:`showCalloutLines`
%End
private:
QgsAnnotationLayer3DRenderer( const QgsAnnotationLayer3DRenderer & );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgsannotationlayer3drenderer.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -0,0 +1,154 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgstextureatlasgenerator.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
class QgsTextureAtlas
{
%Docstring(signature="appended")
Encapsulates a texture atlas.
:py:class:`QgsTextureAtlas` contains the packed regions for aggregated
texture atlases, and optionally the packed texture map.
See :py:class:`QgsTextureAtlasGenerator` for a class which automatically
creates texture atlases.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgstextureatlasgenerator.h"
%End
public:
QgsTextureAtlas();
~QgsTextureAtlas();
QgsTextureAtlas( const QgsTextureAtlas &other );
bool isValid() const;
%Docstring
Returns ``True`` if the atlas is valid.
%End
QSize atlasSize() const;
%Docstring
Returns the total size required for the atlas, i.e. the total size for
the packed images and rectangles.
%End
QRect rect( int index ) const;
%Docstring
Returns the packed rectangle for the texture with the specified
``index``.
:raises IndexError: if no texture with the specified index exists.
%End
%MethodCode
const int count = sipCpp->count();
if ( a0 < 0 || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return sipConvertFromNewType( new QRect( sipCpp->rect( a0 ) ), sipType_QRect, Py_None );
}
%End
QImage renderAtlasTexture() const;
%Docstring
Renders the combined texture atlas, containing all source images.
.. note::
This may be a null image if the atlas was created with rectangles alone.
%End
QImage renderDebugTexture() const;
%Docstring
Renders a debug texture.
The debug texture renders all packed rectangles with a unique color, and
can be used to visualize the solution.
%End
int count() const;
%Docstring
Returns the number of textures in the atlas.
%End
Py_ssize_t __len__() const;
%Docstring
Returns the number of textures in the atlas.
%End
%MethodCode
sipRes
= sipCpp->count();
%End
};
class QgsTextureAtlasGenerator
{
%Docstring(signature="appended")
Generates texture atlases by efficient packing of multiple input
rectangles/images.
:py:class:`QgsTextureAtlasGenerator` can be used to pack either images
or raw rectangles. The static :py:func:`~createFromRects` or
:py:func:`~createFromImages` methods should be called with the source
images or rectangles, which will return a :py:class:`QgsTextureAtlas`
containing the results.
.. versionadded:: 4.0
%End
%TypeHeaderCode
#include "qgstextureatlasgenerator.h"
%End
public:
static QgsTextureAtlas createFromRects( const QVector< QRect > &rectangles, int maxSide = 1000 );
%Docstring
Creates a texture atlas for a set of ``rectangles``.
This method should be used when the generator is used to pack rectangle
shapes only. No image will be associated with the rectangle.
The ``maxSide`` argument specifies the maximum permitted side size for
the atlas. The calculated solution can only be less than or equal to
this size - if it cannot fit, then algorithm will gracefully fail and
return an invalid :py:class:`QgsTextureAtlas`.
.. seealso:: :py:func:`createFromImages`
%End
static QgsTextureAtlas createFromImages( const QVector< QImage > &images, int maxSide = 1000 );
%Docstring
Creates a texture atlas for a set of ``images``.
The ``maxSide`` argument specifies the maximum permitted side size for
the atlas. The calculated solution can only be less than or equal to
this size - if it cannot fit, then algorithm will gracefully fail and
return an invalid :py:class:`QgsTextureAtlas`.
.. seealso:: :py:func:`createFromRects`
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/3d/qgstextureatlasgenerator.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -246,6 +246,22 @@ QgsAbstractVectorLayer3DRenderer.setTilingSettings: src/3d/qgsabstractvectorlaye
QgsAbstractVectorLayer3DRenderer.tilingSettings: src/3d/qgsabstractvectorlayer3drenderer.h#L89
QgsAbstractVectorLayer3DRenderer.writeXmlBaseProperties: src/3d/qgsabstractvectorlayer3drenderer.h#L97
QgsAbstractVectorLayer3DRenderer: src/3d/qgsabstractvectorlayer3drenderer.h#L76
QgsAnnotationLayer3DRenderer.altitudeClamping: src/3d/qgsannotationlayer3drenderer.h#L100
QgsAnnotationLayer3DRenderer.clone: src/3d/qgsannotationlayer3drenderer.h#L88
QgsAnnotationLayer3DRenderer.layer: src/3d/qgsannotationlayer3drenderer.h#L85
QgsAnnotationLayer3DRenderer.readXml: src/3d/qgsannotationlayer3drenderer.h#L92
QgsAnnotationLayer3DRenderer.resolveReferences: src/3d/qgsannotationlayer3drenderer.h#L93
QgsAnnotationLayer3DRenderer.setAltitudeClamping: src/3d/qgsannotationlayer3drenderer.h#L107
QgsAnnotationLayer3DRenderer.setLayer: src/3d/qgsannotationlayer3drenderer.h#L78
QgsAnnotationLayer3DRenderer.setShowCalloutLines: src/3d/qgsannotationlayer3drenderer.h#L135
QgsAnnotationLayer3DRenderer.setZOffset: src/3d/qgsannotationlayer3drenderer.h#L121
QgsAnnotationLayer3DRenderer.showCalloutLines: src/3d/qgsannotationlayer3drenderer.h#L128
QgsAnnotationLayer3DRenderer.type: src/3d/qgsannotationlayer3drenderer.h#L87
QgsAnnotationLayer3DRenderer.writeXml: src/3d/qgsannotationlayer3drenderer.h#L91
QgsAnnotationLayer3DRenderer.zOffset: src/3d/qgsannotationlayer3drenderer.h#L114
QgsAnnotationLayer3DRenderer: src/3d/qgsannotationlayer3drenderer.h#L59
QgsAnnotationLayer3DRendererMetadata.createRenderer: src/3d/qgsannotationlayer3drenderer.h#L50
QgsAnnotationLayer3DRendererMetadata: src/3d/qgsannotationlayer3drenderer.h#L44
QgsCameraController.cameraChanged: src/3d/qgscameracontroller.h#L360
QgsCameraController.cameraMovementSpeed: src/3d/qgscameracontroller.h#L89
QgsCameraController.cameraMovementSpeedChanged: src/3d/qgscameracontroller.h#L368
@ -770,6 +786,17 @@ QgsSingleColorPointCloud3DSymbol.singleColor: src/3d/symbols/qgspointcloud3dsymb
QgsSingleColorPointCloud3DSymbol.symbolType: src/3d/symbols/qgspointcloud3dsymbol.h#L202
QgsSingleColorPointCloud3DSymbol.writeXml: src/3d/symbols/qgspointcloud3dsymbol.h#L205
QgsSingleColorPointCloud3DSymbol: src/3d/symbols/qgspointcloud3dsymbol.h#L197
QgsTextureAtlas.__len__: src/3d/qgstextureatlasgenerator.h#L107
QgsTextureAtlas.atlasSize: src/3d/qgstextureatlasgenerator.h#L58
QgsTextureAtlas.count: src/3d/qgstextureatlasgenerator.h#L104
QgsTextureAtlas.isValid: src/3d/qgstextureatlasgenerator.h#L52
QgsTextureAtlas.rect: src/3d/qgstextureatlasgenerator.h#L71
QgsTextureAtlas.renderAtlasTexture: src/3d/qgstextureatlasgenerator.h#L91
QgsTextureAtlas.renderDebugTexture: src/3d/qgstextureatlasgenerator.h#L99
QgsTextureAtlas: src/3d/qgstextureatlasgenerator.h#L40
QgsTextureAtlasGenerator.createFromImages: src/3d/qgstextureatlasgenerator.h#L161
QgsTextureAtlasGenerator.createFromRects: src/3d/qgstextureatlasgenerator.h#L150
QgsTextureAtlasGenerator: src/3d/qgstextureatlasgenerator.h#L135
QgsTiledSceneLayer3DRenderer.clone: src/3d/qgstiledscenelayer3drenderer.h#L107
QgsTiledSceneLayer3DRenderer.layer: src/3d/qgstiledscenelayer3drenderer.h#L72
QgsTiledSceneLayer3DRenderer.maximumScreenError: src/3d/qgstiledscenelayer3drenderer.h#L81

View File

@ -11742,6 +11742,25 @@ Qgis.RasterProcessingParameterCapability.baseClass = Qgis
Qgis.RasterProcessingParameterCapabilities = lambda flags=0: Qgis.RasterProcessingParameterCapability(flags)
Qgis.RasterProcessingParameterCapabilities.baseClass = Qgis
RasterProcessingParameterCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.DevToolsNodeRole.Status.__doc__ = "Request status role"
Qgis.DevToolsNodeRole.Id.__doc__ = "Request ID role"
Qgis.DevToolsNodeRole.ElapsedTime.__doc__ = "Elapsed time"
Qgis.DevToolsNodeRole.MaximumTime.__doc__ = "Maximum encountered elapsed time"
Qgis.DevToolsNodeRole.Sort.__doc__ = "Sort order role"
Qgis.DevToolsNodeRole.__doc__ = """Dev tools node custom data roles.
.. versionadded:: 4.0
* ``Status``: Request status role
* ``Id``: Request ID role
* ``ElapsedTime``: Elapsed time
* ``MaximumTime``: Maximum encountered elapsed time
* ``Sort``: Sort order role
"""
# --
Qgis.DevToolsNodeRole.baseClass = Qgis
try:
Qgis.__attribute_docs__ = {'QGIS_DEV_VERSION': 'The development version', 'DEFAULT_SEARCH_RADIUS_MM': 'Identify search radius in mm', 'DEFAULT_MAPTOPIXEL_THRESHOLD': 'Default threshold between map coordinates and device coordinates for map2pixel simplification', 'DEFAULT_HIGHLIGHT_COLOR': 'Default highlight color. The transparency is expected to only be applied to polygon\nfill. Lines and outlines are rendered opaque.', 'DEFAULT_HIGHLIGHT_BUFFER_MM': 'Default highlight buffer in mm.', 'DEFAULT_HIGHLIGHT_MIN_WIDTH_MM': 'Default highlight line/stroke minimum width in mm.', 'SCALE_PRECISION': 'Fudge factor used to compare two scales. The code is often going from scale to scale\ndenominator. So it looses precision and, when a limit is inclusive, can lead to errors.\nTo avoid that, use this factor instead of using <= or >=.\n\n.. deprecated:: 3.40\n\n No longer used by QGIS and will be removed in QGIS 4.0.', 'DEFAULT_Z_COORDINATE': 'Default Z coordinate value.\nThis value have to be assigned to the Z coordinate for the vertex.', 'DEFAULT_M_COORDINATE': 'Default M coordinate value.\nThis value have to be assigned to the M coordinate for the vertex.\n\n.. versionadded:: 3.20', 'UI_SCALE_FACTOR': 'UI scaling factor. This should be applied to all widget sizes obtained from font metrics,\nto account for differences in the default font sizes across different platforms.', 'DEFAULT_SNAP_TOLERANCE': 'Default snapping distance tolerance.', 'DEFAULT_SNAP_UNITS': 'Default snapping distance units.', 'USER_CRS_START_ID': 'Minimum ID number for a user-defined projection.', 'DEFAULT_POINT_SIZE': 'The default size (in millimeters) for point marker symbols', 'DEFAULT_LINE_WIDTH': 'The default width (in millimeters) for line symbols', 'DEFAULT_SEGMENT_EPSILON': 'Default snapping tolerance for segments'}
Qgis.__annotations__ = {'QGIS_DEV_VERSION': str, 'DEFAULT_SEARCH_RADIUS_MM': float, 'DEFAULT_MAPTOPIXEL_THRESHOLD': float, 'DEFAULT_HIGHLIGHT_COLOR': 'QColor', 'DEFAULT_HIGHLIGHT_BUFFER_MM': float, 'DEFAULT_HIGHLIGHT_MIN_WIDTH_MM': float, 'SCALE_PRECISION': float, 'DEFAULT_Z_COORDINATE': float, 'DEFAULT_M_COORDINATE': float, 'UI_SCALE_FACTOR': float, 'DEFAULT_SNAP_TOLERANCE': float, 'DEFAULT_SNAP_UNITS': 'Qgis.MapToolUnit', 'USER_CRS_START_ID': int, 'DEFAULT_POINT_SIZE': float, 'DEFAULT_LINE_WIDTH': float, 'DEFAULT_SEGMENT_EPSILON': float}

View File

@ -3446,6 +3446,15 @@ The development version
typedef QFlags<Qgis::RasterProcessingParameterCapability> RasterProcessingParameterCapabilities;
enum class DevToolsNodeRole
{
Status,
Id,
ElapsedTime,
MaximumTime,
Sort,
};
static const double DEFAULT_SEARCH_RADIUS_MM;
static const float DEFAULT_MAPTOPIXEL_THRESHOLD;

View File

@ -1,15 +1,15 @@
Qgis.defaultProjectScales: src/core/qgis.h#L6171
Qgis.defaultProjectScales: src/core/qgis.h#L6185
Qgis.devVersion: src/core/qgis.h#L90
Qgis.geoNone: src/core/qgis.h#L6223
Qgis.geoProj4: src/core/qgis.h#L6253
Qgis.geoWkt: src/core/qgis.h#L6244
Qgis.geographicCrsAuthId: src/core/qgis.h#L6233
Qgis.geosVersion: src/core/qgis.h#L6206
Qgis.geosVersionInt: src/core/qgis.h#L6178
Qgis.geosVersionMajor: src/core/qgis.h#L6185
Qgis.geosVersionMinor: src/core/qgis.h#L6192
Qgis.geosVersionPatch: src/core/qgis.h#L6199
Qgis.hasQtWebkit: src/core/qgis.h#L6213
Qgis.geoNone: src/core/qgis.h#L6237
Qgis.geoProj4: src/core/qgis.h#L6267
Qgis.geoWkt: src/core/qgis.h#L6258
Qgis.geographicCrsAuthId: src/core/qgis.h#L6247
Qgis.geosVersion: src/core/qgis.h#L6220
Qgis.geosVersionInt: src/core/qgis.h#L6192
Qgis.geosVersionMajor: src/core/qgis.h#L6199
Qgis.geosVersionMinor: src/core/qgis.h#L6206
Qgis.geosVersionPatch: src/core/qgis.h#L6213
Qgis.hasQtWebkit: src/core/qgis.h#L6227
Qgis.releaseName: src/core/qgis.h#L80
Qgis.version: src/core/qgis.h#L66
Qgis.versionInt: src/core/qgis.h#L73

View File

@ -0,0 +1,18 @@
# The following has been generated automatically from src/gui/qgsshortcutsmanager.h
# monkey patching scoped based enum
QgsShortcutsManager.CommonAction.CodeToggleComment.__doc__ = "Toggle code comments"
QgsShortcutsManager.CommonAction.CodeReformat.__doc__ = "Reformat code"
QgsShortcutsManager.CommonAction.CodeRunScript.__doc__ = "Run script"
QgsShortcutsManager.CommonAction.CodeRunSelection.__doc__ = "Run selection from script"
QgsShortcutsManager.CommonAction.__doc__ = """Contains common actions which are used across a variety of classes.
.. versionadded:: 4.0
* ``CodeToggleComment``: Toggle code comments
* ``CodeReformat``: Reformat code
* ``CodeRunScript``: Run script
* ``CodeRunSelection``: Run selection from script
"""
# --
QgsShortcutsManager.CommonAction.baseClass = QgsShortcutsManager

View File

@ -23,6 +23,14 @@ rather accessed through :py:func:`QgsGui.shortcutsManager()`.
#include "qgsshortcutsmanager.h"
%End
public:
enum class CommonAction
{
CodeToggleComment,
CodeReformat,
CodeRunScript,
CodeRunSelection,
};
QgsShortcutsManager( QObject *parent /TransferThis/ = 0, const QString &settingsRoot = "/shortcuts/" );
%Docstring
Constructor for QgsShortcutsManager.
@ -36,6 +44,8 @@ Constructor for QgsShortcutsManager.
QGIS actions.
%End
~QgsShortcutsManager();
void registerAllChildren( QObject *object, bool recursive = false, const QString &section = QString() );
%Docstring
Automatically registers all QActions and QShortcuts which are children
@ -105,6 +115,16 @@ in GUI.
.. seealso:: :py:func:`unregisterAction`
.. seealso:: :py:func:`registerAllChildActions`
%End
void initializeCommonAction( QAction *action, CommonAction commonAction );
%Docstring
Initializes an ``action`` as a common action.
This automatically configures the ``action`` to use the properties for
the common action, such as setting the action's tooltip and shortcut.
.. versionadded:: 4.0
%End
bool registerShortcut( QShortcut *shortcut, const QString &defaultSequence = QString(), const QString &section = QString() );
@ -290,6 +310,14 @@ if no shortcut is associated.
.. seealso:: :py:func:`objectForSequence`
.. seealso:: :py:func:`actionForSequence`
%End
QKeySequence sequenceForCommonAction( CommonAction action ) const;
%Docstring
Returns the key sequence which is associated with a common ``action``,
or an empty sequence if no shortcut is assigned to that action.
.. versionadded:: 4.0
%End
QAction *actionByName( const QString &name ) const;

View File

@ -6977,28 +6977,30 @@ QgsShapeburstFillSymbolLayerWidget.setColor: src/gui/symbology/qgssymbollayerwid
QgsShapeburstFillSymbolLayerWidget.setSymbolLayer: src/gui/symbology/qgssymbollayerwidget.h#L426
QgsShapeburstFillSymbolLayerWidget.symbolLayer: src/gui/symbology/qgssymbollayerwidget.h#L427
QgsShapeburstFillSymbolLayerWidget: src/gui/symbology/qgssymbollayerwidget.h#L407
QgsShortcutsManager.actionByName: src/gui/qgsshortcutsmanager.h#L236
QgsShortcutsManager.actionForSequence: src/gui/qgsshortcutsmanager.h#L221
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L163
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L171
QgsShortcutsManager.objectDefaultKeySequence: src/gui/qgsshortcutsmanager.h#L155
QgsShortcutsManager.objectForSequence: src/gui/qgsshortcutsmanager.h#L213
QgsShortcutsManager.objectForSettingKey: src/gui/qgsshortcutsmanager.h#L262
QgsShortcutsManager.objectSettingKey: src/gui/qgsshortcutsmanager.h#L254
QgsShortcutsManager.registerAction: src/gui/qgsshortcutsmanager.h#L94
QgsShortcutsManager.registerAllChildActions: src/gui/qgsshortcutsmanager.h#L70
QgsShortcutsManager.registerAllChildShortcuts: src/gui/qgsshortcutsmanager.h#L81
QgsShortcutsManager.registerAllChildren: src/gui/qgsshortcutsmanager.h#L59
QgsShortcutsManager.registerShortcut: src/gui/qgsshortcutsmanager.h#L106
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L180
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L196
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L204
QgsShortcutsManager.setObjectKeySequence: src/gui/qgsshortcutsmanager.h#L188
QgsShortcutsManager.settingsPath: src/gui/qgsshortcutsmanager.h#L246
QgsShortcutsManager.shortcutByName: src/gui/qgsshortcutsmanager.h#L243
QgsShortcutsManager.shortcutForSequence: src/gui/qgsshortcutsmanager.h#L229
QgsShortcutsManager.unregisterAction: src/gui/qgsshortcutsmanager.h#L116
QgsShortcutsManager.unregisterShortcut: src/gui/qgsshortcutsmanager.h#L126
QgsShortcutsManager.actionByName: src/gui/qgsshortcutsmanager.h#L267
QgsShortcutsManager.actionForSequence: src/gui/qgsshortcutsmanager.h#L246
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L188
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L196
QgsShortcutsManager.initializeCommonAction: src/gui/qgsshortcutsmanager.h#L119
QgsShortcutsManager.objectDefaultKeySequence: src/gui/qgsshortcutsmanager.h#L180
QgsShortcutsManager.objectForSequence: src/gui/qgsshortcutsmanager.h#L238
QgsShortcutsManager.objectForSettingKey: src/gui/qgsshortcutsmanager.h#L293
QgsShortcutsManager.objectSettingKey: src/gui/qgsshortcutsmanager.h#L285
QgsShortcutsManager.registerAction: src/gui/qgsshortcutsmanager.h#L109
QgsShortcutsManager.registerAllChildActions: src/gui/qgsshortcutsmanager.h#L85
QgsShortcutsManager.registerAllChildShortcuts: src/gui/qgsshortcutsmanager.h#L96
QgsShortcutsManager.registerAllChildren: src/gui/qgsshortcutsmanager.h#L74
QgsShortcutsManager.registerShortcut: src/gui/qgsshortcutsmanager.h#L131
QgsShortcutsManager.sequenceForCommonAction: src/gui/qgsshortcutsmanager.h#L260
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L205
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L221
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L229
QgsShortcutsManager.setObjectKeySequence: src/gui/qgsshortcutsmanager.h#L213
QgsShortcutsManager.settingsPath: src/gui/qgsshortcutsmanager.h#L277
QgsShortcutsManager.shortcutByName: src/gui/qgsshortcutsmanager.h#L274
QgsShortcutsManager.shortcutForSequence: src/gui/qgsshortcutsmanager.h#L254
QgsShortcutsManager.unregisterAction: src/gui/qgsshortcutsmanager.h#L141
QgsShortcutsManager.unregisterShortcut: src/gui/qgsshortcutsmanager.h#L151
QgsShortcutsManager: src/gui/qgsshortcutsmanager.h#L36
QgsSimpleFillSymbolLayerWidget.create: src/gui/symbology/qgssymbollayerwidget.h#L264
QgsSimpleFillSymbolLayerWidget.setColor: src/gui/symbology/qgssymbollayerwidget.h#L271

View File

@ -19,5 +19,4 @@ __author__ = "Salvatore Larosa"
__date__ = "September 2012"
__copyright__ = "(C) 2012, Salvatore Larosa"
from .console import show_console # NOQA
from .console import init_options_widget
from .console import show_console, init_console # NOQA

View File

@ -61,6 +61,7 @@ from qgis.gui import (
QgsGui,
QgsApplicationExitBlockerInterface,
QgsCodeEditorDockWidget,
QgsShortcutsManager,
)
from functools import partial
@ -106,8 +107,11 @@ def console_displayhook(obj):
_console_output = obj
def init_options_widget():
"""called from QGIS to add the console options widget"""
def init_console():
"""
Called from QGIS to initialize the console related options and shortcuts,
before the dock is shown
"""
global _options_factory
_options_factory.setTitle(QCoreApplication.translate("PythonConsole", "Python"))
iface.registerOptionsWidgetFactory(_options_factory)
@ -221,145 +225,137 @@ class PythonConsoleWidget(QWidget):
# Action for Open File
openFileBt = QCoreApplication.translate("PythonConsole", "Open Script…")
self.openFileButton = QAction(self)
self.openFileButton.setCheckable(False)
self.openFileButton.setEnabled(True)
self.openFileButton.setIcon(
self.open_file_action = QAction(self)
self.open_file_action.setIcon(
QgsApplication.getThemeIcon("mActionScriptOpen.svg")
)
self.openFileButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.openFileButton.setIconVisibleInMenu(True)
self.openFileButton.setToolTip(openFileBt)
self.openFileButton.setText(openFileBt)
self.open_file_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.open_file_action.setIconVisibleInMenu(True)
self.open_file_action.setToolTip(f"<b>{openFileBt}</b> (Ctrl+O)")
self.open_file_action.setText(openFileBt)
openExtEditorBt = QCoreApplication.translate(
"PythonConsole", "Open in External Editor"
)
self.openInEditorButton = QAction(self)
self.openInEditorButton.setCheckable(False)
self.openInEditorButton.setEnabled(True)
self.openInEditorButton.setIcon(
self.open_in_editor_action = QAction(self)
self.open_in_editor_action.setIcon(
QgsApplication.getThemeIcon("console/iconShowEditorConsole.svg")
)
self.openInEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.openInEditorButton.setIconVisibleInMenu(True)
self.openInEditorButton.setToolTip(openExtEditorBt)
self.openInEditorButton.setText(openExtEditorBt)
self.open_in_editor_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.open_in_editor_action.setIconVisibleInMenu(True)
self.open_in_editor_action.setToolTip(openExtEditorBt)
self.open_in_editor_action.setText(openExtEditorBt)
# Action for Save File
saveFileBt = QCoreApplication.translate("PythonConsole", "Save")
self.saveFileButton = QAction(self)
self.saveFileButton.setCheckable(False)
self.saveFileButton.setEnabled(False)
self.saveFileButton.setIcon(QgsApplication.getThemeIcon("mActionFileSave.svg"))
self.saveFileButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.saveFileButton.setIconVisibleInMenu(True)
self.saveFileButton.setToolTip(saveFileBt)
self.saveFileButton.setText(saveFileBt)
self.save_file_action = QAction(self)
self.save_file_action.setEnabled(False)
self.save_file_action.setIcon(
QgsApplication.getThemeIcon("mActionFileSave.svg")
)
self.save_file_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.save_file_action.setIconVisibleInMenu(True)
self.save_file_action.setToolTip(f"<b>{saveFileBt}</b> (Ctrl+S)")
self.save_file_action.setText(saveFileBt)
# Action for Save File As
saveAsFileBt = QCoreApplication.translate("PythonConsole", "Save As…")
self.saveAsFileButton = QAction(self)
self.saveAsFileButton.setCheckable(False)
self.saveAsFileButton.setEnabled(True)
self.saveAsFileButton.setIcon(
self.save_as_file_action = QAction(self)
self.save_as_file_action.setIcon(
QgsApplication.getThemeIcon("mActionFileSaveAs.svg")
)
self.saveAsFileButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.saveAsFileButton.setIconVisibleInMenu(True)
self.saveAsFileButton.setToolTip(saveAsFileBt)
self.saveAsFileButton.setText(saveAsFileBt)
self.save_as_file_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.save_as_file_action.setIconVisibleInMenu(True)
self.save_as_file_action.setToolTip(f"<b>{saveAsFileBt}</b> (Ctrl+Shift+S)")
self.save_as_file_action.setText(saveAsFileBt)
# Action Cut
cutEditorBt = QCoreApplication.translate("PythonConsole", "Cut")
self.cutEditorButton = QAction(self)
self.cutEditorButton.setCheckable(False)
self.cutEditorButton.setEnabled(True)
self.cutEditorButton.setIcon(QgsApplication.getThemeIcon("mActionEditCut.svg"))
self.cutEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.cutEditorButton.setIconVisibleInMenu(True)
self.cutEditorButton.setToolTip(cutEditorBt)
self.cutEditorButton.setText(cutEditorBt)
self.cut_action = QAction(self)
self.cut_action.setIcon(QgsApplication.getThemeIcon("mActionEditCut.svg"))
self.cut_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.cut_action.setIconVisibleInMenu(True)
self.cut_action.setToolTip(f"<b>{cutEditorBt}</b> (Ctrl+X)")
self.cut_action.setText(cutEditorBt)
# Action Copy
copyEditorBt = QCoreApplication.translate("PythonConsole", "Copy")
self.copyEditorButton = QAction(self)
self.copyEditorButton.setCheckable(False)
self.copyEditorButton.setEnabled(True)
self.copyEditorButton.setIcon(
QgsApplication.getThemeIcon("mActionEditCopy.svg")
)
self.copyEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.copyEditorButton.setIconVisibleInMenu(True)
self.copyEditorButton.setToolTip(copyEditorBt)
self.copyEditorButton.setText(copyEditorBt)
self.copy_action = QAction(self)
self.copy_action.setIcon(QgsApplication.getThemeIcon("mActionEditCopy.svg"))
self.copy_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.copy_action.setIconVisibleInMenu(True)
self.copy_action.setToolTip(f"<b>{copyEditorBt}</b> (Ctrl+C)")
self.copy_action.setText(copyEditorBt)
# Action Paste
pasteEditorBt = QCoreApplication.translate("PythonConsole", "Paste")
self.pasteEditorButton = QAction(self)
self.pasteEditorButton.setCheckable(False)
self.pasteEditorButton.setEnabled(True)
self.pasteEditorButton.setIcon(
QgsApplication.getThemeIcon("mActionEditPaste.svg")
)
self.pasteEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.pasteEditorButton.setIconVisibleInMenu(True)
self.pasteEditorButton.setToolTip(pasteEditorBt)
self.pasteEditorButton.setText(pasteEditorBt)
self.paste_action = QAction(self)
self.paste_action.setIcon(QgsApplication.getThemeIcon("mActionEditPaste.svg"))
self.paste_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.paste_action.setIconVisibleInMenu(True)
self.paste_action.setToolTip(f"<b>{pasteEditorBt}</b> (Ctrl+V)")
self.paste_action.setText(pasteEditorBt)
# Action Run Script (subprocess)
runScriptEditorBt = QCoreApplication.translate("PythonConsole", "Run Script")
self.runScriptEditorButton = QAction(self)
self.runScriptEditorButton.setCheckable(False)
self.runScriptEditorButton.setEnabled(True)
self.runScriptEditorButton.setIcon(
QgsApplication.getThemeIcon("mActionStart.svg")
self.run_script_action = QAction(self)
self.run_script_action.setIcon(QgsApplication.getThemeIcon("mActionStart.svg"))
self.run_script_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.run_script_action.setIconVisibleInMenu(True)
QgsGui.shortcutsManager().initializeCommonAction(
self.run_script_action, QgsShortcutsManager.CommonAction.CodeRunScript
)
# Action Run Selected
self.run_selection_action = QAction(self)
self.run_selection_action.setIcon(
QgsApplication.getThemeIcon("mActionRunSelected.svg")
)
self.run_selection_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.run_selection_action.setIconVisibleInMenu(True)
QgsGui.shortcutsManager().initializeCommonAction(
self.run_selection_action, QgsShortcutsManager.CommonAction.CodeRunSelection
)
self.runScriptEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.runScriptEditorButton.setIconVisibleInMenu(True)
self.runScriptEditorButton.setToolTip(runScriptEditorBt)
self.runScriptEditorButton.setText(runScriptEditorBt)
# Action Toggle comment
toggleText = QCoreApplication.translate("PythonConsole", "Toggle Comment")
self.toggleCommentEditorButton = QAction(self)
self.toggleCommentEditorButton.setCheckable(False)
self.toggleCommentEditorButton.setEnabled(True)
self.toggleCommentEditorButton.setIcon(
self.toggle_comment_action = QAction(self)
self.toggle_comment_action.setIcon(
QgsApplication.getThemeIcon(
"console/iconCommentEditorConsole.svg",
self.palette().color(QPalette.ColorRole.WindowText),
),
)
self.toggleCommentEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.toggleCommentEditorButton.setIconVisibleInMenu(True)
self.toggleCommentEditorButton.setToolTip(toggleText + " <b>Ctrl+:</b>")
self.toggleCommentEditorButton.setText(toggleText)
self.toggle_comment_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.toggle_comment_action.setIconVisibleInMenu(True)
QgsGui.shortcutsManager().initializeCommonAction(
self.toggle_comment_action,
QgsShortcutsManager.CommonAction.CodeToggleComment,
)
# Action Format code
reformatCodeText = QCoreApplication.translate("PythonConsole", "Reformat Code")
self.reformatCodeEditorButton = QAction(self)
self.reformatCodeEditorButton.setCheckable(False)
self.reformatCodeEditorButton.setEnabled(True)
self.reformatCodeEditorButton.setIcon(
self.reformat_code_action = QAction(self)
self.reformat_code_action.setIcon(
QgsApplication.getThemeIcon("console/iconFormatCode.svg")
)
self.reformatCodeEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.reformatCodeEditorButton.setIconVisibleInMenu(True)
self.reformatCodeEditorButton.setToolTip(
reformatCodeText + " <b>Ctrl+Alt+F</b>"
self.reformat_code_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.reformat_code_action.setIconVisibleInMenu(True)
QgsGui.shortcutsManager().initializeCommonAction(
self.reformat_code_action, QgsShortcutsManager.CommonAction.CodeReformat
)
self.reformatCodeEditorButton.setShortcut("Ctrl+Alt+F")
self.reformatCodeEditorButton.setText(reformatCodeText)
# Action for Object browser
objList = QCoreApplication.translate("PythonConsole", "Object Inspector…")
self.objectListButton = QAction(self)
self.objectListButton.setCheckable(True)
self.objectListButton.setEnabled(
self.object_inspector_action = QAction(self)
self.object_inspector_action.setCheckable(True)
self.object_inspector_action.setEnabled(
QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool)
)
self.objectListButton.setIcon(
self.object_inspector_action.setIcon(
QgsApplication.getThemeIcon("console/iconClassBrowserConsole.svg")
)
self.objectListButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.objectListButton.setIconVisibleInMenu(True)
self.objectListButton.setToolTip(objList)
self.objectListButton.setText(objList)
self.object_inspector_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.object_inspector_action.setIconVisibleInMenu(True)
self.object_inspector_action.setToolTip(objList)
self.object_inspector_action.setText(objList)
# Action for Find text
findText = QCoreApplication.translate("PythonConsole", "Find Text")
@ -371,7 +367,7 @@ class PythonConsoleWidget(QWidget):
)
self.find_text_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.find_text_action.setIconVisibleInMenu(True)
self.find_text_action.setToolTip(findText)
self.find_text_action.setToolTip(f"<b>{findText}</b> (Ctrl+F)")
self.find_text_action.setText(findText)
self.tabEditorWidget.search_bar_toggled.connect(
@ -383,72 +379,65 @@ class PythonConsoleWidget(QWidget):
# Action Show Editor
showEditor = QCoreApplication.translate("PythonConsole", "Show Editor")
self.showEditorButton = QAction(self)
self.showEditorButton.setEnabled(True)
self.showEditorButton.setCheckable(True)
self.showEditorButton.setIcon(
self.show_editor_action = QAction(self)
self.show_editor_action.setCheckable(True)
self.show_editor_action.setIcon(
QgsApplication.getThemeIcon("console/iconShowEditorConsole.svg")
)
self.showEditorButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.showEditorButton.setIconVisibleInMenu(True)
self.showEditorButton.setToolTip(showEditor)
self.showEditorButton.setText(showEditor)
self.show_editor_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.show_editor_action.setIconVisibleInMenu(True)
self.show_editor_action.setToolTip(showEditor)
self.show_editor_action.setText(showEditor)
# Action for Clear button
clearBt = QCoreApplication.translate("PythonConsole", "Clear Console")
self.clearButton = QAction(self)
self.clearButton.setCheckable(False)
self.clearButton.setEnabled(True)
self.clearButton.setIcon(
self.clear_action = QAction(self)
self.clear_action.setIcon(
QgsApplication.getThemeIcon("console/iconClearConsole.svg")
)
self.clearButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.clearButton.setIconVisibleInMenu(True)
self.clearButton.setToolTip(clearBt)
self.clearButton.setText(clearBt)
self.clear_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.clear_action.setIconVisibleInMenu(True)
self.clear_action.setToolTip(clearBt)
self.clear_action.setText(clearBt)
# Action for settings
optionsBt = QCoreApplication.translate("PythonConsole", "Options…")
self.optionsButton = QAction(self)
self.optionsButton.setCheckable(False)
self.optionsButton.setEnabled(True)
self.optionsButton.setIcon(
self.options_action = QAction(self)
self.options_action.setIcon(
QgsApplication.getThemeIcon("console/iconSettingsConsole.svg")
)
self.optionsButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.optionsButton.setIconVisibleInMenu(True)
self.optionsButton.setToolTip(optionsBt)
self.optionsButton.setText(optionsBt)
self.options_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.options_action.setIconVisibleInMenu(True)
self.options_action.setToolTip(optionsBt)
self.options_action.setText(optionsBt)
# Action for Run script
runBt = QCoreApplication.translate("PythonConsole", "Run Command")
self.runButton = QAction(self)
self.runButton.setCheckable(False)
self.runButton.setEnabled(True)
self.runButton.setIcon(QgsApplication.getThemeIcon("mActionStart.svg"))
self.runButton.setMenuRole(QAction.MenuRole.PreferencesRole)
self.runButton.setIconVisibleInMenu(True)
self.runButton.setToolTip(runBt)
self.runButton.setText(runBt)
self.run_action = QAction(self)
self.run_action.setIcon(QgsApplication.getThemeIcon("mActionStart.svg"))
self.run_action.setMenuRole(QAction.MenuRole.PreferencesRole)
self.run_action.setIconVisibleInMenu(True)
self.run_action.setToolTip(runBt)
self.run_action.setText(runBt)
# Help button
self.helpConsoleAction = QAction(self)
self.helpConsoleAction.setEnabled(True)
self.helpConsoleAction.setText(
self.help_console_action = QAction(self)
self.help_console_action.setText(
QCoreApplication.translate("PythonConsole", "Python Console Help")
)
self.helpAPIAction = QAction(self)
self.helpAPIAction.setEnabled(True)
self.helpAPIAction.setText(
self.help_api_action = QAction(self)
self.help_api_action.setText(
QCoreApplication.translate("PythonConsole", "PyQGIS API Documentation")
)
self.helpCookbookAction = QAction(self)
self.helpCookbookAction.setEnabled(True)
self.helpCookbookAction.setText(
self.help_cookbook_action = QAction(self)
self.help_cookbook_action.setText(
QCoreApplication.translate("PythonConsole", "PyQGIS Cookbook")
)
self.helpMenu = QMenu(self)
self.helpMenu.addAction(self.helpConsoleAction)
self.helpMenu.addAction(self.helpAPIAction)
self.helpMenu.addAction(self.helpCookbookAction)
self.helpMenu.addAction(self.help_console_action)
self.helpMenu.addAction(self.help_api_action)
self.helpMenu.addAction(self.help_cookbook_action)
helpBt = QCoreApplication.translate("PythonConsole", "Help…")
self.helpButton = QToolButton(self)
@ -468,12 +457,12 @@ class PythonConsoleWidget(QWidget):
self.toolBar.setIconSize(icon_size)
self.toolBar.setMovable(False)
self.toolBar.setFloatable(False)
self.toolBar.addAction(self.clearButton)
self.toolBar.addAction(self.runButton)
self.toolBar.addAction(self.clear_action)
self.toolBar.addAction(self.run_action)
self.toolBar.addSeparator()
self.toolBar.addAction(self.showEditorButton)
self.toolBar.addAction(self.show_editor_action)
self.toolBar.addSeparator()
self.toolBar.addAction(self.optionsButton)
self.toolBar.addAction(self.options_action)
self.toolBar.addWidget(self.helpButton)
self.toolBar.addSeparator()
self.toolBar.addWidget(parent.dockToggleButton())
@ -486,24 +475,25 @@ class PythonConsoleWidget(QWidget):
self.toolBarEditor.setIconSize(icon_size)
self.toolBarEditor.setMovable(False)
self.toolBarEditor.setFloatable(False)
self.toolBarEditor.addAction(self.openFileButton)
self.toolBarEditor.addAction(self.openInEditorButton)
self.toolBarEditor.addAction(self.open_file_action)
self.toolBarEditor.addAction(self.open_in_editor_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.saveFileButton)
self.toolBarEditor.addAction(self.saveAsFileButton)
self.toolBarEditor.addAction(self.save_file_action)
self.toolBarEditor.addAction(self.save_as_file_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.runScriptEditorButton)
self.toolBarEditor.addAction(self.run_script_action)
self.toolBarEditor.addAction(self.run_selection_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.cutEditorButton)
self.toolBarEditor.addAction(self.copyEditorButton)
self.toolBarEditor.addAction(self.pasteEditorButton)
self.toolBarEditor.addAction(self.cut_action)
self.toolBarEditor.addAction(self.copy_action)
self.toolBarEditor.addAction(self.paste_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.find_text_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.toggleCommentEditorButton)
self.toolBarEditor.addAction(self.reformatCodeEditorButton)
self.toolBarEditor.addAction(self.toggle_comment_action)
self.toolBarEditor.addAction(self.reformat_code_action)
self.toolBarEditor.addSeparator()
self.toolBarEditor.addAction(self.objectListButton)
self.toolBarEditor.addAction(self.object_inspector_action)
self.widgetButton = QWidget()
sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
@ -557,24 +547,25 @@ class PythonConsoleWidget(QWidget):
# ------------ Signal -------------------------------
self.objectListButton.toggled.connect(self.toggleObjectListWidget)
self.toggleCommentEditorButton.triggered.connect(self.toggleComment)
self.reformatCodeEditorButton.triggered.connect(self.reformatCode)
self.runScriptEditorButton.triggered.connect(self.runScriptEditor)
self.cutEditorButton.triggered.connect(self.cutEditor)
self.copyEditorButton.triggered.connect(self.copyEditor)
self.pasteEditorButton.triggered.connect(self.pasteEditor)
self.showEditorButton.toggled.connect(self.toggleEditor)
self.clearButton.triggered.connect(self.shell_output.clearConsole)
self.optionsButton.triggered.connect(self.openSettings)
self.runButton.triggered.connect(self.shell.entered)
self.openFileButton.triggered.connect(self.openScriptFile)
self.openInEditorButton.triggered.connect(self.openScriptFileExtEditor)
self.saveFileButton.triggered.connect(self.saveScriptFile)
self.saveAsFileButton.triggered.connect(self.saveAsScriptFile)
self.helpConsoleAction.triggered.connect(self.openHelpConsole)
self.helpAPIAction.triggered.connect(self.openHelpAPI)
self.helpCookbookAction.triggered.connect(self.openHelpCookbook)
self.object_inspector_action.toggled.connect(self.toggleObjectListWidget)
self.toggle_comment_action.triggered.connect(self.toggleComment)
self.reformat_code_action.triggered.connect(self.reformatCode)
self.run_script_action.triggered.connect(self.runScriptEditor)
self.run_selection_action.triggered.connect(self.runSelectedEditor)
self.cut_action.triggered.connect(self.cutEditor)
self.copy_action.triggered.connect(self.copyEditor)
self.paste_action.triggered.connect(self.pasteEditor)
self.show_editor_action.toggled.connect(self.toggleEditor)
self.clear_action.triggered.connect(self.shell_output.clearConsole)
self.options_action.triggered.connect(self.openSettings)
self.run_action.triggered.connect(self.shell.entered)
self.open_file_action.triggered.connect(self.openScriptFile)
self.open_in_editor_action.triggered.connect(self.openScriptFileExtEditor)
self.save_file_action.triggered.connect(self.saveScriptFile)
self.save_as_file_action.triggered.connect(self.saveAsScriptFile)
self.help_console_action.triggered.connect(self.openHelpConsole)
self.help_api_action.triggered.connect(self.openHelpAPI)
self.help_cookbook_action.triggered.connect(self.openHelpCookbook)
self.listClassMethod.itemClicked.connect(self.onClickGoToLine)
if iface is not None:
@ -653,6 +644,9 @@ class PythonConsoleWidget(QWidget):
def runScriptEditor(self):
self.tabEditorWidget.currentWidget().runScriptCode()
def runSelectedEditor(self):
self.tabEditorWidget.currentWidget().runSelectedCode()
def toggleComment(self):
self.tabEditorWidget.currentWidget().toggleComment()

View File

@ -33,7 +33,13 @@ from operator import itemgetter
from pathlib import Path
from qgis.core import Qgis, QgsApplication, QgsBlockingNetworkRequest, QgsSettings
from qgis.gui import QgsCodeEditorPython, QgsCodeEditorWidget, QgsMessageBar
from qgis.gui import (
QgsCodeEditorPython,
QgsCodeEditorWidget,
QgsGui,
QgsMessageBar,
QgsShortcutsManager,
)
from qgis.PyQt.Qsci import QsciScintilla
from qgis.PyQt.QtCore import (
@ -116,9 +122,6 @@ class Editor(QgsCodeEditorPython):
self.redoScut.setContext(Qt.ShortcutContext.WidgetShortcut)
self.redoScut.activated.connect(self.redo)
self.newShortcutCS.activated.connect(self.autoComplete)
self.runScut = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_E), self)
self.runScut.setContext(Qt.ShortcutContext.WidgetShortcut)
self.runScut.activated.connect(self.runSelectedCode) # spellok
self.runScriptScut = QShortcut(
QKeySequence(Qt.Modifier.SHIFT | Qt.Modifier.CTRL | Qt.Key.Key_E), self
)
@ -162,15 +165,6 @@ class Editor(QgsCodeEditorPython):
syntaxCheckAction.setShortcut("Ctrl+4")
menu.addAction(syntaxCheckAction)
runSelected = QAction(
QgsApplication.getThemeIcon("console/mIconRunConsole.svg"), # spellok
QCoreApplication.translate("PythonConsole", "Run Selected"),
menu,
)
runSelected.triggered.connect(self.runSelectedCode) # spellok
runSelected.setShortcut("Ctrl+E") # spellok
menu.addAction(runSelected) # spellok
word = self.selectedText() or self.wordAtPoint(e.pos())
if word:
context_help_action = QAction(
@ -188,13 +182,24 @@ class Editor(QgsCodeEditorPython):
context_help_action.setShortcut(QKeySequence.StandardKey.HelpContents)
menu.addAction(context_help_action)
start_action = QAction(
QgsApplication.getThemeIcon("mActionStart.svg"),
QCoreApplication.translate("PythonConsole", "Run Script"),
menu,
run_selection_action = QAction(menu)
run_selection_action.setIcon(
QgsApplication.getThemeIcon("mActionRunSelected.svg"),
)
run_selection_action.triggered.connect(self.runSelectedCode)
QgsGui.shortcutsManager().initializeCommonAction(
run_selection_action,
QgsShortcutsManager.CommonAction.CodeRunSelection,
)
menu.addAction(run_selection_action)
start_action = QAction(self)
start_action.setIcon(QgsApplication.getThemeIcon("mActionStart.svg"))
start_action.triggered.connect(self.runScriptCode)
start_action.setShortcut("Ctrl+Shift+E")
QgsGui.shortcutsManager().initializeCommonAction(
start_action,
QgsShortcutsManager.CommonAction.CodeRunScript,
)
menu.addAction(start_action)
menu.addSeparator()
@ -223,6 +228,7 @@ class Editor(QgsCodeEditorPython):
menu,
)
find_action.triggered.connect(self.trigger_find)
find_action.setShortcut("Ctrl+F")
menu.addAction(find_action)
cutAction = QAction(
@ -260,18 +266,31 @@ class Editor(QgsCodeEditorPython):
menu.addAction(selectAllAction)
menu.addSeparator()
toggle_comment_action = QAction(
toggle_comment_action = QAction(menu)
toggle_comment_action.setIcon(
QgsApplication.getThemeIcon(
"console/iconCommentEditorConsole.svg",
self.palette().color(QPalette.ColorRole.WindowText),
),
QCoreApplication.translate("PythonConsole", "Toggle Comment"),
menu,
)
)
toggle_comment_action.triggered.connect(self.toggleComment)
toggle_comment_action.setShortcut("Ctrl+:")
QgsGui.shortcutsManager().initializeCommonAction(
toggle_comment_action,
QgsShortcutsManager.CommonAction.CodeToggleComment,
)
menu.addAction(toggle_comment_action)
reformat_code_action = QAction(menu)
reformat_code_action.setIcon(
QgsApplication.getThemeIcon("console/iconFormatCode.svg")
)
reformat_code_action.triggered.connect(self.reformatCode)
QgsGui.shortcutsManager().initializeCommonAction(
reformat_code_action,
QgsShortcutsManager.CommonAction.CodeReformat,
)
menu.addAction(reformat_code_action)
menu.addSeparator()
gist_menu = QMenu(self)
gist_menu.setTitle(
@ -301,14 +320,14 @@ class Editor(QgsCodeEditorPython):
syntaxCheckAction.setEnabled(False)
pasteAction.setEnabled(False)
cutAction.setEnabled(False)
runSelected.setEnabled(False) # spellok
run_selection_action.setEnabled(False)
copyAction.setEnabled(False)
selectAllAction.setEnabled(False)
undoAction.setEnabled(False)
redoAction.setEnabled(False)
showCodeInspection.setEnabled(False)
if self.hasSelectedText():
runSelected.setEnabled(True) # spellok
run_selection_action.setEnabled(True)
copyAction.setEnabled(True)
cutAction.setEnabled(True)
if not self.text() == "":
@ -328,17 +347,17 @@ class Editor(QgsCodeEditorPython):
listObj = self.console_widget.listClassMethod
if listObj.isVisible():
listObj.hide()
self.console_widget.objectListButton.setChecked(False)
self.console_widget.object_inspector_action.setChecked(False)
else:
listObj.show()
self.console_widget.objectListButton.setChecked(True)
self.console_widget.object_inspector_action.setChecked(True)
def shareOnGist(self, is_public):
self.code_editor_widget.shareOnGist(is_public)
def hideEditor(self):
self.console_widget.splitterObj.hide()
self.console_widget.showEditorButton.setChecked(False)
self.console_widget.show_editor_action.setChecked(False)
def createTempFile(self):
name = tempfile.NamedTemporaryFile(delete=False).name
@ -477,7 +496,7 @@ class Editor(QgsCodeEditorPython):
)
self.tab_widget.setTabToolTip(index, self.code_editor_widget.filePath())
self.setModified(False)
self.console_widget.saveFileButton.setEnabled(False)
self.console_widget.save_file_action.setEnabled(False)
self.console_widget.updateTabListScript(
self.code_editor_widget.filePath(), action="append"
)
@ -722,7 +741,7 @@ class EditorTabWidget(QTabWidget):
# New Editor button
self.newTabButton = QToolButton()
txtToolTipNewTab = QCoreApplication.translate("PythonConsole", "New Editor")
self.newTabButton.setToolTip(txtToolTipNewTab)
self.newTabButton.setToolTip(f"<b>{txtToolTipNewTab}</b> (Ctrl+T)")
self.newTabButton.setAutoRaise(True)
self.newTabButton.setIcon(
QgsApplication.getThemeIcon("console/iconNewTabEditorConsole.svg")
@ -812,7 +831,7 @@ class EditorTabWidget(QTabWidget):
def enableSaveIfModified(self, tab):
tabWidget = self.widget(tab)
if tabWidget:
self.console_widget.saveFileButton.setEnabled(tabWidget.isModified())
self.console_widget.save_file_action.setEnabled(tabWidget.isModified())
def enableToolBarEditor(self, enable):
if self.topFrame.isVisible():
@ -866,7 +885,7 @@ class EditorTabWidget(QTabWidget):
index = self.indexOf(tab)
s = self.tabText(index)
self.setTabTitle(index, f"*{s}" if modified else re.sub(r"^(\*)", "", s))
self.console_widget.saveFileButton.setEnabled(modified)
self.console_widget.save_file_action.setEnabled(modified)
def setTabTitle(self, tab, title):
self.setTabText(tab, title)
@ -1073,10 +1092,9 @@ class EditorTabWidget(QTabWidget):
objInspectorEnabled = QgsSettings().value(
"pythonConsole/enableObjectInsp", False, type=bool
)
listObj = self.console_widget.objectListButton
if self.console_widget.listClassMethod.isVisible():
listObj.setChecked(objInspectorEnabled)
listObj.setEnabled(objInspectorEnabled)
self.console_widget.object_inspector_action.setChecked(objInspectorEnabled)
self.console_widget.object_inspector_action.setEnabled(objInspectorEnabled)
if objInspectorEnabled:
cW = self.currentWidget()
if cW and not self.console_widget.listClassMethod.isVisible():

View File

@ -348,7 +348,7 @@ class ShellOutputScintilla(QgsCodeEditorPython):
Ed = self.console_widget.splitterObj
if not Ed.isVisible():
Ed.show()
self.console_widget.showEditorButton.setChecked(True)
self.console_widget.show_editor_action.setChecked(True)
self.shell_editor.setFocus()
def copy(self):

View File

@ -11645,6 +11645,25 @@ Qgis.RasterProcessingParameterCapability.__doc__ = """Capabilities of a raster l
Qgis.RasterProcessingParameterCapability.baseClass = Qgis
Qgis.RasterProcessingParameterCapabilities.baseClass = Qgis
RasterProcessingParameterCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.DevToolsNodeRole.Status.__doc__ = "Request status role"
Qgis.DevToolsNodeRole.Id.__doc__ = "Request ID role"
Qgis.DevToolsNodeRole.ElapsedTime.__doc__ = "Elapsed time"
Qgis.DevToolsNodeRole.MaximumTime.__doc__ = "Maximum encountered elapsed time"
Qgis.DevToolsNodeRole.Sort.__doc__ = "Sort order role"
Qgis.DevToolsNodeRole.__doc__ = """Dev tools node custom data roles.
.. versionadded:: 4.0
* ``Status``: Request status role
* ``Id``: Request ID role
* ``ElapsedTime``: Elapsed time
* ``MaximumTime``: Maximum encountered elapsed time
* ``Sort``: Sort order role
"""
# --
Qgis.DevToolsNodeRole.baseClass = Qgis
from enum import Enum

View File

@ -3446,6 +3446,15 @@ The development version
typedef QFlags<Qgis::RasterProcessingParameterCapability> RasterProcessingParameterCapabilities;
enum class DevToolsNodeRole
{
Status,
Id,
ElapsedTime,
MaximumTime,
Sort,
};
static const double DEFAULT_SEARCH_RADIUS_MM;
static const float DEFAULT_MAPTOPIXEL_THRESHOLD;

View File

@ -1,15 +1,15 @@
Qgis.defaultProjectScales: src/core/qgis.h#L6171
Qgis.defaultProjectScales: src/core/qgis.h#L6185
Qgis.devVersion: src/core/qgis.h#L90
Qgis.geoNone: src/core/qgis.h#L6223
Qgis.geoProj4: src/core/qgis.h#L6253
Qgis.geoWkt: src/core/qgis.h#L6244
Qgis.geographicCrsAuthId: src/core/qgis.h#L6233
Qgis.geosVersion: src/core/qgis.h#L6206
Qgis.geosVersionInt: src/core/qgis.h#L6178
Qgis.geosVersionMajor: src/core/qgis.h#L6185
Qgis.geosVersionMinor: src/core/qgis.h#L6192
Qgis.geosVersionPatch: src/core/qgis.h#L6199
Qgis.hasQtWebkit: src/core/qgis.h#L6213
Qgis.geoNone: src/core/qgis.h#L6237
Qgis.geoProj4: src/core/qgis.h#L6267
Qgis.geoWkt: src/core/qgis.h#L6258
Qgis.geographicCrsAuthId: src/core/qgis.h#L6247
Qgis.geosVersion: src/core/qgis.h#L6220
Qgis.geosVersionInt: src/core/qgis.h#L6192
Qgis.geosVersionMajor: src/core/qgis.h#L6199
Qgis.geosVersionMinor: src/core/qgis.h#L6206
Qgis.geosVersionPatch: src/core/qgis.h#L6213
Qgis.hasQtWebkit: src/core/qgis.h#L6227
Qgis.releaseName: src/core/qgis.h#L80
Qgis.version: src/core/qgis.h#L66
Qgis.versionInt: src/core/qgis.h#L73

View File

@ -0,0 +1,18 @@
# The following has been generated automatically from src/gui/qgsshortcutsmanager.h
# monkey patching scoped based enum
QgsShortcutsManager.CommonAction.CodeToggleComment.__doc__ = "Toggle code comments"
QgsShortcutsManager.CommonAction.CodeReformat.__doc__ = "Reformat code"
QgsShortcutsManager.CommonAction.CodeRunScript.__doc__ = "Run script"
QgsShortcutsManager.CommonAction.CodeRunSelection.__doc__ = "Run selection from script"
QgsShortcutsManager.CommonAction.__doc__ = """Contains common actions which are used across a variety of classes.
.. versionadded:: 4.0
* ``CodeToggleComment``: Toggle code comments
* ``CodeReformat``: Reformat code
* ``CodeRunScript``: Run script
* ``CodeRunSelection``: Run selection from script
"""
# --
QgsShortcutsManager.CommonAction.baseClass = QgsShortcutsManager

View File

@ -23,6 +23,14 @@ rather accessed through :py:func:`QgsGui.shortcutsManager()`.
#include "qgsshortcutsmanager.h"
%End
public:
enum class CommonAction
{
CodeToggleComment,
CodeReformat,
CodeRunScript,
CodeRunSelection,
};
QgsShortcutsManager( QObject *parent /TransferThis/ = 0, const QString &settingsRoot = "/shortcuts/" );
%Docstring
Constructor for QgsShortcutsManager.
@ -36,6 +44,8 @@ Constructor for QgsShortcutsManager.
QGIS actions.
%End
~QgsShortcutsManager();
void registerAllChildren( QObject *object, bool recursive = false, const QString &section = QString() );
%Docstring
Automatically registers all QActions and QShortcuts which are children
@ -105,6 +115,16 @@ in GUI.
.. seealso:: :py:func:`unregisterAction`
.. seealso:: :py:func:`registerAllChildActions`
%End
void initializeCommonAction( QAction *action, CommonAction commonAction );
%Docstring
Initializes an ``action`` as a common action.
This automatically configures the ``action`` to use the properties for
the common action, such as setting the action's tooltip and shortcut.
.. versionadded:: 4.0
%End
bool registerShortcut( QShortcut *shortcut, const QString &defaultSequence = QString(), const QString &section = QString() );
@ -290,6 +310,14 @@ if no shortcut is associated.
.. seealso:: :py:func:`objectForSequence`
.. seealso:: :py:func:`actionForSequence`
%End
QKeySequence sequenceForCommonAction( CommonAction action ) const;
%Docstring
Returns the key sequence which is associated with a common ``action``,
or an empty sequence if no shortcut is assigned to that action.
.. versionadded:: 4.0
%End
QAction *actionByName( const QString &name ) const;

View File

@ -6977,28 +6977,30 @@ QgsShapeburstFillSymbolLayerWidget.setColor: src/gui/symbology/qgssymbollayerwid
QgsShapeburstFillSymbolLayerWidget.setSymbolLayer: src/gui/symbology/qgssymbollayerwidget.h#L426
QgsShapeburstFillSymbolLayerWidget.symbolLayer: src/gui/symbology/qgssymbollayerwidget.h#L427
QgsShapeburstFillSymbolLayerWidget: src/gui/symbology/qgssymbollayerwidget.h#L407
QgsShortcutsManager.actionByName: src/gui/qgsshortcutsmanager.h#L236
QgsShortcutsManager.actionForSequence: src/gui/qgsshortcutsmanager.h#L221
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L163
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L171
QgsShortcutsManager.objectDefaultKeySequence: src/gui/qgsshortcutsmanager.h#L155
QgsShortcutsManager.objectForSequence: src/gui/qgsshortcutsmanager.h#L213
QgsShortcutsManager.objectForSettingKey: src/gui/qgsshortcutsmanager.h#L262
QgsShortcutsManager.objectSettingKey: src/gui/qgsshortcutsmanager.h#L254
QgsShortcutsManager.registerAction: src/gui/qgsshortcutsmanager.h#L94
QgsShortcutsManager.registerAllChildActions: src/gui/qgsshortcutsmanager.h#L70
QgsShortcutsManager.registerAllChildShortcuts: src/gui/qgsshortcutsmanager.h#L81
QgsShortcutsManager.registerAllChildren: src/gui/qgsshortcutsmanager.h#L59
QgsShortcutsManager.registerShortcut: src/gui/qgsshortcutsmanager.h#L106
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L180
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L196
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L204
QgsShortcutsManager.setObjectKeySequence: src/gui/qgsshortcutsmanager.h#L188
QgsShortcutsManager.settingsPath: src/gui/qgsshortcutsmanager.h#L246
QgsShortcutsManager.shortcutByName: src/gui/qgsshortcutsmanager.h#L243
QgsShortcutsManager.shortcutForSequence: src/gui/qgsshortcutsmanager.h#L229
QgsShortcutsManager.unregisterAction: src/gui/qgsshortcutsmanager.h#L116
QgsShortcutsManager.unregisterShortcut: src/gui/qgsshortcutsmanager.h#L126
QgsShortcutsManager.actionByName: src/gui/qgsshortcutsmanager.h#L267
QgsShortcutsManager.actionForSequence: src/gui/qgsshortcutsmanager.h#L246
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L188
QgsShortcutsManager.defaultKeySequence: src/gui/qgsshortcutsmanager.h#L196
QgsShortcutsManager.initializeCommonAction: src/gui/qgsshortcutsmanager.h#L119
QgsShortcutsManager.objectDefaultKeySequence: src/gui/qgsshortcutsmanager.h#L180
QgsShortcutsManager.objectForSequence: src/gui/qgsshortcutsmanager.h#L238
QgsShortcutsManager.objectForSettingKey: src/gui/qgsshortcutsmanager.h#L293
QgsShortcutsManager.objectSettingKey: src/gui/qgsshortcutsmanager.h#L285
QgsShortcutsManager.registerAction: src/gui/qgsshortcutsmanager.h#L109
QgsShortcutsManager.registerAllChildActions: src/gui/qgsshortcutsmanager.h#L85
QgsShortcutsManager.registerAllChildShortcuts: src/gui/qgsshortcutsmanager.h#L96
QgsShortcutsManager.registerAllChildren: src/gui/qgsshortcutsmanager.h#L74
QgsShortcutsManager.registerShortcut: src/gui/qgsshortcutsmanager.h#L131
QgsShortcutsManager.sequenceForCommonAction: src/gui/qgsshortcutsmanager.h#L260
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L205
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L221
QgsShortcutsManager.setKeySequence: src/gui/qgsshortcutsmanager.h#L229
QgsShortcutsManager.setObjectKeySequence: src/gui/qgsshortcutsmanager.h#L213
QgsShortcutsManager.settingsPath: src/gui/qgsshortcutsmanager.h#L277
QgsShortcutsManager.shortcutByName: src/gui/qgsshortcutsmanager.h#L274
QgsShortcutsManager.shortcutForSequence: src/gui/qgsshortcutsmanager.h#L254
QgsShortcutsManager.unregisterAction: src/gui/qgsshortcutsmanager.h#L141
QgsShortcutsManager.unregisterShortcut: src/gui/qgsshortcutsmanager.h#L151
QgsShortcutsManager: src/gui/qgsshortcutsmanager.h#L36
QgsSimpleFillSymbolLayerWidget.create: src/gui/symbology/qgssymbollayerwidget.h#L264
QgsSimpleFillSymbolLayerWidget.setColor: src/gui/symbology/qgssymbollayerwidget.h#L271

View File

@ -266,6 +266,19 @@ class merge(GdalAlgorithm):
arguments.append(list_file)
return [
self.commandName() + (".bat" if isWindows() else ".py"),
self.commandName() + merge.command_ext(),
GdalUtils.escapeAndJoin(arguments),
]
@staticmethod
def command_ext() -> str:
"""
Returns the gdal_merge command extension
"""
if isWindows():
return ".bat"
if GdalUtils.version() < 3090000:
return ".py"
return ""

View File

@ -30,7 +30,7 @@ from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QPalette
from qgis.PyQt.QtWidgets import QMessageBox, QFileDialog, QVBoxLayout
from qgis.gui import QgsGui, QgsErrorDialog, QgsCodeEditorWidget
from qgis.gui import QgsGui, QgsErrorDialog, QgsCodeEditorWidget, QgsShortcutsManager
from qgis.core import (
QgsApplication,
QgsFileUtils,
@ -122,6 +122,9 @@ class ScriptEditorDialog(BASE, WIDGET):
self.palette().color(QPalette.ColorRole.WindowText),
)
)
QgsGui.shortcutsManager().initializeCommonAction(
self.actionToggleComment, QgsShortcutsManager.CommonAction.CodeToggleComment
)
# Connect signals and slots
self.actionOpenScript.triggered.connect(self.openScript)

View File

@ -4866,6 +4866,8 @@ class TestGdalRasterAlgorithms(QgisTestCase, AlgorithmsTestBase.AlgorithmsTest):
alg = merge()
alg.initAlgorithm()
merge_command = alg.commandName() + alg.command_ext()
with tempfile.TemporaryDirectory() as outdir:
# this algorithm creates temporary text file with input layers
# so we strip its path, leaving only filename
@ -4877,7 +4879,7 @@ class TestGdalRasterAlgorithms(QgisTestCase, AlgorithmsTestBase.AlgorithmsTest):
self.assertEqual(
cmd,
[
"gdal_merge.py",
merge_command,
"-ot Float32 -of GTiff "
+ "-o "
+ outdir
@ -4896,7 +4898,7 @@ class TestGdalRasterAlgorithms(QgisTestCase, AlgorithmsTestBase.AlgorithmsTest):
self.assertEqual(
cmd,
[
"gdal_merge.py",
merge_command,
"-separate -ot Float32 -of GTiff "
+ "-o "
+ outdir
@ -4920,7 +4922,7 @@ class TestGdalRasterAlgorithms(QgisTestCase, AlgorithmsTestBase.AlgorithmsTest):
self.assertEqual(
cmd,
[
"gdal_merge.py",
merge_command,
"-ot Float32 -of GTiff -tap -ps 0.1 0.1 "
+ "-o "
+ outdir
@ -4944,7 +4946,7 @@ class TestGdalRasterAlgorithms(QgisTestCase, AlgorithmsTestBase.AlgorithmsTest):
self.assertEqual(
cmd,
[
"gdal_merge.py",
merge_command,
"-a_nodata -9999.0 -ot Float32 -of GTiff "
+ "-o "
+ outdir

View File

@ -41,7 +41,7 @@ def userFolder():
def defaultOutputFolder():
folder = os.path.join(userFolder(), "outputs")
folder = os.path.join(QDir.homePath(), "processing")
if not QDir(folder).exists():
QDir().mkpath(folder)

View File

@ -988,6 +988,11 @@ QTreeView::item:selected, QTreeView::branch:selected {
color: @text;
}
QTreeView::item:selected:disabled, QTreeView::branch:selected:disabled {
background-color: @itemalternativebackground;
color: @background;
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:closed:has-children:has-siblings {
border-image: none;

View File

@ -1019,6 +1019,11 @@ QTreeView::item:selected, QTreeView::branch:selected {
color: @textlight;
}
QTreeView::item:selected:disabled, QTreeView::branch:selected:disabled {
background-color: @itemdarkbackground;
color: @background;
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:closed:has-children:has-siblings {
border-image: none;

View File

@ -8,6 +8,8 @@ set(QGIS_3D_SRCS
qgsaabb.cpp
qgsabstract3dengine.cpp
qgsabstractvectorlayer3drenderer.cpp
qgsannotationlayer3drenderer.cpp
qgsannotationlayerchunkloader_p.cpp
qgs3danimationsettings.cpp
qgs3dexportobject.cpp
qgs3dmapexportsettings.cpp
@ -45,6 +47,7 @@ set(QGIS_3D_SRCS
qgscolorramptexture.cpp
qgsrubberband3d.cpp
qgsambientocclusionsettings.cpp
qgstextureatlasgenerator.cpp
qgspointcloudlayer3drenderer.cpp
qgspointcloudlayerchunkloader_p.cpp
@ -153,6 +156,8 @@ set(QGIS_3D_HDRS
qgsaabb.h
qgsabstract3dengine.h
qgsabstractvectorlayer3drenderer.h
qgsannotationlayer3drenderer.h
qgsannotationlayerchunkloader_p.h
qgscameracontroller.h
qgscamerapose.h
qgsgeotransform.h
@ -174,6 +179,7 @@ set(QGIS_3D_HDRS
qgsshadowsettings.h
qgspointcloudlayer3drenderer.h
qgsambientocclusionsettings.h
qgstextureatlasgenerator.h
lights/qgsdirectionallightsettings.h
lights/qgslightsource.h
@ -312,6 +318,7 @@ target_include_directories(qgis_3d PUBLIC
${CMAKE_BINARY_DIR}/src/3d
${CMAKE_SOURCE_DIR}/external/delaunator-cpp
${CMAKE_SOURCE_DIR}/external/tinygltf
${CMAKE_SOURCE_DIR}/external/rectpack2D
)
target_link_libraries(qgis_3d

View File

@ -27,6 +27,7 @@
#include "qgsmeshlayer3drenderer.h"
#include "qgspointcloudlayer3drenderer.h"
#include "qgstiledscenelayer3drenderer.h"
#include "qgsannotationlayer3drenderer.h"
#include "qgs3dsymbolregistry.h"
#include "qgspoint3dsymbol.h"
#include "qgsline3dsymbol.h"
@ -68,6 +69,7 @@ void Qgs3D::initialize()
QgsApplication::renderer3DRegistry()->addRenderer( new QgsMeshLayer3DRendererMetadata );
QgsApplication::renderer3DRegistry()->addRenderer( new QgsPointCloudLayer3DRendererMetadata );
QgsApplication::renderer3DRegistry()->addRenderer( new QgsTiledSceneLayer3DRendererMetadata );
QgsApplication::renderer3DRegistry()->addRenderer( new QgsAnnotationLayer3DRendererMetadata );
QgsApplication::symbol3DRegistry()->addSymbolType( new Qgs3DSymbolMetadata( QStringLiteral( "point" ), QObject::tr( "Point" ), &QgsPoint3DSymbol::create, nullptr, Qgs3DSymbolImpl::handlerForPoint3DSymbol ) );
QgsApplication::symbol3DRegistry()->addSymbolType( new Qgs3DSymbolMetadata( QStringLiteral( "line" ), QObject::tr( "Line" ), &QgsLine3DSymbol::create, nullptr, Qgs3DSymbolImpl::handlerForLine3DSymbol ) );

View File

@ -45,6 +45,7 @@
#include "qgsapplication.h"
#include "qgsaabb.h"
#include "qgsabstract3dengine.h"
#include "qgsannotationlayer.h"
#include "qgs3dmapsettings.h"
#include "qgs3dutils.h"
#include "qgsabstract3drenderer.h"
@ -66,6 +67,7 @@
#include "qgsterraingenerator.h"
#include "qgstiledscenelayer.h"
#include "qgstiledscenelayer3drenderer.h"
#include "qgsannotationlayer3drenderer.h"
#include "qgsdirectionallightsettings.h"
#include "qgsvectorlayer.h"
#include "qgsvectorlayer3drenderer.h"
@ -746,6 +748,11 @@ void Qgs3DMapScene::addLayerEntity( QgsMapLayer *layer )
QgsTiledSceneLayer3DRenderer *tiledSceneRenderer = static_cast<QgsTiledSceneLayer3DRenderer *>( renderer );
tiledSceneRenderer->setLayer( static_cast<QgsTiledSceneLayer *>( layer ) );
}
else if ( layer->type() == Qgis::LayerType::Annotation && renderer->type() == QLatin1String( "annotation" ) )
{
auto annotationLayerRenderer = qgis::down_cast<QgsAnnotationLayer3DRenderer *>( renderer );
annotationLayerRenderer->setLayer( qobject_cast<QgsAnnotationLayer *>( layer ) );
}
Qt3DCore::QEntity *newEntity = renderer->createEntity( &mMap );
if ( newEntity )

View File

@ -83,6 +83,7 @@ typedef Qt3DCore::QGeometry Qt3DQGeometry;
#include "qgs3dutils.h"
#include "qgsimagetexture.h"
#include "qgstessellatedpolygongeometry.h"
#include "qgsgeotransform.h"
#include <numeric>
@ -285,15 +286,15 @@ void Qgs3DSceneExporter::parseTerrain( QgsTerrainEntity *terrain, const QString
switch ( generator->type() )
{
case QgsTerrainGenerator::Dem:
terrainTile = getDemTerrainEntity( terrain, node );
terrainTile = getDemTerrainEntity( terrain, node, settings->origin() );
parseDemTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
case QgsTerrainGenerator::Flat:
terrainTile = getFlatTerrainEntity( terrain, node );
terrainTile = getFlatTerrainEntity( terrain, node, settings->origin() );
parseFlatTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
case QgsTerrainGenerator::Mesh:
terrainTile = getMeshTerrainEntity( terrain, node );
terrainTile = getMeshTerrainEntity( terrain, node, settings->origin() );
parseMeshTile( terrainTile, layerName + QStringLiteral( "_" ) );
break;
// TODO: implement other terrain types
@ -304,7 +305,7 @@ void Qgs3DSceneExporter::parseTerrain( QgsTerrainEntity *terrain, const QString
textureGenerator->setTextureSize( oldResolution );
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
QgsFlatTerrainGenerator *generator = dynamic_cast<QgsFlatTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
FlatTerrainChunkLoader *flatTerrainLoader = qobject_cast<FlatTerrainChunkLoader *>( generator->createChunkLoader( node ) );
@ -314,10 +315,17 @@ QgsTerrainTileEntity *Qgs3DSceneExporter::getFlatTerrainEntity( QgsTerrainEntity
// the entity we created will be deallocated once the scene exporter is deallocated
Qt3DCore::QEntity *entity = flatTerrainLoader->createEntity( this );
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( entity );
const QList<QgsGeoTransform *> transforms = entity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
return tileEntity;
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
// Just create a new tile (we don't need to export exact level of details as in the scene)
// create the entity synchronously and then it will be deleted once our scene exporter instance is deallocated
@ -329,17 +337,31 @@ QgsTerrainTileEntity *Qgs3DSceneExporter::getDemTerrainEntity( QgsTerrainEntity
if ( mExportTextures )
terrain->textureGenerator()->waitForFinished();
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( loader->createEntity( this ) );
const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
delete generator;
return tileEntity;
}
QgsTerrainTileEntity *Qgs3DSceneExporter::getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node )
QgsTerrainTileEntity *Qgs3DSceneExporter::getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin )
{
QgsMeshTerrainGenerator *generator = dynamic_cast<QgsMeshTerrainGenerator *>( terrain->mapSettings()->terrainGenerator() );
QgsMeshTerrainTileLoader *loader = qobject_cast<QgsMeshTerrainTileLoader *>( generator->createChunkLoader( node ) );
loader->start();
// TODO: export textures
QgsTerrainTileEntity *tileEntity = qobject_cast<QgsTerrainTileEntity *>( loader->createEntity( this ) );
const QList<QgsGeoTransform *> transforms = tileEntity->findChildren<QgsGeoTransform *>();
for ( QgsGeoTransform *transform : transforms )
{
transform->setOrigin( mapOrigin );
}
return tileEntity;
}

View File

@ -37,12 +37,13 @@ class QgsDemTerrainGenerator;
class QgsChunkNode;
class Qgs3DExportObject;
class QgsTerrainTextureGenerator;
class QgsVector3D;
class QgsVectorLayer;
class QgsPolygon3DSymbol;
class QgsLine3DSymbol;
class QgsPoint3DSymbol;
class QgsMeshEntity;
class TestQgs3DRendering;
class TestQgs3DExporter;
#define SIP_NO_FILE
@ -126,11 +127,11 @@ class _3D_EXPORT Qgs3DSceneExporter : public Qt3DCore::QEntity
Qgs3DExportObject *processPoints( Qt3DCore::QEntity *entity, const QString &objectNamePrefix );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getFlatTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getDemTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Returns a tile entity that contains the geometry to be exported and necessary scaling parameters
QgsTerrainTileEntity *getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node );
QgsTerrainTileEntity *getMeshTerrainEntity( QgsTerrainEntity *terrain, QgsChunkNode *node, const QgsVector3D &mapOrigin );
//! Constructs a Qgs3DExportObject from the DEM tile entity
void parseDemTile( QgsTerrainTileEntity *tileEntity, const QString &layerName );
@ -157,7 +158,7 @@ class _3D_EXPORT Qgs3DSceneExporter : public Qt3DCore::QEntity
friend QgsPolygon3DSymbol;
friend QgsLine3DSymbol;
friend QgsPoint3DSymbol;
friend TestQgs3DRendering;
friend TestQgs3DExporter;
};
#endif // QGS3DSCENEEXPORTER_H

View File

@ -1161,7 +1161,7 @@ QgsCameraPose Qgs3DUtils::lineSegmentToCameraPose( const QgsVector3D &startPoint
std::unique_ptr<Qt3DRender::QCamera> Qgs3DUtils::copyCamera( Qt3DRender::QCamera *cam )
{
std::unique_ptr<Qt3DRender::QCamera> copy = std::make_unique<Qt3DRender::QCamera>();
auto copy = std::make_unique<Qt3DRender::QCamera>();
copy->setPosition( cam->position() );
copy->setViewCenter( cam->viewCenter() );
copy->setUpVector( cam->upVector() );

View File

@ -0,0 +1,155 @@
/***************************************************************************
qgsannotationlayer3drenderer.cpp
--------------------------------------
Date : January 2020
Copyright : (C) 2020 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsannotationlayer3drenderer.h"
#include "qgsannotationlayer.h"
#include "qgsannotationlayerchunkloader_p.h"
#include "qgis.h"
//
// QgsAnnotationLayer3DRendererMetadata
//
QgsAnnotationLayer3DRendererMetadata::QgsAnnotationLayer3DRendererMetadata()
: Qgs3DRendererAbstractMetadata( QStringLiteral( "annotation" ) )
{
}
QgsAbstract3DRenderer *QgsAnnotationLayer3DRendererMetadata::createRenderer( QDomElement &elem, const QgsReadWriteContext &context )
{
auto r = std::make_unique< QgsAnnotationLayer3DRenderer >();
r->readXml( elem, context );
return r.release();
}
//
// QgsAnnotationLayer3DRenderer
//
QgsAnnotationLayer3DRenderer::QgsAnnotationLayer3DRenderer() = default;
void QgsAnnotationLayer3DRenderer::setLayer( QgsAnnotationLayer *layer )
{
mLayerRef = QgsMapLayerRef( layer );
}
QgsAnnotationLayer *QgsAnnotationLayer3DRenderer::layer() const
{
return qobject_cast<QgsAnnotationLayer *>( mLayerRef.layer );
}
void QgsAnnotationLayer3DRenderer::resolveReferences( const QgsProject &project )
{
mLayerRef.resolve( &project );
}
bool QgsAnnotationLayer3DRenderer::showCalloutLines() const
{
return mShowCalloutLines;
}
void QgsAnnotationLayer3DRenderer::setShowCalloutLines( bool show )
{
mShowCalloutLines = show;
}
void QgsAnnotationLayer3DRenderer::setCalloutLineColor( const QColor &color )
{
mCalloutLineColor = color;
}
QColor QgsAnnotationLayer3DRenderer::calloutLineColor() const
{
return mCalloutLineColor;
}
void QgsAnnotationLayer3DRenderer::setCalloutLineWidth( double width )
{
mCalloutLineWidth = width;
}
double QgsAnnotationLayer3DRenderer::calloutLineWidth() const
{
return mCalloutLineWidth;
}
QString QgsAnnotationLayer3DRenderer::type() const
{
return "annotation";
}
QgsAnnotationLayer3DRenderer *QgsAnnotationLayer3DRenderer::clone() const
{
auto r = std::make_unique< QgsAnnotationLayer3DRenderer >();
r->mLayerRef = mLayerRef;
r->mAltClamping = mAltClamping;
r->mZOffset = mZOffset;
r->mShowCalloutLines = mShowCalloutLines;
r->mCalloutLineColor = mCalloutLineColor;
r->mCalloutLineWidth = mCalloutLineWidth;
return r.release();
}
Qt3DCore::QEntity *QgsAnnotationLayer3DRenderer::createEntity( Qgs3DMapSettings *map ) const
{
QgsAnnotationLayer *l = layer();
if ( !l )
return nullptr;
// For some cases we start with a maximal z range because we can't know this upfront, as it potentially involves terrain heights.
// This range will be refined after populating the nodes to the actual z range of the generated chunks nodes.
// Assuming the vertical height is in meter, then it's extremely unlikely that a real vertical
// height will exceed this amount!
constexpr double MINIMUM_ANNOTATION_Z_ESTIMATE = -100000;
constexpr double MAXIMUM_ANNOTATION_Z_ESTIMATE = 100000;
double minimumZ = MINIMUM_ANNOTATION_Z_ESTIMATE;
double maximumZ = MAXIMUM_ANNOTATION_Z_ESTIMATE;
switch ( mAltClamping )
{
case Qgis::AltitudeClamping::Absolute:
// special case where we DO know the exact z range upfront!
minimumZ = mZOffset;
maximumZ = mZOffset;
break;
case Qgis::AltitudeClamping::Relative:
case Qgis::AltitudeClamping::Terrain:
break;
}
return new QgsAnnotationLayerChunkedEntity( map, l, mAltClamping, mZOffset, mShowCalloutLines, mCalloutLineColor, mCalloutLineWidth, minimumZ, maximumZ );
}
void QgsAnnotationLayer3DRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext & ) const
{
QDomDocument doc = elem.ownerDocument();
elem.setAttribute( QStringLiteral( "layer" ), mLayerRef.layerId );
elem.setAttribute( QStringLiteral( "clamping" ), qgsEnumValueToKey( mAltClamping ) );
elem.setAttribute( QStringLiteral( "offset" ), mZOffset );
if ( mShowCalloutLines )
elem.setAttribute( QStringLiteral( "callouts" ), QStringLiteral( "1" ) );
}
void QgsAnnotationLayer3DRenderer::readXml( const QDomElement &elem, const QgsReadWriteContext & )
{
mLayerRef = QgsMapLayerRef( elem.attribute( QStringLiteral( "layer" ) ) );
mAltClamping = qgsEnumKeyToValue( elem.attribute( QStringLiteral( "clamping" ) ), Qgis::AltitudeClamping::Relative );
mZOffset = elem.attribute( QStringLiteral( "offset" ), QString::number( DEFAULT_Z_OFFSET ) ).toDouble();
mShowCalloutLines = elem.attribute( QStringLiteral( "callouts" ), QStringLiteral( "0" ) ).toInt();
}

View File

@ -0,0 +1,185 @@
/***************************************************************************
qgsannotationlayer3drenderer.h
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSANNOTATIONLAYER3DRENDERER_H
#define QGSANNOTATIONLAYER3DRENDERER_H
#include "qgis_3d.h"
#include "qgis_sip.h"
#include "qgs3drendererregistry.h"
#include "qgsabstract3drenderer.h"
#include "qgsmaplayerref.h"
class QgsAnnotationLayer;
#ifdef SIP_RUN
// this is needed for the "convert to subclass" code below to compile
% ModuleHeaderCode
#include "qgsannotationlayer3drenderer.h"
% End
#endif
/**
* \ingroup core
* \brief Metadata for annotation layer 3D renderer to allow creation of its instances from XML.
*
* \warning This is not considered stable API, and may change in future QGIS releases. It is
* exposed to the Python bindings as a tech preview only.
*
* \since QGIS 4.0
*/
class _3D_EXPORT QgsAnnotationLayer3DRendererMetadata : public Qgs3DRendererAbstractMetadata
{
public:
QgsAnnotationLayer3DRendererMetadata();
//! Creates an instance of a 3D renderer based on a DOM element with renderer configuration
QgsAbstract3DRenderer *createRenderer( QDomElement &elem, const QgsReadWriteContext &context ) override SIP_FACTORY;
};
/**
* \ingroup qgis_3d
* \brief 3D renderers for annotation layers.
*
* \since QGIS 4.0
*/
class _3D_EXPORT QgsAnnotationLayer3DRenderer : public QgsAbstract3DRenderer
{
#ifdef SIP_RUN
SIP_CONVERT_TO_SUBCLASS_CODE
if ( dynamic_cast<QgsAnnotationLayer3DRenderer *>( sipCpp ) != nullptr )
sipType = sipType_QgsAnnotationLayer3DRenderer;
else
sipType = nullptr;
SIP_END
#endif
public:
QgsAnnotationLayer3DRenderer();
/**
* Sets the annotation layer associated with the renderer.
*
* \see layer()
*/
void setLayer( QgsAnnotationLayer *layer );
/**
* Returns the annotation layer associated with the renderer.
*
* \see setLayer()
*/
QgsAnnotationLayer *layer() const;
QString type() const override;
QgsAnnotationLayer3DRenderer *clone() const override SIP_FACTORY;
Qt3DCore::QEntity *createEntity( Qgs3DMapSettings *map ) const override SIP_SKIP;
void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const override;
void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) override;
void resolveReferences( const QgsProject &project ) override;
/**
* Returns the altitude clamping method, which determines the vertical position of annotations.
*
* \see setAltitudeClamping()
*/
Qgis::AltitudeClamping altitudeClamping() const { return mAltClamping; }
/**
* Sets the altitude \a clamping method, which determines the vertical position of annotations.
*
* \see altitudeClamping()
*/
void setAltitudeClamping( Qgis::AltitudeClamping clamping ) { mAltClamping = clamping; }
/**
* Returns the z offset, which is a fixed offset amount which should be added to z values for the annotations.
*
* \see setZOffset()
*/
double zOffset() const { return mZOffset; }
/**
* Sets the z \a offset, which is a fixed offset amount which will be added to z values for the annotations.
*
* \see zOffset()
*/
void setZOffset( double offset ) { mZOffset = offset; }
/**
* Returns TRUE if callout lines are shown, vertically joining the annotations to the terrain.
*
* \see setShowCalloutLines()
*/
bool showCalloutLines() const;
/**
* Sets whether callout lines are shown, vertically joining the annotations to the terrain.
*
* \see showCalloutLines()
*/
void setShowCalloutLines( bool show );
// TODO -- consider exposing via QgsSimpleLineMaterialSettings, for now, for testing only
/**
* Sets the callout line \a color.
*
* \see calloutLineColor()
* \note Not available in Python bindings
*/
SIP_SKIP void setCalloutLineColor( const QColor &color );
/**
* Returns the callout line color.
*
* \see setCalloutLineColor()
* \note Not available in Python bindings
*/
SIP_SKIP QColor calloutLineColor() const;
/**
* Sets the callout line \a width.
*
* \see calloutLineWidth()
* \note Not available in Python bindings
*/
SIP_SKIP void setCalloutLineWidth( double width );
/**
* Returns the callout line width.
*
* \see setCalloutLineWidth()
* \note Not available in Python bindings
*/
SIP_SKIP double calloutLineWidth() const;
private:
#ifdef SIP_RUN
QgsAnnotationLayer3DRenderer( const QgsAnnotationLayer3DRenderer & );
#endif
static constexpr double DEFAULT_Z_OFFSET = 50;
QgsMapLayerRef mLayerRef;
Qgis::AltitudeClamping mAltClamping = Qgis::AltitudeClamping::Relative;
double mZOffset = DEFAULT_Z_OFFSET;
bool mShowCalloutLines = true;
QColor mCalloutLineColor { 0, 0, 0 };
double mCalloutLineWidth = 2;
};
#endif // QGSANNOTATIONLAYER3DRENDERER_H

View File

@ -0,0 +1,396 @@
/***************************************************************************
qgsannotationlayerchunkloader_p.cpp
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsannotationlayerchunkloader_p.h"
#include "moc_qgsannotationlayerchunkloader_p.cpp"
#include "qgs3dutils.h"
#include "qgsannotationitem.h"
#include "qgstessellatedpolygongeometry.h"
#include "qgschunknode.h"
#include "qgseventtracing.h"
#include "qgslogger.h"
#include "qgsannotationlayer.h"
#include "qgsabstract3dsymbol.h"
#include "qgsabstractterrainsettings.h"
#include "qgsannotationmarkeritem.h"
#include "qgsbillboardgeometry.h"
#include "qgspoint3dbillboardmaterial.h"
#include "qgsgeotransform.h"
#include "qgsexpressioncontextutils.h"
#include "qgstextureatlasgenerator.h"
#include "qgslinevertexdata_p.h"
#include "qgslinematerial_p.h"
#include <QtConcurrent>
#include <Qt3DCore/QTransform>
///@cond PRIVATE
QgsAnnotationLayerChunkLoader::QgsAnnotationLayerChunkLoader( const QgsAnnotationLayerChunkLoaderFactory *factory, QgsChunkNode *node )
: QgsChunkLoader( node )
, mFactory( factory )
, mRenderContext( factory->mRenderContext )
{
}
struct Billboard
{
QVector3D position;
int textureId;
};
void QgsAnnotationLayerChunkLoader::start()
{
QgsChunkNode *node = chunk();
if ( node->level() < mFactory->mLeafLevel )
{
QTimer::singleShot( 0, this, &QgsAnnotationLayerChunkLoader::finished );
return;
}
QgsAnnotationLayer *layer = mFactory->mLayer;
mLayerName = mFactory->mLayer->name();
// only a subset of data to be queried
const QgsRectangle rect = node->box3D().toRectangle();
// origin for coordinates of the chunk - it is kind of arbitrary, but it should be
// picked so that the coordinates are relatively small to avoid numerical precision issues
mChunkOrigin = QgsVector3D( rect.center().x(), rect.center().y(), 0 );
QgsExpressionContext exprContext;
exprContext.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
mRenderContext.setExpressionContext( exprContext );
QgsCoordinateTransform layerToMapTransform( layer->crs(), mRenderContext.crs(), mRenderContext.transformContext() );
QgsRectangle layerExtent;
try
{
layerExtent = layerToMapTransform.transformBoundingBox( rect, Qgis::TransformDirection::Reverse );
}
catch ( QgsCsException &e )
{
QgsDebugError( QStringLiteral( "Error transforming annotation layer extent to 3d map extent: %1" ).arg( e.what() ) );
return;
}
const double zOffset = mFactory->mZOffset;
const Qgis::AltitudeClamping altitudeClamping = mFactory->mClamping;
bool showCallouts = mFactory->mShowCallouts;
// see logic from QgsAnnotationLayerRenderer
const QStringList itemsList = layer->queryIndex( layerExtent );
QSet< QString > itemIds( itemsList.begin(), itemsList.end() );
// we also have NO choice but to clone ALL non-indexed items (i.e. those with a scale-dependent bounding box)
// since these won't be in the layer's spatial index, and it's too expensive to determine their actual bounding box
// upfront (we are blocking the main thread right now!)
// TODO -- come up with some brilliant way to avoid this and also index scale-dependent items ;)
itemIds.unite( layer->mNonIndexedItems );
mItemsToRender.reserve( itemIds.size() );
std::transform( itemIds.begin(), itemIds.end(), std::back_inserter( mItemsToRender ), [layer]( const QString &id ) -> std::unique_ptr< QgsAnnotationItem > {
return std::unique_ptr< QgsAnnotationItem >( layer->item( id )->clone() );
} );
//
// this will be run in a background thread
//
mFutureWatcher = new QFutureWatcher<void>( this );
connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
const QFuture<void> future = QtConcurrent::run( [this, rect, layerToMapTransform, zOffset, altitudeClamping, showCallouts] {
const QgsEventTracing::ScopedEvent e( QStringLiteral( "3D" ), QStringLiteral( "Annotation layer chunk load" ) );
std::vector< Billboard > billboards;
billboards.reserve( mItemsToRender.size() );
QVector< QImage > textures;
textures.reserve( mItemsToRender.size() );
for ( const std::unique_ptr< QgsAnnotationItem > &item : std::as_const( mItemsToRender ) )
{
if ( mCanceled )
break;
QgsAnnotationItem *annotation = item.get();
if ( !annotation->enabled() )
continue;
if ( QgsAnnotationMarkerItem *marker = dynamic_cast< QgsAnnotationMarkerItem * >( annotation ) )
{
if ( marker->symbol() )
{
QgsPointXY p = marker->geometry();
try
{
const QgsPointXY mapPoint = layerToMapTransform.transform( p );
if ( !rect.contains( mapPoint ) )
continue;
double z = 0;
const float terrainZ = ( altitudeClamping == Qgis::AltitudeClamping::Absolute && !showCallouts ) ? 0 : mRenderContext.terrainRenderingEnabled() && mRenderContext.terrainGenerator() ? static_cast<float>( mRenderContext.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), mRenderContext ) * mRenderContext.terrainSettings()->verticalScale() )
: 0.f;
switch ( altitudeClamping )
{
case Qgis::AltitudeClamping::Absolute:
z = zOffset;
break;
case Qgis::AltitudeClamping::Terrain:
z = terrainZ;
break;
case Qgis::AltitudeClamping::Relative:
z = terrainZ + zOffset;
break;
}
Billboard billboard;
billboard.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - mChunkOrigin ).toVector3D();
billboard.textureId = textures.size();
textures.append( QgsPoint3DBillboardMaterial::renderSymbolToImage( marker->symbol(), mRenderContext ) );
billboards.emplace_back( std::move( billboard ) );
if ( showCallouts )
{
mCalloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
}
mZMax = std::max( mZMax, showCallouts ? std::max( 0.0, z ) : z );
mZMin = std::min( mZMin, showCallouts ? std::min( 0.0, z ) : z );
}
catch ( QgsCsException &e )
{
QgsDebugError( e.what() );
}
}
}
}
// free memory
mItemsToRender.clear();
const QgsTextureAtlas atlas = QgsTextureAtlasGenerator::createFromImages( textures, 2048 );
if ( atlas.isValid() )
{
mBillboardAtlas = atlas.renderAtlasTexture();
mBillboardPositions.reserve( static_cast< int >( billboards.size() ) );
for ( Billboard &billboard : billboards )
{
const QRect textureRect = atlas.rect( billboard.textureId );
QgsBillboardGeometry::BillboardAtlasData geometry;
geometry.position = billboard.position;
geometry.textureAtlasOffset = QVector2D( static_cast< float >( textureRect.left() ) / static_cast< float>( mBillboardAtlas.width() ), 1 - ( static_cast< float >( textureRect.bottom() ) / static_cast< float>( mBillboardAtlas.height() ) ) );
geometry.textureAtlasSize = QVector2D( static_cast< float >( textureRect.width() ) / static_cast< float>( mBillboardAtlas.width() ), static_cast< float>( textureRect.height() ) / static_cast< float>( mBillboardAtlas.height() ) );
mBillboardPositions.append( geometry );
}
}
else
{
QgsDebugError( QStringLiteral( "Error encountered building texture atlas" ) );
mBillboardAtlas = QImage();
}
} );
// emit finished() as soon as the handler is populated with features
mFutureWatcher->setFuture( future );
}
QgsAnnotationLayerChunkLoader::~QgsAnnotationLayerChunkLoader()
{
if ( mFutureWatcher && !mFutureWatcher->isFinished() )
{
disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
mFutureWatcher->waitForFinished();
}
}
void QgsAnnotationLayerChunkLoader::cancel()
{
mCanceled = true;
}
Qt3DCore::QEntity *QgsAnnotationLayerChunkLoader::createEntity( Qt3DCore::QEntity *parent )
{
if ( mNode->level() < mFactory->mLeafLevel )
{
Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent ); // dummy entity
entity->setObjectName( mLayerName + "_CONTAINER_" + mNode->tileId().text() );
return entity;
}
if ( mBillboardPositions.empty() )
{
// an empty node, so we return no entity. This tags the node as having no data and effectively removes it.
// we just make sure first that its initial estimated vertical range does not affect its parents' bboxes calculation
mNode->setExactBox3D( QgsBox3D() );
mNode->updateParentBoundingBoxesRecursively();
return nullptr;
}
Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
entity->setObjectName( mLayerName + "_" + mNode->tileId().text() );
QgsBillboardGeometry *billboardGeometry = new QgsBillboardGeometry();
billboardGeometry->setBillboardData( mBillboardPositions );
Qt3DRender::QGeometryRenderer *billboardGeometryRenderer = new Qt3DRender::QGeometryRenderer;
billboardGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::Points );
billboardGeometryRenderer->setGeometry( billboardGeometry );
billboardGeometryRenderer->setVertexCount( billboardGeometry->count() );
QgsPoint3DBillboardMaterial *billboardMaterial = new QgsPoint3DBillboardMaterial( QgsPoint3DBillboardMaterial::Mode::AtlasTexture );
billboardMaterial->setTexture2DFromImage( mBillboardAtlas );
QgsGeoTransform *billboardTransform = new QgsGeoTransform;
billboardTransform->setGeoTranslation( mChunkOrigin );
Qt3DCore::QEntity *billboardEntity = new Qt3DCore::QEntity;
billboardEntity->addComponent( billboardMaterial );
billboardEntity->addComponent( billboardTransform );
billboardEntity->addComponent( billboardGeometryRenderer );
billboardEntity->setParent( entity );
if ( mFactory->mShowCallouts )
{
QgsLineVertexData lineData;
lineData.withAdjacency = true;
lineData.geocentricCoordinates = false; // mMapSettings->sceneMode() == Qgis::SceneMode::Globe;
lineData.init( Qgis::AltitudeClamping::Absolute, Qgis::AltitudeBinding::Vertex, 0, mRenderContext, mChunkOrigin );
for ( const QgsLineString &line : mCalloutLines )
{
lineData.addLineString( line, 0, false );
}
QgsLineMaterial *mat = new QgsLineMaterial;
mat->setLineColor( mFactory->mCalloutLineColor );
mat->setLineWidth( mFactory->mCalloutLineWidth );
Qt3DCore::QEntity *calloutEntity = new Qt3DCore::QEntity;
calloutEntity->setObjectName( parent->objectName() + "_CALLOUTS" );
// geometry renderer
Qt3DRender::QGeometryRenderer *calloutRenderer = new Qt3DRender::QGeometryRenderer;
calloutRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::LineStripAdjacency );
calloutRenderer->setGeometry( lineData.createGeometry( calloutEntity ) );
calloutRenderer->setVertexCount( lineData.indexes.count() );
calloutRenderer->setPrimitiveRestartEnabled( true );
calloutRenderer->setRestartIndexValue( 0 );
// make entity
calloutEntity->addComponent( calloutRenderer );
calloutEntity->addComponent( mat );
calloutEntity->setParent( billboardEntity );
}
// fix the vertical range of the node from the estimated vertical range to the true range
if ( mZMin != std::numeric_limits<float>::max() && mZMax != std::numeric_limits<float>::lowest() )
{
QgsBox3D box = mNode->box3D();
box.setZMinimum( mZMin );
box.setZMaximum( mZMax );
mNode->setExactBox3D( box );
mNode->updateParentBoundingBoxesRecursively();
}
return entity;
}
///////////////
QgsAnnotationLayerChunkLoaderFactory::QgsAnnotationLayerChunkLoaderFactory( const Qgs3DRenderContext &context, QgsAnnotationLayer *layer, int leafLevel, Qgis::AltitudeClamping clamping, double zOffset, bool showCallouts, const QColor &calloutLineColor, double calloutLineWidth, double zMin, double zMax )
: mRenderContext( context )
, mLayer( layer )
, mLeafLevel( leafLevel )
, mClamping( clamping )
, mZOffset( zOffset )
, mShowCallouts( showCallouts )
, mCalloutLineColor( calloutLineColor )
, mCalloutLineWidth( calloutLineWidth )
{
if ( context.crs().type() == Qgis::CrsType::Geocentric )
{
// TODO: add support for handling of annotation layers
// (we're using dummy quadtree here to make sure the empty extent does not break the scene completely)
QgsDebugError( QStringLiteral( "Annotation layers in globe scenes are not supported yet!" ) );
setupQuadtree( QgsBox3D( -1e7, -1e7, -1e7, 1e7, 1e7, 1e7 ), -1, leafLevel );
return;
}
QgsBox3D rootBox3D( context.extent(), zMin, zMax );
// add small padding to avoid clipping of point features located at the edge of the bounding box
rootBox3D.grow( 1.0 );
setupQuadtree( rootBox3D, -1, leafLevel ); // negative root error means that the node does not contain anything
}
QgsChunkLoader *QgsAnnotationLayerChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
{
return new QgsAnnotationLayerChunkLoader( this, node );
}
///////////////
QgsAnnotationLayerChunkedEntity::QgsAnnotationLayerChunkedEntity( Qgs3DMapSettings *map, QgsAnnotationLayer *layer, Qgis::AltitudeClamping clamping, double zOffset, bool showCallouts, const QColor &calloutLineColor, double calloutLineWidth, double zMin, double zMax )
: QgsChunkedEntity( map,
-1, // max. allowed screen error (negative tau means that we need to go until leaves are reached)
new QgsAnnotationLayerChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), layer, 3, clamping, zOffset, showCallouts, calloutLineColor, calloutLineWidth, zMin, zMax ), true )
{
mTransform = new Qt3DCore::QTransform;
if ( applyTerrainOffset() )
{
mTransform->setTranslation( QVector3D( 0.0f, 0.0f, static_cast<float>( map->terrainSettings()->elevationOffset() ) ) );
}
this->addComponent( mTransform );
connect( map, &Qgs3DMapSettings::terrainSettingsChanged, this, &QgsAnnotationLayerChunkedEntity::onTerrainElevationOffsetChanged );
}
QgsAnnotationLayerChunkedEntity::~QgsAnnotationLayerChunkedEntity()
{
// cancel / wait for jobs
cancelActiveJobs();
}
// if the AltitudeClamping is `Absolute`, do not apply the offset
bool QgsAnnotationLayerChunkedEntity::applyTerrainOffset() const
{
if ( auto loaderFactory = static_cast<QgsAnnotationLayerChunkLoaderFactory *>( mChunkLoaderFactory ) )
{
return loaderFactory->mClamping != Qgis::AltitudeClamping::Absolute;
}
return true;
}
void QgsAnnotationLayerChunkedEntity::onTerrainElevationOffsetChanged()
{
QgsDebugMsgLevel( QStringLiteral( "QgsAnnotationLayerChunkedEntity::onTerrainElevationOffsetChanged" ), 2 );
float newOffset = static_cast<float>( qobject_cast<Qgs3DMapSettings *>( sender() )->terrainSettings()->elevationOffset() );
if ( !applyTerrainOffset() )
{
newOffset = 0.0;
}
mTransform->setTranslation( QVector3D( 0.0f, 0.0f, newOffset ) );
}
/// @endcond

View File

@ -0,0 +1,146 @@
/***************************************************************************
qgsannotationlayerchunkloader_p.h
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSANNOTATIONLAYERCHUNKLOADER_P_H
#define QGSANNOTATIONLAYERCHUNKLOADER_P_H
///@cond PRIVATE
//
// W A R N I N G
// -------------
//
// This file is not part of the QGIS API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
#include "qgschunkloader.h"
#include "qgschunkedentity.h"
#include "qgs3drendercontext.h"
#include "qgsbillboardgeometry.h"
#include <QImage>
#define SIP_NO_FILE
class QgsAnnotationLayer;
class QgsAnnotationItem;
namespace Qt3DCore
{
class QTransform;
}
#include <QFutureWatcher>
/**
* \ingroup qgis_3d
* \brief This loader factory is responsible for creation of loaders of QgsAnnotationLayerChunkedEntity.
*
* \since QGIS 4.0
*/
class QgsAnnotationLayerChunkLoaderFactory : public QgsQuadtreeChunkLoaderFactory
{
Q_OBJECT
public:
//! Constructs the factory
QgsAnnotationLayerChunkLoaderFactory( const Qgs3DRenderContext &context, QgsAnnotationLayer *layer, int leafLevel, Qgis::AltitudeClamping clamping, double zOffset, bool showCallouts, const QColor &calloutLineColor, double calloutLineWidth, double zMin, double zMax );
//! Creates loader for the given chunk node. Ownership of the returned is passed to the caller.
virtual QgsChunkLoader *createChunkLoader( QgsChunkNode *node ) const override;
Qgs3DRenderContext mRenderContext;
QgsAnnotationLayer *mLayer = nullptr;
int mLeafLevel = 0;
Qgis::AltitudeClamping mClamping = Qgis::AltitudeClamping::Relative;
double mZOffset = 0;
bool mShowCallouts = false;
QColor mCalloutLineColor;
double mCalloutLineWidth = 2;
};
/**
* \ingroup qgis_3d
* \brief This loader class is responsible for async loading of data for QgsAnnotationLayerChunkedEntity
* and creation of final 3D entity from the data previously prepared in a worker thread.
*
* \since QGIS 4.0
*/
class QgsAnnotationLayerChunkLoader : public QgsChunkLoader
{
Q_OBJECT
public:
//! Constructs the loader
QgsAnnotationLayerChunkLoader( const QgsAnnotationLayerChunkLoaderFactory *factory, QgsChunkNode *node );
~QgsAnnotationLayerChunkLoader() override;
void start() override;
virtual void cancel() override;
virtual Qt3DCore::QEntity *createEntity( Qt3DCore::QEntity *parent ) override;
private:
const QgsAnnotationLayerChunkLoaderFactory *mFactory = nullptr;
Qgs3DRenderContext mRenderContext;
bool mCanceled = false;
QFutureWatcher<void> *mFutureWatcher = nullptr;
QString mLayerName;
QgsVector3D mChunkOrigin;
std::vector< std::unique_ptr< QgsAnnotationItem > > mItemsToRender;
QVector< QgsBillboardGeometry::BillboardAtlasData > mBillboardPositions;
QVector< QgsLineString > mCalloutLines;
QImage mBillboardAtlas;
double mZMin = std::numeric_limits< double >::max();
double mZMax = std::numeric_limits< double >::lowest();
};
/**
* \ingroup qgis_3d
* \brief 3D entity used for rendering of annotation layers.
*
* Internally it uses QgsAnnotationLayerChunkLoaderFactory and
* QgsAnnotationLayerChunkLoader to do the actual work
* of loading and creating 3D sub-entities for the layer.
*
* \since QGIS 4.0
*/
class QgsAnnotationLayerChunkedEntity : public QgsChunkedEntity
{
Q_OBJECT
public:
//! Constructs the entity.
explicit QgsAnnotationLayerChunkedEntity( Qgs3DMapSettings *map, QgsAnnotationLayer *layer, Qgis::AltitudeClamping clamping, double zOffset, bool showCallouts, const QColor &calloutLineColor, double calloutLineWidth, double zMin, double zMax );
~QgsAnnotationLayerChunkedEntity();
private slots:
void onTerrainElevationOffsetChanged();
private:
Qt3DCore::QTransform *mTransform = nullptr;
bool applyTerrainOffset() const;
friend class TestQgsChunkedEntity;
};
/// @endcond
#endif // QGSANNOTATIONLAYERCHUNKLOADER_P_H

View File

@ -0,0 +1,193 @@
/***************************************************************************
qgstextureatlasgenerator.cpp
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgstextureatlasgenerator.h"
#include "qgscolorrampimpl.h"
#include <QPainter>
// rectpack2D library
#include <finders_interface.h>
///@cond PRIVATE
class QgsTextureRect
{
public:
QgsTextureRect( const rectpack2D::rect_xywh &rect, int id, const QImage &image = QImage() )
: rect( rect )
, id( id )
, image( image )
{
}
// get_rect must be implemented for rectpack2D compatibility:
auto &get_rect()
{
return rect;
}
const auto &get_rect() const
{
return rect;
}
QRect asQRect() const
{
return QRect( rect.x, rect.y, rect.w, rect.h );
}
rectpack2D::rect_xywh rect;
int id = 0;
QImage image;
};
///@endcond
QgsTextureAtlas::QgsTextureAtlas() = default;
QgsTextureAtlas::~QgsTextureAtlas() = default;
QgsTextureAtlas::QgsTextureAtlas( const QgsTextureAtlas &other ) = default;
QgsTextureAtlas &QgsTextureAtlas::operator=( const QgsTextureAtlas &other ) = default;
int QgsTextureAtlas::count() const
{
return static_cast< int >( mRects.size() );
}
QRect QgsTextureAtlas::rect( int id ) const
{
return mRects[id].asQRect();
}
QImage QgsTextureAtlas::renderAtlasTexture() const
{
if ( mAtlasSize.isEmpty() )
return QImage();
QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
res.fill( Qt::transparent );
QPainter painter( &res );
for ( const QgsTextureRect &rect : mRects )
{
if ( !rect.image.isNull() )
{
painter.drawImage( rect.asQRect(), rect.image );
}
}
painter.end();
return res;
}
QImage QgsTextureAtlas::renderDebugTexture() const
{
if ( mAtlasSize.isEmpty() )
return QImage();
QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
res.fill( Qt::transparent );
QPainter painter( &res );
painter.setPen( Qt::NoPen );
QgsRandomColorRamp ramp;
ramp.setTotalColorCount( static_cast< int >( mRects.size() ) );
double index = 0;
for ( const QgsTextureRect &rect : mRects )
{
const QColor color = ramp.color( index / ( static_cast< int >( mRects.size() ) - 1 ) );
index += 1;
painter.setBrush( QBrush( color ) );
painter.drawRect( rect.asQRect() );
}
painter.end();
return res;
}
//
// QgsTextureAtlasGenerator
//
QgsTextureAtlas QgsTextureAtlasGenerator::createFromRects( const QVector<QRect> &rectangles, int maxSide )
{
std::vector< QgsTextureRect > rects;
rects.reserve( rectangles.size() );
int index = 0;
for ( const QRect &rect : rectangles )
{
rects.emplace_back( QgsTextureRect( rectpack2D::rect_xywh( 0, 0, rect.width(), rect.height() ), index++ ) );
}
return generateAtlas( rects, maxSide );
}
QgsTextureAtlas QgsTextureAtlasGenerator::createFromImages( const QVector<QImage> &images, int maxSide )
{
std::vector< QgsTextureRect > rects;
rects.reserve( images.size() );
int index = 0;
for ( const QImage &image : images )
{
rects.emplace_back( QgsTextureRect( rectpack2D::rect_xywh( 0, 0, image.width(), image.height() ), index++, image ) );
}
return generateAtlas( rects, maxSide );
}
QgsTextureAtlas QgsTextureAtlasGenerator::generateAtlas( std::vector< QgsTextureRect > &rects, int maxSide )
{
using spacesType = rectpack2D::empty_spaces<false, rectpack2D::default_empty_spaces>;
bool result = true;
auto reportSuccessful = []( rectpack2D::rect_xywh & ) {
return rectpack2D::callback_result::CONTINUE_PACKING;
};
auto reportUnsuccessful = [&result]( rectpack2D::rect_xywh & ) {
result = false;
return rectpack2D::callback_result::ABORT_PACKING;
};
const auto discardStep = -4;
auto byWidth = []( const rectpack2D::rect_xywh *a, const rectpack2D::rect_xywh *b ) {
return a->w > b->w;
};
const rectpack2D::rect_wh resultSize = rectpack2D::find_best_packing<spacesType>(
rects,
rectpack2D::make_finder_input(
maxSide,
discardStep,
reportSuccessful,
reportUnsuccessful,
rectpack2D::flipping_option::DISABLED
),
byWidth
);
if ( !result )
return QgsTextureAtlas();
// rectpack2D::find_best_packing will have rearranged rects. Sort it back to the original order
// so that we can retreive the results by their original indices.
std::sort( rects.begin(), rects.end(), []( const QgsTextureRect &a, const QgsTextureRect &b ) {
return a.id < b.id;
} );
QgsTextureAtlas res;
res.mRects = std::move( rects );
res.mAtlasSize = QSize( resultSize.w, resultSize.h );
return res;
}

View File

@ -0,0 +1,174 @@
/***************************************************************************
qgstextureatlasgenerator.h
--------------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSTEXTUREATLASGENERATOR_H
#define QGSTEXTUREATLASGENERATOR_H
#include "qgis_3d.h"
#include <vector>
#include <QRect>
#include <QImage>
///@cond PRIVATE
class QgsTextureRect;
///@endcond
/**
* \ingroup qgis_3d
* \brief Encapsulates a texture atlas.
*
* QgsTextureAtlas contains the packed regions for aggregated texture atlases, and optionally the packed
* texture map.
*
* See QgsTextureAtlasGenerator for a class which automatically creates texture atlases.
*
* \since QGIS 4.0
*/
class _3D_EXPORT QgsTextureAtlas
{
public:
QgsTextureAtlas();
~QgsTextureAtlas();
QgsTextureAtlas( const QgsTextureAtlas &other );
QgsTextureAtlas &operator=( const QgsTextureAtlas &other );
/**
* Returns TRUE if the atlas is valid.
*/
bool isValid() const { return mAtlasSize.isValid(); }
/**
* Returns the total size required for the atlas, i.e. the total
* size for the packed images and rectangles.
*/
QSize atlasSize() const { return mAtlasSize; }
#ifndef SIP_RUN
/**
* Returns the packed rectangle for the texture with the specified \a index.
*/
QRect rect( int index ) const;
#else
/**
* Returns the packed rectangle for the texture with the specified \a index.
*
* \throws IndexError if no texture with the specified index exists.
*/
QRect rect( int index ) const;
//%MethodCode
const int count = sipCpp->count();
if ( a0 < 0 || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return sipConvertFromNewType( new QRect( sipCpp->rect( a0 ) ), sipType_QRect, Py_None );
}
//%End
#endif
/**
* Renders the combined texture atlas, containing all source images.
*
* \note This may be a null image if the atlas was created with rectangles alone.
*/
QImage renderAtlasTexture() const;
/**
* Renders a debug texture.
*
* The debug texture renders all packed rectangles with a unique color, and can be used
* to visualize the solution.
*/
QImage renderDebugTexture() const;
/**
* Returns the number of textures in the atlas.
*/
int count() const;
#ifdef SIP_RUN
int __len__() const;
% Docstring
Returns the number of textures in the atlas.
% End
//%MethodCode
sipRes
= sipCpp->count();
//% End
#endif
private:
std::vector< QgsTextureRect >
mRects;
QSize mAtlasSize;
friend class QgsTextureAtlasGenerator;
};
/**
* \ingroup qgis_3d
* \brief Generates texture atlases by efficient packing of multiple input rectangles/images.
*
* QgsTextureAtlasGenerator can be used to pack either images or raw rectangles. The
* static createFromRects() or createFromImages() methods should be called with the
* source images or rectangles, which will return a QgsTextureAtlas containing the results.
*
* \since QGIS 4.0
*/
class _3D_EXPORT QgsTextureAtlasGenerator
{
public:
/**
* Creates a texture atlas for a set of \a rectangles.
*
* This method should be used when the generator is used to pack rectangle shapes only.
* No image will be associated with the rectangle.
*
* The \a maxSide argument specifies the maximum permitted side size for the atlas.
* The calculated solution can only be less than or equal to this size - if it cannot fit,
* then algorithm will gracefully fail and return an invalid QgsTextureAtlas.
*
* \see createFromImages()
*/
static QgsTextureAtlas createFromRects( const QVector< QRect > &rectangles, int maxSide = 1000 );
/**
* Creates a texture atlas for a set of \a images.
*
* The \a maxSide argument specifies the maximum permitted side size for the atlas.
* The calculated solution can only be less than or equal to this size - if it cannot fit,
* then algorithm will gracefully fail and return an invalid QgsTextureAtlas.
*
* \see createFromRects()
*/
static QgsTextureAtlas createFromImages( const QVector< QImage > &images, int maxSide = 1000 );
private:
/**
* Generates the packing solution for a set of texture \a rects.
*
* The \a maxSide argument specifies the maximum permitted side size for the atlas.
* The calculated solution can only be less than or equal to this size - if it cannot fit,
* then algorithm will gracefully fail and return an invalid QgsTextureAtlas.
*/
static QgsTextureAtlas generateAtlas( std::vector< QgsTextureRect > &rects, int maxSide );
};
#endif // QGSTEXTUREATLASGENERATOR_H

View File

@ -296,7 +296,7 @@ QgsPointCloudLayer *QgsPdalAlgorithmBase::parameterAsPointCloudLayer( const QVar
return nullptr;
// if COPC provider, return as it is
if ( layer->dataProvider()->name() == QStringLiteral( "copc" ) )
if ( layer->dataProvider()->name() == QLatin1String( "copc" ) )
{
return layer;
}

View File

@ -94,7 +94,7 @@ void QgsGeometryCheckAngleAlgorithm::initAlgorithm( const QVariantMap &configura
QStringLiteral( "ERRORS" ), QObject::tr( "Small angle errors" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -97,7 +97,7 @@ void QgsGeometryCheckAreaAlgorithm::initAlgorithm( const QVariantMap &configurat
QStringLiteral( "OUTPUT" ), QObject::tr( "Small polygons features" ), Qgis::ProcessingSourceType::VectorPolygon, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -98,7 +98,7 @@ void QgsGeometryCheckContainedAlgorithm::initAlgorithm( const QVariantMap &confi
QStringLiteral( "OUTPUT" ), QObject::tr( "Contained features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);

View File

@ -88,7 +88,7 @@ void QgsGeometryCheckDangleAlgorithm::initAlgorithm( const QVariantMap &configur
QStringLiteral( "OUTPUT" ), QObject::tr( "Dangle-end features" ), Qgis::ProcessingSourceType::VectorLine, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -87,7 +87,7 @@ void QgsGeometryCheckDegeneratePolygonAlgorithm::initAlgorithm( const QVariantMa
QStringLiteral( "OUTPUT" ), QObject::tr( "Degenerate polygons features" ), Qgis::ProcessingSourceType::VectorPolygon, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13 );
auto tolerance = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13 );
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );
tolerance->setHelp( QObject::tr( "The \"Tolerance\" advanced parameter defines the numerical precision of geometric operations, "
"given as an integer n, meaning that any difference smaller than 10⁻ⁿ (in map units) is considered zero." ) );

View File

@ -94,7 +94,7 @@ void QgsGeometryCheckDuplicateAlgorithm::initAlgorithm( const QVariantMap &confi
QStringLiteral( "OUTPUT" ), QObject::tr( "Duplicate geometries" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -89,7 +89,7 @@ void QgsGeometryCheckDuplicateNodesAlgorithm::initAlgorithm( const QVariantMap &
QStringLiteral( "OUTPUT" ), QObject::tr( "Duplicated vertices features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -92,7 +92,7 @@ void QgsGeometryCheckFollowBoundariesAlgorithm::initAlgorithm( const QVariantMap
QStringLiteral( "REF_LAYER" ), QObject::tr( "Reference layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorPolygon )
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -108,7 +108,7 @@ void QgsGeometryCheckGapAlgorithm::initAlgorithm( const QVariantMap &configurati
QStringLiteral( "OUTPUT" ), QObject::tr( "Gap features" ), Qgis::ProcessingSourceType::VectorPolygon
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -94,7 +94,7 @@ void QgsGeometryCheckHoleAlgorithm::initAlgorithm( const QVariantMap &configurat
QStringLiteral( "OUTPUT" ), QObject::tr( "Polygons with holes" ), Qgis::ProcessingSourceType::VectorPolygon, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -88,7 +88,7 @@ void QgsGeometryCheckLineIntersectionAlgorithm::initAlgorithm( const QVariantMap
QStringLiteral( "OUTPUT" ), QObject::tr( "Intersecting feature" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -94,7 +94,7 @@ void QgsGeometryCheckLineLayerIntersectionAlgorithm::initAlgorithm( const QVaria
QStringLiteral( "OUTPUT" ), QObject::tr( "Line intersecting other layer features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -95,7 +95,7 @@ void QgsGeometryCheckMissingVertexAlgorithm::initAlgorithm( const QVariantMap &c
QStringLiteral( "OUTPUT" ), QObject::tr( "Missing vertices features" ), Qgis::ProcessingSourceType::VectorPolygon, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -93,7 +93,7 @@ void QgsGeometryCheckMultipartAlgorithm::initAlgorithm( const QVariantMap &confi
QStringLiteral( "OUTPUT" ), QObject::tr( "One-part geometry features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -92,7 +92,7 @@ void QgsGeometryCheckOverlapAlgorithm::initAlgorithm( const QVariantMap &configu
QStringLiteral( "MIN_OVERLAP_AREA" ), QObject::tr( "Minimum overlap area" ), Qgis::ProcessingNumberParameterType::Double, 0, false, 0.0
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -88,7 +88,7 @@ void QgsGeometryCheckPointCoveredByLineAlgorithm::initAlgorithm( const QVariantM
QStringLiteral( "ERRORS" ), QObject::tr( "Points not covered by a line" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -88,7 +88,7 @@ void QgsGeometryCheckPointInPolygonAlgorithm::initAlgorithm( const QVariantMap &
QStringLiteral( "ERRORS" ), QObject::tr( "Points outside polygons errors" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -93,7 +93,7 @@ void QgsGeometryCheckSegmentLengthAlgorithm::initAlgorithm( const QVariantMap &c
QStringLiteral( "MIN_SEGMENT_LENGTH" ), QObject::tr( "Minimum segment length" ), Qgis::ProcessingNumberParameterType::Double, 0, false, 0.0
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -89,7 +89,7 @@ void QgsGeometryCheckSelfContactAlgorithm::initAlgorithm( const QVariantMap &con
QStringLiteral( "OUTPUT" ), QObject::tr( "Self contact features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -91,7 +91,7 @@ void QgsGeometryCheckSelfIntersectionAlgorithm::initAlgorithm( const QVariantMap
QStringLiteral( "OUTPUT" ), QObject::tr( "Self-intersecting features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, false
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -100,7 +100,7 @@ void QgsGeometryCheckSliverPolygonAlgorithm::initAlgorithm( const QVariantMap &c
QStringLiteral( "MAX_AREA" ), QObject::tr( "Maximum area (map units squared)" ), Qgis::ProcessingNumberParameterType::Double, 0, false, 0.0
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -196,11 +196,11 @@ QVariantMap QgsClimbAlgorithm::processAlgorithm( const QVariantMap &parameters,
if ( !noGeometry.empty() )
{
feedback->pushInfo( QObject::tr( "The following features do not have geometry: %1" ).arg( noGeometry.join( QStringLiteral( ", " ) ) ) );
feedback->pushInfo( QObject::tr( "The following features do not have geometry: %1" ).arg( noGeometry.join( QLatin1String( ", " ) ) ) );
}
if ( !noZValue.empty() )
{
feedback->pushInfo( QObject::tr( "The following points do not have Z value: %1" ).arg( noZValue.join( QStringLiteral( ", " ) ) ) );
feedback->pushInfo( QObject::tr( "The following points do not have Z value: %1" ).arg( noZValue.join( QLatin1String( ", " ) ) ) );
}
QVariantMap results;

View File

@ -107,7 +107,7 @@ void QgsFixGeometryAngleAlgorithm::initAlgorithm( const QVariantMap &configurati
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing small angles" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -120,7 +120,7 @@ void QgsFixGeometryAreaAlgorithm::initAlgorithm( const QVariantMap &configuratio
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from merging small polygons" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -104,7 +104,7 @@ void QgsFixGeometryDuplicateNodesAlgorithm::initAlgorithm( const QVariantMap &co
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing duplicate vertices" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -108,7 +108,7 @@ void QgsFixGeometryGapAlgorithm::initAlgorithm( const QVariantMap &configuration
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing gaps" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -104,7 +104,7 @@ void QgsFixGeometryHoleAlgorithm::initAlgorithm( const QVariantMap &configuratio
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing holes" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -105,7 +105,7 @@ void QgsFixGeometryMissingVertexAlgorithm::initAlgorithm( const QVariantMap &con
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing border vertices" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -91,7 +91,7 @@ void QgsFixGeometryMultipartAlgorithm::initAlgorithm( const QVariantMap &configu
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing multiparts" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -99,7 +99,7 @@ void QgsFixGeometryOverlapAlgorithm::initAlgorithm( const QVariantMap &configura
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing overlaps" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -129,7 +129,7 @@ void QgsFixGeometrySelfIntersectionAlgorithm::initAlgorithm( const QVariantMap &
QStringLiteral( "REPORT" ), QObject::tr( "Report layer from fixing self-intersections" ), Qgis::ProcessingSourceType::VectorPoint
) );
std::unique_ptr<QgsProcessingParameterNumber> tolerance = std::make_unique<QgsProcessingParameterNumber>(
auto tolerance = std::make_unique<QgsProcessingParameterNumber>(
QStringLiteral( "TOLERANCE" ), QObject::tr( "Tolerance" ), Qgis::ProcessingNumberParameterType::Integer, 8, false, 1, 13
);
tolerance->setFlags( tolerance->flags() | Qgis::ProcessingParameterFlag::Advanced );

View File

@ -90,7 +90,7 @@ Qgis::ProcessingFeatureSourceFlags QgsRemovePartsByAreaAlgorithm::sourceFlags()
void QgsRemovePartsByAreaAlgorithm::initParameters( const QVariantMap & )
{
std::unique_ptr< QgsProcessingParameterArea > minArea = std::make_unique< QgsProcessingParameterArea >( QStringLiteral( "MIN_AREA" ), QObject::tr( "Remove parts with area less than" ), 0.0, QStringLiteral( "INPUT" ), false, 0 );
auto minArea = std::make_unique< QgsProcessingParameterArea >( QStringLiteral( "MIN_AREA" ), QObject::tr( "Remove parts with area less than" ), 0.0, QStringLiteral( "INPUT" ), false, 0 );
minArea->setIsDynamic( true );
minArea->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "MIN_AREA" ), QObject::tr( "Remove parts with area less than" ), QgsPropertyDefinition::DoublePositive ) );
minArea->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) );

View File

@ -90,7 +90,7 @@ Qgis::ProcessingFeatureSourceFlags QgsRemovePartsByLengthAlgorithm::sourceFlags(
void QgsRemovePartsByLengthAlgorithm::initParameters( const QVariantMap & )
{
std::unique_ptr< QgsProcessingParameterDistance > minLength = std::make_unique< QgsProcessingParameterDistance >( QStringLiteral( "MIN_LENGTH" ), QObject::tr( "Remove parts with lengths less than" ), 0.0, QStringLiteral( "INPUT" ), false, 0 );
auto minLength = std::make_unique< QgsProcessingParameterDistance >( QStringLiteral( "MIN_LENGTH" ), QObject::tr( "Remove parts with lengths less than" ), 0.0, QStringLiteral( "INPUT" ), false, 0 );
minLength->setIsDynamic( true );
minLength->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "MIN_LENGTH" ), QObject::tr( "Remove parts with length less than" ), QgsPropertyDefinition::DoublePositive ) );
minLength->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) );

View File

@ -0,0 +1,199 @@
/***************************************************************************
qgsannotationlayer3drendererwidget.cpp
------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsannotationlayer3drendererwidget.h"
#include "moc_qgsannotationlayer3drendererwidget.cpp"
#include "qgsapplication.h"
#include "qgsannotationlayer.h"
#include "qgsannotationlayer3drenderer.h"
#include <QBoxLayout>
#include <QCheckBox>
QgsAnnotationLayer3DRendererWidget::QgsAnnotationLayer3DRendererWidget( QgsAnnotationLayer *layer, QgsMapCanvas *canvas, QWidget *parent )
: QgsMapLayerConfigWidget( layer, canvas, parent )
{
setPanelTitle( tr( "3D View" ) );
setObjectName( QStringLiteral( "mOptsPage_3DView" ) );
setupUi( this );
mComboRendererType->addItem( QgsApplication::getThemeIcon( QStringLiteral( "rendererNullSymbol.svg" ) ), tr( "No Symbols" ), QVariant::fromValue( RendererType::None ) );
mComboRendererType->addItem( QgsApplication::getThemeIcon( QStringLiteral( "rendererSingleSymbol.svg" ) ), tr( "3D Billboards" ), QVariant::fromValue( RendererType::Billboards ) );
mComboRendererType->setCurrentIndex( mComboRendererType->findData( QVariant::fromValue( RendererType::None ) ) );
mStackedWidget->setCurrentWidget( mPageNoRenderer );
connect( mComboRendererType, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsAnnotationLayer3DRendererWidget::rendererTypeChanged );
mComboClamping->addItem( tr( "Relative to Terrain" ), QVariant::fromValue( Qgis::AltitudeClamping::Relative ) );
mComboClamping->addItem( tr( "Absolute" ), QVariant::fromValue( Qgis::AltitudeClamping::Absolute ) );
connect( mComboClamping, qOverload<int>( &QComboBox::currentIndexChanged ), this, &QgsAnnotationLayer3DRendererWidget::clampingChanged );
mComboClamping->setCurrentIndex( mComboClamping->findData( QVariant::fromValue( Qgis::AltitudeClamping::Relative ) ) );
clampingChanged();
connect( mOffsetZSpinBox, qOverload< double >( &QDoubleSpinBox::valueChanged ), this, [this]( double ) {
if ( !mBlockChanges )
emit widgetChanged();
} );
connect( mCheckShowCallouts, &QCheckBox::toggled, this, [this] {
if ( !mBlockChanges )
emit widgetChanged();
} );
syncToLayer( layer );
}
void QgsAnnotationLayer3DRendererWidget::setRenderer( const QgsAnnotationLayer3DRenderer *renderer )
{
mBlockChanges++;
mRenderer.reset( renderer ? renderer->clone() : nullptr );
if ( renderer )
{
mComboRendererType->setCurrentIndex( mComboRendererType->findData( QVariant::fromValue( RendererType::Billboards ) ) );
mComboClamping->setCurrentIndex( mComboClamping->findData( QVariant::fromValue( renderer->altitudeClamping() ) ) );
mOffsetZSpinBox->setValue( renderer->zOffset() );
mCheckShowCallouts->setChecked( renderer->showCalloutLines() );
}
else
{
mComboRendererType->setCurrentIndex( mComboRendererType->findData( QVariant::fromValue( RendererType::None ) ) );
}
mBlockChanges--;
}
std::unique_ptr< QgsAnnotationLayer3DRenderer > QgsAnnotationLayer3DRendererWidget::renderer()
{
const RendererType type = mComboRendererType->currentData().value< RendererType >();
switch ( type )
{
case RendererType::None:
return nullptr;
break;
case RendererType::Billboards:
{
auto renderer = std::make_unique< QgsAnnotationLayer3DRenderer >();
renderer->setAltitudeClamping( mComboClamping->currentData().value< Qgis::AltitudeClamping >() );
renderer->setZOffset( mOffsetZSpinBox->value() );
renderer->setShowCalloutLines( mCheckShowCallouts->isChecked() );
return renderer;
}
}
BUILTIN_UNREACHABLE
}
void QgsAnnotationLayer3DRendererWidget::apply()
{
std::unique_ptr< QgsAnnotationLayer3DRenderer > r = renderer();
mLayer->setRenderer3D( r.release() );
}
void QgsAnnotationLayer3DRendererWidget::rendererTypeChanged()
{
const RendererType type = mComboRendererType->currentData().value< RendererType >();
switch ( type )
{
case RendererType::None:
mStackedWidget->setCurrentWidget( mPageNoRenderer );
break;
case RendererType::Billboards:
mStackedWidget->setCurrentWidget( mPageBillboards );
break;
}
if ( !mBlockChanges )
emit widgetChanged();
}
void QgsAnnotationLayer3DRendererWidget::clampingChanged()
{
switch ( mComboClamping->currentData().value< Qgis::AltitudeClamping >() )
{
case Qgis::AltitudeClamping::Absolute:
mLabelClampingExplanation->setText(
QStringLiteral( "<p><b>%1</b></p><p>%2</p>" ).arg( tr( "All billboards will be placed at the same elevation." ), tr( "The terrain height will be ignored." ) )
);
break;
case Qgis::AltitudeClamping::Relative:
mLabelClampingExplanation->setText(
QStringLiteral( "<p><b>%1</b></p>" ).arg( tr( "Billboard elevation is relative to terrain height." ) )
);
break;
case Qgis::AltitudeClamping::Terrain:
mLabelClampingExplanation->setText(
QStringLiteral( "<p><b>%1</b></p><p>%2</p>" ).arg( tr( "Billboard elevation will be taken directly from the terrain height." ), tr( "Billboards will be placed directly on the terrain." ) )
);
break;
}
if ( !mBlockChanges )
emit widgetChanged();
}
void QgsAnnotationLayer3DRendererWidget::syncToLayer( QgsMapLayer *layer )
{
mLayer = layer;
QgsAbstract3DRenderer *r = layer->renderer3D();
if ( r && r->type() == QLatin1String( "annotation" ) )
{
QgsAnnotationLayer3DRenderer *annotationRenderer = qgis::down_cast<QgsAnnotationLayer3DRenderer *>( r );
setRenderer( annotationRenderer );
}
else
{
setRenderer( nullptr );
}
}
//
// QgsAnnotationLayer3DRendererWidgetFactory
//
QgsAnnotationLayer3DRendererWidgetFactory::QgsAnnotationLayer3DRendererWidgetFactory( QObject *parent )
: QObject( parent )
{
setIcon( QIcon( ":/images/themes/default/3d.svg" ) );
setTitle( tr( "3D View" ) );
}
QgsMapLayerConfigWidget *QgsAnnotationLayer3DRendererWidgetFactory::createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget, QWidget *parent ) const
{
Q_UNUSED( dockWidget )
QgsAnnotationLayer *annotationLayer = qobject_cast<QgsAnnotationLayer *>( layer );
if ( !annotationLayer )
return nullptr;
return new QgsAnnotationLayer3DRendererWidget( annotationLayer, canvas, parent );
}
bool QgsAnnotationLayer3DRendererWidgetFactory::supportLayerPropertiesDialog() const
{
return true;
}
bool QgsAnnotationLayer3DRendererWidgetFactory::supportsStyleDock() const
{
return true;
}
bool QgsAnnotationLayer3DRendererWidgetFactory::supportsLayer( QgsMapLayer *layer ) const
{
return layer->type() == Qgis::LayerType::Annotation;
}
QString QgsAnnotationLayer3DRendererWidgetFactory::layerPropertiesPagePositionHint() const
{
return QStringLiteral( "mOptsPage_Rendering" );
}

View File

@ -0,0 +1,75 @@
/***************************************************************************
qgsannotationlayer3drendererwidget.h
------------------------------
Date : September 2025
Copyright : (C) 2025 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSANNOTATIONLAYER3DRENDERERWIDGET_H
#define QGSANNOTATIONLAYER3DRENDERERWIDGET_H
#include <memory>
#include "qgsmaplayerconfigwidget.h"
#include "qgsmaplayerconfigwidgetfactory.h"
#include "ui_qgsannotationlayer3drendererwidget.h"
class QgsAnnotationLayer;
class QgsAnnotationLayer3DRenderer;
class QgsMapCanvas;
//! Widget for configuration of 3D renderer of an annotation layer
class QgsAnnotationLayer3DRendererWidget : public QgsMapLayerConfigWidget, private Ui::QgsAnnotationLayer3dRendererWidgetBase
{
Q_OBJECT
public:
enum class RendererType
{
None,
Billboards,
};
Q_ENUM( RendererType )
explicit QgsAnnotationLayer3DRendererWidget( QgsAnnotationLayer *layer, QgsMapCanvas *canvas, QWidget *parent = nullptr );
void syncToLayer( QgsMapLayer *layer ) final;
//! no transfer of ownership
void setRenderer( const QgsAnnotationLayer3DRenderer *renderer );
std::unique_ptr< QgsAnnotationLayer3DRenderer > renderer();
public slots:
void apply() override;
private slots:
void rendererTypeChanged();
void clampingChanged();
private:
int mBlockChanges = 0;
std::unique_ptr<QgsAnnotationLayer3DRenderer> mRenderer;
};
class QgsAnnotationLayer3DRendererWidgetFactory : public QObject, public QgsMapLayerConfigWidgetFactory
{
Q_OBJECT
public:
explicit QgsAnnotationLayer3DRendererWidgetFactory( QObject *parent = nullptr );
QgsMapLayerConfigWidget *createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget, QWidget *parent ) const override;
bool supportLayerPropertiesDialog() const override;
bool supportsStyleDock() const override;
bool supportsLayer( QgsMapLayer *layer ) const override;
QString layerPropertiesPagePositionHint() const override;
};
#endif // QGSANNOTATIONLAYER3DRENDERERWIDGET_H

Some files were not shown because too many files have changed in this diff Show More