diffstat for f3d-3.2.0+dfsg f3d-3.4.1

 .clang-tidy                                                                           |    2 
 .cppcheck.supp                                                                        |   20 
 .github/FUNDING.yml                                                                   |    4 
 .github/ISSUE_TEMPLATE/new_release.md                                                 |   28 
 .github/PULL_REQUEST_TEMPLATE.md                                                      |   23 
 .github/actions/alembic-install-dep/action.yml                                        |   17 
 .github/actions/alembic-install-dep/fix_cmake_rpath.patch                             |   26 
 .github/actions/assimp-install-dep/action.yml                                         |    6 
 .github/actions/blosc-install-dep/action.yml                                          |   20 
 .github/actions/coverage-ci/action.yml                                                |   40 
 .github/actions/draco-install-dep/action.yml                                          |    6 
 .github/actions/external-build-ci/action.yml                                          |   20 
 .github/actions/f3d-dependencies/action.yml                                           |   33 
 .github/actions/generic-ci/action.yml                                                 |  111 
 .github/actions/generic-dependencies/action.yml                                       |   37 
 .github/actions/imath-install-dep/action.yml                                          |   18 
 .github/actions/occt-install-dep/action.yml                                           |   10 
 .github/actions/openexr-install-dep/action.yml                                        |   18 
 .github/actions/openexr-install-dep/include_cstdint.patch                             |   39 
 .github/actions/openvdb-install-dep/action.yml                                        |   20 
 .github/actions/ospray-sb-install-dep/0001-fix-embree-for-modern-clang.patch          |   74 
 .github/actions/ospray-sb-install-dep/action.yml                                      |   29 
 .github/actions/pybind11-install-dep/action.yml                                       |   11 
 .github/actions/python-ci/action.yml                                                  |   44 
 .github/actions/sanitizer-ci/action.yml                                               |   38 
 .github/actions/static-analysis-ci/action.yml                                         |   33 
 .github/actions/tbb-install-dep/action.yml                                            |   16 
 .github/actions/usd-install-dep/action.yml                                            |   19 
 .github/actions/usd-install-dep/usd-install-bin-mindep.patch                          |   77 
 .github/actions/usd-install-dep/usd-install-bin.patch                                 |   33 
 .github/actions/vtk-dependencies/action.yml                                           |   23 
 .github/actions/vtk-install-dep/action.yml                                            |   34 
 .github/actions/wasm-ci/action.yml                                                    |   45 
 .github/actions/webp-install-dep/action.yml                                           |   86 
 .github/actions/zlib-install-dep/action.yml                                           |   16 
 .github/baselines/install_example_plugin_output.png                                   |binary
 .github/baselines/install_output.png                                                  |binary
 .github/workflows/build-docker-images.yml                                             |   42 
 .github/workflows/build-website.yml                                                   |   38 
 .github/workflows/ci.yml                                                              |  555 +
 .github/workflows/labelize.yml                                                        |   52 
 .github/workflows/nightly_vtk_master.yml                                              |   98 
 .github/workflows/public-api-warn.yml                                                 |    2 
 .github/workflows/style-checks.yml                                                    |   77 
 .github/workflows/version-timestamp-warn.yml                                          |    2 
 .github/workflows/versions.json                                                       |   43 
 .gitignore                                                                            |    1 
 .lsan.supp                                                                            |   15 
 .tsan.supp                                                                            |    3 
 CMakeLists.txt                                                                        |   76 
 CNAME                                                                                 |    1 
 CODE_OF_CONDUCT.md                                                                    |    4 
 CONTRIBUTING.md                                                                       |   50 
 LICENSE.md                                                                            |    1 
 README.md                                                                             |   42 
 THIRD_PARTY_LICENSES.md                                                               |  268 
 _config.yml                                                                           |  340 -
 _includes/head_custom.html                                                            |    9 
 _licenses.md                                                                          |    7 
 _sass/custom/custom.scss                                                              |   69 
 application/CMakeLists.txt                                                            |   14 
 application/F3DNSDelegate.h                                                           |    1 
 application/F3DNSDelegate.mm                                                          |   74 
 application/F3DOptionsTools.cxx                                                       |  185 
 application/F3DOptionsTools.h                                                         |   54 
 application/F3DStarter.cxx                                                            |  768 +-
 application/F3DStarter.h                                                              |    9 
 application/main.cxx                                                                  |    6 
 application/testing/CMakeLists.txt                                                    |  590 +
 application/testing/test_watch.ps1                                                    |   17 
 application/testing/test_watch.sh                                                     |   17 
 assets/main.js                                                                        |  115 
 c/CMakeLists.txt                                                                      |  111 
 c/camera_c_api.cxx                                                                    |  285 
 c/camera_c_api.h                                                                      |  204 
 c/context_c_api.cxx                                                                   |   59 
 c/context_c_api.h                                                                     |   88 
 c/engine_c_api.cxx                                                                    |  497 +
 c/engine_c_api.h                                                                      |  362 +
 c/image_c_api.cxx                                                                     |  271 
 c/image_c_api.h                                                                       |  266 
 c/interactor_c_api.cxx                                                                |  657 ++
 c/interactor_c_api.h                                                                  |  504 +
 c/log_c_api.cxx                                                                       |  106 
 c/log_c_api.h                                                                         |  107 
 c/options_c_api.cxx                                                                   |  527 +
 c/options_c_api.h                                                                     |  405 +
 c/scene_c_api.cxx                                                                     |  315 
 c/scene_c_api.h                                                                       |  150 
 c/testing/CMakeLists.txt                                                              |   31 
 c/testing/test_camera_c_api.c                                                         |   81 
 c/testing/test_engine_c_api.c                                                         |   93 
 c/testing/test_image_c_api.c                                                          |  100 
 c/testing/test_interactor_c_api.c                                                     |  142 
 c/testing/test_log_c_api.c                                                            |   88 
 c/testing/test_options_c_api.c                                                        |  178 
 c/testing/test_scene_c_api.c                                                          |  108 
 c/testing/test_types_c_api.c                                                          |   36 
 c/testing/test_utils_c_api.c                                                          |   78 
 c/testing/test_window_c_api.c                                                         |   64 
 c/types_c_api.cxx                                                                     |  257 
 c/types_c_api.h                                                                       |  207 
 c/utils_c_api.cxx                                                                     |  134 
 c/utils_c_api.h                                                                       |  109 
 c/window_c_api.cxx                                                                    |  174 
 c/window_c_api.h                                                                      |  158 
 cmake/c_api-config.cmake                                                              |   10 
 cmake/f3dConfig.cmake.in                                                              |    4 
 cmake/f3dPlugin.cmake                                                                 |    3 
 cmake/pluginsdk-config.cmake                                                          |    2 
 debian/changelog                                                                      |    9 
 debian/patches/older-imgui.patch                                                      |   13 
 debian/patches/series                                                                 |    1 
 doc/CHANGELOG.md                                                                      |  124 
 doc/GALLERY.md                                                                        |   74 
 doc/THIRD_PARTY_LICENSES.md                                                           |  268 
 doc/dev/03-GETTING_STARTED.md                                                         |  251 
 doc/dev/04-BUILD.md                                                                   |  105 
 doc/dev/05-TESTING.md                                                                 |  161 
 doc/dev/06-TOOLING.md                                                                 |   42 
 doc/dev/07-ARCHITECTURE.md                                                            |   96 
 doc/dev/08-CODING_STYLE.md                                                            |   87 
 doc/dev/09-ROADMAPS_AND_RELEASES.md                                                   |   24 
 doc/dev/10-GOVERNANCE.md                                                              |   94 
 doc/dev/11-MAINTAINERS_AND_CONTRIBUTORS.md                                            |   34 
 doc/dev/12-MENTORING.md                                                               |   62 
 doc/dev/13-BUILD_WASM.md                                                              |   52 
 doc/dev/ARCHITECTURE.md                                                               |   95 
 doc/dev/BUILD.md                                                                      |  100 
 doc/dev/BUILD_WASM.md                                                                 |   43 
 doc/dev/CODING_STYLE.md                                                               |   87 
 doc/dev/GENERATE.md                                                                   |   32 
 doc/dev/GETTING_STARTED.md                                                            |  259 
 doc/dev/GOVERNANCE.md                                                                 |   93 
 doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md                                               |   32 
 doc/dev/README_DEV.md                                                                 |   14 
 doc/dev/ROADMAPS_AND_RELEASES.md                                                      |   24 
 doc/dev/TESTING.md                                                                    |  161 
 doc/libf3d/01-OVERVIEW.md                                                             |  151 
 doc/libf3d/02-CLASSES.md                                                              |   82 
 doc/libf3d/03-OPTIONS.md                                                              |  623 +
 doc/libf3d/04-LANGUAGE_BINDINGS.md                                                    |  177 
 doc/libf3d/05-PLUGINS.md                                                              |   88 
 doc/libf3d/CLASSES.md                                                                 |   82 
 doc/libf3d/LANGUAGE_BINDINGS.md                                                       |   61 
 doc/libf3d/OPTIONS.md                                                                 |  203 
 doc/libf3d/OVERVIEW.md                                                                |  151 
 doc/libf3d/PLUGINS.md                                                                 |   85 
 doc/libf3d/README_LIBF3D.md                                                           |    7 
 doc/user/01-QUICKSTART.md                                                             |  102 
 doc/user/02-SUPPORTED_FORMATS.md                                                      |   88 
 doc/user/03-OPTIONS.md                                                                |  578 +
 doc/user/04-INTERACTIONS.md                                                           |  138 
 doc/user/05-ANIMATIONS.md                                                             |   84 
 doc/user/06-CONFIGURATION_FILE.md                                                     |  254 
 doc/user/07-COMMANDS.md                                                               |  173 
 doc/user/08-PARSING.md                                                                |  117 
 doc/user/09-COLOR_MAPS.md                                                             |   57 
 doc/user/10-FINAL_SHADER.md                                                           |   76 
 doc/user/11-DESKTOP_INTEGRATION.md                                                    |   74 
 doc/user/12-PLUGINS.md                                                                |   52 
 doc/user/13-LIMITATIONS_AND_TROUBLESHOOTING.md                                        |  162 
 doc/user/ANIMATIONS.md                                                                |   81 
 doc/user/COLOR_MAPS.md                                                                |   65 
 doc/user/COMMANDS.md                                                                  |  145 
 doc/user/CONFIGURATION_FILE.md                                                        |  254 
 doc/user/DESKTOP_INTEGRATION.md                                                       |   74 
 doc/user/FINAL_SHADER.md                                                              |   76 
 doc/user/INSTALLATION.md                                                              |   62 
 doc/user/INTERACTIONS.md                                                              |  127 
 doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md                                           |  155 
 doc/user/OPTIONS.md                                                                   |  208 
 doc/user/PARSING.md                                                                   |  110 
 doc/user/PLUGINS.md                                                                   |   51 
 doc/user/QUICKSTART.md                                                                |  102 
 doc/user/README_USER.md                                                               |   17 
 doc/user/SPONSORING.md                                                                |   50 
 doc/user/SUPPORTED_FORMATS.md                                                         |   89 
 examples/CMakeLists.txt                                                               |   12 
 examples/README.md                                                                    |   20 
 examples/libf3d/CMakeLists.txt                                                        |   20 
 examples/libf3d/c/CMakeLists.txt                                                      |    5 
 examples/libf3d/c/interactive-app/CMakeLists.txt                                      |   12 
 examples/libf3d/c/interactive-app/main.c                                              |   97 
 examples/libf3d/c/tests/CMakeLists.txt                                                |    4 
 examples/libf3d/c/tests/data/cow.vtp                                                  |    3 
 examples/libf3d/cpp/CMakeLists.txt                                                    |   26 
 examples/libf3d/cpp/check-engine/CMakeLists.txt                                       |    7 
 examples/libf3d/cpp/external-glfw/CMakeLists.txt                                      |   10 
 examples/libf3d/cpp/external-glfw/main.cxx                                            |  435 +
 examples/libf3d/cpp/fltk/CMakeLists.txt                                               |   23 
 examples/libf3d/cpp/fltk/main.cxx                                                     |  427 +
 examples/libf3d/cpp/interactive-app/CMakeLists.txt                                    |    9 
 examples/libf3d/cpp/interactive-app/main.cxx                                          |  235 
 examples/libf3d/cpp/multi-files/CMakeLists.txt                                        |    8 
 examples/libf3d/cpp/multi-files/data/mb_0_0.vtu                                       |binary
 examples/libf3d/cpp/multi-files/data/mb_1_0.vtp                                       |binary
 examples/libf3d/cpp/multi-files/data/mb_2_0.vtp                                       |binary
 examples/libf3d/cpp/multi-files/main.cxx                                              |   23 
 examples/libf3d/cpp/offscreen-thumbnail/CMakeLists.txt                                |    9 
 examples/libf3d/cpp/offscreen-thumbnail/main.cxx                                      |  148 
 examples/libf3d/cpp/qml/CMakeLists.txt                                                |   36 
 examples/libf3d/cpp/qml/F3DRenderer.cxx                                               |  344 +
 examples/libf3d/cpp/qml/F3DRenderer.h                                                 |   77 
 examples/libf3d/cpp/qml/F3DView.cxx                                                   |  101 
 examples/libf3d/cpp/qml/F3DView.h                                                     |   40 
 examples/libf3d/cpp/qml/Main.qml                                                      |   77 
 examples/libf3d/cpp/qml/main.cxx                                                      |   78 
 examples/libf3d/cpp/qt6/CMakeLists.txt                                                |   12 
 examples/libf3d/cpp/qt6/main.cxx                                                      |  429 +
 examples/libf3d/cpp/render-image/CMakeLists.txt                                       |   12 
 examples/libf3d/cpp/render-image/baselines/cow.png                                    |binary
 examples/libf3d/cpp/render-image/check.cxx                                            |   17 
 examples/libf3d/cpp/render-image/data/cow.vtp                                         |   33 
 examples/libf3d/cpp/render-interact/CMakeLists.txt                                    |    8 
 examples/libf3d/cpp/render-interact/data/cow.vtp                                      |   33 
 examples/libf3d/cpp/render-interact/main.cxx                                          |   24 
 examples/libf3d/cpp/tests/CMakeLists.txt                                              |   67 
 examples/libf3d/cpp/tests/baselines/cow.png                                           |    3 
 examples/libf3d/cpp/tests/baselines/thumbnail.png                                     |    3 
 examples/libf3d/cpp/tests/check.cxx                                                   |   17 
 examples/libf3d/cpp/tests/data/cow.vtp                                                |    3 
 examples/libf3d/cpp/tests/data/mb/mb_0_0.vtu                                          |    3 
 examples/libf3d/cpp/tests/data/mb/mb_1_0.vtp                                          |    3 
 examples/libf3d/cpp/tests/data/mb/mb_2_0.vtp                                          |    3 
 examples/libf3d/cpp/use-options-string/CMakeLists.txt                                 |    8 
 examples/libf3d/cpp/use-options-string/data/cow.vtp                                   |   33 
 examples/libf3d/cpp/use-options-string/main.cxx                                       |   25 
 examples/libf3d/cpp/use-options-struct/CMakeLists.txt                                 |    8 
 examples/libf3d/cpp/use-options-struct/data/cow.vtp                                   |   33 
 examples/libf3d/cpp/use-options-struct/main.cxx                                       |   24 
 examples/libf3d/cpp/use-options-variant/CMakeLists.txt                                |    8 
 examples/libf3d/cpp/use-options-variant/data/cow.vtp                                  |   33 
 examples/libf3d/cpp/use-options-variant/main.cxx                                      |   25 
 examples/libf3d/java/CMakeLists.txt                                                   |    3 
 examples/libf3d/java/interactive-app/CMakeLists.txt                                   |   34 
 examples/libf3d/java/interactive-app/InteractiveApp.java                              |   69 
 examples/libf3d/python/check-engine/check_engine.py                                   |    9 
 examples/libf3d/python/check-engine/test_check_engine.py                              |    5 
 examples/libf3d/python/img-cmp/img-cmp.py                                             |   19 
 examples/libf3d/python/img-cmp/img_cmp.py                                             |   27 
 examples/libf3d/python/img-cmp/test_img_cmp.py                                        |    8 
 examples/libf3d/python/interactive-app/interactive_app.py                             |  243 
 examples/libf3d/python/interactive-app/test_interactive_app.py                        |    8 
 examples/libf3d/python/offscreen-thumbnail/offscreen_thumbnail.py                     |   75 
 examples/libf3d/python/offscreen-thumbnail/test_offscreen_thumbnail.py                |   14 
 examples/libf3d/python/qml/Main.qml                                                   |   73 
 examples/libf3d/python/qml/minimal_qml.py                                             |  317 
 examples/libf3d/python/qml/test_minimal_qml.py                                        |   14 
 examples/libf3d/python/qt6/minimal_qt6.py                                             |  311 
 examples/libf3d/python/qt6/test_minimal_qt6.py                                        |   15 
 examples/libf3d/python/render-image/render_image.py                                   |    9 
 examples/libf3d/python/render-image/test_render_image.py                              |    9 
 examples/libf3d/python/render-interact/render_interact.py                             |   21 
 examples/libf3d/python/render-interact/test_render_interact.py                        |    8 
 examples/libf3d/python/render-terminal/render_terminal.py                             |    2 
 examples/libf3d/python/render-terminal/test_render_terminal.py                        |    5 
 examples/libf3d/python/tests/baselines/cow.png                                        |    3 
 examples/libf3d/python/tests/data/cow.vtp                                             |    3 
 examples/libf3d/python/tkinter/minimal_tkinter.py                                     |   20 
 examples/libf3d/python/tkinter/test_minimal_tkinter.py                                |   10 
 examples/libf3d/web/index.html                                                        |  133 
 examples/libf3d/web/package-lock.json                                                 | 1043 +++
 examples/libf3d/web/package.json                                                      |   19 
 examples/libf3d/web/src/main.js                                                       |  153 
 examples/libf3d/web/vite.config.js                                                    |   32 
 examples/plugins/CMakeLists.txt                                                       |    1 
 external/.clang-tidy                                                                  |    2 
 external/VERSIONS.md                                                                  |    6 
 external/cxxopts/cxxopts.hpp                                                          |  104 
 external/dmon/dmon.h                                                                  |    4 
 external/imgui/imconfig.h                                                             |    5 
 external/imgui/imgui.cpp                                                              | 1986 ++++--
 external/imgui/imgui.h                                                                |  937 ++
 external/imgui/imgui_demo.cpp                                                         |  771 +-
 external/imgui/imgui_draw.cpp                                                         | 3280 +++++++---
 external/imgui/imgui_internal.h                                                       |  584 +
 external/imgui/imgui_tables.cpp                                                       |   75 
 external/imgui/imgui_widgets.cpp                                                      |  973 +-
 external/imgui/imstb_textedit.h                                                       |  148 
 external/imgui/imstb_truetype.h                                                       |    4 
 external/tinyfiledialogs/tinyfiledialogs.cxx                                          | 1555 ++--
 external/tinyfiledialogs/tinyfiledialogs.h                                            |   10 
 java/CMakeLists.txt                                                                   |   22 
 java/Camera.java                                                                      |  241 
 java/Context.java                                                                     |   60 
 java/Engine.java                                                                      |  296 
 java/F3DCameraBindings.cxx                                                            |  196 
 java/F3DContextBindings.cxx                                                           |   61 
 java/F3DEngineBindings.cxx                                                            |  252 
 java/F3DImageBindings.cxx                                                             |  259 
 java/F3DInteractorBindings.cxx                                                        |  645 +
 java/F3DJavaBindings.cxx                                                              |  201 
 java/F3DJavaBindings.h                                                                |   70 
 java/F3DLogBindings.cxx                                                               |  172 
 java/F3DOptionsBindings.cxx                                                           |  244 
 java/F3DSceneBindings.cxx                                                             |  382 +
 java/F3DTypesBindings.cxx                                                             |  134 
 java/F3DUtilsBindings.cxx                                                             |  131 
 java/F3DWindowBindings.cxx                                                            |  140 
 java/Image.java                                                                       |  237 
 java/Interactor.java                                                                  |  525 +
 java/Log.java                                                                         |  129 
 java/Options.java                                                                     |  245 
 java/Scene.java                                                                       |  111 
 java/Transform2D.java                                                                 |   31 
 java/Types.java                                                                       |  136 
 java/Utils.java                                                                       |  124 
 java/Window.java                                                                      |  127 
 java/testing/CMakeLists.txt                                                           |   11 
 java/testing/TestCamera.java                                                          |   64 
 java/testing/TestEngine.java                                                          |   50 
 java/testing/TestImage.java                                                           |   61 
 java/testing/TestInteractor.java                                                      |   63 
 java/testing/TestJavaBindings.java                                                    |   35 
 java/testing/TestLog.java                                                             |   71 
 java/testing/TestOptions.java                                                         |   57 
 java/testing/TestScene.java                                                           |   73 
 java/testing/TestTypes.java                                                           |   63 
 java/testing/TestUtils.java                                                           |   44 
 java/testing/TestWindow.java                                                          |   57 
 library/CMakeLists.txt                                                                |   14 
 library/doxygen/conf.py                                                               |   80 
 library/doxygen/index.rst                                                             |   26 
 library/options.json                                                                  |   76 
 library/private/animationManager.h                                                    |   60 
 library/private/camera_impl.h                                                         |   27 
 library/private/interactor_impl.h                                                     |   32 
 library/private/options_tools.h                                                       |  216 
 library/private/scene_impl.h                                                          |    6 
 library/private/window_impl.h                                                         |   11 
 library/public/camera.h                                                               |   48 
 library/public/context.h                                                              |    4 
 library/public/engine.h                                                               |  115 
 library/public/exception.h                                                            |    2 
 library/public/image.h                                                                |   62 
 library/public/interactor.h                                                           |  197 
 library/public/log.h                                                                  |   27 
 library/public/options.h.in                                                           |   46 
 library/public/scene.h                                                                |   45 
 library/public/types.h                                                                |   59 
 library/public/utils.h                                                                |   56 
 library/public/window.h                                                               |   25 
 library/src/animationManager.cxx                                                      |   99 
 library/src/camera_impl.cxx                                                           |   51 
 library/src/engine.cxx                                                                |   24 
 library/src/factory.cxx.in                                                            |   23 
 library/src/image.cxx                                                                 |    3 
 library/src/init.cxx                                                                  |   13 
 library/src/interactor_impl.cxx                                                       |  901 ++
 library/src/log.cxx                                                                   |   48 
 library/src/scene_impl.cxx                                                            |  131 
 library/src/types.cxx                                                                 |   35 
 library/src/utils.cxx                                                                 |    9 
 library/src/window_impl.cxx                                                           |  182 
 library/testing/CMakeLists.txt                                                        |  143 
 library/testing/PseudoUnitTest.h                                                      |    2 
 library/testing/TestPseudoUnitTest.cxx                                                |    2 
 library/testing/TestSDKAnimation.cxx                                                  |   29 
 library/testing/TestSDKCamera.cxx                                                     |   62 
 library/testing/TestSDKCompareWithFile.cxx                                            |    2 
 library/testing/TestSDKConsole.cxx                                                    |    2 
 library/testing/TestSDKDeprecatedOptions.cxx                                          |    3 
 library/testing/TestSDKDropZone.cxx                                                   |    7 
 library/testing/TestSDKDropZoneDeprecated.cxx                                         |   61 
 library/testing/TestSDKDropZoneMinSize.cxx                                            |    7 
 library/testing/TestSDKDropZoneNoLogo.cxx                                             |   23 
 library/testing/TestSDKDynamicBackgroundColor.cxx                                     |    2 
 library/testing/TestSDKDynamicFontFile.cxx                                            |    2 
 library/testing/TestSDKDynamicHDRI.cxx                                                |   16 
 library/testing/TestSDKDynamicLightIntensity.cxx                                      |    8 
 library/testing/TestSDKDynamicProperties.cxx                                          |    2 
 library/testing/TestSDKDynamicUpDirection.cxx                                         |   71 
 library/testing/TestSDKEngine.cxx                                                     |   26 
 library/testing/TestSDKEngineExceptions.cxx                                           |   19 
 library/testing/TestSDKEngineRecreation.cxx                                           |    2 
 library/testing/TestSDKExternalWindowCOCOA.mm                                         |    4 
 library/testing/TestSDKExternalWindowEGL.cxx                                          |    2 
 library/testing/TestSDKExternalWindowGLFW.cxx                                         |   12 
 library/testing/TestSDKExternalWindowGLX.cxx                                          |   32 
 library/testing/TestSDKExternalWindowOSMesa.cxx                                       |    2 
 library/testing/TestSDKExternalWindowQT.cxx                                           |    2 
 library/testing/TestSDKExternalWindowWGL.cxx                                          |    2 
 library/testing/TestSDKHelpers.h                                                      |   23 
 library/testing/TestSDKImage.cxx                                                      |   31 
 library/testing/TestSDKInteractorCallBack.cxx                                         |   32 
 library/testing/TestSDKInteractorCommand.cxx                                          |    5 
 library/testing/TestSDKInteractorDocumentation.cxx                                    |   19 
 library/testing/TestSDKInteractorDropFullScene.cxx                                    |    4 
 library/testing/TestSDKInvalidForceReader.cxx                                         |    2 
 library/testing/TestSDKLog.cxx                                                        |   11 
 library/testing/TestSDKMultiColoring.cxx                                              |    2 
 library/testing/TestSDKMultiOptions.cxx                                               |    2 
 library/testing/TestSDKOptions.cxx                                                    |   58 
 library/testing/TestSDKOptionsIO.cxx                                                  |   48 
 library/testing/TestSDKRenderAndInteract.cxx                                          |    2 
 library/testing/TestSDKRenderFinalShader.cxx                                          |    2 
 library/testing/TestSDKScene.cxx                                                      |   48 
 library/testing/TestSDKSceneFromMemory.cxx                                            |    2 
 library/testing/TestSDKSceneInvalid.cxx                                               |    2 
 library/testing/TestSDKStartInteractor.cxx                                            |   16 
 library/testing/TestSDKTriggerInteractions.cxx                                        |  144 
 library/testing/TestSDKUI.cxx                                                         |   39 
 library/testing/TestSDKUtils.cxx                                                      |   14 
 library/testing/TestSDKWindowAuto.cxx                                                 |    9 
 package.json                                                                          |   33 
 plugins/CMakeLists.txt                                                                |    7 
 plugins/alembic/configs/config.d/10_alembic.json                                      |    1 
 plugins/alembic/configs/thumbnail.d/10_alembic.json                                   |    1 
 plugins/alembic/module/CMakeLists.txt                                                 |    9 
 plugins/alembic/module/Testing/CMakeLists.txt                                         |    7 
 plugins/alembic/module/Testing/TestF3DAlembicReader.cxx                               |    3 
 plugins/alembic/module/Testing/TestF3DAlembicReaderStream.cxx                         |   20 
 plugins/alembic/module/Testing/TestF3DAlembicReaderStreamError.cxx                    |   70 
 plugins/alembic/module/vtkF3DAlembicReader.cxx                                        |  245 
 plugins/alembic/module/vtkF3DAlembicReader.h                                          |   20 
 plugins/assimp/module/CMakeLists.txt                                                  |    9 
 plugins/assimp/module/Testing/CMakeLists.txt                                          |    7 
 plugins/assimp/module/Testing/TestF3DAssimpImportError.cxx                            |   10 
 plugins/assimp/module/Testing/TestF3DAssimpImporter.cxx                               |    8 
 plugins/assimp/module/Testing/TestF3DAssimpImporterStream.cxx                         |   34 
 plugins/assimp/module/Testing/TestF3DAssimpImporterStreamError.cxx                    |   26 
 plugins/assimp/module/vtkF3DAssimpImporter.cxx                                        |   75 
 plugins/assimp/module/vtkF3DAssimpImporter.h                                          |   29 
 plugins/draco/module/CMakeLists.txt                                                   |    7 
 plugins/draco/module/Testing/CMakeLists.txt                                           |    2 
 plugins/draco/module/Testing/TestF3DDracoReader.cxx                                   |    8 
 plugins/draco/module/Testing/TestF3DDracoReaderError.cxx                              |   64 
 plugins/draco/module/Testing/TestF3DDracoReaderStream.cxx                             |   21 
 plugins/draco/module/vtkF3DDracoReader.cxx                                            |   47 
 plugins/draco/module/vtkF3DDracoReader.h                                              |   12 
 plugins/native/CMakeLists.txt                                                         |   61 
 plugins/native/configs/config.d/10_native.json                                        |   10 
 plugins/native/configs/thumbnail.d/10_native.json                                     |   10 
 plugins/native/module/CMakeLists.txt                                                  |   15 
 plugins/native/module/Testing/CMakeLists.txt                                          |    6 
 plugins/native/module/Testing/TestF3DPLYReader.cxx                                    |   18 
 plugins/native/module/Testing/TestF3DQuakeMDLImporterInexistent.cxx                   |   19 
 plugins/native/module/Testing/TestF3DQuakeMDLImporterStream.cxx                       |   33 
 plugins/native/module/Testing/TestF3DQuakeMDLParser.cxx                               |  377 +
 plugins/native/module/Testing/TestF3DSPZReader.cxx                                    |    4 
 plugins/native/module/Testing/TestF3DSplatReader.cxx                                  |    4 
 plugins/native/module/vtkF3DQuakeMDLImporter.cxx                                      |  711 +-
 plugins/native/module/vtkF3DQuakeMDLImporter.h                                        |   13 
 plugins/native/module/vtkF3DSPZReader.cxx                                             |   92 
 plugins/occt/module/CMakeLists.txt                                                    |    7 
 plugins/occt/module/Testing/CMakeLists.txt                                            |   12 
 plugins/occt/module/Testing/TestF3DOCCTReader.cxx                                     |   19 
 plugins/occt/module/Testing/TestF3DOCCTReaderStream.cxx                               |   40 
 plugins/occt/module/Testing/TestF3DOCCTReaderStreamError.cxx                          |   89 
 plugins/occt/module/vtk.module                                                        |    1 
 plugins/occt/module/vtkF3DOCCTReader.cxx                                              |  156 
 plugins/occt/module/vtkF3DOCCTReader.h                                                |   24 
 plugins/usd/module/Testing/TestF3DUSDImporter.cxx                                     |    4 
 plugins/usd/module/vtkF3DUSDImporter.cxx                                              |   33 
 plugins/usd/module/vtkF3DUSDImporter.h                                                |   44 
 pyproject.toml                                                                        |   11 
 python/CMakeLists.txt                                                                 |    4 
 python/F3DPythonBindings.cxx                                                          |  190 
 python/__init__.py.in                                                                 |   22 
 python/generate_stubs.py                                                              |  150 
 python/test_generate_stubs.py                                                         |  137 
 python/testing/CMakeLists.txt                                                         |    8 
 python/testing/test_camera.py                                                         |    6 
 python/testing/test_engine.py                                                         |    1 
 python/testing/test_interactor.py                                                     |  137 
 python/testing/test_interactor_start.py                                               |    6 
 python/testing/test_log.py                                                            |  123 
 python/testing/test_options.py                                                        |   16 
 python/testing/test_scene.py                                                          |   37 
 python/testing/test_utils.py                                                          |   19 
 resources/app.f3d.F3D.metainfo.xml                                                    |   20 
 resources/configs/config.d/05_all.json                                                |    8 
 resources/configs/thumbnail.d/05_all.json                                             |    5 
 resources/logo-black.png                                                              |binary
 resources/logo-white.png                                                              |binary
 testing/baselines/Test3DGSPLY.png                                                     |binary
 testing/baselines/Test3DGSPLYHDRI.png                                                 |binary
 testing/baselines/Test3DGaussiansSplatting.png                                        |binary
 testing/baselines/Test3DGaussiansSplattingStochastic.png                              |    3 
 testing/baselines/Test3DSImporter.png                                                 |binary
 testing/baselines/Test3MF.png                                                         |binary
 testing/baselines/TestABC.png                                                         |binary
 testing/baselines/TestABCAnimation.png                                                |binary
 testing/baselines/TestABCNonFaceVarying.png                                           |binary
 testing/baselines/TestAMRDataSet.png                                                  |binary
 testing/baselines/TestAlembic.png                                                     |    3 
 testing/baselines/TestAlembicAnimation.png                                            |    3 
 testing/baselines/TestAlembicAnimationXForm.png                                       |    3 
 testing/baselines/TestAlembicAnimationXFormRotation.png                               |    3 
 testing/baselines/TestAlembicCurves.png                                               |    3 
 testing/baselines/TestAlembicNonFaceVarying.png                                       |    3 
 testing/baselines/TestAnimationAllAnimations.png                                      |binary
 testing/baselines/TestAnimationAutoplay.png                                           |binary
 testing/baselines/TestAnimationGenericImporter.png                                    |binary
 testing/baselines/TestAnimationGenericImporterPointSprites.png                        |binary
 testing/baselines/TestAnimationGenericImporterScalarRange.png                         |binary
 testing/baselines/TestAnimationIndexDeprecated.png                                    |binary
 testing/baselines/TestAnimationIndicesMulti.png                                       |binary
 testing/baselines/TestAnimationIndicesSingle.png                                      |binary
 testing/baselines/TestAnimationInputChangeColoring.png                                |binary
 testing/baselines/TestAnimationInputChangePointSprites.png                            |binary
 testing/baselines/TestAnimationMorphColoring.png                                      |binary
 testing/baselines/TestAnimationSkinColoring.png                                       |binary
 testing/baselines/TestAnimationTimeLimitsHigh.png                                     |binary
 testing/baselines/TestAnimationTimeLimitsLow.png                                      |binary
 testing/baselines/TestAnimationUserMatrixColoring.png                                 |binary
 testing/baselines/TestAnimationUserMatrixPointSprites.png                             |binary
 testing/baselines/TestAssimpAnimationNegativeIndex.png                                |binary
 testing/baselines/TestAssimpMetaDataImporter.png                                      |binary
 testing/baselines/TestAxesCustomColor.png                                             |    3 
 testing/baselines/TestAxesGridEnable.png                                              |    3 
 testing/baselines/TestAxis.png                                                        |binary
 testing/baselines/TestBREP.png                                                        |binary
 testing/baselines/TestBREPDefines.png                                                 |binary
 testing/baselines/TestBackdropOpacityMedium.png                                       |    3 
 testing/baselines/TestBackdropOpacityOpaque.png                                       |    3 
 testing/baselines/TestBackdropOpacityTransparent.png                                  |    3 
 testing/baselines/TestBackfaceHidden.png                                              |binary
 testing/baselines/TestBackfaceVisible.png                                             |binary
 testing/baselines/TestBackground.png                                                  |binary
 testing/baselines/TestBadRef.png                                                      |binary
 testing/baselines/TestBinaryBREP.png                                                  |binary
 testing/baselines/TestCameraClipping.png                                              |binary
 testing/baselines/TestCameraConfiguration.png                                         |binary
 testing/baselines/TestCameraDirection.png                                             |binary
 testing/baselines/TestCameraIndexConfiguration.png                                    |binary
 testing/baselines/TestCameraOrtho.png                                                 |binary
 testing/baselines/TestCameraOrthographic.png                                          |binary
 testing/baselines/TestCameraPersp.png                                                 |binary
 testing/baselines/TestCameraZoomFactor.png                                            |binary
 testing/baselines/TestCityGML.png                                                     |binary
 testing/baselines/TestCollinearVectors.png                                            |binary
 testing/baselines/TestColorDiscretization.png                                         |binary
 testing/baselines/TestColorMap16bits.png                                              |binary
 testing/baselines/TestColorMap32bits.png                                              |binary
 testing/baselines/TestColorMapFile.png                                                |binary
 testing/baselines/TestColorMapFileFullPath.png                                        |binary
 testing/baselines/TestColorMapStem.png                                                |binary
 testing/baselines/TestColormap.png                                                    |binary
 testing/baselines/TestCommandScriptAlias.png                                          |binary
 testing/baselines/TestCommandScriptAzimuth.png                                        |binary
 testing/baselines/TestCommandScriptBasic.png                                          |binary
 testing/baselines/TestCommandScriptElevation.png                                      |binary
 testing/baselines/TestCommandScriptJumpToFirstFrame.png                               |    3 
 testing/baselines/TestCommandScriptJumpToLastFrame.png                                |    3 
 testing/baselines/TestCommandScriptJumpToMiddleFrame.png                              |    3 
 testing/baselines/TestCommandScriptJumpToNextFrame.png                                |    3 
 testing/baselines/TestCommandScriptJumpToPreviousFrame.png                            |    3 
 testing/baselines/TestCommandScriptRemoveCurrentFileGroup.png                         |    3 
 testing/baselines/TestCommandScriptRemoveFileGroups.png                               |    3 
 testing/baselines/TestCommandScriptReset.png                                          |binary
 testing/baselines/TestComponentName.png                                               |binary
 testing/baselines/TestConfigFileBuild.png                                             |binary
 testing/baselines/TestConfigFileHDRI.png                                              |binary
 testing/baselines/TestConfigFileMultiFileSTL.png                                      |binary
 testing/baselines/TestConfigFileMultiFileVTP.png                                      |binary
 testing/baselines/TestConfigFileUpperCase.png                                         |binary
 testing/baselines/TestConfigImperative.png                                            |binary
 testing/baselines/TestConfigOrder.png                                                 |binary
 testing/baselines/TestConfigReset.png                                                 |binary
 testing/baselines/TestConfigResolution.png                                            |binary
 testing/baselines/TestConfigStemBuild.png                                             |binary
 testing/baselines/TestConsoleBadgeError.png                                           |binary
 testing/baselines/TestConsoleBadgeQuiet.png                                           |binary
 testing/baselines/TestConsoleBadgeWarning.png                                         |binary
 testing/baselines/TestCustomOptionsNone.png                                           |    3 
 testing/baselines/TestDAE.png                                                         |binary
 testing/baselines/TestDAEAnimationLights.png                                          |binary
 testing/baselines/TestDRACO.png                                                       |binary
 testing/baselines/TestDRACOColoring.png                                               |binary
 testing/baselines/TestDXF.png                                                         |binary
 testing/baselines/TestDefaultConfigFileAlembic.png                                    |binary
 testing/baselines/TestDefaultConfigFileAndCommand.png                                 |binary
 testing/baselines/TestDefaultConfigFileAssimpDAE.png                                  |binary
 testing/baselines/TestDefaultConfigFileAssimpDXF.png                                  |binary
 testing/baselines/TestDefaultConfigFileAssimpFBX.png                                  |binary
 testing/baselines/TestDefaultConfigFileAssimpOFF.png                                  |binary
 testing/baselines/TestDefaultConfigFileDraco.png                                      |binary
 testing/baselines/TestDefaultConfigFileExodus.png                                     |binary
 testing/baselines/TestDefaultConfigFileHDRIFilename.png                               |    3 
 testing/baselines/TestDefaultConfigFileNetCDF.png                                     |binary
 testing/baselines/TestDefaultConfigFileOCCT.png                                       |binary
 testing/baselines/TestDefaultConfigFilePLY.png                                        |binary
 testing/baselines/TestDefaultConfigFileQuakeMDL.png                                   |binary
 testing/baselines/TestDefaultConfigFileSPLAT.png                                      |binary
 testing/baselines/TestDefaultConfigFileSPZ.png                                        |binary
 testing/baselines/TestDefaultConfigFileSTL.png                                        |binary
 testing/baselines/TestDefaultConfigFileTIFF.png                                       |binary
 testing/baselines/TestDefaultConfigFileUSD.png                                        |binary
 testing/baselines/TestDefaultConfigFileVDB.png                                        |binary
 testing/baselines/TestDefaultConfigFileVTI.png                                        |binary
 testing/baselines/TestDefaultConfigFileVTKHDF.png                                     |binary
 testing/baselines/TestDefaultConfigFileVTU.png                                        |binary
 testing/baselines/TestDefaultConfigRemoveEmptyFileGroups.png                          |    3 
 testing/baselines/TestDefaultConfigTranslucent.png                                    |binary
 testing/baselines/TestDefines.png                                                     |binary
 testing/baselines/TestDepthPeeling.png                                                |binary
 testing/baselines/TestDepthPeelingToneMapping.png                                     |binary
 testing/baselines/TestDicom.png                                                       |binary
 testing/baselines/TestEdges.png                                                       |binary
 testing/baselines/TestEmbeddedTextureFBX.png                                          |binary
 testing/baselines/TestExactMatch.png                                                  |binary
 testing/baselines/TestExodus.png                                                      |binary
 testing/baselines/TestExodusConfig.png                                                |binary
 testing/baselines/TestF3DRenderer.png                                                 |binary
 testing/baselines/TestFBX.png                                                         |binary
 testing/baselines/TestFBX16bits.png                                                   |binary
 testing/baselines/TestFBXAnimation.png                                                |binary
 testing/baselines/TestFBXAnimationCamera.png                                          |binary
 testing/baselines/TestFBXAnimationLights.png                                          |binary
 testing/baselines/TestFBXBone.png                                                     |binary
 testing/baselines/TestFBXBoneAnimation.png                                            |binary
 testing/baselines/TestFBXNormalMapping.png                                            |binary
 testing/baselines/TestFBXSkinningAnimation.png                                        |binary
 testing/baselines/TestFilename.png                                                    |binary
 testing/baselines/TestFilenameCommasSpaces.png                                        |binary
 testing/baselines/TestFilenameHDRIFilename.png                                        |    3 
 testing/baselines/TestFilenameWhiteBg.png                                             |binary
 testing/baselines/TestFinalShaderNegative.png                                         |binary
 testing/baselines/TestFinalShaderNegativeFileName.png                                 |binary
 testing/baselines/TestFont.png                                                        |binary
 testing/baselines/TestFontScale2.png                                                  |binary
 testing/baselines/TestFontScale3.png                                                  |binary
 testing/baselines/TestForceReaderGLTFDraco.png                                        |binary
 testing/baselines/TestForceReaderPass.png                                             |binary
 testing/baselines/TestGLTFDracoImporter.png                                           |binary
 testing/baselines/TestGLTFDracoImporterWithoutCompression.png                         |binary
 testing/baselines/TestGLTFImporter.png                                                |binary
 testing/baselines/TestGLTFImporterUnlit.png                                           |binary
 testing/baselines/TestGLTFImporterWithAnimation.png                                   |binary
 testing/baselines/TestGLTFMorph.png                                                   |binary
 testing/baselines/TestGLTFRigArmature.png                                             |binary
 testing/baselines/TestGLTFRigArmatureNoArmature.png                                   |binary
 testing/baselines/TestGLTFRigArmatureSphereTube.png                                   |binary
 testing/baselines/TestGLTFRigArmatureWithOpacity.png                                  |binary
 testing/baselines/TestGLTFSkin.png                                                    |binary
 testing/baselines/TestGLTFURI.png                                                     |binary
 testing/baselines/TestGenRef.png                                                      |binary
 testing/baselines/TestGridAbsolute.png                                                |binary
 testing/baselines/TestGridClipping.png                                                |binary
 testing/baselines/TestGridColor.png                                                   |binary
 testing/baselines/TestGridOptions.png                                                 |binary
 testing/baselines/TestGridUp000.png                                                   |binary
 testing/baselines/TestGridUp100.png                                                   |binary
 testing/baselines/TestGridUp123.png                                                   |binary
 testing/baselines/TestGridWithDepthPeeling.png                                        |binary
 testing/baselines/TestGridX.png                                                       |binary
 testing/baselines/TestGridY.png                                                       |binary
 testing/baselines/TestGridZ.png                                                       |binary
 testing/baselines/TestHDRI.png                                                        |binary
 testing/baselines/TestHDRI8Bit.png                                                    |binary
 testing/baselines/TestHDRIAmbientOnly.png                                             |binary
 testing/baselines/TestHDRIAmbientOnlyNoBackground.png                                 |binary
 testing/baselines/TestHDRIBlur.png                                                    |binary
 testing/baselines/TestHDRIBlurCoCLarge.png                                            |binary
 testing/baselines/TestHDRIBlurCoCMedium.png                                           |binary
 testing/baselines/TestHDRIBlurCoCNegative.png                                         |binary
 testing/baselines/TestHDRIBlurCoCSmall.png                                            |binary
 testing/baselines/TestHDRIBlurCoCZero.png                                             |binary
 testing/baselines/TestHDRIBlurRatio.png                                               |binary
 testing/baselines/TestHDRICache.png                                                   |binary
 testing/baselines/TestHDRIDefault.png                                                 |binary
 testing/baselines/TestHDRIEXR.png                                                     |binary
 testing/baselines/TestHDRIEdges.png                                                   |binary
 testing/baselines/TestHDRIFilename.png                                                |    3 
 testing/baselines/TestHDRIFilenameDefault.png                                         |    3 
 testing/baselines/TestHDRINone.png                                                    |binary
 testing/baselines/TestHDRIOrient.png                                                  |binary
 testing/baselines/TestHDRIRaytracing.png                                              |binary
 testing/baselines/TestHDRIRaytracingAmbientOnly.png                                   |binary
 testing/baselines/TestHDRIRaytracingAmbientOnlyNoBackground.png                       |binary
 testing/baselines/TestHDRIRaytracingNone.png                                          |binary
 testing/baselines/TestHDRIRaytracingSkyboxOnly.png                                    |binary
 testing/baselines/TestHDRISkyboxOnly.png                                              |binary
 testing/baselines/TestHDRIToneMapping.png                                             |binary
 testing/baselines/TestHDRIToneMappingSSAA.png                                         |binary
 testing/baselines/TestHOMEColorMapFile.png                                            |binary
 testing/baselines/TestHOMEConfig.png                                                  |binary
 testing/baselines/TestHOMEFontFile.png                                                |binary
 testing/baselines/TestHOMEHDRI.png                                                    |binary
 testing/baselines/TestHOMEInput.png                                                   |binary
 testing/baselines/TestHOMEInteractionDropHDRICollapse.png                             |binary
 testing/baselines/TestHOMETexture.png                                                 |binary
 testing/baselines/TestIGES.png                                                        |binary
 testing/baselines/TestIGESDefines.png                                                 |binary
 testing/baselines/TestInteractionActors.png                                           |binary
 testing/baselines/TestInteractionAndCLIBlending.png                                   |    3 
 testing/baselines/TestInteractionAnimation.png                                        |binary
 testing/baselines/TestInteractionAnimationBackward.png                                |    3 
 testing/baselines/TestInteractionAnimationCameraMovement.png                          |binary
 testing/baselines/TestInteractionAnimationCycleAnimation.png                          |binary
 testing/baselines/TestInteractionAnimationCycleAnimationSingle.png                    |binary
 testing/baselines/TestInteractionAnimationDropHDRI.png                                |binary
 testing/baselines/TestInteractionAnimationFBXBone.png                                 |binary
 testing/baselines/TestInteractionAnimationFast.png                                    |binary
 testing/baselines/TestInteractionAnimationFrameRate.png                               |binary
 testing/baselines/TestInteractionAnimationIndexDeprecatedCycleAnimation.png           |binary
 testing/baselines/TestInteractionAnimationInvert.png                                  |binary
 testing/baselines/TestInteractionAnimationSlow.png                                    |binary
 testing/baselines/TestInteractionAxesGridToggle.png                                   |    3 
 testing/baselines/TestInteractionCameraHotkeys.png                                    |binary
 testing/baselines/TestInteractionCameraUpdate.png                                     |binary
 testing/baselines/TestInteractionCheatsheet.png                                       |binary
 testing/baselines/TestInteractionCheatsheetAnimationName.png                          |binary
 testing/baselines/TestInteractionCheatsheetBlackBG.png                                |binary
 testing/baselines/TestInteractionCheatsheetCentered.png                               |binary
 testing/baselines/TestInteractionCheatsheetConfigFile.png                             |binary
 testing/baselines/TestInteractionCheatsheetCycle.png                                  |    3 
 testing/baselines/TestInteractionCheatsheetMultiModifierBinding.png                   |    3 
 testing/baselines/TestInteractionCheatsheetNoFile.png                                 |binary
 testing/baselines/TestInteractionCheatsheetOpacity.png                                |binary
 testing/baselines/TestInteractionCheatsheetSSAA.png                                   |binary
 testing/baselines/TestInteractionCheatsheetScalars.png                                |binary
 testing/baselines/TestInteractionCheatsheetScalarsNoArray.png                         |    3 
 testing/baselines/TestInteractionCheatsheetWhiteBG.png                                |binary
 testing/baselines/TestInteractionConfigFileAndCommand.png                             |binary
 testing/baselines/TestInteractionConfigFileBindings.png                               |binary
 testing/baselines/TestInteractionConfigFileImperative.png                             |binary
 testing/baselines/TestInteractionConfigFileImperativeNoData.png                       |binary
 testing/baselines/TestInteractionConfigFileMulti.png                                  |binary
 testing/baselines/TestInteractionConfigFileNoColorBar.png                             |binary
 testing/baselines/TestInteractionConfigFileOptional.png                               |binary
 testing/baselines/TestInteractionConsoleAutoComplete.png                              |binary
 testing/baselines/TestInteractionConsoleAutoCompleteCamera.png                        |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteCandidates.png                    |binary
 testing/baselines/TestInteractionConsoleAutoCompleteColoring.png                      |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.png  |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.png  |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteDirCaseSensitive.png              |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteFilesystem.png                    |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteHelp.png                          |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteHide.png                          |binary
 testing/baselines/TestInteractionConsoleAutoCompleteNoMatch.png                       |binary
 testing/baselines/TestInteractionConsoleAutoCompleteReaderOptions.png                 |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteSetDisplayCompl.png               |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteSetOptions.png                    |    3 
 testing/baselines/TestInteractionConsoleAutoCompleteToggleOptions.png                 |    3 
 testing/baselines/TestInteractionConsoleCamera.png                                    |binary
 testing/baselines/TestInteractionConsoleClear.png                                     |binary
 testing/baselines/TestInteractionConsoleEmptyCommand.png                              |binary
 testing/baselines/TestInteractionConsoleHistory.png                                   |binary
 testing/baselines/TestInteractionConsoleHistoryEmpty.png                              |binary
 testing/baselines/TestInteractionConsoleHistoryHide.png                               |binary
 testing/baselines/TestInteractionConsoleInvalidCommand.png                            |binary
 testing/baselines/TestInteractionConsoleInvalidOption.png                             |binary
 testing/baselines/TestInteractionConsoleLoadNext.png                                  |binary
 testing/baselines/TestInteractionConsoleOpen.png                                      |binary
 testing/baselines/TestInteractionConsoleOpenWarningClick.png                          |binary
 testing/baselines/TestInteractionConsoleOpenWarningKeyboard.png                       |binary
 testing/baselines/TestInteractionConsoleOverCheatSheet.png                            |    3 
 testing/baselines/TestInteractionConsoleReload.png                                    |binary
 testing/baselines/TestInteractionConsoleRender.png                                    |binary
 testing/baselines/TestInteractionConsoleScrollbar.png                                 |binary
 testing/baselines/TestInteractionConsoleToggleGrid.png                                |binary
 testing/baselines/TestInteractionConsoleTypingSceneInfo.png                           |binary
 testing/baselines/TestInteractionCorrectCameraForVolumeSwitch.png                     |    3 
 testing/baselines/TestInteractionCycleBlending.png                                    |    3 
 testing/baselines/TestInteractionCycleCell.png                                        |binary
 testing/baselines/TestInteractionCycleCellInvalidIndex.png                            |binary
 testing/baselines/TestInteractionCycleComp.png                                        |binary
 testing/baselines/TestInteractionCycleScalars.png                                     |binary
 testing/baselines/TestInteractionCycleScalarsCompCheck.png                            |binary
 testing/baselines/TestInteractionDirectory.png                                        |binary
 testing/baselines/TestInteractionDirectoryEmpty.png                                   |binary
 testing/baselines/TestInteractionDirectoryLoop.png                                    |binary
 testing/baselines/TestInteractionDragRotateVertical.png                               |binary
 testing/baselines/TestInteractionDropFileBackslashes.png                              |binary
 testing/baselines/TestInteractionDropFiles.png                                        |binary
 testing/baselines/TestInteractionDropHDRI.png                                         |binary
 testing/baselines/TestInteractionDropHDRIExr.png                                      |binary
 testing/baselines/TestInteractionDropHDRIInvert.png                                   |binary
 testing/baselines/TestInteractionDropHDRIModifiers.png                                |binary
 testing/baselines/TestInteractionDropHDRIMulti.png                                    |binary
 testing/baselines/TestInteractionDropSameFiles.png                                    |binary
 testing/baselines/TestInteractionDynamicUpDirection.png                               |    3 
 testing/baselines/TestInteractionElevationCameraRotation.png                          |binary
 testing/baselines/TestInteractionFocalPointPickingDefault.png                         |binary
 testing/baselines/TestInteractionFocalPointPickingPoints.png                          |binary
 testing/baselines/TestInteractionFocalPointPickingShift.png                           |binary
 testing/baselines/TestInteractionHDRIBlur.png                                         |binary
 testing/baselines/TestInteractionHDRICache.png                                        |binary
 testing/baselines/TestInteractionHDRIChange.png                                       |binary
 testing/baselines/TestInteractionHDRIFullFromNone.png                                 |binary
 testing/baselines/TestInteractionHDRILoop.png                                         |binary
 testing/baselines/TestInteractionHDRIMove.png                                         |binary
 testing/baselines/TestInteractionHDRIRaytracingFullFromNone.png                       |binary
 testing/baselines/TestInteractionHDRIRaytracingLoop.png                               |binary
 testing/baselines/TestInteractionHDRIRaytracingRemoveAmbient.png                      |binary
 testing/baselines/TestInteractionHDRIRaytracingRemoveBoth.png                         |binary
 testing/baselines/TestInteractionHDRIRaytracingRemoveSkybox.png                       |binary
 testing/baselines/TestInteractionHDRIReload.png                                       |binary
 testing/baselines/TestInteractionHDRIRemoveAmbient.png                                |binary
 testing/baselines/TestInteractionHDRIRemoveBoth.png                                   |binary
 testing/baselines/TestInteractionHDRIRemoveSkybox.png                                 |binary
 testing/baselines/TestInteractionInitUpVectorOption.png                               |binary
 testing/baselines/TestInteractionInvalidFile.png                                      |binary
 testing/baselines/TestInteractionInvertZoom.png                                       |binary
 testing/baselines/TestInteractionLightIntensity.png                                   |binary
 testing/baselines/TestInteractionLoadParentDirectory.png                              |binary
 testing/baselines/TestInteractionMinimalConsoleBadgeRemains.png                       |binary
 testing/baselines/TestInteractionMinimalConsoleEscape.png                             |binary
 testing/baselines/TestInteractionMinimalConsoleInvalidCommand.png                     |binary
 testing/baselines/TestInteractionMinimalConsoleOpen.png                               |binary
 testing/baselines/TestInteractionMinimalConsoleOverCheatSheet.png                     |    3 
 testing/baselines/TestInteractionMinimalConsoleOverCheatSheetAndFilename.png          |    3 
 testing/baselines/TestInteractionMinimalConsoleResize.png                             |binary
 testing/baselines/TestInteractionMinimalConsoleReturn.png                             |binary
 testing/baselines/TestInteractionMinimalConsoleValidCommand.png                       |binary
 testing/baselines/TestInteractionMultiFileColoring.png                                |binary
 testing/baselines/TestInteractionMultiFileDrop.png                                    |binary
 testing/baselines/TestInteractionMultiFileLoadParentDirectory.png                     |binary
 testing/baselines/TestInteractionMultiFileVolume.png                                  |binary
 testing/baselines/TestInteractionNoModelScrollBar.png                                 |binary
 testing/baselines/TestInteractionNoModelScrollWheel.png                               |binary
 testing/baselines/TestInteractionOpacity.png                                          |binary
 testing/baselines/TestInteractionOrthographicProjection.png                           |binary
 testing/baselines/TestInteractionPanWithShift.png                                     |binary
 testing/baselines/TestInteractionPointCloud.png                                       |binary
 testing/baselines/TestInteractionPointCloudTAA.png                                    |    3 
 testing/baselines/TestInteractionPointSpritesCycle.png                                |    3 
 testing/baselines/TestInteractionPostFX.png                                           |binary
 testing/baselines/TestInteractionRaytracingDenoise.png                                |binary
 testing/baselines/TestInteractionReload.png                                           |binary
 testing/baselines/TestInteractionResetCamera.png                                      |binary
 testing/baselines/TestInteractionResetCameraWithCameraIndex.png                       |binary
 testing/baselines/TestInteractionRollCameraRotation.png                               |binary
 testing/baselines/TestInteractionRotateCamera90.png                                   |binary
 testing/baselines/TestInteractionRotateCameraMinus90.png                              |binary
 testing/baselines/TestInteractionStochasticTAA.png                                    |    3 
 testing/baselines/TestInteractionSwitchFileNextCameraKeeping.png                      |binary
 testing/baselines/TestInteractionSwitchFileNoCameraKeeping.png                        |binary
 testing/baselines/TestInteractionSwitchFilePrevCameraKeeping.png                      |binary
 testing/baselines/TestInteractionTAA.png                                              |    3 
 testing/baselines/TestInteractionTAADolly.png                                         |    3 
 testing/baselines/TestInteractionTAAMiddleClick.png                                   |    3 
 testing/baselines/TestInteractionTensorsCycleComp.png                                 |binary
 testing/baselines/TestInteractionTrackball.png                                        |binary
 testing/baselines/TestInteractionTrackballBind.png                                    |binary
 testing/baselines/TestInteractionVerticalDragRotate.png                               |binary
 testing/baselines/TestInteractionVolumeAfterColoring.png                              |binary
 testing/baselines/TestInteractionVolumeCycle.png                                      |binary
 testing/baselines/TestInteractionVolumeInverse.png                                    |binary
 testing/baselines/TestInteractionZoomToMouse.png                                      |binary
 testing/baselines/TestInteractionZoomToggleOrthographicProjection.png                 |binary
 testing/baselines/TestInvalidFileFileName.png                                         |binary
 testing/baselines/TestInvalidFileFileNameEmpty.png                                    |    3 
 testing/baselines/TestLightIntensityBrighter.png                                      |binary
 testing/baselines/TestLightIntensityBrighterFullScene.png                             |binary
 testing/baselines/TestLightIntensityDarker.png                                        |binary
 testing/baselines/TestLightIntensityDarkerFullScene.png                               |binary
 testing/baselines/TestLineWidth.png                                                   |binary
 testing/baselines/TestLineWidthFullScene.png                                          |binary
 testing/baselines/TestMHD.png                                                         |binary
 testing/baselines/TestMatchFirstConfigGlobOptionBlock.png                             |binary
 testing/baselines/TestMatchSecondConfigGlobOptionBlock.png                            |binary
 testing/baselines/TestMaterial.png                                                    |binary
 testing/baselines/TestMaterialFullScene.png                                           |binary
 testing/baselines/TestMaxBaseIOR.png                                                  |binary
 testing/baselines/TestMaxSizeAboveMultiFile.png                                       |binary
 testing/baselines/TestMaxSizeBelow.png                                                |binary
 testing/baselines/TestMetaData.png                                                    |binary
 testing/baselines/TestMetaDataImporter.png                                            |binary
 testing/baselines/TestMinBaseIOR.png                                                  |binary
 testing/baselines/TestMorphColoring.png                                               |binary
 testing/baselines/TestMultiFile.png                                                   |binary
 testing/baselines/TestMultiFileAnimationAnySingle.png                                 |binary
 testing/baselines/TestMultiFileAnimationIndices.png                                   |binary
 testing/baselines/TestMultiFileAnimationMultiUnique.png                               |binary
 testing/baselines/TestMultiFileAnimationNoAnimationSupport.png                        |binary
 testing/baselines/TestMultiFileAnimationNoneMulti.png                                 |binary
 testing/baselines/TestMultiFileAnimationUniqueUnique.png                              |binary
 testing/baselines/TestMultiFileCameraIndex.png                                        |binary
 testing/baselines/TestMultiFileColoring.png                                           |binary
 testing/baselines/TestMultiFileColoringTexture.png                                    |binary
 testing/baselines/TestMultiFileInvalid.png                                            |binary
 testing/baselines/TestMultiFileInvalidFilesFileName.png                               |binary
 testing/baselines/TestMultiFileMetaData.png                                           |binary
 testing/baselines/TestMultiFileModeDir.png                                            |binary
 testing/baselines/TestMultiFileNonCoherentComponentNames.png                          |binary
 testing/baselines/TestMultiFilePositionals.png                                        |binary
 testing/baselines/TestMultiFileRecursive.png                                          |    3 
 testing/baselines/TestMultiFileRegexA.png                                             |    3 
 testing/baselines/TestMultiFileRegexB.png                                             |    3 
 testing/baselines/TestMultiFileUnsupportedFilesFileName.png                           |    3 
 testing/baselines/TestMultiFileVolume.png                                             |binary
 testing/baselines/TestMultiInputArg.png                                               |binary
 testing/baselines/TestMultiInputMultiArgs.png                                         |binary
 testing/baselines/TestMultiblockMetaData.png                                          |binary
 testing/baselines/TestNRRD.png                                                        |binary
 testing/baselines/TestNetCDF.png                                                      |binary
 testing/baselines/TestNoBackground.png                                                |binary
 testing/baselines/TestNoConfigWithConfig.png                                          |binary
 testing/baselines/TestNoFile.png                                                      |binary
 testing/baselines/TestNoFileEmptyFileName.png                                         |binary
 testing/baselines/TestNonExistentFile.png                                             |binary
 testing/baselines/TestOBJ.png                                                         |binary
 testing/baselines/TestOBJWebP.png                                                     |    3 
 testing/baselines/TestOBJWithTGATexture.png                                           |binary
 testing/baselines/TestOFF.png                                                         |binary
 testing/baselines/TestPLY.png                                                         |binary
 testing/baselines/TestPTS.png                                                         |binary
 testing/baselines/TestPointCloud.png                                                  |binary
 testing/baselines/TestPointCloudBar.png                                               |binary
 testing/baselines/TestPointCloudDefaultScene.png                                      |binary
 testing/baselines/TestPointCloudFullScene.png                                         |binary
 testing/baselines/TestPointCloudUG.png                                                |binary
 testing/baselines/TestPointCloudVolume.png                                            |binary
 testing/baselines/TestPythonCompareWithFile.png                                       |binary
 testing/baselines/TestPythonScene.png                                                 |binary
 testing/baselines/TestPythonSceneMemory.png                                           |binary
 testing/baselines/TestPythonSceneRedLight.png                                         |    3 
 testing/baselines/TestQuakeMDL.png                                                    |binary
 testing/baselines/TestQuakeMDLActorCollection.png                                     |binary
 testing/baselines/TestQuakeMDLAnimationBetween.png                                    |binary
 testing/baselines/TestQuakeMDLAnimationGroupFrame.png                                 |binary
 testing/baselines/TestQuakeMDLAnimationLastFrame.png                                  |    3 
 testing/baselines/TestQuakeMDLAnimationMulti.png                                      |binary
 testing/baselines/TestQuakeMDLAnimationSimpleFrame.png                                |binary
 testing/baselines/TestQuakeMDLDisableAnimation.png                                    |binary
 testing/baselines/TestQuakeMDLGroupSkin.png                                           |    3 
 testing/baselines/TestQuakeMDLSkinIndex.png                                           |binary
 testing/baselines/TestQuakeMDLSkinIndexNegative.png                                   |binary
 testing/baselines/TestQuakeMDLSkinIndexNonInteger.png                                 |binary
 testing/baselines/TestQuakeMDLSkinIndexOutOfBounds.png                                |binary
 testing/baselines/TestQuakeMDLSkinIndexOverflow.png                                   |binary
 testing/baselines/TestRaytracingBackground.png                                        |binary
 testing/baselines/TestRaytracingDefaultConfigFile.png                                 |binary
 testing/baselines/TestRaytracingDenoise.png                                           |binary
 testing/baselines/TestRaytracingGLTF.png                                              |binary
 testing/baselines/TestRaytracingNoBackground.png                                      |binary
 testing/baselines/TestRaytracingNoDenoise.png                                         |binary
 testing/baselines/TestRaytracingPointCloud.png                                        |binary
 testing/baselines/TestRaytracingScalarBar.png                                         |binary
 testing/baselines/TestRaytracingThumbnailConfigFile.png                               |binary
 testing/baselines/TestRemoveEmptyFileGroups.png                                       |    3 
 testing/baselines/TestRemoveFileGroups.png                                            |binary
 testing/baselines/TestRenderingBackendAuto.png                                        |binary
 testing/baselines/TestRenderingBackendEGL.png                                         |binary
 testing/baselines/TestRenderingBackendGLX.png                                         |binary
 testing/baselines/TestRenderingBackendOSMesa.png                                      |binary
 testing/baselines/TestRenderingBackendWGL.png                                         |binary
 testing/baselines/TestSDKCompareWithFile.png                                          |binary
 testing/baselines/TestSDKConsole.png                                                  |binary
 testing/baselines/TestSDKDeprecatedOptions.png                                        |binary
 testing/baselines/TestSDKDropZone.png                                                 |binary
 testing/baselines/TestSDKDropZoneDeprecated.png                                       |    3 
 testing/baselines/TestSDKDropZoneNoLogo.png                                           |    3 
 testing/baselines/TestSDKDynamicBackgrounColor.png                                    |binary
 testing/baselines/TestSDKDynamicFontFile.png                                          |binary
 testing/baselines/TestSDKDynamicHDRI.png                                              |binary
 testing/baselines/TestSDKDynamicHDRIDefault.png                                       |binary
 testing/baselines/TestSDKDynamicHDRIExr.png                                           |binary
 testing/baselines/TestSDKDynamicLightIntensity-5x-brighter.png                        |binary
 testing/baselines/TestSDKDynamicLightIntensity-5x-darker.png                          |binary
 testing/baselines/TestSDKDynamicLightIntensity-default.png                            |binary
 testing/baselines/TestSDKDynamicProperties.png                                        |binary
 testing/baselines/TestSDKExternalWindowCOCOA.png                                      |binary
 testing/baselines/TestSDKExternalWindowEGL.png                                        |binary
 testing/baselines/TestSDKExternalWindowGLFW.png                                       |binary
 testing/baselines/TestSDKExternalWindowGLX.png                                        |binary
 testing/baselines/TestSDKExternalWindowOSMesa.png                                     |binary
 testing/baselines/TestSDKExternalWindowQT.png                                         |binary
 testing/baselines/TestSDKExternalWindowWGL.png                                        |binary
 testing/baselines/TestSDKImage.png                                                    |binary
 testing/baselines/TestSDKImage16.png                                                  |binary
 testing/baselines/TestSDKInteractionDirectory.png                                     |binary
 testing/baselines/TestSDKInteractorCallBackDefault.png                                |binary
 testing/baselines/TestSDKInteractorCallBackDefaultAgain.png                           |binary
 testing/baselines/TestSDKInteractorCallBackModified.png                               |binary
 testing/baselines/TestSDKInteractorCallBackModifiedAgain.png                          |binary
 testing/baselines/TestSDKInteractorDropFullScene.png                                  |binary
 testing/baselines/TestSDKMultiColoring.png                                            |binary
 testing/baselines/TestSDKMultiOptions.png                                             |binary
 testing/baselines/TestSDKRenderFinalShaderNegative.png                                |binary
 testing/baselines/TestSDKRenderFinalShaderVignette.png                                |binary
 testing/baselines/TestSDKScene.png                                                    |binary
 testing/baselines/TestSDKSceneFromMemory.png                                          |binary
 testing/baselines/TestSDKSceneRedLight.png                                            |    3 
 testing/baselines/TestSDKTriggerInteractionsKeyPress.png                              |    3 
 testing/baselines/TestSDKTriggerInteractionsKeyPressMod.png                           |    3 
 testing/baselines/TestSDKTriggerInteractionsMousePan.png                              |    3 
 testing/baselines/TestSDKTriggerInteractionsMouseRotate.png                           |    3 
 testing/baselines/TestSDKTriggerInteractionsMouseZoom.png                             |    3 
 testing/baselines/TestSDKTriggerInteractionsTextInput.png                             |    3 
 testing/baselines/TestSDKTriggerInteractionsWheelBackward.png                         |    3 
 testing/baselines/TestSDKTriggerInteractionsWheelForward.png                          |    3 
 testing/baselines/TestSDKUIAnimationProgressColor.png                                 |    3 
 testing/baselines/TestSDKWindowStandard.png                                           |binary
 testing/baselines/TestSPLAT.png                                                       |binary
 testing/baselines/TestSPZDegree0.png                                                  |binary
 testing/baselines/TestSPZDegree1.png                                                  |binary
 testing/baselines/TestSPZDegree1Stochastic.png                                        |    3 
 testing/baselines/TestSPZDegree2.png                                                  |binary
 testing/baselines/TestSPZDegree3.png                                                  |binary
 testing/baselines/TestSPZVersion3.png                                                 |    3 
 testing/baselines/TestSSAO.png                                                        |binary
 testing/baselines/TestSTEP.png                                                        |binary
 testing/baselines/TestSTEPDefines.png                                                 |binary
 testing/baselines/TestSTL.png                                                         |binary
 testing/baselines/TestScalars.png                                                     |binary
 testing/baselines/TestScalarsCell.png                                                 |binary
 testing/baselines/TestScalarsRange.png                                                |binary
 testing/baselines/TestScalarsWithBar.png                                              |binary
 testing/baselines/TestScalarsWithBarContinuity.png                                    |    3 
 testing/baselines/TestSkinColoring.png                                                |binary
 testing/baselines/TestSkinningManyBones.png                                           |binary
 testing/baselines/TestTGATextureFBX.png                                               |binary
 testing/baselines/TestTIFF.png                                                        |binary
 testing/baselines/TestTextureColor.png                                                |binary
 testing/baselines/TestTextureColorWithOptions.png                                     |binary
 testing/baselines/TestTextureEmissive.png                                             |binary
 testing/baselines/TestTextureMatCap.png                                               |binary
 testing/baselines/TestTextureMatCapWithEdges.png                                      |binary
 testing/baselines/TestTextureMatCapWithTCoords.png                                    |binary
 testing/baselines/TestTextureMaterial.png                                             |binary
 testing/baselines/TestTextureMaterialWithOptions.png                                  |binary
 testing/baselines/TestTextureNormal.png                                               |binary
 testing/baselines/TestTextures.png                                                    |binary
 testing/baselines/TestTexturesTransform.png                                           |binary
 testing/baselines/TestTexturesTransformGL.png                                         |binary
 testing/baselines/TestThumbnailConfigFileAlembic.png                                  |binary
 testing/baselines/TestThumbnailConfigFileAssimpDAE.png                                |binary
 testing/baselines/TestThumbnailConfigFileAssimpDXF.png                                |binary
 testing/baselines/TestThumbnailConfigFileAssimpFBX.png                                |binary
 testing/baselines/TestThumbnailConfigFileAssimpOFF.png                                |binary
 testing/baselines/TestThumbnailConfigFileDraco.png                                    |binary
 testing/baselines/TestThumbnailConfigFileExodus.png                                   |binary
 testing/baselines/TestThumbnailConfigFileNetCDF.png                                   |binary
 testing/baselines/TestThumbnailConfigFileOCCT.png                                     |binary
 testing/baselines/TestThumbnailConfigFilePLY.png                                      |binary
 testing/baselines/TestThumbnailConfigFileQuakeMDL.png                                 |binary
 testing/baselines/TestThumbnailConfigFileSPLAT.png                                    |binary
 testing/baselines/TestThumbnailConfigFileSPZ.png                                      |binary
 testing/baselines/TestThumbnailConfigFileSTL.png                                      |binary
 testing/baselines/TestThumbnailConfigFileUSD.png                                      |binary
 testing/baselines/TestThumbnailConfigFileVDB.png                                      |binary
 testing/baselines/TestThumbnailConfigFileVTI.png                                      |binary
 testing/baselines/TestThumbnailConfigFileVTKHDF.png                                   |binary
 testing/baselines/TestThumbnailConfigFileVTU.png                                      |binary
 testing/baselines/TestTimeRangeLessThanZeroNegativeAnimationTime.png                  |binary
 testing/baselines/TestTimeRangeLessThanZeroNoAnimationTime.png                        |binary
 testing/baselines/TestTimeRangeLessThanZeroWithAnimationTime.png                      |binary
 testing/baselines/TestToneMapping.png                                                 |binary
 testing/baselines/TestUSD.png                                                         |binary
 testing/baselines/TestUSDAGlyphs.png                                                  |binary
 testing/baselines/TestUSDAInstancing.png                                              |binary
 testing/baselines/TestUSDAPrimitives.png                                              |binary
 testing/baselines/TestUSDAPrimitivesZAxis.png                                         |binary
 testing/baselines/TestUSDInterpolation.png                                            |binary
 testing/baselines/TestUSDPurpose.png                                                  |binary
 testing/baselines/TestUSDTeapot.png                                                   |binary
 testing/baselines/TestUSDZAnimated.png                                                |binary
 testing/baselines/TestUSDZMaterials.png                                               |binary
 testing/baselines/TestUSDZMemEXR.png                                                  |binary
 testing/baselines/TestUSDZRigged.png                                                  |binary
 testing/baselines/TestUTF8.png                                                        |binary
 testing/baselines/TestUnsupportedFile.png                                             |binary
 testing/baselines/TestUpDirectionNoSign.png                                           |binary
 testing/baselines/TestUserMatrixColoring.png                                          |binary
 testing/baselines/TestVDBCommandScriptReaderOptions.png                               |binary
 testing/baselines/TestVDBDefinesDownsamplingFactor.png                                |binary
 testing/baselines/TestVDBPoints.png                                                   |binary
 testing/baselines/TestVDBVolume.png                                                   |binary
 testing/baselines/TestVRMLImporter.png                                                |binary
 testing/baselines/TestVTICell.png                                                     |binary
 testing/baselines/TestVTK.png                                                         |binary
 testing/baselines/TestVTKHDF.png                                                      |binary
 testing/baselines/TestVTKHDFPartitionedDataSetCollection.png                          |    3 
 testing/baselines/TestVTM.png                                                         |binary
 testing/baselines/TestVTP.png                                                         |binary
 testing/baselines/TestVTR.png                                                         |binary
 testing/baselines/TestVTS.png                                                         |binary
 testing/baselines/TestVTU.png                                                         |binary
 testing/baselines/TestVerboseMultiFileCompare.png                                     |binary
 testing/baselines/TestVerboseWarning.png                                              |binary
 testing/baselines/TestVolume.png                                                      |binary
 testing/baselines/TestVolumeCells.png                                                 |binary
 testing/baselines/TestVolumeColoringArray.png                                         |binary
 testing/baselines/TestVolumeComp.png                                                  |binary
 testing/baselines/TestVolumeDirect.png                                                |binary
 testing/baselines/TestVolumeInverse.png                                               |binary
 testing/baselines/TestVolumeMag.png                                                   |binary
 testing/baselines/TestWasmAnimation.png                                               |    3 
 testing/baselines/TestWasmCamera.png                                                  |    3 
 testing/baselines/TestWasmInteraction.png                                             |    3 
 testing/baselines/TestWasmOptions.png                                                 |    3 
 testing/baselines/TestWasmSplats.png                                                  |    3 
 testing/baselines/TestWinUserProfileNonStandard.png                                   |binary
 testing/baselines/TestX.png                                                           |binary
 testing/baselines/TestXBF.png                                                         |binary
 testing/baselines/TestXBFDefines.png                                                  |binary
 testing/baselines/TestXCAFColors.png                                                  |binary
 testing/baselines/TestXCAFColorsXBF.png                                               |binary
 testing/baselines/TestXDG_CONFIG_HOME.png                                             |binary
 testing/configs/match_type_no_match.json                                              |    2 
 testing/recordings/TestInteractionAndCLIBlending.log                                  |   19 
 testing/recordings/TestInteractionAnimationBackward.log                               |   20 
 testing/recordings/TestInteractionAxesGridToggle.log                                  |    9 
 testing/recordings/TestInteractionCheatsheet.log                                      |    6 
 testing/recordings/TestInteractionCheatsheetConfigFile.log                            |   16 
 testing/recordings/TestInteractionCheatsheetCycle.log                                 |   28 
 testing/recordings/TestInteractionCheatsheetMultiModifierBinding.log                  |   34 
 testing/recordings/TestInteractionCheatsheetOpacity.log                               |   11 
 testing/recordings/TestInteractionCheatsheetSSAA.log                                  |   13 
 testing/recordings/TestInteractionCheatsheetScalars.log                               |    8 
 testing/recordings/TestInteractionCheatsheetScalarsNoArray.log                        |    8 
 testing/recordings/TestInteractionConfigFileAndCommand.log                            |   21 
 testing/recordings/TestInteractionConsoleAutoCompleteCamera.log                       |   45 
 testing/recordings/TestInteractionConsoleAutoCompleteColoring.log                     |   41 
 testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.log |   83 
 testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.log |   87 
 testing/recordings/TestInteractionConsoleAutoCompleteDirCaseSensitive.log             |   87 
 testing/recordings/TestInteractionConsoleAutoCompleteFilesystem.log                   |  101 
 testing/recordings/TestInteractionConsoleAutoCompleteFilesystemError.log              | 1489 ++++
 testing/recordings/TestInteractionConsoleAutoCompleteHelp.log                         |   29 
 testing/recordings/TestInteractionConsoleAutoCompleteReaderOptions.log                |   45 
 testing/recordings/TestInteractionConsoleAutoCompleteSetDisplayCompl.log              |   75 
 testing/recordings/TestInteractionConsoleAutoCompleteSetOptions.log                   |   85 
 testing/recordings/TestInteractionConsoleAutoCompleteToggleOptions.log                |   70 
 testing/recordings/TestInteractionConsoleAutoCompleteTokenizeError.log                |   17 
 testing/recordings/TestInteractionConsoleOverCheatSheet.log                           |   24 
 testing/recordings/TestInteractionCorrectCameraForVolumeSwitch.log                    |    8 
 testing/recordings/TestInteractionCycleBlending.log                                   |   18 
 testing/recordings/TestInteractionDynamicUpDirection.log                              |    9 
 testing/recordings/TestInteractionInitUpVectorOption.log                              |  113 
 testing/recordings/TestInteractionMinimalConsoleOverCheatSheet.log                    |   25 
 testing/recordings/TestInteractionMinimalConsoleOverCheatSheetAndFilename.log         |   34 
 testing/recordings/TestInteractionNoModelScrollBar.log                                |  780 --
 testing/recordings/TestInteractionNoModelScrollWheel.log                              |  726 --
 testing/recordings/TestInteractionOpacity.log                                         |   68 
 testing/recordings/TestInteractionPointCloudTAA.log                                   |   20 
 testing/recordings/TestInteractionPointSpritesCycle.log                               |   32 
 testing/recordings/TestInteractionResetCameraWithCameraIndex.log                      |  444 -
 testing/recordings/TestInteractionStochasticTAA.log                                   |   57 
 testing/recordings/TestInteractionSwitchFileNextCameraKeeping.log                     |  246 
 testing/recordings/TestInteractionSwitchFileNoCameraKeeping.log                       |  190 
 testing/recordings/TestInteractionSwitchFilePrevCameraKeeping.log                     |  248 
 testing/recordings/TestInteractionTAA.log                                             |   20 
 testing/recordings/TestInteractionTAADolly.log                                        |   11 
 testing/recordings/TestInteractionTAAMiddleClick.log                                  |   22 
 testing/recordings/TestInteractionTrackball.log                                       |  484 -
 testing/recordings/TestInteractionTrackballBind.log                                   |  546 -
 testing/recordings/TestInteractionVerboseWatchUnwatch.log                             |   15 
 testing/recordings/TestInteractionVolumeCycle.log                                     |   12 
 testing/scripts/TestCommandScriptHelp.txt                                             |    1 
 testing/scripts/TestCommandScriptHelpInvalid.txt                                      |    1 
 testing/scripts/TestCommandScriptJumpToFirstFrame.txt                                 |    1 
 testing/scripts/TestCommandScriptJumpToLastFrame.txt                                  |    1 
 testing/scripts/TestCommandScriptJumpToMiddleFrame.txt                                |    1 
 testing/scripts/TestCommandScriptJumpToNextFrame.txt                                  |    4 
 testing/scripts/TestCommandScriptJumpToPreviousFrame.txt                              |    1 
 testing/scripts/TestCommandScriptParseOptionalBoolExtraArg.txt                        |    1 
 testing/scripts/TestCommandScriptRemoveCurrentFileGroup.txt                           |    1 
 testing/scripts/TestCommandScriptRemoveFileGroups.txt                                 |    1 
 testing/scripts/TestParseOptionalBoolExtraArg.txt                                     |    1 
 testing/scripts/TestRemoveFileGroups.txt                                              |    1 
 vtkext/private/CMakeLists.txt                                                         |    1 
 vtkext/private/module/CMakeLists.txt                                                  |   48 
 vtkext/private/module/F3DColoringInfoHandler.cxx                                      |    2 
 vtkext/private/module/F3DColoringInfoHandler.h                                        |    2 
 vtkext/private/module/F3DImguiStyle.h                                                 |   68 
 vtkext/private/module/F3DLog.cxx                                                      |   16 
 vtkext/private/module/F3DLog.h                                                        |   13 
 vtkext/private/module/F3DStyle.cxx                                                    |   52 
 vtkext/private/module/F3DStyle.h                                                      |   92 
 vtkext/private/module/Testing/CMakeLists.txt                                          |   13 
 vtkext/private/module/Testing/TestF3DBitonicSort.cxx                                  |   12 
 vtkext/private/module/Testing/TestF3DCachedTexturesPrint.cxx                          |    6 
 vtkext/private/module/Testing/TestF3DEXRMemReader.cxx                                 |   20 
 vtkext/private/module/Testing/TestF3DEXRReader.cxx                                    |    6 
 vtkext/private/module/Testing/TestF3DFpsCounter.cxx                                   |    6 
 vtkext/private/module/Testing/TestF3DGenericImporter.cxx                              |  459 +
 vtkext/private/module/Testing/TestF3DMetaImporterAnimation.cxx                        |   18 
 vtkext/private/module/Testing/TestF3DMetaImporterMultiColoring.cxx                    |   18 
 vtkext/private/module/Testing/TestF3DObjectFactory.cxx                                |   19 
 vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx                             |    6 
 vtkext/private/module/Testing/TestF3DRenderPass.cxx                                   |    7 
 vtkext/private/module/Testing/TestF3DRendererWithColoring.cxx                         |    8 
 vtkext/private/module/Testing/TestF3DWebPMemReader.cxx                                |   70 
 vtkext/private/module/Testing/TestF3DWebPReader.cxx                                   |   29 
 vtkext/private/module/Testing/TestF3DWebPReaderInvalid.cxx                            |   27 
 vtkext/private/module/glsl/vtkF3DPointSplatVS.glsl                                    |  153 
 vtkext/private/module/glsl/vtkF3DRandomFS.glsl                                        |   19 
 vtkext/private/module/vtk.module                                                      |    2 
 vtkext/private/module/vtkF3DCachedLUTTexture.cxx                                      |    2 
 vtkext/private/module/vtkF3DCachedSpecularTexture.cxx                                 |    4 
 vtkext/private/module/vtkF3DConsoleOutputWindow.cxx                                   |   51 
 vtkext/private/module/vtkF3DEXRReader.cxx                                             |   50 
 vtkext/private/module/vtkF3DExternalRenderWindow.cxx                                  |   17 
 vtkext/private/module/vtkF3DGLXRenderWindow.cxx                                       |   27 
 vtkext/private/module/vtkF3DGenericImporter.cxx                                       |  388 +
 vtkext/private/module/vtkF3DGenericImporter.h                                         |   45 
 vtkext/private/module/vtkF3DHexagonalBokehBlurPass.cxx                                |   26 
 vtkext/private/module/vtkF3DImguiActor.cxx                                            |  451 +
 vtkext/private/module/vtkF3DImguiActor.h                                              |    5 
 vtkext/private/module/vtkF3DImguiConsole.cxx                                          |  130 
 vtkext/private/module/vtkF3DImguiConsole.h                                            |   10 
 vtkext/private/module/vtkF3DImguiObserver.cxx                                         |  158 
 vtkext/private/module/vtkF3DInteractorEventRecorder.cxx                               |   18 
 vtkext/private/module/vtkF3DInteractorStyle.cxx                                       |   11 
 vtkext/private/module/vtkF3DMetaImporter.cxx                                          |   58 
 vtkext/private/module/vtkF3DMetaImporter.h                                            |    4 
 vtkext/private/module/vtkF3DObjectFactory.cxx                                         |    6 
 vtkext/private/module/vtkF3DOrientationMarkerWidget.cxx                               |   10 
 vtkext/private/module/vtkF3DOrientationMarkerWidget.h                                 |   31 
 vtkext/private/module/vtkF3DOverlayRenderPass.cxx                                     |    1 
 vtkext/private/module/vtkF3DPointSplatMapper.cxx                                      |  203 
 vtkext/private/module/vtkF3DPointSplatMapper.h                                        |   13 
 vtkext/private/module/vtkF3DPolyDataMapper.cxx                                        |   11 
 vtkext/private/module/vtkF3DPolyDataMapper.h                                          |    6 
 vtkext/private/module/vtkF3DPostProcessFilter.cxx                                     |   51 
 vtkext/private/module/vtkF3DRenderPass.cxx                                            |   17 
 vtkext/private/module/vtkF3DRenderPass.h                                              |    2 
 vtkext/private/module/vtkF3DRenderer.cxx                                              |  735 +-
 vtkext/private/module/vtkF3DRenderer.h                                                |  173 
 vtkext/private/module/vtkF3DStochasticTransparentPass.cxx                             |  145 
 vtkext/private/module/vtkF3DStochasticTransparentPass.h                               |   63 
 vtkext/private/module/vtkF3DTAAResolvePass.cxx                                        |  147 
 vtkext/private/module/vtkF3DTAAResolvePass.h                                          |   58 
 vtkext/private/module/vtkF3DUIActor.cxx                                               |   81 
 vtkext/private/module/vtkF3DUIActor.h                                                 |   60 
 vtkext/private/module/vtkF3DWebPReader.cxx                                            |  146 
 vtkext/private/module/vtkF3DWebPReader.h                                              |   51 
 vtkext/public/CMakeLists.txt                                                          |    7 
 vtkext/public/doxygen/conf.py                                                         |   80 
 vtkext/public/doxygen/index.rst                                                       |   26 
 vtkext/public/module/CMakeLists.txt                                                   |   16 
 vtkext/public/module/F3DUtils.h                                                       |    2 
 vtkext/public/module/Testing/TestF3DBitonicSort.cxx                                   |   13 
 vtkext/public/module/vtk.module                                                       |    1 
 vtkext/public/module/vtkF3DBitonicSort.cxx                                            |    4 
 vtkext/public/module/vtkF3DBitonicSort.h                                              |   25 
 vtkext/public/module/vtkF3DFaceVaryingPointDispatcher.h                               |   11 
 vtkext/public/module/vtkF3DGLTFImporter.h                                             |    2 
 vtkext/public/module/vtkF3DImporter.cxx                                               |   10 
 vtkext/public/module/vtkF3DImporter.h                                                 |   42 
 webassembly/CMakeLists.txt                                                            |   43 
 webassembly/F3DEmscriptenBindings.cxx                                                 |  484 +
 webassembly/README.md                                                                 |   13 
 webassembly/app.html                                                                  |  489 -
 webassembly/build.sh                                                                  |   11 
 webassembly/manifest.json                                                             |   15 
 webassembly/package.json                                                              |   34 
 webassembly/testing/CMakeLists.txt                                                    |   48 
 webassembly/testing/test.html.in                                                      |   22 
 webassembly/testing/test_camera.js                                                    |   59 
 webassembly/testing/test_engine.js                                                    |   46 
 webassembly/testing/test_image.js                                                     |   93 
 webassembly/testing/test_interactor.js                                                |  123 
 webassembly/testing/test_options.js                                                   |  122 
 webassembly/testing/test_render.js                                                    |   56 
 webassembly/testing/test_scene.js                                                     |   38 
 webassembly/testing/test_splats.js                                                    |   37 
 webassembly/testing/utils.js                                                          |  145 
 1255 files changed, 46659 insertions(+), 15870 deletions(-)

diff -Nru f3d-3.2.0+dfsg/application/CMakeLists.txt f3d-3.4.1/application/CMakeLists.txt
--- f3d-3.2.0+dfsg/application/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -77,11 +77,21 @@
     target_compile_definitions(${target_name} PRIVATE F3D_MODULE_EXR)
   endif ()
 
+  # webp
+  if (F3D_MODULE_WEBP)
+    target_compile_definitions(${target_name} PRIVATE F3D_MODULE_WEBP)
+  endif ()
+
   # dmon
   if (F3D_MODULE_DMON)
     target_compile_definitions(${target_name} PRIVATE F3D_MODULE_DMON)
   endif ()
 
+  #ui
+  if (F3D_MODULE_UI)
+    target_compile_definitions(${target_name} PRIVATE F3D_MODULE_UI)
+  endif()
+
   # tinyfiledialogs
   if (F3D_MODULE_TINYFILEDIALOGS)
     target_compile_definitions(${target_name} PRIVATE F3D_MODULE_TINYFILEDIALOGS)
@@ -136,7 +146,7 @@
     set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
     set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${F3D_VERSION})
     set(MACOSX_BUNDLE_BUNDLE_VERSION ${F3D_VERSION})
-    set(MACOSX_BUNDLE_COPYRIGHT "Michael Migliore, Mathieu Westphal")
+    set(MACOSX_BUNDLE_COPYRIGHT "F3D-APP Foundation")
 
     # Generate MacOS bundle using CMake variable generated by plugins
     get_property(F3D_MACOS_BUNDLE_XML GLOBAL PROPERTY F3D_MACOS_BUNDLE_XML)
@@ -146,7 +156,7 @@
   endif()
 
   if(APPLE)
-    target_link_libraries(${target_name} PRIVATE "-framework Cocoa")
+    target_link_libraries(${target_name} PRIVATE "-framework Cocoa -framework UniformTypeIdentifiers")
   endif()
 
   if(WIN32)
diff -Nru f3d-3.2.0+dfsg/application/F3DNSDelegate.h f3d-3.4.1/application/F3DNSDelegate.h
--- f3d-3.2.0+dfsg/application/F3DNSDelegate.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DNSDelegate.h	2026-01-08 08:41:55.000000000 +0200
@@ -15,6 +15,7 @@
 {
 public:
   static void InitializeDelegate(F3DStarter* Starter);
+  static void ShowOpenFileDialog(const char* const* extensions, unsigned long long extensionsSize);
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/application/F3DNSDelegate.mm f3d-3.4.1/application/F3DNSDelegate.mm
--- f3d-3.2.0+dfsg/application/F3DNSDelegate.mm	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DNSDelegate.mm	2026-01-08 08:41:55.000000000 +0200
@@ -6,6 +6,8 @@
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wmissing-method-return-type"
 #import "AppKit/NSApplication.h"
+#import "AppKit/NSOpenPanel.h"
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
 #pragma clang diagnostic pop
 
 #import "F3DConfig.h"
@@ -88,6 +90,17 @@
                       action:@selector(terminate:)
               keyEquivalent:@"q"];
 
+  // setup "file menu"
+  NSMenuItem* fileMenuItem = [bar addItemWithTitle:@"" action:nil keyEquivalent:@""];
+  NSMenu* fileMenu = [[NSMenu alloc] initWithTitle:@"File"];
+  [fileMenuItem setSubmenu:fileMenu];
+  [fileMenu release];
+
+  [fileMenuItem setSubmenu:fileMenu];
+  [fileMenu addItemWithTitle:@"Open File"
+                        action:@selector(openFile:)
+                  keyEquivalent:@"o"];
+
   // setup "window menu"
   NSMenuItem* windowMenuItem = [bar addItemWithTitle:@"" action:nil keyEquivalent:@""];
   NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
@@ -125,6 +138,54 @@
   ShouldHandleFileOpening = true;
 }
 
+- (void)openFile:(id)sender
+{
+  const auto& extensions = F3DStarter::GetExtensions();
+  NSMutableArray<NSString*>* allowedTypes = [NSMutableArray arrayWithCapacity:extensions.size()];
+
+  for (const auto& ext : extensions)
+  {
+    [allowedTypes addObject:[NSString stringWithUTF8String:ext.c_str()]];
+  }
+
+  [self openFileDialogWithAllowedTypes:allowedTypes];
+}
+
+- (void)openFileDialogWithAllowedTypes:(NSArray<NSString*>*)allowedTypes
+{
+  NSOpenPanel* openPanel = [NSOpenPanel openPanel];
+
+  [openPanel setCanChooseFiles:YES];
+  [openPanel setCanChooseDirectories:NO];
+  [openPanel setAllowsMultipleSelection:NO];
+
+  NSMutableArray<UTType *> *contentTypes = [NSMutableArray array];
+  for (NSString *type in allowedTypes) 
+  {
+      UTType *utType = [UTType typeWithFilenameExtension:type];
+      if (utType)
+      {
+          [contentTypes addObject:utType];
+      }
+  }
+  openPanel.allowedContentTypes = contentTypes;
+
+  NSModalResponse result = [openPanel runModal];
+  if (result == NSModalResponseOK)
+  {
+    NSURL *selectedFile = [[openPanel URLs] firstObject];
+    if (selectedFile) 
+    {
+      int index = self.Starter->AddFile([[selectedFile path] UTF8String]);
+      if (index > -1)
+      {
+        self.Starter->LoadFileGroup(index);
+      }
+      self.Starter->Render();
+    }
+  }
+}
+
 @end
 
 // ----------------------------------------------------------------------------
@@ -135,3 +196,16 @@
   [NSApp setDelegate:delegate];
   [delegate setStarter:Starter];
 }
+
+void F3DNSDelegate::ShowOpenFileDialog(const char* const* extensions, unsigned long long extensionsSize)
+{
+  F3DNSDelegateInternal* delegate = (F3DNSDelegateInternal*)[NSApp delegate];
+
+  NSMutableArray<NSString*>* allowedTypes = [NSMutableArray arrayWithCapacity:extensionsSize];
+  for (size_t i = 0; i < extensionsSize; ++i)
+  {
+    [allowedTypes addObject:[NSString stringWithUTF8String:extensions[i]]];
+  }
+
+  [delegate openFileDialogWithAllowedTypes:allowedTypes];
+}
diff -Nru f3d-3.2.0+dfsg/application/F3DOptionsTools.cxx f3d-3.4.1/application/F3DOptionsTools.cxx
--- f3d-3.2.0+dfsg/application/F3DOptionsTools.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DOptionsTools.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -13,6 +13,7 @@
 #include <cassert>
 #include <filesystem>
 #include <iomanip>
+#include <numeric>
 #include <set>
 #include <sstream>
 
@@ -85,12 +86,19 @@
       { "loading-progress", "", "Show loading progress bar", "<bool>", "1" },
       { "animation-progress", "", "Show animation progress bar", "<bool>", "1" },
       { "multi-file-mode", "", R"(Choose the behavior when opening multiple files. "single" will show one file at a time, "all" will show all files in a single scene, "dir" will show files from the same directory in the same scene.)", "<single|all|dir>", "" },
+      { "multi-file-regex", "", R"_(Regular expression pattern to group files. Captured groups are replaced with "*" so that, for example, the pattern "part(\d+)" would group files "foo-part1.xyz" and "foo-part2.xyz" together as "foo-part*.xyz")_", "<regex>", "" },
+      { "recursive-dir-add", "", "Add directories recursively", "<bool>", "1" },
+      { "remove-empty-file-groups", "", "Remove file groups that results into an empty scene", "<bool>", "1" },
       { "up", "", "Up direction", "<direction>", "" },
       { "axis", "x", "Show axes", "<bool>", "1" }, { "grid", "g", "Show grid", "<bool>", "1" },
+      { "x-color", "", "Color of the X axis", "<color>", "" },
+      { "y-color", "", "Color of the Y axis", "<color>", "" },
+      { "z-color", "", "Color of the Z axis", "<color>", "" },
       { "grid-absolute", "", "Position grid at the absolute origin instead of below the model", "<bool>", "1" },
       { "grid-unit", "", "Size of grid unit square, automatically computed by default", "<value>", "" },
       { "grid-subdivisions", "", "Number of grid subdivisions", "<value>", "" },
       { "grid-color", "", "Color of main grid lines", "<color>", "" },
+      { "axes-grid", "", "Enable grid axis", "<bool>", "1" },
       { "edges", "e", "Show cell edges", "<bool>", "1" },
       { "armature", "", "Enable armature visualization", "<bool>", "1" },
       { "camera-index", "", "Select the camera to use", "<index>", "" },
@@ -103,11 +111,15 @@
       { "animation-time", "", "Set animation time to load", "<time>", "" },
       { "font-file", "", "Path to a FreeType compatible font file", "<file_path>", ""},
       { "font-scale", "", "Scale fonts", "<ratio>", ""},
+#if F3D_MODULE_UI
+      { "backdrop-opacity", "", "UI backdrop opacity", "<value>", ""},
+#endif
       { "command-script", "", "Path to a script file containing commands to execute", "<file_path>", "" } } },
   { "Material",
-    { {"point-sprites", "o", "Show sphere sprites instead of surfaces", "<bool>", "1" },
-      {"point-sprites-type", "", "Point sprites type", "<sphere|gaussian>", ""},
-      {"point-sprites-size", "", "Point sprites size", "<size>", ""},
+    { {"point-sprites", "o", R"(Select point sprites type ("none", "sphere", "gaussian"))", "<string>", "sphere" },
+      {"point-sprites-type", "", "Point sprites type (deprecated)", "<sphere|gaussian>", ""},
+      {"point-sprites-size", "", "Point sprites sphere size", "<size>", ""},
+      {"point-sprites-absolute-size", "", "Do not scale point sprites size by scene size", "<bool>", "1"},
       {"point-size", "", "Point size when showing vertices, model specified by default", "<size>", ""},
       {"line-width", "", "Line width when showing edges, model specified by default", "<width>", ""},
       {"backface-type", "", "Backface type, can be visible or hidden, model specified by default", "<visible|hidden>", ""},
@@ -134,6 +146,7 @@
       {"fps", "z", "Display rendering frame per second", "<bool>", "1"},
       {"filename", "n", "Display filename", "<bool>", "1"},
       {"metadata", "m", "Display file metadata", "<bool>", "1"},
+      {"hdri-filename", "", "Display hdri filename", "<bool>", "1"},
       {"blur-background", "u", "Blur background", "<bool>", "1" },
       {"blur-coc", "", "Blur circle of confusion radius", "<value>", ""},
       {"light-intensity", "", "Light intensity", "<value>", ""} } },
@@ -167,10 +180,11 @@
       {"raytracing-denoise", "d", "Denoise the image", "<bool>", "1"} } },
 #endif
   {"PostFX (OpenGL)",
-    { {"translucency-support", "p", "Enable translucency support, implemented using depth peeling", "<bool>", "1"},
+    { {"blending", "p", R"(Select translucency blending mode ("none", "ddp", "sort" or "stochastic"))", "<string>", "ddp"},
+      {"translucency-support", "", "Enable translucency blending (deprecated)", "<bool>", "1"},
       {"ambient-occlusion", "q", "Enable ambient occlusion providing approximate shadows for better depth perception, implemented using SSAO", "<bool>", "1"},
-      {"anti-aliasing", "a", "Enable anti-aliasing", "<bool>", "1"},
-      {"anti-aliasing-mode", "", R"(Select anti-aliasing method ("fxaa" or "ssaa"))", "<string>", "fxaa"},
+      {"anti-aliasing", "a", R"(Select anti-aliasing method ("none", "fxaa", "ssaa" or "taa"))", "<string>", "fxaa"},
+      {"anti-aliasing-mode", "", R"(Select anti-aliasing method ("fxaa", "ssaa" or "taa") (deprecated))", "<string>", "fxaa"},
       {"tone-mapping", "t", "Enable Tone Mapping, providing balanced coloring", "<bool>", "1"},
       {"final-shader", "", "Execute the final shader at the end of the rendering pipeline", "<GLSL code>", ""} } },
   {"Testing",
@@ -216,13 +230,9 @@
   }};
 
   f3d::log::setUseColoring(false);
-  std::vector<std::string> orderedCLIGroupNames;
-  orderedCLIGroupNames.reserve(::CLIOptions.size());
-  for (const ::CLIGroup& optionGroup : ::CLIOptions)
-  {
-    // This ensure help is provided in the expected group order
-    orderedCLIGroupNames.emplace_back(optionGroup.GroupName);
-  }
+  std::vector<std::string> orderedCLIGroupNames(CLIOptions.size());
+  std::transform(CLIOptions.cbegin(), CLIOptions.cend(), orderedCLIGroupNames.begin(),
+    [](const ::CLIGroup& cliGroup) { return cliGroup.GroupName; });
   f3d::log::info(cxxOptions.help(orderedCLIGroupNames));
   f3d::log::info("\nExamples:");
   for (const auto& [cmd, desc] : examples)
@@ -317,14 +327,10 @@
     descColSize = std::max(descColSize, reader.Description.length());
     plugColSize = std::max(plugColSize, reader.PluginName.length());
 
-    for (const auto& ext : reader.Extensions)
-    {
-      extsColSize = std::max(extsColSize, ext.length());
-    }
-    for (const auto& mime : reader.MimeTypes)
-    {
-      mimeColSize = std::max(mimeColSize, mime.length());
-    }
+    extsColSize = std::accumulate(reader.Extensions.cbegin(), reader.Extensions.cend(), extsColSize,
+      [](size_t size, const auto& ext) { return std::max(size, ext.length()); });
+    mimeColSize = std::accumulate(reader.MimeTypes.cbegin(), reader.MimeTypes.cend(), mimeColSize,
+      [](size_t size, const auto& mime) { return std::max(size, mime.length()); });
   }
   const size_t colGap = 4;
   nameColSize += colGap;
@@ -391,6 +397,12 @@
     checkDistance(key, option, ret);
   }
 
+  // Check cli names in custom mapping options
+  for (const auto& [key, value] : F3DOptionsTools::CustomMappingOptions)
+  {
+    checkDistance(std::string(key), option, ret);
+  }
+
   // Check cli names for libf3d options
   for (const auto& [key, value] : F3DOptionsTools::LibOptionsNames)
   {
@@ -459,23 +471,32 @@
           // Add the default value to the help text if any
           std::string defaultValue;
           std::string helpText(cliOption.HelpText);
+          std::string longName(cliOption.LongName);
 
           // Recover default value from app options
-          auto appIter = F3DOptionsTools::DefaultAppOptions.find(std::string(cliOption.LongName));
+          auto appIter = F3DOptionsTools::DefaultAppOptions.find(longName);
           if (appIter != F3DOptionsTools::DefaultAppOptions.end())
           {
             defaultValue = appIter->second;
           }
           else
           {
-            // Recover default value from lib options
-            auto libIter = F3DOptionsTools::LibOptionsNames.find(cliOption.LongName);
-            if (libIter != F3DOptionsTools::LibOptionsNames.end())
+            auto customIter = F3DOptionsTools::CustomMappingOptions.find(longName);
+            if (customIter != F3DOptionsTools::CustomMappingOptions.end())
+            {
+              defaultValue = customIter->second;
+            }
+            else
             {
-              f3d::options opt;
-              std::string name = std::string(libIter->second);
-              // let default value empty for unset options
-              defaultValue = opt.hasValue(name) ? opt.getAsString(name) : "";
+              // Recover default value from lib options
+              auto libIter = F3DOptionsTools::LibOptionsNames.find(cliOption.LongName);
+              if (libIter != F3DOptionsTools::LibOptionsNames.end())
+              {
+                f3d::options opt;
+                std::string name = std::string(libIter->second);
+                // let default value empty for unset options
+                defaultValue = opt.hasValue(name) ? opt.getAsString(name) : "";
+              }
             }
           }
 
@@ -625,3 +646,109 @@
   ss << " " << std::setw(helpWidth) << help;
   f3d::log::info(ss.str());
 }
+
+//----------------------------------------------------------------------------
+std::vector<std::pair<std::string, std::string>> F3DOptionsTools::ConvertToLibf3dOptions(const std::string& key, const std::string& value)
+{
+  std::vector<std::pair<std::string, std::string>> libf3dOptions;
+
+  // Simple one-to-one case
+  auto libf3dIter = F3DOptionsTools::LibOptionsNames.find(key);
+  if (libf3dIter != F3DOptionsTools::LibOptionsNames.end())
+  {
+    libf3dOptions.emplace_back(std::make_pair(libf3dIter->second, value));
+  }
+
+  // anti-aliasing is handled in two options in the lib
+  else if (key == "anti-aliasing")
+  {
+    if (value != "none")
+    {
+      // Handle deprecated boolean option
+      bool deprecatedBooleanOption;
+      if (F3DOptionsTools::Parse(value, deprecatedBooleanOption))
+      {
+        f3d::log::warn("--anti-aliasing is a now a string, please specify the type of "
+                       "anti-aliasing or use the implicit default");
+        libf3dOptions.emplace_back(std::make_pair("render.effect.antialiasing.enable", value));
+      }
+      else
+      {
+        libf3dOptions.emplace_back(std::make_pair("render.effect.antialiasing.enable", "true"));
+        libf3dOptions.emplace_back(std::make_pair("render.effect.antialiasing.mode", value));
+      }
+    }
+    else
+    {
+      libf3dOptions.emplace_back(std::make_pair("render.effect.antialiasing.enable", "false"));
+    }
+  }
+
+  // handle deprecated anti-aliasing option
+  else if (key == "anti-aliasing-mode")
+  {
+    f3d::log::warn("--anti-aliasing-mode is deprecated");
+    libf3dOptions.emplace_back(std::make_pair("render.effect.antialiasing.mode", value));
+  }
+
+  // blending is handled in two options in the lib
+  else if (key == "blending")
+  {
+    if (value != "none")
+    {
+      libf3dOptions.emplace_back(std::make_pair("render.effect.blending.enable", "true"));
+      libf3dOptions.emplace_back(std::make_pair("render.effect.blending.mode", value));
+    }
+    else
+    {
+      libf3dOptions.emplace_back(std::make_pair("render.effect.blending.enable", "false"));
+    }
+  }
+
+  // handle deprecated translucency support
+  else if (key == "translucency-support")
+  {
+    f3d::log::warn("--translucency-support is deprecated, please use --blending instead");
+    libf3dOptions.emplace_back(std::make_pair("render.effect.blending.enable", value));
+  }
+
+  // point sprites is handled in two options in the lib
+  else if (key == "point-sprites")
+  {
+    if (value != "none")
+    {
+      // Handle deprecated boolean option
+      bool deprecatedBooleanOption;
+      if (F3DOptionsTools::Parse(value, deprecatedBooleanOption))
+      {
+        f3d::log::warn("--point-sprites is a now a string, please specify the type of "
+                       "point sprites to use or use the implicit default");
+        libf3dOptions.emplace_back(std::make_pair("model.point_sprites.enable", value));
+      }
+      else
+      {
+        libf3dOptions.emplace_back(std::make_pair("model.point_sprites.enable", "true"));
+        libf3dOptions.emplace_back(std::make_pair("model.point_sprites.type", value));
+      }
+    }
+    else
+    {
+      libf3dOptions.emplace_back(std::make_pair("model.point_sprites.enable", "false"));
+    }
+  }
+
+  // handle deprecated point-sprites-type option
+  else if (key == "point-sprites-type")
+  {
+    f3d::log::warn("--point-sprites-type is deprecated");
+    libf3dOptions.emplace_back(std::make_pair("model.point_sprites.mode", value));
+  }
+
+  else
+  {
+    // If nothing to convert, just return the input
+    libf3dOptions.emplace_back(std::make_pair(key, value));
+  }
+
+  return libf3dOptions;
+}
diff -Nru f3d-3.2.0+dfsg/application/F3DOptionsTools.h f3d-3.4.1/application/F3DOptionsTools.h
--- f3d-3.2.0+dfsg/application/F3DOptionsTools.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DOptionsTools.h	2026-01-08 08:41:55.000000000 +0200
@@ -5,6 +5,8 @@
  * @class   F3DOptionsTools
  * @brief   A namespace to handle and parse F3D Options
  */
+#include <options.h>
+
 #include <filesystem>
 #include <map>
 #include <string>
@@ -40,6 +42,9 @@
   { "screenshot-filename", "{app}/{model}_{n}.png" },
   { "verbose", "info" },
   { "multi-file-mode", "single" },
+  { "multi-file-regex", "" },
+  { "recursive-dir-add", "false" },
+  { "remove-empty-file-groups", "false" },
   { "resolution", "1000, 600" },
   { "position", "" },
   { "colormap-file", "" },
@@ -67,11 +72,15 @@
   { "animation-progress", "ui.animation_progress" },
   { "up", "scene.up_direction" },
   { "axis", "ui.axis" },
+  { "x-color", "ui.x_color" },
+  { "y-color", "ui.y_color" },
+  { "z-color", "ui.z_color" },
   { "grid", "render.grid.enable" },
   { "grid-absolute", "render.grid.absolute" },
   { "grid-unit", "render.grid.unit" },
   { "grid-subdivisions", "render.grid.subdivisions" },
   { "grid-color", "render.grid.color" },
+  { "axes-grid", "render.axes_grid.enable" },
   { "edges", "render.show_edges" },
   { "armature", "render.armature.enable" },
   { "camera-index", "scene.camera.index" },
@@ -84,9 +93,9 @@
   { "force-reader", "scene.force_reader" },
   { "font-file", "ui.font_file" },
   { "font-scale", "ui.scale" },
-  { "point-sprites", "model.point_sprites.enable" },
-  { "point-sprites-type", "model.point_sprites.type" },
+  { "backdrop-opacity", "ui.backdrop.opacity" },
   { "point-sprites-size", "model.point_sprites.size" },
+  { "point-sprites-absolute-size", "model.point_sprites.absolute_size" },
   { "point-size", "render.point_size" },
   { "line-width", "render.line_width" },
   { "backface-type", "render.backface_type" },
@@ -109,6 +118,7 @@
   { "fps", "ui.fps" },
   { "filename", "ui.filename" },
   { "metadata", "ui.metadata" },
+  { "hdri-filename", "ui.hdri_filename" },
   { "blur-background", "render.background.blur.enable" },
   { "blur-coc", "render.background.blur.coc" },
   { "scalar-coloring", "model.scivis.enable" },
@@ -126,16 +136,31 @@
   { "raytracing", "render.raytracing.enable" },
   { "raytracing-samples", "render.raytracing.samples" },
   { "raytracing-denoise", "render.raytracing.denoise" },
-  { "translucency-support", "render.effect.translucency_support" },
   { "ambient-occlusion", "render.effect.ambient_occlusion" },
-  { "anti-aliasing", "render.effect.antialiasing.enable" },
-  { "anti-aliasing-mode", "render.effect.antialiasing.mode" },
   { "tone-mapping", "render.effect.tone_mapping" },
   { "final-shader", "render.effect.final_shader" },
   { "textures-transform", "model.textures_transform" },
 };
 
 /**
+ * List of CLI option names that requires custom mapping
+ */
+static inline const std::map<std::string_view, std::string_view> CustomMappingOptions = {
+  { "anti-aliasing", "none" },
+  { "anti-aliasing-mode", "" },
+  { "translucency-support", "false" },
+  { "blending", "none" },
+  { "point-sprites", "none" },
+  { "point-sprites-type", "" },
+};
+
+/**
+ * Convert a CLI options key/value to a vector of libf3d options key/value
+ */
+std::vector<std::pair<std::string, std::string>> ConvertToLibf3dOptions(
+  const std::string& key, const std::string& value);
+
+/**
  * Browse through all possible option names to find one that have the smallest distance to the
  * provided option.
  * If checkLibAndReaders is true, even check in the libf3d and reader option names from plugins
@@ -157,6 +182,25 @@
  */
 void PrintHelpPair(
   std::string_view key, std::string_view help, int keyWidth = 10, int helpWidth = 70);
+
+/**
+ * Parse provided string into provided typed var.
+ * Return true if successful, false otherwise.
+ */
+template<typename T>
+bool Parse(const std::string& optionString, T& option)
+{
+  try
+  {
+    option = f3d::options::parse<T>(optionString);
+    return true;
+  }
+  catch (const f3d::options::parsing_exception&)
+  {
+    return false;
+  }
+}
+
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/application/F3DStarter.cxx f3d-3.4.1/application/F3DStarter.cxx
--- f3d-3.2.0+dfsg/application/F3DStarter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DStarter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -49,6 +49,7 @@
 #include <iomanip>
 #include <iostream>
 #include <mutex>
+#include <numeric>
 #include <regex>
 #include <set>
 
@@ -94,6 +95,9 @@
     std::string ScreenshotFilename;
     std::string VerboseLevel;
     std::string MultiFileMode;
+    std::string MultiFileRegex;
+    bool RecursiveDirAdd;
+    bool RemoveEmptyFileGroups;
     std::vector<int> Resolution;
     std::vector<int> Position;
     std::string ColorMapFile;
@@ -103,6 +107,12 @@
     std::string InteractionTestRecordFile;
     std::string InteractionTestPlayFile;
     std::string CommandScriptFile;
+    std::string AntiAliasing;
+    std::string AntiAliasingMode; // Deprecated
+    std::string PointSprites;
+    std::string PointSpritesType; // Deprecated
+    bool TranslucencySupport;     // Deprecated
+    std::string Blending;
   };
 
   void SetupCamera(const CameraConfiguration& camConf)
@@ -129,8 +139,6 @@
       cam.setViewAngle(camConf.CameraViewAngle);
     }
 
-    bool reset = false;
-    double zoomFactor = 0.9;
     if (camConf.CameraPosition.size() != 3 && camConf.CameraDirection.has_value())
     {
       f3d::vector3_t dir = camConf.CameraDirection.value();
@@ -142,24 +150,16 @@
         pos[i] = foc[i] - dir[i];
       }
       cam.setPosition(pos);
-      reset = true;
     }
+
+    cam.azimuth(camConf.CameraAzimuthAngle).elevation(camConf.CameraElevationAngle);
+
     if (camConf.CameraPosition.size() != 3)
     {
-      if (camConf.CameraZoomFactor > 0)
-      {
-        zoomFactor = camConf.CameraZoomFactor;
-      }
-      reset = true;
-    }
-    if (reset)
-    {
-      cam.resetToBounds(zoomFactor);
+      cam.resetToBounds(camConf.CameraZoomFactor > 0 ? camConf.CameraZoomFactor : 0.9);
     }
 
-    cam.azimuth(camConf.CameraAzimuthAngle)
-      .elevation(camConf.CameraElevationAngle)
-      .setCurrentAsDefault();
+    cam.setCurrentAsDefault();
   }
 
   static bool HasHDRIExtension(const std::string& file)
@@ -243,10 +243,10 @@
     dmon_watch_id, dmon_action, const char*, const char* filename, const char*, void* userData)
   {
     F3DStarter* self = reinterpret_cast<F3DStarter*>(userData);
-    const std::lock_guard<std::mutex> lock(self->Internals->LoadedFilesMutex);
-    if (std::find_if(self->Internals->LoadedFiles.begin(), self->Internals->LoadedFiles.end(),
+    const std::lock_guard<std::mutex> lock(self->Internals->FilesToWatchMutex);
+    if (std::find_if(self->Internals->FilesToWatch.begin(), self->Internals->FilesToWatch.end(),
           [&](const auto& path)
-          { return path.filename() == filename; }) != self->Internals->LoadedFiles.end())
+          { return path.filename() == filename; }) != self->Internals->FilesToWatch.end())
     {
       self->Internals->ReloadFileRequested = true;
     }
@@ -512,7 +512,13 @@
 
     // Initialize libf3dOptions
     f3d::options libOptions;
-    libOptions.ui.dropzone_info = "Drop a file or HDRI to load it\nPress H to show cheatsheet";
+#if F3D_MODULE_UI
+#if F3D_MODULE_TINYFILEDIALOGS
+    libOptions.ui.drop_zone.custom_binds = "None+Drop Ctrl+O None+H";
+#else
+    libOptions.ui.drop_zone.custom_binds = "None+Drop None+H";
+#endif
+#endif
 
     // Copy appOptions
     F3DOptionsTools::OptionsDict appOptions = F3DOptionsTools::DefaultAppOptions;
@@ -550,117 +556,114 @@
                 continue;
               }
 
-              // Handle CLI options deprecation
+              // Handle CLI options deprecation simple warnings
               if (key == "animation-index")
               {
                 f3d::log::warn("animation-index is deprecated, please use animation-indices");
               }
 
               // Convert key into a libf3d option name if possible
-              std::string libf3dOptionName = key;
               std::string keyForLog = key;
-              auto libf3dIter = F3DOptionsTools::LibOptionsNames.find(libf3dOptionName);
-              if (libf3dIter != F3DOptionsTools::LibOptionsNames.end())
+              for (const auto& libf3dOption : F3DOptionsTools::ConvertToLibf3dOptions(key, value))
               {
-                libf3dOptionName = std::string(libf3dIter->second);
-              }
+                auto [libf3dOptionName, libf3dOptionValue] = libf3dOption;
 
-              std::string libf3dOptionValue = value;
-              bool reset = false;
+                bool reset = false;
 
-              // Handle options reset
-              // XXX: Use starts_with once C++20 is supported
-              if (libf3dOptionName.rfind("reset-", 0) == 0)
-              {
-                if (libf3dOptionName.size() > 6)
-                {
-                  reset = true;
-                  libf3dOptionName = libf3dOptionName.substr(6);
-                  keyForLog = libf3dOptionName;
-                  libf3dOptionValue = "reset";
-                }
-                else
+                // Handle options reset
+                // XXX: Use starts_with once C++20 is supported
+                if (libf3dOptionName.rfind("reset-", 0) == 0)
                 {
-                  f3d::log::warn("Invalid option: 'reset' must be followed by a valid option "
-                                 "name, ignoring entry");
-                  continue;
+                  if (libf3dOptionName.size() > 6)
+                  {
+                    reset = true;
+                    libf3dOptionName = libf3dOptionName.substr(6);
+                    keyForLog = libf3dOptionName;
+                    libf3dOptionValue = "reset";
+                  }
+                  else
+                  {
+                    f3d::log::warn("Invalid option: 'reset' must be followed by a valid option "
+                                   "name, ignoring entry");
+                    continue;
+                  }
                 }
-              }
 
-              // Handle reader options
-              std::vector<std::string> readerOptionNames = f3d::engine::getAllReaderOptionNames();
-              if (std::find(readerOptionNames.begin(), readerOptionNames.end(), libf3dOptionName) !=
-                readerOptionNames.end())
-              {
-                f3d::engine::setReaderOption(libf3dOptionName, libf3dOptionValue);
-                continue;
-              }
-
-              try
-              {
-                // Assume this is a libf3d option and set/reset the value
-                if (reset)
-                {
-                  libOptions.reset(libf3dOptionName);
-                }
-                else
+                // Handle reader options
+                std::vector<std::string> readerOptionNames = f3d::engine::getAllReaderOptionNames();
+                if (std::find(readerOptionNames.begin(), readerOptionNames.end(),
+                      libf3dOptionName) != readerOptionNames.end())
                 {
-                  libOptions.setAsString(libf3dOptionName, libf3dOptionValue);
+                  f3d::engine::setReaderOption(libf3dOptionName, libf3dOptionValue);
+                  continue;
                 }
 
-                // Log the option if needed
-                if (logOptions)
-                {
-                  loggingMap[libf3dOptionName] =
-                    std::tuple(keyForLog, source, matchType, match, libf3dOptionValue);
-                }
-              }
-              catch (const f3d::options::parsing_exception& ex)
-              {
-                if (!quiet)
+                try
                 {
-                  std::string origin;
-                  if (source.empty())
+                  // Assume this is a libf3d option and set/reset the value
+                  if (reset)
                   {
-                    origin = match;
+                    libOptions.reset(libf3dOptionName);
                   }
                   else
                   {
-                    // TODO: Use std::format once C++20 is supported
-                    origin = source;
-                    origin += ":`";
-                    origin += match;
-                    origin += "` (";
-                    origin += matchType;
-                    origin += ")";
+                    libOptions.setAsString(libf3dOptionName, libf3dOptionValue);
+                  }
+
+                  // Log the option if needed
+                  if (logOptions)
+                  {
+                    loggingMap[libf3dOptionName] =
+                      std::tuple(keyForLog, source, matchType, match, libf3dOptionValue);
                   }
-                  f3d::log::warn("Could not set '", keyForLog, "' to '", libf3dOptionValue,
-                    "' from ", origin, " because: ", ex.what());
                 }
-              }
-              catch (const f3d::options::inexistent_exception&)
-              {
-                if (!quiet)
+                catch (const f3d::options::parsing_exception& ex)
                 {
-                  std::string origin;
-                  if (source.empty())
+                  if (!quiet)
                   {
-                    origin = match;
+                    std::string origin;
+                    if (source.empty())
+                    {
+                      origin = match;
+                    }
+                    else
+                    {
+                      // TODO: Use std::format once C++20 is supported
+                      origin = source;
+                      origin += ":`";
+                      origin += match;
+                      origin += "` (";
+                      origin += matchType;
+                      origin += ")";
+                    }
+                    f3d::log::warn("Could not set '", keyForLog, "' to '", libf3dOptionValue,
+                      "' from ", origin, " because: ", ex.what());
                   }
-                  else
+                }
+                catch (const f3d::options::inexistent_exception&)
+                {
+                  if (!quiet)
                   {
-                    // TODO: Use std::format once C++20 is supported
-                    origin = source;
-                    origin += ":`";
-                    origin += match;
-                    origin += "` (";
-                    origin += matchType;
-                    origin += ")";
+                    std::string origin;
+                    if (source.empty())
+                    {
+                      origin = match;
+                    }
+                    else
+                    {
+                      // TODO: Use std::format once C++20 is supported
+                      origin = source;
+                      origin += ":`";
+                      origin += match;
+                      origin += "` (";
+                      origin += matchType;
+                      origin += ")";
+                    }
+                    auto [closestName, dist] =
+                      F3DOptionsTools::GetClosestOption(libf3dOptionName, true);
+                    f3d::log::warn("'", keyForLog, "' option from ", origin,
+                      " does not exists , did you mean '", closestName, "'?");
                   }
-                  auto [closestName, dist] =
-                    F3DOptionsTools::GetClosestOption(libf3dOptionName, true);
-                  f3d::log::warn("'", keyForLog, "' option from ", origin,
-                    " does not exists , did you mean '", closestName, "'?");
                 }
               }
             }
@@ -683,34 +686,30 @@
     // Update libf3d options
     this->LibOptions = libOptions;
 
-    // Update options that depends on both app and libf3d options
-    this->UpdateInterdependentOptions();
-  }
-
-  template<typename T>
-  bool Parse(const std::string& optionString, T& option)
-  {
-    try
-    {
-      option = f3d::options::parse<T>(optionString);
-      return true;
-    }
-    catch (const f3d::options::parsing_exception&)
-    {
-      return false;
-    }
+    // Update options that depends on both libf3d and app options
+    // Note that this should not be used for interactive options
+    // As this break the logic of updating options by priority
+    // TODO: Rework to avoid this
+    this->UpdateInterdependantOptions();
   }
 
+  /**
+   * Parse a named string option from appOptions into provided typed option.
+   */
   template<typename T>
   void ParseOption(
     const F3DOptionsTools::OptionsDict& appOptions, const std::string& name, T& option)
   {
-    if (!this->Parse(appOptions.at(name), option))
+    if (!F3DOptionsTools::Parse(appOptions.at(name), option))
     {
       f3d::log::warn("Could not parse '" + appOptions.at(name) + "' into '" + name + "' option");
     }
   }
 
+  /**
+   * Parse a named string option from appOptions into provided typed optional option.
+   * If the string is empty, set it to nullopt
+   */
   template<typename T>
   void ParseOption(const F3DOptionsTools::OptionsDict& appOptions, const std::string& name,
     std::optional<T>& option)
@@ -744,6 +743,10 @@
     this->ParseOption(appOptions, "screenshot-filename", this->AppOptions.ScreenshotFilename);
     this->ParseOption(appOptions, "verbose", this->AppOptions.VerboseLevel);
     this->ParseOption(appOptions, "multi-file-mode", this->AppOptions.MultiFileMode);
+    this->ParseOption(appOptions, "multi-file-regex", this->AppOptions.MultiFileRegex);
+    this->ParseOption(appOptions, "recursive-dir-add", this->AppOptions.RecursiveDirAdd);
+    this->ParseOption(
+      appOptions, "remove-empty-file-groups", this->AppOptions.RemoveEmptyFileGroups);
     this->ParseOption(appOptions, "resolution", this->AppOptions.Resolution);
     this->ParseOption(appOptions, "position", this->AppOptions.Position);
     this->ParseOption(appOptions, "colormap-file", this->AppOptions.ColorMapFile);
@@ -768,7 +771,7 @@
     this->ParseOption(appOptions, "command-script", this->AppOptions.CommandScriptFile);
   }
 
-  void UpdateInterdependentOptions()
+  void UpdateInterdependantOptions()
   {
     // colormap-file and colormap are interdependent
     const std::string& colorMapFile = this->AppOptions.ColorMapFile;
@@ -910,17 +913,14 @@
     }
   }
 
-  // Recover a vector of unique parent paths from a vector of paths
-  static std::vector<fs::path> ParentPaths(const std::vector<fs::path>& paths)
+  // Recover a set of parent paths from paths
+  template<typename T>
+  static std::set<fs::path> ParentPaths(const T& paths)
   {
-    std::vector<fs::path> parents;
+    std::set<fs::path> parents;
     for (const auto& tmpPath : paths)
     {
-      fs::path parentPath = tmpPath.parent_path();
-      if (std::find(parents.begin(), parents.end(), tmpPath) == parents.end())
-      {
-        parents.emplace_back(parentPath);
-      }
+      parents.insert(tmpPath.parent_path());
     }
     return parents;
   }
@@ -929,7 +929,7 @@
   {
     if (GlobalInteractor)
     {
-      GlobalInteractor->stop();
+      GlobalInteractor->requestStop();
       GlobalInteractor = nullptr;
     }
   }
@@ -944,12 +944,13 @@
   std::unique_ptr<f3d::engine> Engine;
   std::vector<std::pair<std::string, std::vector<fs::path>>> FilesGroups;
   std::vector<fs::path> LoadedFiles;
+  std::set<fs::path> FilesToWatch;
   int CurrentFilesGroupIndex = -1;
 
 #if F3D_MODULE_DMON
   // dmon related
-  std::mutex LoadedFilesMutex;
-  std::vector<dmon_watch_id> FolderWatchIds;
+  std::mutex FilesToWatchMutex;
+  std::map<fs::path, dmon_watch_id> FolderWatchIds;
 #endif
 
   // Event loop atomics
@@ -996,7 +997,7 @@
   iter = cliOptionsDict.find("no-config");
   if (iter != cliOptionsDict.end())
   {
-    if (!this->Internals->Parse(iter->second, noConfig))
+    if (!F3DOptionsTools::Parse(iter->second, noConfig))
     {
       f3d::log::warn(
         "Could not parse '" + iter->second + "' into 'no-config' option, assuming false");
@@ -1010,7 +1011,7 @@
     if (iter != cliOptionsDict.end())
     {
       // XXX: Discarding bool return because this cannot return false with a string
-      this->Internals->Parse(iter->second, config);
+      F3DOptionsTools::Parse(iter->second, config);
     }
   }
 
@@ -1020,7 +1021,7 @@
   {
     std::string localOutput;
     // XXX: Discarding bool return because this cannot return false with a string
-    this->Internals->Parse(iter->second, localOutput);
+    F3DOptionsTools::Parse(iter->second, localOutput);
     renderToStdout = localOutput == "-";
   }
 
@@ -1029,7 +1030,7 @@
   if (iter != cliOptionsDict.end())
   {
     // XXX: Discarding bool return because this cannot return false with a string
-    this->Internals->Parse(iter->second, this->Internals->AppOptions.VerboseLevel);
+    F3DOptionsTools::Parse(iter->second, this->Internals->AppOptions.VerboseLevel);
   }
 
   // Set verbosity level early from command line
@@ -1062,6 +1063,20 @@
     this->Internals->AppOptions.MultiFileMode = "single";
   }
 
+  if (!this->Internals->AppOptions.MultiFileRegex.empty())
+  {
+    try
+    {
+      std::regex regex(this->Internals->AppOptions.MultiFileRegex);
+    }
+    catch (const std::regex_error&)
+    {
+      f3d::log::error(
+        "invalid regular expression: ", std::quoted(this->Internals->AppOptions.MultiFileRegex));
+      this->Internals->AppOptions.MultiFileRegex = "";
+    }
+  }
+
 #if __APPLE__
   // Initialize MacOS delegate
   F3DNSDelegate::InitializeDelegate(this);
@@ -1420,17 +1435,24 @@
     // XXX: Each group contains at least one path
     std::string groupIdx = "(" + std::to_string(groupIndex + 1) + "/" +
       std::to_string(this->Internals->FilesGroups.size()) + ")";
-    this->LoadFileGroup(this->Internals->FilesGroups[groupIndex].second, clear, groupIdx);
+    this->LoadFileGroupInternal(this->Internals->FilesGroups[groupIndex].second, clear, groupIdx);
+
+    if (this->Internals->AppOptions.RemoveEmptyFileGroups && this->Internals->LoadedFiles.empty())
+    {
+      this->Internals->FilesGroups.erase(
+        this->Internals->FilesGroups.begin() + this->Internals->CurrentFilesGroupIndex);
+      this->LoadRelativeFileGroup(0, false, true);
+    }
   }
   else
   {
     this->Internals->CurrentFilesGroupIndex = groupIndex;
-    this->LoadFileGroup(std::vector<fs::path>{}, true, "");
+    this->LoadFileGroupInternal(std::vector<fs::path>{}, true, "");
   }
 }
 
 //----------------------------------------------------------------------------
-void F3DStarter::LoadFileGroup(
+void F3DStarter::LoadFileGroupInternal(
   const std::vector<fs::path>& paths, bool clear, const std::string& groupIdx)
 {
   // Make sure the animation is stopped before trying to load any file
@@ -1445,7 +1467,7 @@
   f3d::options& dynamicOptions = this->Internals->Engine->getOptions();
 
   // reset forced options to avoid logging noise
-  dynamicOptions.ui.dropzone = false;
+  dynamicOptions.ui.drop_zone.enable = false;
   dynamicOptions.ui.filename_info = "";
 
   // Detect interactively changed options and store them into the dynamic options dict
@@ -1493,13 +1515,14 @@
   {
 #if F3D_MODULE_DMON
     // In the main thread, we only need to guard writing
-    const std::lock_guard<std::mutex> lock(this->Internals->LoadedFilesMutex);
+    const std::lock_guard<std::mutex> lock(this->Internals->FilesToWatchMutex);
 #endif
 
     if (clear)
     {
       scene.clear();
       this->Internals->LoadedFiles.clear();
+      this->Internals->FilesToWatch.clear();
     }
 
     if (paths.empty())
@@ -1534,34 +1557,52 @@
         if (std::find(this->Internals->LoadedFiles.begin(), this->Internals->LoadedFiles.end(),
               tmpPath) == this->Internals->LoadedFiles.end())
         {
-          if (scene.supports(tmpPath))
+          // Always add files to the watch set
+          if (this->Internals->AppOptions.Watch)
           {
-            // Check the size of the file before loading it
-            static constexpr int BYTES_IN_MIB = 1048576;
-            if (this->Internals->AppOptions.MaxSize.has_value() &&
-              fs::file_size(tmpPath) >
-                static_cast<std::uintmax_t>(
-                  this->Internals->AppOptions.MaxSize.value() * BYTES_IN_MIB))
-            {
-              f3d::log::info(tmpPath.string(), " skipped, file is bigger than max size");
-            }
-            else
-            {
-              localPaths.emplace_back(tmpPath);
-            }
+            this->Internals->FilesToWatch.insert(tmpPath);
           }
-          else
+
+          try
           {
-            auto forceReader = this->Internals->LibOptions.scene.force_reader;
-            if (forceReader)
+            if (!fs::exists(tmpPath))
+            {
+              f3d::log::error(tmpPath.string(), " does not exist");
+            }
+            else if (scene.supports(tmpPath))
             {
-              f3d::log::warn("Forced reader ", *forceReader, " doesn't exist");
+              // Check the size of the file before loading it
+              static constexpr int BYTES_IN_MIB = 1048576;
+              if (this->Internals->AppOptions.MaxSize.has_value() &&
+                fs::file_size(tmpPath) >
+                  static_cast<std::uintmax_t>(
+                    this->Internals->AppOptions.MaxSize.value() * BYTES_IN_MIB))
+              {
+                f3d::log::info(tmpPath.string(), " skipped, file is bigger than max size");
+              }
+              else
+              {
+                localPaths.emplace_back(tmpPath);
+              }
             }
             else
             {
-              f3d::log::warn(tmpPath.string(), " is not a file of a supported file format");
+              auto forceReader = this->Internals->LibOptions.scene.force_reader;
+              if (forceReader)
+              {
+                f3d::log::warn("Forced reader ", *forceReader, " doesn't exist");
+              }
+              else
+              {
+                f3d::log::warn(tmpPath.string(), " is not a file of a supported file format");
+              }
+              unsupported = true;
             }
-            unsupported = true;
+          }
+          catch (const fs::filesystem_error& ex)
+          {
+            // Unreachable
+            f3d::log::error("Error loading file: ", ex.what());
           }
         }
       }
@@ -1591,13 +1632,12 @@
     {
       f3d::log::error("  ", tmpPath.string());
     }
-    unsupported = true;
   }
 
   std::string filenameInfo;
-  if (this->Internals->LoadedFiles.size() > 0)
+  if (!this->Internals->LoadedFiles.empty())
   {
-    // Loaded files, create a filename info like this:
+    // Scene is not empty, create a filename info like this:
     // "(1/5) cow.vtp + N [+UNSUPPORTED]"
     filenameInfo = groupIdx + " " + this->Internals->LoadedFiles.at(0).filename().string();
     if (this->Internals->LoadedFiles.size() > 1)
@@ -1608,44 +1648,51 @@
     {
       filenameInfo += " [+UNSUPPORTED]";
     }
+  }
+  else if (!groupIdx.empty())
+  {
+    // Scene is empty but groupIdx were not, create a filename info like this:
+    // "(1/5) [EMPTY]"
+    filenameInfo = groupIdx + " [EMPTY]";
+  }
 
 #if F3D_MODULE_DMON
-    // Update dmon watch logic
-    if (this->Internals->AppOptions.Watch)
-    {
-      // Always unwatch and watch current folder, even on reload
-      for (const auto& dmonId : this->Internals->FolderWatchIds)
-      {
-        if (dmonId.id > 0)
-        {
-          dmon_unwatch(dmonId);
-        }
-      }
-      this->Internals->FolderWatchIds.clear();
+  // Update dmon watch logic
+  if (this->Internals->AppOptions.Watch)
+  {
+    // Recover all parents paths in a set
+    std::set<fs::path> parentPaths = F3DInternals::ParentPaths(this->Internals->FilesToWatch);
 
-      for (const auto& parentPath : F3DInternals::ParentPaths(this->Internals->LoadedFiles))
+    // Unwatch and erase paths that should not be watched anymore
+    for (auto it = this->Internals->FolderWatchIds.begin();
+         it != this->Internals->FolderWatchIds.end();)
+    {
+      const fs::path& path = it->first;
+      const dmon_watch_id& dmonId = it->second;
+      if (dmonId.id > 0 && parentPaths.count(path) == 0)
+      {
+        f3d::log::debug("Stopped watching: ", path.string());
+        dmon_unwatch(dmonId);
+        it = this->Internals->FolderWatchIds.erase(it);
+      }
+      else
       {
-        this->Internals->FolderWatchIds.emplace_back(
-          dmon_watch(parentPath.string().c_str(), &F3DInternals::dmonFolderChanged, 0, this));
+        it++;
       }
     }
-#endif
-  }
-  else
-  {
-    // No files loaded, create a simple filename info like this:
-    // (1/5) cow.vtt [UNSUPPORTED]
-    // (1/1) cow.vtt [+UNSUPPORTED]
-    if (unsupported)
+
+    // Watch any not yet watched paths
+    for (const auto& parentPath : parentPaths)
     {
-      filenameInfo = groupIdx + " " + paths.at(0).filename().string() + " [";
-      if (paths.size() > 1)
+      if (this->Internals->FolderWatchIds.count(parentPath) == 0)
       {
-        filenameInfo += "+";
+        f3d::log::debug("Started watching: ", parentPath.string());
+        this->Internals->FolderWatchIds.emplace(parentPath,
+          dmon_watch(parentPath.string().c_str(), &F3DInternals::dmonFolderChanged, 0, this));
       }
-      filenameInfo += "UNSUPPORTED]";
     }
   }
+#endif
 
   if (!this->Internals->AppOptions.NoRender)
   {
@@ -1670,7 +1717,8 @@
   // but there is no way to detect if an option has been set
   // by the user or not.
   f3d::options& options = this->Internals->Engine->getOptions();
-  options.ui.dropzone = this->Internals->LoadedFiles.empty();
+  options.ui.drop_zone.enable = options.ui.drop_zone.show_logo =
+    this->Internals->LoadedFiles.empty();
   options.ui.filename_info = filenameInfo;
 }
 
@@ -1734,23 +1782,19 @@
 {
   try
   {
-    // Check file exists
     auto tmpPath = fs::absolute(path);
-    if (!fs::exists(tmpPath))
-    {
-      if (!quiet)
-      {
-        f3d::log::error("File ", tmpPath.string(), " does not exist");
-      }
-      return -1;
-    }
-    // If file is a folder, add files recursively
-    else if (fs::is_directory(tmpPath))
+
+    // If file is a directory, add files recursively
+    if (fs::is_directory(tmpPath))
     {
       std::set<fs::path> sortedPaths;
       for (const auto& entry : fs::directory_iterator(tmpPath))
       {
-        sortedPaths.insert(entry.path());
+        if (entry.is_regular_file() ||
+          (this->Internals->AppOptions.RecursiveDirAdd && entry.is_directory()))
+        {
+          sortedPaths.insert(entry.path());
+        }
       }
       for (const auto& entryPath : sortedPaths)
       {
@@ -1765,6 +1809,30 @@
       const auto pathToGroupKey = [&]()
       {
         // XXX more multi-file mode may be added in the future
+        std::string tmpPathString = tmpPath.string();
+        if (!this->Internals->AppOptions.MultiFileRegex.empty())
+        {
+          const std::regex regex(this->Internals->AppOptions.MultiFileRegex);
+          std::smatch match;
+          if (std::regex_search(tmpPathString, match, regex))
+          {
+            // Replace captured groups with `*` so that, for example,
+            // `"foo-part12.xyz"` matching `part(\d+)` becomes `"foo-part*.xyz"`
+            std::stringstream groupKey;
+            size_t j = 0;
+            for (size_t i = 1; i <= regex.mark_count(); ++i)
+            {
+              if (match.length(i) &&                                 // skip empty
+                match.position(i) >= static_cast<std::ptrdiff_t>(j)) // or nested groups
+              {
+                groupKey << tmpPathString.substr(j, match.position(i) - j) << "*";
+                j = match.position(i) + match.length(i);
+              }
+            }
+            groupKey << tmpPathString.substr(j);
+            return groupKey.str();
+          }
+        }
         if (this->Internals->AppOptions.MultiFileMode == "all")
         {
           return std::string("");
@@ -1773,7 +1841,7 @@
         {
           return tmpPath.parent_path().string();
         }
-        return tmpPath.string();
+        return tmpPathString;
       };
 
       const std::string groupKey = pathToGroupKey();
@@ -1845,6 +1913,25 @@
 }
 
 //----------------------------------------------------------------------------
+std::vector<std::string> F3DStarter::GetExtensions()
+{
+  std::vector<std::string> filters;
+  for (const auto& info : f3d::engine::getReadersInfo())
+  {
+    for (const auto& ext : info.Extensions)
+    {
+#ifdef __APPLE__
+      filters.push_back(ext);
+#else
+      filters.push_back("*." + ext);
+#endif
+    }
+  }
+
+  return filters;
+}
+
+//----------------------------------------------------------------------------
 void F3DStarter::AddCommands()
 {
   f3d::interactor& interactor = this->Internals->Engine->getInteractor();
@@ -1866,7 +1953,174 @@
     return f3d::options::parse<bool>(args[0]);
   };
 
-  interactor.addCommand("remove_file_groups",
+  auto complFilesystem = [](const std::vector<std::string>& args)
+  {
+    std::vector<std::string> candidates;
+
+    try
+    {
+      // Without args, explore the current directory with an empty pattern
+      std::string filePattern = "";
+      fs::path parentPath = fs::current_path();
+      fs::path completionPath;
+      fs::path completionParentPath;
+
+      // With an arg, get the last one, recover its parent directory
+      // and use its filename as pattern.
+      // If the arg contains a completed path ending with a separator
+      // then the filePattern is empty
+      if (!args.empty())
+      {
+        completionPath = args.back();
+        completionParentPath = completionPath.parent_path();
+        filePattern = completionPath.filename().string();
+        if (!completionParentPath.empty())
+        {
+          // collapsePath is needed for HOME support
+          parentPath = f3d::utils::collapsePath(completionParentPath);
+        }
+      }
+
+      if (fs::exists(parentPath))
+      {
+        std::vector<fs::path> dirContent;
+
+        // Auto complete dot and dot-dot only if the pattern start with a dot
+        if (!filePattern.empty() && filePattern[0] == '.')
+        {
+          dirContent.emplace_back(".");
+          dirContent.emplace_back("..");
+        }
+
+        // Copy the directory content as paths
+        fs::directory_iterator iter(parentPath);
+        std::transform(fs::begin(iter), fs::end(iter), std::back_inserter(dirContent),
+          [&](const auto& entry) { return entry.path(); });
+
+        // Recover supported extensions
+        std::set<std::string> supportedExtensions;
+        for (const auto& info : f3d::engine::getReadersInfo())
+        {
+          std::transform(info.Extensions.begin(), info.Extensions.end(),
+            std::inserter(supportedExtensions, supportedExtensions.begin()),
+            [&](const std::string& ext) { return "." + ext; });
+        }
+
+        const auto filenameStartsWithPattern = [&filePattern](std::string_view filename)
+        {
+#if defined(_WIN32) || defined(__APPLE__)
+          // Windows and Linux filesystems are case-insensitive by default
+          // Perform a case insensitive search
+          if (filePattern.size() > filename.size())
+          {
+            return false;
+          }
+
+          return std::equal(filePattern.begin(), filePattern.end(), filename.begin(),
+            [](char a, char b)
+            {
+              return std::tolower(static_cast<unsigned char>(a)) ==
+                std::tolower(static_cast<unsigned char>(b));
+            });
+#else
+          // Linux filesystems are typically case-sensitive
+          // Perform a case sensitive search
+          // Using rfind to avoid dependency for C++20 starts_with
+          return filename.rfind(filePattern, 0) == 0;
+#endif
+        };
+
+        for (const auto& path : dirContent)
+        {
+          // Select candidates that starts with filePattern
+          if (filenameStartsWithPattern(path.filename().string()))
+          {
+            // filter out candidate files with the unsupported extensions
+            if (fs::is_regular_file(path) &&
+              supportedExtensions.find(path.extension().string()) == supportedExtensions.end())
+            {
+              continue;
+            }
+
+            // Keep the completionParentPath to avoid path normalization
+            candidates.emplace_back((completionParentPath / path.filename()).string());
+          }
+        }
+      }
+
+      // Sort for output readability and test reproducibility
+      std::sort(candidates.begin(), candidates.end());
+
+      if (candidates.size() == 1 && fs::is_directory(candidates[0]))
+      {
+        // Single directory candidate, add a separator
+        candidates[0] += fs::path::preferred_separator;
+      }
+    }
+    catch (const fs::filesystem_error& ex)
+    {
+      f3d::log::error("Error completing a filesystem path: ", ex.what());
+    }
+
+    // Multi args, reconstruct the full candidates
+    if (args.size() > 1)
+    {
+      std::vector<std::string> originalCandidates = candidates;
+      candidates.clear();
+
+      std::vector<std::string> multiArgsCandidate;
+      const std::string accum = std::accumulate(args.begin() + 1, args.end() - 1, args[0],
+        [](const std::string& a, const std::string& b) { return a + " " + b; });
+      std::transform(originalCandidates.begin(), originalCandidates.end(),
+        std::back_inserter(candidates),
+        [&](const auto& candidate) { return accum + " " + candidate; });
+    }
+
+    // On system using backward slash as separator, escape them
+    if constexpr (fs::path::preferred_separator == '\\')
+    {
+      std::vector<std::string> originalCandidates = candidates;
+      candidates.clear();
+
+      std::transform(originalCandidates.begin(), originalCandidates.end(),
+        std::back_inserter(candidates),
+        [&](std::string candidate)
+        {
+          for (size_t i = 0; i < candidate.size(); i++)
+          {
+            if (candidate[i] == '\\')
+            {
+              candidate.insert(i, 1, '\\');
+              i++;
+            }
+          }
+          return candidate;
+        });
+    }
+
+    return candidates;
+  };
+
+  interactor.addCommand(
+    "remove_current_file_group",
+    [this](const std::vector<std::string>&)
+    {
+      if (this->Internals->CurrentFilesGroupIndex >= 0)
+      {
+        if (!this->Internals->AppOptions.NoRender)
+        {
+          this->Internals->Engine->getInteractor().stopAnimation();
+        }
+        this->Internals->FilesGroups.erase(
+          this->Internals->FilesGroups.begin() + this->Internals->CurrentFilesGroupIndex);
+        this->LoadRelativeFileGroup(0, false, true);
+      }
+    },
+    f3d::interactor::command_documentation_t{ "remove_current_file_group",
+      "remove current file group and load the next file group if any" });
+
+  interactor.addCommand(
+    "remove_file_groups",
     [this](const std::vector<std::string>&)
     {
       if (!this->Internals->AppOptions.NoRender)
@@ -1876,26 +2130,36 @@
       this->Internals->FilesGroups.clear();
       this->LoadFileGroup(0, false, true);
       this->ResetWindowName();
-    });
+    },
+    f3d::interactor::command_documentation_t{ "remove_file_groups", "remove all files" });
 
-  interactor.addCommand("load_previous_file_group",
+  interactor.addCommand(
+    "load_previous_file_group",
     [this](const std::vector<std::string>& args)
     {
       this->LoadRelativeFileGroup(
         -1, parse_optional_bool_flag(args, "load_previous_file_group", false));
-    });
-
-  interactor.addCommand("load_next_file_group",
-    [this](const std::vector<std::string>& args)
-    {
+    },
+    f3d::interactor::command_documentation_t{
+      "load_previous_file_group [keep_camera]", "load the previous file or file group" });
+
+  interactor.addCommand(
+    "load_next_file_group",
+    [this](const std::vector<std::string>& args) {
       this->LoadRelativeFileGroup(
         +1, parse_optional_bool_flag(args, "load_next_file_group", false));
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "load_next_file_group [keep_camera]", "load the next file or file group" });
+
+  interactor.addCommand(
+    "reload_current_file_group",
+    [this](const std::vector<std::string>&) { this->LoadRelativeFileGroup(0, true, true); },
+    f3d::interactor::command_documentation_t{
+      "reload_current_file_group", "reload the current file or file group" });
 
-  interactor.addCommand("reload_current_file_group",
-    [this](const std::vector<std::string>&) { this->LoadRelativeFileGroup(0, true, true); });
-
-  interactor.addCommand("add_current_directories",
+  interactor.addCommand(
+    "add_current_directories",
     [this](const std::vector<std::string>&)
     {
       if (!this->Internals->LoadedFiles.empty())
@@ -1906,29 +2170,40 @@
         }
         this->LoadRelativeFileGroup(0);
       }
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "add_current_directories", "add all files from the current file or file group directories" });
 
-  interactor.addCommand("take_screenshot",
+  interactor.addCommand(
+    "take_screenshot",
     [this](const std::vector<std::string>& args)
     {
       // XXX: Add a test for this one this can be reached with a non empty filename
       std::string filename =
         args.empty() ? this->Internals->AppOptions.ScreenshotFilename : args[0];
       this->SaveScreenshot(filename);
-    });
+    },
+    f3d::interactor::command_documentation_t{ "take_screenshot [filename]",
+      "take a screenshot into provided file or --screenshot-filename" },
+    complFilesystem);
 
-  interactor.addCommand("take_minimal_screenshot",
+  interactor.addCommand(
+    "take_minimal_screenshot",
     [this](const std::vector<std::string>& args)
     {
       // XXX: Add a test for this one this can be reached with a non empty filename
       std::string filename =
         args.empty() ? this->Internals->AppOptions.ScreenshotFilename : args[0];
       this->SaveScreenshot(filename, true);
-    });
+    },
+    f3d::interactor::command_documentation_t{ "take_minimal_screenshot [filename]",
+      "take a minimal screenshot into provided file or --screenshot-filename" },
+    complFilesystem);
 
   // This replace an existing command in libf3d
   interactor.removeCommand("add_files");
-  interactor.addCommand("add_files",
+  interactor.addCommand(
+    "add_files",
     [this](const std::vector<std::string>& files)
     {
       int index = -1;
@@ -1940,9 +2215,13 @@
       {
         this->LoadFileGroup(index);
       }
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "add_files path/to/file [path/to/another_file]", "add files to the scene" },
+    complFilesystem);
 
-  interactor.addCommand("set_hdri",
+  interactor.addCommand(
+    "set_hdri",
     [this](const std::vector<std::string>& files)
     {
       if (!files.empty())
@@ -1956,9 +2235,13 @@
         // Rendering now is needed for correct lighting
         this->Render();
       }
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "set_hdri [path/to/hdri]", "set and use an HDRI image" },
+    complFilesystem);
 
-  interactor.addCommand("add_files_or_set_hdri",
+  interactor.addCommand(
+    "add_files_or_set_hdri",
     [this](const std::vector<std::string>& files)
     {
       int index = -1;
@@ -1986,21 +2269,18 @@
       {
         this->LoadFileGroup(index);
       }
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "add_files_or_set_hdri [path/to/file1] [path/to/file2]",
+      "add_files or set_hdri depending on the file extension" },
+    complFilesystem);
 
 #if F3D_MODULE_TINYFILEDIALOGS
-  interactor.addCommand("open_file_dialog",
+  interactor.addCommand(
+    "open_file_dialog",
     [this](const std::vector<std::string>&)
     {
-      std::vector<std::string> filters;
-      for (const auto& info : f3d::engine::getReadersInfo())
-      {
-        for (const auto& ext : info.Extensions)
-        {
-          filters.push_back("*." + ext);
-        }
-      }
-
+      std::vector<std::string> filters = F3DStarter::GetExtensions();
       std::vector<const char*> cstrings;
       cstrings.reserve(filters.size());
       for (const auto& filter : filters)
@@ -2011,12 +2291,16 @@
       std::optional<std::string> file = f3d::utils::getEnv("CTEST_OPEN_DIALOG_FILE");
       if (!file.has_value())
       {
+#ifdef __APPLE__
+        F3DNSDelegate::ShowOpenFileDialog(cstrings.data(), cstrings.size());
+#else
         char* ptr = tinyfd_openFileDialog("Open File", nullptr, static_cast<int>(cstrings.size()),
           cstrings.data(), "Supported Files", false);
         if (ptr)
         {
           file = ptr;
         }
+#endif
       }
 
       if (file.has_value())
@@ -2027,7 +2311,11 @@
           this->LoadFileGroup(index);
         }
       }
-    });
+    },
+    f3d::interactor::command_documentation_t{
+      "open_file_dialog", "open a file dialog to select a file to load" });
 #endif
-  interactor.addCommand("exit", [&](const std::vector<std::string>&) { interactor.stop(); });
+  interactor.addCommand(
+    "exit", [&](const std::vector<std::string>&) { interactor.stop(); },
+    f3d::interactor::command_documentation_t{ "exit", "quit the application" });
 }
diff -Nru f3d-3.2.0+dfsg/application/F3DStarter.h f3d-3.4.1/application/F3DStarter.h
--- f3d-3.2.0+dfsg/application/F3DStarter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/F3DStarter.h	2026-01-08 08:41:55.000000000 +0200
@@ -33,9 +33,12 @@
   void LoadFileGroup(int index = 0, bool relativeIndex = false, bool forceClear = false);
 
   /**
-   * Trigger a render on the next event loop
+   * Get supported extensions for open file command
+   * On macOS, the extensions are returned in the format: {"x", "y", "z"}
+   * On other operating systems, the format is: {"*.x", "*.y", "*.z"}
+   * Returns a vector of supported file extension strings
    */
-  void RequestRender();
+  static std::vector<std::string> GetExtensions();
 
   /**
    * Trigger a render immediately (must be called by the main thread)
@@ -72,7 +75,7 @@
    * Set clear to true to clear the scene first
    * GroupIdx is only used for display purposes of the filename
    */
-  void LoadFileGroup(
+  void LoadFileGroupInternal(
     const std::vector<std::filesystem::path>& paths, bool clear, const std::string& groupIdx);
 
   /**
diff -Nru f3d-3.2.0+dfsg/application/main.cxx f3d-3.4.1/application/main.cxx
--- f3d-3.2.0+dfsg/application/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -43,6 +43,12 @@
 
 int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
 {
+  // enable formatting on windows terminal
+  HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
+  DWORD dwMode;
+  GetConsoleMode(hOut, &dwMode);
+  SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+
   // Set console code page to UTF-8 so console known how to interpret string data
   SetConsoleOutputCP(CP_UTF8);
 
diff -Nru f3d-3.2.0+dfsg/application/testing/CMakeLists.txt f3d-3.4.1/application/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/application/testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,7 +1,7 @@
 # F3D Testing
 function(f3d_test)
 
-	cmake_parse_arguments(F3D_TEST "TONE_MAPPING;LONG_TIMEOUT;INTERACTION;INTERACTION_CONFIGURE;NO_BASELINE;NO_RENDER;NO_OUTPUT;WILL_FAIL;NO_DATA_FORCE_RENDER;UI" "NAME;CONFIG;RESOLUTION;THRESHOLD;REGEXP;REGEXP_FAIL;HDRI;SCRIPT;RENDERING_BACKEND" "DATA;DEPENDS;ENV;ARGS" ${ARGN})
+  cmake_parse_arguments(F3D_TEST "TONE_MAPPING;LONG_TIMEOUT;INTERACTION;INTERACTION_CONFIGURE;NO_BASELINE;NO_RENDER;NO_OUTPUT;WILL_FAIL;NO_DATA_FORCE_RENDER;UI" "NAME;CONFIG;RESOLUTION;THRESHOLD;REGEXP;REGEXP_FAIL;HDRI;SCRIPT;RENDERING_BACKEND;WORKING_DIR" "DATA;DEPENDS;ENV;ARGS" ${ARGN})
 
   if(F3D_TEST_CONFIG)
     list(APPEND F3D_TEST_ARGS "--config=${F3D_TEST_CONFIG}")
@@ -127,6 +127,10 @@
     set_tests_properties(f3d::${F3D_TEST_NAME} PROPERTIES FIXTURES_REQUIRED "${_depends_fixtures}")
   endif()
 
+  if(F3D_TEST_WORKING_DIR)
+    set_tests_properties(f3d::${F3D_TEST_NAME} PROPERTIES WORKING_DIRECTORY "${F3D_TEST_WORKING_DIR}")
+  endif()
+
   set(f3d_test_env_vars ${F3D_TEST_ENV})
   list(APPEND f3d_test_env_vars "CTEST_F3D_PROGRESS_BAR=1")
   if (F3D_TEST_UI)
@@ -160,14 +164,16 @@
 f3d_test(NAME TestVTM DATA mb.vtm)
 f3d_test(NAME TestVTK DATA cow.vtk)
 f3d_test(NAME TestNRRD DATA beach.nrrd ARGS -s)
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221216)
-  f3d_test(NAME TestSPLAT DATA small.splat ARGS -osy --up=-Y --point-sprites-size=1)
+f3d_test(NAME TestVRMLImporter DATA bot2.wrl)
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231102)
+  f3d_test(NAME TestSPLAT DATA small.splat ARGS -osy --up=-Y --point-sprites-absolute-size --point-sprites-size=1)
 endif()
 f3d_test(NAME TestQuakeMDL DATA zombie.mdl)
 f3d_test(NAME TestQuakeMDLAnimationSimpleFrame DATA zombie.mdl ARGS --animation-time=0.5)
 f3d_test(NAME TestQuakeMDLAnimationBetween DATA zombie.mdl ARGS --animation-time=1.42)
 f3d_test(NAME TestQuakeMDLAnimationMulti DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-time=0.5)
 f3d_test(NAME TestQuakeMDLAnimationGroupFrame DATA flame_mixed.mdl ARGS --animation-indices=1 --animation-time=0.5)
+f3d_test(NAME TestQuakeMDLAnimationLastFrame DATA flame_mixed.mdl ARGS --camera-position=90,0,0 --animation-time=0.65)
 f3d_test(NAME TestQuakeMDLDisableAnimation DATA zombie.mdl soldier_animations.mdl ARGS --multi-file-mode=all --animation-time=0.5)
 f3d_test(NAME TestQuakeMDLSkinIndex DATA armor.mdl ARGS -DQuakeMDL.skin_index=2)
 f3d_test(NAME TestQuakeMDLSkinIndexNegative DATA armor.mdl ARGS -DQuakeMDL.skin_index=-1)
@@ -177,6 +183,7 @@
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240707)
   f3d_test(NAME TestQuakeMDLActorCollection DATA zombie.mdl ARGS --scalar-coloring)
 endif()
+f3d_test(NAME TestQuakeMDLGroupSkin DATA groupskin.mdl ARGS --animation-indices=1 --animation-time=0.3)
 f3d_test(NAME TestGridX DATA suzanne.ply ARGS -g --up=+X)
 f3d_test(NAME TestGridY DATA suzanne.ply ARGS -g --up=+Y)
 f3d_test(NAME TestGridZ DATA suzanne.ply ARGS -g --up=+Z)
@@ -187,7 +194,15 @@
 f3d_test(NAME TestGridAbsolute DATA f3d.vtp ARGS -g --up=-Y --camera-direction=-.5,+1,+1 --grid-absolute)
 f3d_test(NAME TestGridClipping DATA offset-flat-box.glb ARGS -g --grid-absolute --camera-position=70,120,350)
 f3d_test(NAME TestGridColor DATA suzanne.ply ARGS -g --grid-color=1,1,1)
-f3d_test(NAME TestAxis DATA suzanne.ply ARGS -x)
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestAxis DATA suzanne.ply ARGS -x UI)
+  f3d_test(NAME TestAxesCustomColor DATA suzanne.ply ARGS -x -g --x-color=1,0,0.643 --y-color=0,1,0.894 --z-color=0.89,1,0 UI)
+endif()
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11209
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250513)
+  f3d_test(NAME TestAxesGridEnable DATA suzanne.ply ARGS --axes-grid THRESHOLD 0.08) # Threshold required for MacOS due to line rendering differences
+endif()
 f3d_test(NAME TestBackfaceVisible DATA backface.vtp ARGS --backface-type=visible)
 f3d_test(NAME TestBackfaceHidden DATA backface.vtp ARGS --backface-type=hidden)
 f3d_test(NAME TestPointCloud DATA pointsCloud.vtp ARGS -o --point-sprites-size=20)
@@ -200,6 +215,7 @@
 f3d_test(NAME TestScalarsCell DATA f3d.vtp ARGS --scalar-coloring --coloring-by-cells --coloring-component=-2 --up=+Z)
 f3d_test(NAME TestScalarsRange DATA suzanne.ply ARGS -s --coloring-array=Normals --coloring-component=1 --coloring-range=0,1)
 f3d_test(NAME TestScalarsWithBar DATA suzanne.ply ARGS -b -s --coloring-array=Normals --coloring-component=0)
+f3d_test(NAME TestScalarsWithBarContinuity DATA suzanne.ply ARGS -b -s --coloring-array=Normals --coloring-component=0)
 f3d_test(NAME TestGLTFImporter DATA f3d.glb)
 f3d_test(NAME TestGLTFImporterWithAnimation DATA BoxAnimated.gltf ARGS --animation-time=2 --animation-progress)
 f3d_test(NAME TestGLTFSkin DATA SimpleSkin.gltf)
@@ -211,16 +227,23 @@
 f3d_test(NAME TestBackground DATA suzanne.ply ARGS --background-color=0.8,0.2,0.9 THRESHOLD 0.1) # Threshold is needed for legacy image comparison for VTK 9.3
 f3d_test(NAME TestGridWithDepthPeeling DATA suzanne.ply ARGS -gp --opacity=0.2)
 f3d_test(NAME TestFilename DATA suzanne.ply ARGS -n UI)
+f3d_test(NAME TestHDRIFilename DATA dragon.vtu ARGS --hdri-filename -f -j --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr UI LONG_TIMEOUT)
+f3d_test(NAME TestFilenameHDRIFilename DATA dragon.vtu ARGS --hdri-filename -n -f -j --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr RESOLUTION 400,400 UI LONG_TIMEOUT)
+f3d_test(NAME TestHDRIFilenameDefault DATA dragon.vtu ARGS --hdri-filename -f -j UI LONG_TIMEOUT)
 f3d_test(NAME TestFilenameWhiteBg DATA suzanne.ply ARGS -n --background-color=1,1,1 UI)
 f3d_test(NAME TestConsoleBadgeWarning DATA suzanne.ply ARGS --position=0 UI)
 f3d_test(NAME TestConsoleBadgeQuiet DATA suzanne.ply ARGS --position=0 --verbose=quiet UI)
 f3d_test(NAME TestCityGML DATA Part-4-Buildings-V4-one.gml)
 f3d_test(NAME TestPTS DATA samplePTS.pts)
 f3d_test(NAME TestColormap DATA IM-0001-1983.dcm ARGS --scalar-coloring --roughness=1 --colormap=0,1,0,0,1,0,1,0)
-f3d_test(NAME TestCameraConfiguration DATA suzanne.obj ARGS --camera-position=0,0,-10 -x --camera-view-up=+X --camera-focal-point=1,0,0 --camera-view-angle=20 --camera-azimuth-angle=40 --camera-elevation-angle=-80 --camera-direction=12,34,56 --camera-zoom-factor=78)
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestCameraConfiguration DATA suzanne.obj ARGS --camera-position=0,0,-10 -x --camera-view-up=+X --camera-focal-point=1,0,0 --camera-view-angle=20 --camera-azimuth-angle=40 --camera-elevation-angle=-80 --camera-direction=12,34,56 --camera-zoom-factor=78 UI)
+endif()
 f3d_test(NAME TestCameraDirection DATA suzanne.obj ARGS --camera-direction=-xy+z)
 f3d_test(NAME TestCameraClipping DATA checkerboard_colorful.obj CONFIG ${F3D_SOURCE_DIR}/testing/configs/checkerboard_colorful.json RESOLUTION 800,600)
 f3d_test(NAME TestCameraOrthographic ARGS --camera-orthographic DATA cow.vtp)
+f3d_test(NAME TestCameraZoomFactor DATA suzanne.obj ARGS --camera-direction=-1,-1,1 --camera-zoom-factor=1.5)
 f3d_test(NAME TestToneMapping DATA suzanne.ply ARGS -t TONE_MAPPING)
 f3d_test(NAME TestDepthPeelingToneMapping DATA suzanne.ply ARGS --opacity=0.9 -pt TONE_MAPPING)
 f3d_test(NAME TestVolume DATA HeadMRVolume.mhd ARGS -v --camera-position=127.5,-400,127.5 --camera-view-up=0,0,1)
@@ -237,8 +260,17 @@
 f3d_test(NAME TestTextures DATA WaterBottle.glb ARGS --texture-material=${F3D_SOURCE_DIR}/testing/data/red.jpg --roughness=1 --metallic=1 --texture-base-color=${F3D_SOURCE_DIR}/testing/data/albedo.png --texture-normal=${F3D_SOURCE_DIR}/testing/data/normal.png --texture-emissive=${F3D_SOURCE_DIR}/testing/data/red.jpg --emissive-factor=0.1,0.1,0.1)
 f3d_test(NAME TestMetaDataImporter DATA BoxAnimated.gltf ARGS -m UI)
 f3d_test(NAME TestMultiblockMetaData DATA mb.vtm ARGS -m UI)
+f3d_test(NAME TestRemoveEmptyFileGroups DATA mb/mb_3_0.vtt mb/mb_0_0.vtu ARGS -n --remove-empty-file-groups UI)
 f3d_test(NAME TestMultiFileMetaData DATA mb/recursive ARGS -m --multi-file-mode=all UI)
-f3d_test(NAME TestMultiFileModeDir DATA mb ARGS -n --multi-file-mode=dir UI)
+f3d_test(NAME TestMultiFileModeDir DATA mb ARGS -n --multi-file-mode=dir --recursive-dir-add UI)
+set(MULTIFILES
+  multi-file/cube-part1of2.glb multi-file/cube-part2of2.glb
+  multi-file/cube-part1of3.glb multi-file/cube-part2of3.glb multi-file/cube-part3of3.glb
+  multi-file/tet-part1of2.glb multi-file/tet-part2of2.glb
+  multi-file/tet-part1of3.glb multi-file/tet-part2of3.glb multi-file/tet-part3of3.glb)
+f3d_test(NAME TestMultiFileRegexA DATA ${MULTIFILES} ARGS -n "--multi-file-regex=(\\d+)of\\d" --camera-direction=-xyz UI)
+f3d_test(NAME TestMultiFileRegexB DATA ${MULTIFILES} ARGS -n "--multi-file-regex=part(.+)" --camera-direction=-xyz UI)
+f3d_test(NAME TestMultiFileRegexInvalid ARGS "--multi-file-regex=(.+" REGEXP "invalid regular expression: \"\\(.+")
 f3d_test(NAME TestTIFF DATA f3d.tif ARGS -sy --up=-Y)
 f3d_test(NAME TestLightIntensityBrighter DATA cow.vtp ARGS --light-intensity=5.0)
 f3d_test(NAME TestLightIntensityDarker DATA cow.vtp ARGS --light-intensity=0.2)
@@ -254,7 +286,10 @@
 f3d_test(NAME TestDefinesInexistent DATA dragon.vtu ARGS -Dscene.up_director=+Z REGEXP "option from CLI options does not exists" NO_BASELINE)
 f3d_test(NAME TestConfigReset DATA suzanne.stl ARGS -Rrender.grid.enable --reset=ui.axis CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json)
 f3d_test(NAME TestConfigResetInexistent DATA suzanne.stl ARGS -Rrender.glid.enable REGEXP "option from CLI options does not exists" NO_BASELINE)
-f3d_test(NAME TestConfigImperative DATA dragon.vtu ARGS --axis=false CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json)
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestConfigImperative DATA dragon.vtu ARGS --axis=false CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json UI)
+endif()
 f3d_test(NAME TestAnimationIndicesSingle DATA soldier_animations.mdl ARGS --animation-indices=7 --animation-time=0.5 --animation-progress)
 f3d_test(NAME TestAnimationIndicesMulti DATA InterpolationTest.glb ARGS --animation-indices=7,6 --animation-time=0.5 --animation-progress)
 f3d_test(NAME TestAnimationIndexDeprecated DATA InterpolationTest.glb ARGS --animation-index=7 --animation-time=0.5 --animation-progress)
@@ -265,7 +300,7 @@
 f3d_test(NAME TestAnimationAutoplay DATA InterpolationTest.glb ARGS --animation-autoplay)
 f3d_test(NAME TestMaxSizeBelow DATA suzanne.stl ARGS --max-size=1)
 f3d_test(NAME TestMaxSizeAbove DATA WaterBottle.glb ARGS --max-size=0.2 REGEXP "file is bigger than max size" NO_BASELINE)
-f3d_test(NAME TestMaxSizeAboveMultiFile DATA suzanne.obj WaterBottle.glb ARGS --multi-file-mode=all --max-size=0.6 --translucency-support --opacity=0.5)
+f3d_test(NAME TestMaxSizeAboveMultiFile DATA suzanne.obj WaterBottle.glb ARGS --multi-file-mode=all --max-size=0.6 --blending --opacity=0.5)
 f3d_test(NAME TestAlternativeOptionSyntax DATA WaterBottle.glb ARGS --max-size 0.2 REGEXP "file is bigger than max size" NO_BASELINE)
 f3d_test(NAME TestNonExistentFile DATA nonExistentFile.vtp ARGS --filename WILL_FAIL)
 f3d_test(NAME TestUnsupportedFile DATA unsupportedFile.dummy ARGS --filename WILL_FAIL)
@@ -275,6 +310,7 @@
 f3d_test(NAME TestNoFile NO_DATA_FORCE_RENDER UI)
 f3d_test(NAME TestNoFileEmptyFileName ARGS --filename NO_DATA_FORCE_RENDER UI)
 f3d_test(NAME TestMultiFile DATA mb/recursive ARGS --multi-file-mode=all)
+f3d_test(NAME TestMultiFileRecursive DATA mb ARGS --multi-file-mode=all --recursive-dir-add)
 f3d_test(NAME TestMultiFileColoring DATA mb/recursive ARGS --multi-file-mode=all -s --coloring-array=Polynomial -b)
 f3d_test(NAME TestMultiFileVolume DATA multi ARGS --multi-file-mode=all -vsb --coloring-array=Scalars_)
 f3d_test(NAME TestMultiFileColoringTexture DATA mb/recursive/mb_1_0.vtp mb/recursive/mb_2_0.vtp world.obj ARGS --multi-file-mode=all -sb --coloring-array=Normals --coloring-component=1)
@@ -288,9 +324,16 @@
 f3d_test(NAME TestTexturesTransform DATA world.obj ARGS --textures-transform=1,0,0,0,-1,0,0,0,1)
 f3d_test(NAME TestTexturesTransformGL DATA WaterBottle.glb ARGS --textures-transform=1,0,0,0,-1,0,0,0,1 --camera-direction=-1,0,0)
 f3d_test(NAME TestConfigOrder DATA suzanne.ply ARGS CONFIG ${F3D_SOURCE_DIR}/testing/configs/config_order.json) # `.+` > `.*` alphabetically but overridden by the order
-f3d_test(NAME TestOutputStream DATA suzanne.ply ARGS --verbose=quiet --output=- REGEXP "^.PNG" NO_BASELINE NO_OUTPUT)
+f3d_test(NAME TestOutputStream DATA suzanne.ply ARGS --verbose=quiet --output=- REGEXP ".PNG" NO_BASELINE NO_OUTPUT)
 f3d_test(NAME TestOutputStreamInfo DATA suzanne.ply ARGS --verbose=info --output=- REGEXP "redirected to stderr" NO_BASELINE NO_OUTPUT)
-f3d_test(NAME TestInvalidAntiAliasingMode DATA suzanne.ply ARGS -a --anti-aliasing-mode=foo REGEXP "foo is an invalid antialiasing mode" NO_BASELINE)
+f3d_test(NAME TestInvalidAntiAliasingMode DATA suzanne.ply ARGS --anti-aliasing=foo REGEXP "foo is an invalid antialiasing mode" NO_BASELINE)
+f3d_test(NAME TestAntiAliasingModeDeprecated DATA suzanne.ply ARGS -a --anti-aliasing-mode=ssaa REGEXP "--anti-aliasing-mode is deprecated" NO_BASELINE)
+f3d_test(NAME TestAntiAliasingDeprecated DATA suzanne.ply ARGS --anti-aliasing=true REGEXP "please specify the type of anti-aliasing" NO_BASELINE)
+f3d_test(NAME TestTranslucencySupportDeprecated DATA suzanne.ply ARGS --translucency-support REGEXP "--translucency-support is deprecated" NO_BASELINE)
+f3d_test(NAME TestInvalidPointSprites DATA suzanne.ply ARGS --point-sprites=foo REGEXP "foo is an invalid point sprites type" NO_BASELINE)
+f3d_test(NAME TestPointSpritesTypeDeprecated DATA pointsCloud.vtp ARGS --point-sprites-type=sphere REGEXP "--point-sprites-type is deprecated" NO_BASELINE)
+f3d_test(NAME TestPointSpritesDeprecated DATA pointsCloud.vtp ARGS --point-sprites=true REGEXP "please specify the type of point sprites" NO_BASELINE)
+f3d_test(NAME TestInvalidBlendingMode DATA suzanne.ply ARGS --blending=foo REGEXP "foo is an invalid blending mode" NO_BASELINE)
 f3d_test(NAME TestUserMatrixColoring DATA InterpolationTest.glb ARGS --scalar-coloring --coloring-array=TEXCOORD_0)
 f3d_test(NAME TestSkinColoring DATA SimpleSkin.gltf ARGS --scalar-coloring --coloring-array=WEIGHTS_0)
 f3d_test(NAME TestMorphColoring DATA SimpleMorph.gltf ARGS --scalar-coloring)
@@ -301,6 +344,10 @@
 f3d_test(NAME TestAnimationUserMatrixPointSprites DATA BoxAnimated.gltf ARGS --point-sprites --point-sprites-size=50 --animation-time=2)
 f3d_test(NAME TestCollinearVectors DATA dragon.vtu ARGS --up=0,1,0 --camera-direction=0,2,0)
 f3d_test(NAME TestAnimationInputChangePointSprites DATA v_rock2.mdl ARGS --point-sprites --point-sprites-size=50 --animation-time=0.01 --animation-indices=1)
+f3d_test(NAME TestBackdropOpacityOpaque DATA suzanne.ply ARGS -n --backdrop-opacity=1.0 UI)
+f3d_test(NAME TestBackdropOpacityTransparent DATA suzanne.ply ARGS -n --backdrop-opacity=0.0 UI)
+f3d_test(NAME TestBackdropOpacityMedium DATA suzanne.ply ARGS -n --backdrop-opacity=0.5 UI)
+f3d_test(NAME TestCustomOptionsNone DATA red_translucent_monkey.gltf ARGS --blending=none --anti-aliasing=none --point-sprites=none)
 
 # Color map files testing
 f3d_test(NAME TestColorMapFileFullPath DATA dragon.vtu ARGS --colormap-file=${F3D_SOURCE_DIR}/testing/data/viridis8.png --scalar-coloring --coloring-component=1)
@@ -371,7 +418,7 @@
 set_tests_properties(f3d::TestScreenshotUserModelN PROPERTIES ENVIRONMENT "XDG_PICTURES_DIR=${_screenshot_user_dir};HOME=${_screenshot_user_dir};USERPROFILE=${_screenshot_user_dir}")
 
 if(NOT APPLE OR VTK_VERSION VERSION_GREATER_EQUAL 9.3.0)
-  f3d_test(NAME TestTextureColor DATA WaterBottle.glb ARGS --texture-base-color=${F3D_SOURCE_DIR}/testing/data/albedo_mod.png --translucency-support)
+  f3d_test(NAME TestTextureColor DATA WaterBottle.glb ARGS --texture-base-color=${F3D_SOURCE_DIR}/testing/data/albedo_mod.png --blending)
 endif()
 
 # Needs SSBO: https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10675
@@ -389,38 +436,60 @@
   f3d_test(NAME TestSkinningManyBonesWarning DATA tube_254bones.glb ARGS --verbose REGEXP "with more than 250 bones \\\(254\\\)" NO_BASELINE)
 endif()
 
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestDefaultConfigFileHDRIFilename DATA dragon.vtu CONFIG config_build ARGS -j --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr UI LONG_TIMEOUT)
+endif()
+
 # Needs splat sorting with compute shaders
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240203)
   if(NOT APPLE) # MacOS does not support compute shaders
-    f3d_test(NAME Test3DGaussiansSplatting DATA small.splat ARGS -osy --up=-Y --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-3.6,0.5,-4.2)
-    f3d_test(NAME TestDefaultConfigFileSPLAT DATA small.splat CONFIG config_build LONG_TIMEOUT UI)
+    f3d_test(NAME Test3DGaussiansSplatting DATA small.splat ARGS -sy --up=-Y --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-3.6,0.5,-4.2)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileSPLAT DATA small.splat CONFIG config_build LONG_TIMEOUT UI)
+    endif()
     f3d_test(NAME TestThumbnailConfigFileSPLAT DATA small.splat CONFIG thumbnail_build LONG_TIMEOUT)
 
-    f3d_test(NAME Test3DGSPLY DATA bonsai_small.ply ARGS -osy --up=-Y --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-2.6,0.5,-3.2)
-    f3d_test(NAME Test3DGSPLYHDRI DATA bonsai_small.ply HDRI shanghai_bund_1k.hdr ARGS -osy --up=-Y --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-2.6,0.5,-3.2)
+    f3d_test(NAME Test3DGSPLY DATA bonsai_small.ply ARGS -sy --up=-Y --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-2.6,0.5,-3.2)
+    f3d_test(NAME Test3DGSPLYHDRI DATA bonsai_small.ply HDRI shanghai_bund_1k.hdr ARGS -sy --up=-Y --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-2.6,0.5,-3.2)
+    f3d_test(NAME TestInteractionPointSpritesCycle DATA bonsai_small.ply ARGS -sy --point-sprites-absolute-size --up=-Y --blending=sort --camera-position=-2.6,0.5,-3.2 INTERACTION) #OOOOO
 
-    f3d_test(NAME TestSPZDegree0 DATA hornedlizard_small_d0.spz ARGS -osy --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-3.6,0.5,4.2)
+    f3d_test(NAME TestSPZDegree0 DATA hornedlizard_small_d0.spz ARGS -sy --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-3.6,0.5,4.2)
     f3d_test(NAME TestInvalidSH DATA invalidSH.vtp ARGS -osy --verbose REGEXP "Spherical harmonics array is not valid" NO_BASELINE)
 
     # Needs texture array support: https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12112
     if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250513)
-      f3d_test(NAME TestSPZDegree1 DATA hornedlizard_small_d1.spz ARGS -osy --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-3.6,0.5,4.2)
-      f3d_test(NAME TestSPZDegree2 DATA hornedlizard_small_d2.spz ARGS -osy --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-3.6,0.5,4.2)
-      f3d_test(NAME TestSPZDegree3 DATA hornedlizard_small_d3.spz ARGS -osy --point-sprites-size=1 --point-sprites-type=gaussian --camera-position=-3.6,0.5,4.2)
-
-      f3d_test(NAME TestDefaultConfigFileSPZ DATA hornedlizard_small_d3.spz CONFIG config_build LONG_TIMEOUT UI)
+      f3d_test(NAME TestSPZDegree1 DATA hornedlizard_small_d1.spz ARGS -sy --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-3.6,0.5,4.2)
+      f3d_test(NAME TestSPZDegree2 DATA hornedlizard_small_d2.spz ARGS -sy --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-3.6,0.5,4.2)
+      f3d_test(NAME TestSPZDegree3 DATA hornedlizard_small_d3.spz ARGS -sy --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-3.6,0.5,4.2)
+      f3d_test(NAME TestSPZDegree1Stochastic DATA hornedlizard_small_d1.spz ARGS -sy --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=stochastic --camera-position=-3.6,0.5,4.2) # Test instancing with spherical harmonics
+
+      # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+      if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+        f3d_test(NAME TestDefaultConfigFileSPZ DATA hornedlizard_small_d3.spz CONFIG config_build LONG_TIMEOUT UI)
+      endif()
       f3d_test(NAME TestThumbnailConfigFileSPZ DATA hornedlizard_small_d3.spz CONFIG thumbnail_build LONG_TIMEOUT)
+      f3d_test(NAME TestSPZVersion3 DATA bonsai.spz ARGS -sy --up=-Y --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=sort --camera-position=-2.6,0.5,-3.2)
+      f3d_test(NAME TestSPZCorruptedGZIP DATA wrong_spz_gzip_corrupted.spz ARGS -osy --verbose REGEXP "Invalid GZIP file" NO_BASELINE)
+      f3d_test(NAME TestSPZTruncatedGZIP DATA truncated_gzip.spz ARGS -osy --verbose REGEXP "Invalid GZIP file" NO_BASELINE)
+      f3d_test(NAME TestWrongSPZMagic DATA wrong_spz_magic.spz ARGS -osy --verbose REGEXP "Incompatible SPZ header" NO_BASELINE)
+      f3d_test(NAME TestWrongSPZVersion DATA wrong_spz_version.spz ARGS -osy --verbose REGEXP "Incompatible SPZ version. Only 2 and 3 are supported" NO_BASELINE)
     endif()
   endif()
 endif()
 
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10662
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231102)
+  f3d_test(NAME Test3DGaussiansSplattingStochastic DATA small.splat ARGS -sy --up=-Y --point-sprites-absolute-size --point-sprites-size=1 --point-sprites=gaussian --blending=stochastic --camera-position=-3.6,0.5,-4.2)
+endif()
+
 # Test plugin fail code path
 f3d_test(NAME TestPluginVerbose ARGS --verbose REGEXP "Loading plugin \"native\"" NO_BASELINE)
 f3d_test(NAME TestPluginNonExistent ARGS --load-plugins=dummy REGEXP "Plugin failed to load" NO_BASELINE)
 if(WIN32)
   set(_TEST_PLUGIN_INVALID_REGEXP "is not a valid Win32 application")
 elseif(APPLE)
-  set(_TEST_PLUGIN_INVALID_REGEXP "not a mach-o file")
+  set(_TEST_PLUGIN_INVALID_REGEXP "not.*mach-o file")
 else()
   set(_TEST_PLUGIN_INVALID_REGEXP "file too short")
 endif()
@@ -443,21 +512,25 @@
 if(NOT F3D_MACOS_BUNDLE)
   # On linux, we can easily test the config file search from the binary code by positioning a config file in the binary dir
   configure_file("${F3D_SOURCE_DIR}/testing/configs/complex.json" "${CMAKE_BINARY_DIR}/share/f3d/configs/complex_build.json" COPYONLY)
-  f3d_test(NAME TestConfigFileBuild DATA dragon.vtu CONFIG complex_build.json UI)
-  f3d_test(NAME TestConfigStemBuild DATA dragon.vtu CONFIG complex_build UI)
-  f3d_test(NAME TestConfigFileUpperCase DATA suzanne_upper.STL CONFIG complex_build)
-  f3d_test(NAME TestConfigFileMultiFileSTL DATA mb/recursive/mb_1_0.vtp suzanne.stl ARGS --multi-file-mode=all CONFIG complex_build)
-  f3d_test(NAME TestConfigFileMultiFileVTP DATA mb/recursive/mb_1_0.vtp suzanne.stl mb/recursive/mb_2_0.vtp ARGS --multi-file-mode=all CONFIG complex_build UI)
-
   file(COPY "${F3D_SOURCE_DIR}/resources/configs/config.d/" "${F3D_SOURCE_DIR}/plugins/native/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-  f3d_test(NAME TestDefaultConfigFileVTU DATA dragon.vtu CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigFileVTI DATA vase_4comp.vti CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigFileSTL DATA suzanne.stl CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigFileTIFF DATA f3d.tif CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI) # Note: This tests config file order as camera_direction is set in different files
-  f3d_test(NAME TestDefaultConfigFilePLY DATA suzanneRGBA.ply CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigFileQuakeMDL DATA zombie.mdl CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigFileAndCommand DATA suzanne.stl ARGS --up=-Y --camera-direction=-1,0.5,-1 CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-  f3d_test(NAME TestDefaultConfigTranslucent DATA red_translucent_monkey.gltf CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+    f3d_test(NAME TestConfigFileBuild DATA dragon.vtu CONFIG complex_build.json UI)
+    f3d_test(NAME TestConfigStemBuild DATA dragon.vtu CONFIG complex_build UI)
+    f3d_test(NAME TestConfigFileUpperCase DATA suzanne_upper.STL CONFIG complex_build UI)
+    f3d_test(NAME TestConfigFileMultiFileSTL DATA mb/recursive/mb_1_0.vtp suzanne.stl ARGS --multi-file-mode=all CONFIG complex_build UI)
+    f3d_test(NAME TestConfigFileMultiFileVTP DATA mb/recursive/mb_1_0.vtp suzanne.stl mb/recursive/mb_2_0.vtp ARGS --multi-file-mode=all CONFIG complex_build UI)
+
+    f3d_test(NAME TestDefaultConfigFileVTU DATA dragon.vtu CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigFileVTI DATA vase_4comp.vti CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigFileSTL DATA suzanne.stl CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigFileTIFF DATA f3d.tif CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI) # Note: This tests config file order as camera_direction is set in different files
+    f3d_test(NAME TestDefaultConfigFilePLY DATA suzanneRGBA.ply CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigFileQuakeMDL DATA zombie.mdl CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigFileAndCommand DATA suzanne.stl ARGS --up=-Y --camera-direction=-1,0.5,-1 CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigTranslucent DATA red_translucent_monkey.gltf CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    f3d_test(NAME TestDefaultConfigRemoveEmptyFileGroups DATA invalid.vtp cow.vtp CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+  endif()
 
   file(COPY "${F3D_SOURCE_DIR}/resources/configs/thumbnail.d/" "${F3D_SOURCE_DIR}/plugins/native/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
   f3d_test(NAME TestThumbnailConfigFileVTU DATA dragon.vtu CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -467,11 +540,7 @@
   f3d_test(NAME TestThumbnailConfigFileQuakeMDL DATA zombie.mdl CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
 endif()
 
-# color texture with opacity needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9467
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220811)
-  f3d_test(NAME TestTextureColorWithOptions DATA WaterBottle.glb ARGS --texture-base-color=${F3D_SOURCE_DIR}/testing/data/albedo_mod.png --color=1,1,0 --opacity=0.4 --translucency-support)
-endif()
-
+f3d_test(NAME TestTextureColorWithOptions DATA WaterBottle.glb ARGS --texture-base-color=${F3D_SOURCE_DIR}/testing/data/albedo_mod.png --color=1,1,0 --opacity=0.4 --blending)
 f3d_test(NAME TestGLTFImporterUnlit DATA UnlitTest.glb)
 f3d_test(NAME TestMaterial DATA suzanne.ply ARGS --color=0.72,0.45,0.2 --metallic=0.7 --roughness=0.2)
 f3d_test(NAME TestMaterialFullScene DATA WaterBottle.glb ARGS --color=0.9,0.1,0.1 --metallic=0.7 --roughness=0.2)
@@ -481,7 +550,6 @@
 f3d_test(NAME TestEdges DATA suzanne.ply ARGS -e)
 f3d_test(NAME TestLineWidth DATA cow.vtk ARGS -e --line-width=5)
 f3d_test(NAME TestLineWidthFullScene DATA suzanne.obj ARGS -e --line-width=3 --up=-Y)
-f3d_test(NAME TestPointCloudFullScene DATA pointsCloud.gltf ARGS --point-size=20)
 f3d_test(NAME TestTextureMatCapWithEdges DATA suzanne.ply ARGS -e --texture-matcap=${F3D_SOURCE_DIR}/testing/data/skin.png)
 
 # Test enabling all animations
@@ -500,12 +568,37 @@
 f3d_test(NAME TestInteractionAnimationFrameRate DATA f3d.glb ARGS --animation-progress --frame-rate=1 INTERACTION)#Space;Wait;Space;
 f3d_test(NAME TestInteractionAnimationCameraMovement DATA CameraAnimated.glb ARGS --camera-index=0 --animation-progress INTERACTION)#Space;MouseMovement;Space;
 f3d_test(NAME TestInteractionAnimationInvert DATA f3d.glb ARGS --animation-speed-factor=-1 --animation-progress INTERACTION)#Space;Wait;Space;
+f3d_test(NAME TestInteractionAnimationBackward DATA f3d.glb ARGS --animation-progress INTERACTION)#Ctrl+Shift+Space;Wait;Space;
+f3d_test(NAME TestCommandScriptJumpToPreviousFrame DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-time=0.5 --animation-progress SCRIPT TestCommandScriptJumpToPreviousFrame.txt)
+f3d_test(NAME TestCommandScriptJumpToNextFrame DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-progress SCRIPT TestCommandScriptJumpToNextFrame.txt)
+f3d_test(NAME TestCommandScriptJumpToFirstFrame DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-time=0.5 --animation-progress SCRIPT TestCommandScriptJumpToFirstFrame.txt)
+f3d_test(NAME TestCommandScriptJumpToLastFrame DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-time=0.5 --animation-progress SCRIPT TestCommandScriptJumpToLastFrame.txt)
+f3d_test(NAME TestCommandScriptJumpToMiddleFrame DATA soldier_animations.mdl ARGS --animation-indices=2 --animation-time=0.5 --animation-progress SCRIPT TestCommandScriptJumpToMiddleFrame.txt)
+
+# Test temporal anti-aliasing
+f3d_test(NAME TestInteractionTAA DATA suzanne.ply ARGS --anti-aliasing=taa INTERACTION) #Render;Render...
+f3d_test(NAME TestInteractionTAAMiddleClick DATA suzanne.ply ARGS --anti-aliasing=taa INTERACTION) #Render;Render...;MiddleClick;Render;Render...
+
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231102)
+  f3d_test(NAME TestInteractionPointCloudTAA DATA pointsCloud.vtp ARGS --anti-aliasing=taa -o --point-sprites-size=20 INTERACTION) #Render;Render...
+endif()
+
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12098
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250504)
+  f3d_test(NAME TestInteractionStochasticTAA DATA alpha.glb ARGS --anti-aliasing=taa --blending=stochastic INTERACTION) #Render;Render...
+  f3d_test(NAME TestInteractionAndCLIBlending ARGS --blending DATA suzanne.stl alpha.glb INTERACTION)#PP;Right
+endif()
 
 # Need SSIM comparison for some reason
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240729)
   # Test scrolling cheatsheet with no model by mouse wheel and by using scrollbar
-  f3d_test(NAME TestInteractionNoModelScrollWheel RESOLUTION 1000,600 NO_DATA_FORCE_RENDER INTERACTION UI)
-  f3d_test(NAME TestInteractionNoModelScrollBar RESOLUTION 1000,600 NO_DATA_FORCE_RENDER INTERACTION UI)
+  f3d_test(NAME TestInteractionNoModelScrollWheel RESOLUTION 1000,600 NO_DATA_FORCE_RENDER LONG_TIMEOUT INTERACTION UI)
+  f3d_test(NAME TestInteractionNoModelScrollBar RESOLUTION 1000,600 NO_DATA_FORCE_RENDER LONG_TIMEOUT INTERACTION UI)
+endif()
+
+# https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12678
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251206)
+  f3d_test(NAME TestPointCloudFullScene DATA pointsCloud.gltf ARGS --point-size=20)
 endif()
 
 # A verbose test that needs animation index support
@@ -516,8 +609,8 @@
 f3d_test(NAME TestVerboseAnimationTimeRange DATA InterpolationTest.glb ARGS --verbose REGEXP "0, 1.66667" NO_BASELINE)
 f3d_test(NAME TestCommandScriptVerboseMultiAnimationTimeRange SCRIPT TestCommandScriptVerboseMultiAnimationTimeRange.txt DATA InterpolationTest.glb ARGS --verbose REGEXP "0, 1.70833" NO_BASELINE)# cycle_animation x3
 f3d_test(NAME TestVerboseQuakeMDLAnimationNoNamingScheme ARGS --verbose DATA v_rock2.mdl REGEXP "0: flame" NO_BASELINE)
-f3d_test(NAME TestVerboseQuakeMDLGroupSkinUnsupported ARGS --verbose DATA groupskin.mdl REGEXP "Groupskin are not supported" NO_BASELINE)
-f3d_test(NAME TestVerboseQuakeMDLInvalid ARGS --verbose DATA w_medkit_hl.mdl REGEXP "No frame read" NO_BASELINE)
+f3d_test(NAME TestVerboseQuakeMDLGroupSkin ARGS --verbose DATA groupskin.mdl REGEXP "0: group_skin" NO_BASELINE)
+f3d_test(NAME TestVerboseQuakeMDLInvalid ARGS --verbose DATA w_medkit_hl.mdl REGEXP "Unsupported MDL version" NO_BASELINE)
 
 # Armature test
 f3d_test(NAME TestGLTFRigArmatureNoArmature DATA RiggedFigure.glb ARGS --animation-time=1 --opacity=0.5 -p)
@@ -533,14 +626,19 @@
 # Cheatsheet test with minimal resolution to avoid repeated change of baselines
 f3d_test(NAME TestInteractionCheatsheetWhiteBG DATA cow.vtp ARGS --background-color=1,1,1 INTERACTION UI) #H
 f3d_test(NAME TestInteractionCheatsheetBlackBG DATA cow.vtp ARGS --background-color=0,0,0 INTERACTION UI) #H
-f3d_test(NAME TestInteractionCheatsheetNoFile INTERACTION UI NO_DATA_FORCE_RENDER) #HXM
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionCheatsheetNoFile INTERACTION UI NO_DATA_FORCE_RENDER) #HXM
+endif()
 f3d_test(NAME TestInteractionCheatsheetScalars DATA dragon.vtu ARGS --scalar-coloring --coloring-component=-2 INTERACTION UI LONG_TIMEOUT) #HSSS
+f3d_test(NAME TestInteractionCheatsheetScalarsNoArray DATA dragon.vtu ARGS --scalar-coloring INTERACTION UI RESOLUTION 500,300) #H
 f3d_test(NAME TestInteractionCheatsheetOpacity DATA cow.vtp INTERACTION UI ARGS --opacity=0.5 RESOLUTION 300,700) #H;ScrollDown
 f3d_test(NAME TestInteractionCheatsheetAnimationName DATA InterpolationTest.glb ARGS --animation-indices=6 INTERACTION UI) #HWWW
 f3d_test(NAME TestInteractionCheatsheetConfigFile DATA dragon.vtu CONFIG ${F3D_SOURCE_DIR}/testing/configs/bindings.json INTERACTION UI) #H;ScrollDown
-f3d_test(NAME TestInteractionCheatsheetSSAA DATA cow.vtp INTERACTION UI) #H;A;A
+f3d_test(NAME TestInteractionCheatsheetMultiModifierBinding DATA dragon.vtu RESOLUTION 1200,300 CONFIG ${F3D_SOURCE_DIR}/testing/configs/bindings.json INTERACTION UI) #H;ScrollDown
+f3d_test(NAME TestInteractionCheatsheetCycle DATA cow.vtp RESOLUTION 800,300 INTERACTION UI LONG_TIMEOUT) #HAAPO
 
-if(NOT F3D_MODULE_RAYTRACING)
+if(F3D_MODULE_RAYTRACING)
   f3d_test(NAME TestInteractionCheatsheetCentered DATA cow.vtp RESOLUTION 500,1500 INTERACTION UI LONG_TIMEOUT) #H
 endif()
 
@@ -552,10 +650,10 @@
 
 # Require improved importer support https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11303
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240910)
-  f3d_test(NAME TestInvalidFileFileName DATA invalid.vtp ARGS --filename NO_DATA_FORCE_RENDER UI)
+  f3d_test(NAME TestInvalidFileFileNameEmpty DATA invalid.vtp ARGS --filename NO_DATA_FORCE_RENDER UI)
   f3d_test(NAME TestMultiFileInvalid DATA cow.vtp invalid.vtp dragon.vtu ARGS --multi-file-mode=all NO_DATA_FORCE_RENDER UI)
-  f3d_test(NAME TestMultiFileInvalidFilesFileName DATA mb/mb_3_0.vtt invalid.vtp ARGS --multi-file-mode=all --filename NO_DATA_FORCE_RENDER UI)
-  f3d_test(NAME TestMultiFileCameraIndex DATA Cameras.gltf CameraAnimated.glb ARGS --multi-file-mode=all --camera-index=2 --opacity=0.5 --translucency-support)
+  f3d_test(NAME TestMultiFileUnsupportedFilesFileName DATA unsupportedFile.dummy cow.vtp ARGS --multi-file-mode=all --filename NO_DATA_FORCE_RENDER UI)
+  f3d_test(NAME TestMultiFileCameraIndex DATA Cameras.gltf CameraAnimated.glb ARGS --multi-file-mode=all --camera-index=2 --opacity=0.5 --blending)
   f3d_test(NAME TestConsoleBadgeError DATA invalid.vtp NO_DATA_FORCE_RENDER UI)
 endif()
 
@@ -567,55 +665,58 @@
 
 f3d_test(NAME TestNoBackground DATA cow.vtp ARGS --no-background)
 
-# HDRI test needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9767
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221220)
-  f3d_test(NAME TestHDRI DATA suzanne.ply HDRI palermo_park_1k.hdr)
-  f3d_test(NAME TestHDRICache DATA suzanne.ply HDRI palermo_park_1k.hdr DEPENDS TestHDRI)
-  f3d_test(NAME TestHDRIBlur DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -u)
-  f3d_test(NAME TestHDRIBlurCoCSmall DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=10 --camera-position=-20,0,20)
-  f3d_test(NAME TestHDRIBlurCoCMedium DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=50 --camera-position=-20,0,20)
-  f3d_test(NAME TestHDRIBlurCoCLarge DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=100 --camera-position=-20,0,20)
-  f3d_test(NAME TestHDRIBlurCoCZero DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=0 --camera-position=-20,0,20 THRESHOLD 0.08) # Threshold is needed for legacy comparison method for VTK 9.3
-  f3d_test(NAME TestHDRIBlurCoCNegative DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=-100 --camera-position=-20,0,20)
-  f3d_test(NAME TestHDRIBlurRatio DATA suzanne.ply HDRI palermo_park_1k.hdr RESOLUTION 600,100 ARGS -u)
-  f3d_test(NAME TestHDRIEdges DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -e)
-  f3d_test(NAME TestHDRI8Bit DATA suzanne.ply HDRI f3d.tif ARGS --color=1.0,0.0,0.0)
-  f3d_test(NAME TestHDRIOrient DATA suzanne.stl HDRI palermo_park_1k.hdr ARGS --up=+Z)
-  f3d_test(NAME TestHDRIToneMapping DATA suzanne.ply HDRI palermo_park_1k.hdr TONE_MAPPING ARGS -t)
-  f3d_test(NAME TestInteractionHDRIMove DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION THRESHOLD 0.05) #Shift+MouseRight; # Threshold is needed for legacy image comparison for VTK 9.3
-  f3d_test(NAME TestInteractionHDRIBlur DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION) #U
-  f3d_test(NAME TestInteractionHDRIReload DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION) #Up
-  f3d_test(NAME TestInteractionHDRIChange DATA multi HDRI palermo_park_1k.hdr CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION) #Left
-
-  # Test non existent HDRI, do not add a dummy.png
-  f3d_test(NAME TestNonExistentHDRI DATA cow.vtp HDRI dummy.png REGEXP "HDRI file does not exist" NO_BASELINE)
-
-  # Test invalid provided HDRI
-  f3d_test(NAME TestInvalidHDRI DATA cow.vtp HDRI invalid.png REGEXP "Cannot open HDRI file" NO_BASELINE)
-
-  # Use a dummy HDRI for simplicity to test default HDRI
-  f3d_test(NAME TestHDRIDefault DATA suzanne.ply HDRI dummy.png THRESHOLD 0.11) # Threshold is needed for legacy comparison method for VTK 9.3
-
-  configure_file("${F3D_SOURCE_DIR}/testing/configs/hdri.json.in" "${CMAKE_BINARY_DIR}/hdri.json")
-  f3d_test(NAME TestConfigFileHDRI DATA dragon.vtu CONFIG "${CMAKE_BINARY_DIR}/hdri.json" LONG_TIMEOUT)
-
-  if(F3D_MODULE_EXR)
-    f3d_test(NAME TestHDRIEXR DATA suzanne.ply HDRI kloofendal_43d_clear_1k.exr)
-  endif()
-
-  f3d_test(NAME TestHDRISkyboxOnly DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-skybox LONG_TIMEOUT)
-  f3d_test(NAME TestHDRIAmbientOnly DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-ambient LONG_TIMEOUT)
-  f3d_test(NAME TestHDRIAmbientOnlyNoBackground DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-ambient --no-background LONG_TIMEOUT)
-  f3d_test(NAME TestHDRINone DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr LONG_TIMEOUT)
-
-  f3d_test(NAME TestInteractionHDRICache DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION DEPENDS TestHDRI) #FFFFJJJJ
-  f3d_test(NAME TestInteractionHDRIRemoveSkybox DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
-  f3d_test(NAME TestInteractionHDRIRemoveAmbient DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
-  f3d_test(NAME TestInteractionHDRIRemoveBoth DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
-  f3d_test(NAME TestInteractionHDRILoop DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
-  f3d_test(NAME TestInteractionHDRIFullFromNone DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr LONG_TIMEOUT INTERACTION)
+# HDRI tests
+f3d_test(NAME TestHDRI DATA suzanne.ply HDRI palermo_park_1k.hdr)
+f3d_test(NAME TestHDRICache DATA suzanne.ply HDRI palermo_park_1k.hdr DEPENDS TestHDRI)
+f3d_test(NAME TestHDRIBlur DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -u)
+f3d_test(NAME TestHDRIBlurCoCSmall DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=10 --camera-position=-20,0,20)
+f3d_test(NAME TestHDRIBlurCoCMedium DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=50 --camera-position=-20,0,20)
+f3d_test(NAME TestHDRIBlurCoCLarge DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=100 --camera-position=-20,0,20)
+f3d_test(NAME TestHDRIBlurCoCZero DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=0 --camera-position=-20,0,20 THRESHOLD 0.08) # Threshold is needed for legacy comparison method for VTK 9.3
+f3d_test(NAME TestHDRIBlurCoCNegative DATA suzanne.ply HDRI shanghai_bund_1k.hdr ARGS -u --blur-coc=-100 --camera-position=-20,0,20)
+f3d_test(NAME TestHDRIBlurRatio DATA suzanne.ply HDRI palermo_park_1k.hdr RESOLUTION 600,100 ARGS -u)
+f3d_test(NAME TestHDRIEdges DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -e THRESHOLD 0.06)
+f3d_test(NAME TestHDRI8Bit DATA suzanne.ply HDRI f3d.tif ARGS --color=1.0,0.0,0.0)
+f3d_test(NAME TestHDRIOrient DATA suzanne.stl HDRI palermo_park_1k.hdr ARGS --up=+Z)
+f3d_test(NAME TestHDRIToneMapping DATA suzanne.ply HDRI palermo_park_1k.hdr TONE_MAPPING ARGS -t)
+f3d_test(NAME TestInteractionHDRIMove DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION THRESHOLD 0.05) #Shift+MouseRight; # Threshold is needed for legacy image comparison for VTK 9.3
+f3d_test(NAME TestInteractionHDRIBlur DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION) #U
+f3d_test(NAME TestInteractionHDRIReload DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION) #Up
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionHDRIChange DATA multi HDRI palermo_park_1k.hdr CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #Left
+endif()
+# Test non existent HDRI, do not add a dummy.png
+f3d_test(NAME TestNonExistentHDRI DATA cow.vtp HDRI dummy.png REGEXP "HDRI file does not exist" NO_BASELINE)
+
+# Test invalid provided HDRI
+f3d_test(NAME TestInvalidHDRI DATA cow.vtp HDRI invalid.png REGEXP "Cannot open HDRI file" NO_BASELINE)
 
-  if(F3D_MODULE_RAYTRACING)
+# Use a dummy HDRI for simplicity to test default HDRI
+f3d_test(NAME TestHDRIDefault DATA suzanne.ply HDRI dummy.png THRESHOLD 0.11) # Threshold is needed for legacy comparison method for VTK 9.3
+
+configure_file("${F3D_SOURCE_DIR}/testing/configs/hdri.json.in" "${CMAKE_BINARY_DIR}/hdri.json")
+f3d_test(NAME TestConfigFileHDRI DATA dragon.vtu CONFIG "${CMAKE_BINARY_DIR}/hdri.json" LONG_TIMEOUT)
+
+if(F3D_MODULE_EXR)
+  f3d_test(NAME TestHDRIEXR DATA suzanne.ply HDRI kloofendal_43d_clear_1k.exr)
+endif()
+
+f3d_test(NAME TestHDRISkyboxOnly DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-skybox LONG_TIMEOUT)
+f3d_test(NAME TestHDRIAmbientOnly DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-ambient LONG_TIMEOUT)
+f3d_test(NAME TestHDRIAmbientOnlyNoBackground DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-ambient --no-background LONG_TIMEOUT)
+f3d_test(NAME TestHDRINone DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr LONG_TIMEOUT)
+
+f3d_test(NAME TestInteractionHDRICache DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION DEPENDS TestHDRI) #FFFFJJJJ
+f3d_test(NAME TestInteractionHDRIRemoveSkybox DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
+f3d_test(NAME TestInteractionHDRIRemoveAmbient DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
+f3d_test(NAME TestInteractionHDRIRemoveBoth DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
+f3d_test(NAME TestInteractionHDRILoop DATA suzanne.ply HDRI palermo_park_1k.hdr INTERACTION)
+f3d_test(NAME TestInteractionHDRIFullFromNone DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr LONG_TIMEOUT INTERACTION)
+
+if(F3D_MODULE_RAYTRACING)
+  # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10753
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231213)
     # XXX: These tests are impacted by https://github.com/f3d-app/f3d/issues/933
     f3d_test(NAME TestHDRIRaytracing DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -rd --raytracing-samples=4)
     f3d_test(NAME TestHDRIRaytracingSkyboxOnly DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr --hdri-skybox -rd --raytracing-samples=4 LONG_TIMEOUT)
@@ -628,23 +729,12 @@
     f3d_test(NAME TestInteractionHDRIRaytracingRemoveBoth DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -rd --raytracing-samples=4 INTERACTION)
     f3d_test(NAME TestInteractionHDRIRaytracingLoop DATA suzanne.ply HDRI palermo_park_1k.hdr ARGS -rd --raytracing-samples=4 INTERACTION)
     f3d_test(NAME TestInteractionHDRIRaytracingFullFromNone DATA suzanne.ply ARGS --hdri-file=${F3D_SOURCE_DIR}/testing/data/palermo_park_1k.hdr -rd --raytracing-samples=4 INTERACTION LONG_TIMEOUT)
-
   endif()
 endif()
 
 # SSAA with HDR framebuffer support in https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12026
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250329)
-  f3d_test(NAME TestHDRIToneMappingSSAA DATA suzanne.ply HDRI palermo_park_1k.hdr TONE_MAPPING ARGS -t -a --anti-aliasing-mode=ssaa)
-endif()
-
-# Zoom factor was introduced in https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9958
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20230221)
-  f3d_test(NAME TestCameraZoomFactor DATA suzanne.obj ARGS --camera-direction=-1,-1,1 --camera-zoom-factor=1.5)
-endif()
-
-# VRML was fixed in https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10235
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20230603)
-  f3d_test(NAME TestVRMLImporter DATA bot2.wrl)
+  f3d_test(NAME TestHDRIToneMappingSSAA DATA suzanne.ply HDRI palermo_park_1k.hdr TONE_MAPPING ARGS -t --anti-aliasing=ssaa)
 endif()
 
 # TGA support for OBJ added in https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11922
@@ -653,22 +743,29 @@
 endif()
 
 if(F3D_MODULE_RAYTRACING)
-  f3d_test(NAME TestRaytracingGLTF DATA WaterBottle.glb ARGS -rd --raytracing-samples=4)
-  f3d_test(NAME TestRaytracingBackground DATA suzanne.ply ARGS -rd --raytracing-samples=4 --background-color=1,0,0 THRESHOLD 0.05) # Threshold needed because of difference in rendering in VTK 9.3
-  f3d_test(NAME TestRaytracingPointCloud DATA pointsCloud.vtp ARGS -rd --raytracing-samples=4 --point-size=20 THRESHOLD 0.1) # Threshold needed because of difference in rendering in VTK 9.3
-  f3d_test(NAME TestRaytracingDenoise DATA suzanne.ply ARGS -rd --raytracing-samples=4)
-  f3d_test(NAME TestRaytracingNoDenoise DATA suzanne.ply ARGS -r --raytracing-samples=20)
-  f3d_test(NAME TestVersionRaytracing ARGS --version REGEXP "Module Raytracing: ON")
-  f3d_test(NAME TestInteractionRaytracingDenoise DATA suzanne.ply ARGS --raytracing-samples=4 INTERACTION) #RD
-  f3d_test(NAME TestRaytracingScalarBar DATA dragon.vtu ARGS -rsbd --raytracing-samples=4 THRESHOLD 0.06) # Threshold needed because of difference in rendering in VTK 9.3 on macOS
+  # Ideally we could test these with ospray 2.7 and VTK 9.3
+  # https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10753
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231213)
+    f3d_test(NAME TestRaytracingGLTF DATA WaterBottle.glb ARGS -rd --raytracing-samples=4)
+    f3d_test(NAME TestRaytracingBackground DATA suzanne.ply ARGS -rd --raytracing-samples=4 --background-color=1,0,0 THRESHOLD 0.05) # Threshold needed because of difference in rendering in VTK 9.3
+    f3d_test(NAME TestRaytracingPointCloud DATA pointsCloud.vtp ARGS -rd --raytracing-samples=4 --point-size=20 THRESHOLD 0.1) # Threshold needed because of difference in rendering in VTK 9.3
+    f3d_test(NAME TestRaytracingDenoise DATA suzanne.ply ARGS -rd --raytracing-samples=4)
+    f3d_test(NAME TestRaytracingNoDenoise DATA suzanne.ply ARGS -r --raytracing-samples=20)
+    f3d_test(NAME TestVersionRaytracing ARGS --version REGEXP "Module Raytracing: ON")
+    f3d_test(NAME TestInteractionRaytracingDenoise DATA suzanne.ply ARGS --raytracing-samples=4 INTERACTION) #RD
+    f3d_test(NAME TestRaytracingScalarBar DATA dragon.vtu ARGS -rsbd --raytracing-samples=4 THRESHOLD 0.06) # Threshold needed because of difference in rendering in VTK 9.3 on macOS
+
+    if(NOT F3D_MACOS_BUNDLE)
+      # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+      if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+        f3d_test(NAME TestRaytracingDefaultConfigFile DATA dragon.vtu CONFIG config_build ARGS -rd --raytracing-samples=4 LONG_TIMEOUT TONE_MAPPING)
+      endif()
 
-  if(NOT F3D_MACOS_BUNDLE)
-    f3d_test(NAME TestRaytracingDefaultConfigFile DATA dragon.vtu CONFIG config_build ARGS -rd --raytracing-samples=4 LONG_TIMEOUT TONE_MAPPING)
-    f3d_test(NAME TestRaytracingThumbnailConfigFile DATA dragon.vtu CONFIG thumbnail_build ARGS -rd --raytracing-samples=4 LONG_TIMEOUT TONE_MAPPING)
-  endif()
-
-  f3d_test(NAME TestRaytracingNoBackground DATA suzanne.ply ARGS -rd --raytracing-samples=4 --no-background)
+      f3d_test(NAME TestRaytracingThumbnailConfigFile DATA dragon.vtu CONFIG thumbnail_build ARGS -rd --raytracing-samples=4 LONG_TIMEOUT TONE_MAPPING)
+    endif()
 
+    f3d_test(NAME TestRaytracingNoBackground DATA suzanne.ply ARGS -rd --raytracing-samples=4 --no-background)
+  endif()
 else(F3D_MODULE_RAYTRACING)
   f3d_test(NAME TestCommandScriptRaytracingNoRaytracing DATA suzanne.ply SCRIPT TestCommandScriptRaytracing.txt NO_BASELINE REGEXP "Raytracing options can't be used if F3D has not been built with raytracing")
 endif()
@@ -677,14 +774,32 @@
   f3d_test(NAME TestVersionEXR ARGS --version REGEXP "Module OpenEXR: ON")
 endif()
 
+if(F3D_MODULE_WEBP)
+  f3d_test(NAME TestVersionWebP ARGS --version REGEXP "Module WebP: ON")
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11922
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250220)
+    f3d_test(NAME TestOBJWebP DATA cube_webp.obj)
+  endif()
+endif()
+
 if(F3D_PLUGIN_BUILD_ALEMBIC)
-  f3d_test(NAME TestABC DATA suzanne.abc ARGS -s --load-plugins=alembic)
-  f3d_test(NAME TestABCNonFaceVarying DATA tetrahedron_non_facevarying_uv.abc ARGS -s --load-plugins=alembic)
-  f3d_test(NAME TestABCAnimation DATA drop.abc ARGS --animation-time=2 --load-plugins=alembic --animation-progress)
+  f3d_test(NAME TestAlembic DATA suzanne.abc ARGS -s --load-plugins=alembic)
+  f3d_test(NAME TestAlembicNonFaceVarying DATA tetrahedron_non_facevarying_uv.abc ARGS -s --load-plugins=alembic)
+  f3d_test(NAME TestAlembicAnimation DATA drop.abc ARGS --animation-time=2 --load-plugins=alembic --animation-progress)
+  f3d_test(NAME TestAlembicAnimationXForm DATA xform_anim.abc ARGS -g --animation-time=1.5 --load-plugins=alembic --animation-progress)
+  f3d_test(NAME TestAlembicAnimationXFormRotation DATA joint1.abc ARGS -g --animation-time=1.5 --load-plugins=alembic --animation-progress)
+  f3d_test(NAME TestAlembicCurves DATA monkey_curves.abc ARGS --load-plugins=alembic THRESHOLD 0.07) #  High threshold because of line rendering
+
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240707)
+    f3d_test(NAME TestAlembicInvalid DATA invalid.abc ARGS -s --load-plugins=alembic REGEXP "failed to load scene" NO_BASELINE)
+  endif()
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/alembic/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileAlembic DATA suzanne.abc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileAlembic DATA suzanne.abc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/alembic/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileAlembic DATA suzanne.abc CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -737,10 +852,13 @@
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/assimp/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileAssimpFBX DATA phong_cube.fbx CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-    f3d_test(NAME TestDefaultConfigFileAssimpDXF DATA PinkEggFromLW.dxf ARGS -p CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-    f3d_test(NAME TestDefaultConfigFileAssimpOFF DATA teapot.off CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
-    f3d_test(NAME TestDefaultConfigFileAssimpDAE DATA duck.dae CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileAssimpFBX DATA phong_cube.fbx CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+      f3d_test(NAME TestDefaultConfigFileAssimpDXF DATA PinkEggFromLW.dxf ARGS -p CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+      f3d_test(NAME TestDefaultConfigFileAssimpOFF DATA teapot.off CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+      f3d_test(NAME TestDefaultConfigFileAssimpDAE DATA duck.dae CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/assimp/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileAssimpFBX DATA phong_cube.fbx CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -769,7 +887,10 @@
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/draco/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileDraco DATA suzanne.drc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileDraco DATA suzanne.drc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/draco/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileDraco DATA suzanne.drc CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -783,6 +904,10 @@
     f3d_test(NAME TestAMRDataSet DATA amr.vtkhdf ARGS --load-plugins=hdf -s)
   endif()
 
+  if (VTK_VERSION VERSION_GREATER_EQUAL 9.4.0)
+    f3d_test(NAME TestVTKHDFPartitionedDataSetCollection DATA pdc_sphere_cone.vtkhdf ARGS --load-plugins=hdf -s)
+  endif()
+
   f3d_test(NAME TestExodus DATA disk_out_ref.ex2 ARGS --load-plugins=hdf -s --camera-position=-11,-2,-49)
   f3d_test(NAME TestExodusExo DATA box.exo ARGS --load-plugins=hdf NO_RENDER NO_BASELINE REGEXP "Number of points: 24")
   f3d_test(NAME TestExodusG DATA box.g ARGS --load-plugins=hdf NO_RENDER NO_BASELINE REGEXP "Number of points: 24")
@@ -830,13 +955,19 @@
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/hdf/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileExodus DATA disk_out_ref.ex2 CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileExodus DATA disk_out_ref.ex2 CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/hdf/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileExodus DATA disk_out_ref.ex2 CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
 
-    if (VTK_VERSION VERSION_GREATER_EQUAL 9.3.0)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
       f3d_test(NAME TestDefaultConfigFileVTKHDF DATA blob.vtkhdf CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
+    if (VTK_VERSION VERSION_GREATER_EQUAL 9.3.0)
       f3d_test(NAME TestThumbnailConfigFileVTKHDF DATA blob.vtkhdf CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
     endif()
   endif()
@@ -855,7 +986,10 @@
   f3d_test(NAME TestNetCDF DATA temperature_grid.nc ARGS --load-plugins=hdf -s)
 
   if(NOT F3D_MACOS_BUNDLE)
-    f3d_test(NAME TestDefaultConfigFileNetCDF DATA temperature_grid.nc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileNetCDF DATA temperature_grid.nc CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
     f3d_test(NAME TestThumbnailConfigFileNetCDF DATA temperature_grid.nc CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
   endif()
 endif()
@@ -870,6 +1004,7 @@
   f3d_test(NAME TestBREPDefines DATA cheese.brep ARGS --load-plugins=occt -DBREP.linear_deflection=100 --up=+Z)
 
   if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240707)
+    f3d_test(NAME TestInvalidSTEP DATA invalid.step ARGS --verbose --load-plugins=occt REGEXP "failed to load scene" NO_BASELINE)
     f3d_test(NAME TestInvalidBREP DATA invalid.brep ARGS --verbose --load-plugins=occt REGEXP "failed to load scene" NO_BASELINE)
   endif()
 
@@ -884,7 +1019,10 @@
 
     if (NOT F3D_MACOS_BUNDLE)
       file(COPY "${F3D_SOURCE_DIR}/plugins/occt/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-      f3d_test(NAME TestDefaultConfigFileOCCT DATA f3d.stp CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+      # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+      if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+        f3d_test(NAME TestDefaultConfigFileOCCT DATA f3d.stp CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+      endif()
 
       file(COPY "${F3D_SOURCE_DIR}/plugins/occt/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
       f3d_test(NAME TestThumbnailConfigFileOCCT DATA f3d.stp CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -901,20 +1039,23 @@
   f3d_test(NAME TestUSDInvalid DATA invalid.usd REGEXP "Stage failed to open" ARGS --verbose --load-plugins=usd NO_BASELINE)
   f3d_test(NAME TestUSDPurpose DATA purpose.usdc ARGS --load-plugins=usd)
   f3d_test(NAME TestUSDInterpolation DATA two_quads_interp.usda ARGS --load-plugins=usd)
+  f3d_test(NAME TestUSDZMemEXR DATA small.usdz ARGS --load-plugins=usd)
 
   # This test is there to test occlusion texture and face-varying point data
   # TODO: Note that the result looks incorrect because of face-varying attributes and must be fixed later
   f3d_test(NAME TestUSDTeapot DATA Teapot.usd ARGS --load-plugins=usd)
 
-  f3d_test(NAME TestUSDZAnimated DATA AnimatedCube.usdz ARGS --load-plugins=usd --animation-time=0.3 --animation-progress)
+  f3d_test(NAME TestUSDZAnimated DATA AnimatedCube.usdz ARGS --load-plugins=usd --animation-time=0.3 --animation-progress THRESHOLD 0.05)
   f3d_test(NAME TestUSDZRigged DATA RiggedSimple.usdz ARGS --load-plugins=usd --animation-time=0.3)
   f3d_test(NAME TestUSDZMaterials DATA McUsd.usdz ARGS --load-plugins=usd --camera-position=1055,912,-247 --camera-focal-point=69,173,63 THRESHOLD 0.3) # High threshold because of legacy comparison methods in VTK 9.3
   f3d_test(NAME TestUSDZMaterialsInterationReload DATA McUsd.usdz ARGS --load-plugins=usd INTERACTION NO_BASELINE) # Up
-  f3d_test(NAME TestUSDZMemEXR DATA small.usdz ARGS --load-plugins=usd)
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/usd/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileUSD DATA suzanne.usd CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileUSD DATA suzanne.usd CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/usd/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileUSD DATA suzanne.usd CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -933,7 +1074,10 @@
 
   if(NOT F3D_MACOS_BUNDLE)
     file(COPY "${F3D_SOURCE_DIR}/plugins/vdb/configs/config.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/config_build.d")
-    f3d_test(NAME TestDefaultConfigFileVDB DATA icosahedron.vdb CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      f3d_test(NAME TestDefaultConfigFileVDB DATA icosahedron.vdb CONFIG config_build LONG_TIMEOUT TONE_MAPPING UI)
+    endif()
 
     file(COPY "${F3D_SOURCE_DIR}/plugins/vdb/configs/thumbnail.d/" DESTINATION "${CMAKE_BINARY_DIR}/share/f3d/configs/thumbnail_build.d")
     f3d_test(NAME TestThumbnailConfigFileVDB DATA icosahedron.vdb CONFIG thumbnail_build LONG_TIMEOUT TONE_MAPPING)
@@ -952,14 +1096,18 @@
 ## Interaction Tests
 # Test hotkeys
 f3d_test(NAME TestInteractionPostFX DATA cow.vtp INTERACTION TONE_MAPPING LONG_TIMEOUT) #PQAAAAT
-f3d_test(NAME TestInteractionActors DATA cow.vtp INTERACTION UI) #EXGMN
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionActors DATA cow.vtp INTERACTION UI) #EXGMN
+endif()
 f3d_test(NAME TestInteractionTimer DATA cow.vtp NO_BASELINE INTERACTION UI LONG_TIMEOUT) #Z
-f3d_test(NAME TestInteractionTrackball DATA cow.vtp ARGS --interaction-trackball INTERACTION) #Mouse movements;
-f3d_test(NAME TestInteractionTrackballBind DATA cow.vtp INTERACTION) #K#Mouse movements;
+f3d_test(NAME TestInteractionTrackball DATA cow.vtp ARGS --interaction-trackball INTERACTION LONG_TIMEOUT) #Mouse movements;
+f3d_test(NAME TestInteractionTrackballBind DATA cow.vtp INTERACTION LONG_TIMEOUT) #K#Mouse movements;
 f3d_test(NAME TestInteractionCycleCell DATA waveletArrays.vti INTERACTION LONG_TIMEOUT) #VCCC
 f3d_test(NAME TestInteractionCycleComp DATA dragon.vtu INTERACTION) #SYYYY
 f3d_test(NAME TestInteractionCycleScalars DATA dragon.vtu INTERACTION) #BSSSS
 f3d_test(NAME TestInteractionCycleCellInvalidIndex DATA waveletArrays.vti INTERACTION) #SSC
+f3d_test(NAME TestInteractionCycleBlending DATA suzanne.ply ARGS --opacity=0.8 INTERACTION LONG_TIMEOUT) #PPPPP # Cycle to ddp
 f3d_test(NAME TestInteractionVolumeCycle DATA waveletArrays.vti ARGS INTERACTION) #VSS
 f3d_test(NAME TestInteractionVolumeAfterColoring DATA waveletArrays.vti ARGS INTERACTION) #SSSV
 f3d_test(NAME TestInteractionVolumeInverse DATA HeadMRVolume.mhd ARGS --camera-position=127.5,-400,127.5 --camera-view-up=0,0,1 INTERACTION THRESHOLD 0.05)#VI #Small rendering differences due to volume rendering
@@ -974,13 +1122,15 @@
 f3d_test(NAME TestInteractionResetCameraWithCameraIndex DATA CameraAnimated.glb ARGS --camera-index=0 INTERACTION)#MouseMovements;Return;
 f3d_test(NAME TestInteractionTensorsCycleComp DATA tensors.vti ARGS --scalar-coloring --coloring-component=-2 INTERACTION) #SYYYYYYYYYY
 f3d_test(NAME TestInteractionCycleScalarsCompCheck DATA dragon.vtu ARGS -b --scalar-coloring --coloring-component=2 INTERACTION) #S
-f3d_test(NAME TestInteractionConfigFileNoColorBar DATA multi CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION) #Right;Right;Left
-f3d_test(NAME TestInteractionConfigFileBindings DATA dragon.vtu CONFIG ${F3D_SOURCE_DIR}/testing/configs/bindings.json INTERACTION UI) #Ctrl+Shift+O;Ctrl+O;Shift+O;O;3
 f3d_test(NAME TestInteractionConfigFileMulti DATA multi CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #SY;Right;XG;Right;N;Right;Right
-f3d_test(NAME TestInteractionConfigFileAndCommand DATA multi ARGS -o CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #OX;Right;N;Right;Right;Right
-f3d_test(NAME TestInteractionConfigFileImperative DATA dragon.vtu suzanne.stl ARGS --edges CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json INTERACTION) #E;Right
-f3d_test(NAME TestInteractionConfigFileImperativeNoData CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json INTERACTION NO_DATA_FORCE_RENDER UI) #X;Up
-f3d_test(NAME TestInteractionConfigFileOptional DATA zombie.mdl f3d.glb CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #Right
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionConfigFileNoColorBar DATA multi CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #Right;Right;Left
+  f3d_test(NAME TestInteractionConfigFileBindings DATA dragon.vtu CONFIG ${F3D_SOURCE_DIR}/testing/configs/bindings.json INTERACTION UI) #Ctrl+Shift+O;Ctrl+O;Shift+O;O;3
+  f3d_test(NAME TestInteractionConfigFileImperative DATA dragon.vtu suzanne.stl ARGS --edges CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json INTERACTION UI) #E;Right
+  f3d_test(NAME TestInteractionConfigFileImperativeNoData CONFIG ${F3D_SOURCE_DIR}/testing/configs/imperative.json INTERACTION NO_DATA_FORCE_RENDER UI) #X;Up
+  f3d_test(NAME TestInteractionConfigFileOptional DATA zombie.mdl f3d.glb CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI) #Right
+endif()
 f3d_test(NAME TestInteractionCycleVerbose DATA dragon.vtu ARGS --verbose -s NO_BASELINE INTERACTION REGEXP "Not coloring")#SSSSYC
 f3d_test(NAME TestInteractionCycleVerboseLevelsUsingBinding DATA dragon.vtu ARGS --verbose=info NO_BASELINE INTERACTION REGEXP "Verbose level changed to: Debug")#Shift+V;Shift+V;Shift+V;Shift+V;Shift+V
 f3d_test(NAME TestInteractionVerboseLevelPreservedOnReload DATA dragon.vtu NO_BASELINE INTERACTION REGEXP "Not coloring")#Shift+V;Shift+V;Shift+V;Shift+V;Up
@@ -991,10 +1141,14 @@
 f3d_test(NAME TestInteractionFocalPointPickingPoints DATA pointsCloud.vtp INTERACTION THRESHOLD 0.05) # Threshold needed because sometime a point does not appear
 f3d_test(NAME TestInteractionLightIntensity DATA dragon.vtu INTERACTION LONG_TIMEOUT)
 f3d_test(NAME TestInteractionMultiFileColoring DATA mb/recursive ARGS --multi-file-mode=all INTERACTION) #SSSB
-f3d_test(NAME TestInteractionOpacity DATA dragon.vtu INTERACTION)
-f3d_test(NAME TestInteractionInitUpVectorOption DATA dragon.vtu ARGS --up=-Y INTERACTION) #Small drag left, camera should maintain orientation
-f3d_test(NAME TestInteractionDragRotateVertical DATA offset-flat-box.glb ARGS -g -x --up=y INTERACTION) # Drag down, should look straight down and avoid gimbal lock
+f3d_test(NAME TestInteractionOpacity DATA dragon.vtu INTERACTION) # Shift+P * 21; Ctrl+P * 5
+f3d_test(NAME TestInteractionInitUpVectorOption DATA dragon.vtu ARGS --up=-Y INTERACTION LONG_TIMEOUT) #Small drag left, camera should maintain orientation
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionDragRotateVertical DATA offset-flat-box.glb ARGS -g -x --up=y INTERACTION UI) # Drag down, should look straight down and avoid gimbal lock
+endif()
 f3d_test(NAME TestInteractionVerticalDragRotate DATA dragon.vtu ARGS --up=y INTERACTION LONG_TIMEOUT) # 7 (top-view); left click and drag
+f3d_test(NAME TestInteractionDynamicUpDirection DATA dragon.vtu INTERACTION) # Ctrl+Z
 f3d_test(NAME TestInteractionReload DATA dragon.vtu ARGS -e INTERACTION) #Up;
 f3d_test(NAME TestInteractionLoadParentDirectory DATA multi/dragon.vtu ARGS --filename INTERACTION UI) #Down;
 f3d_test(NAME TestInteractionEmptyLoadParentDirectory INTERACTION NO_BASELINE REGEXP "No files loaded, no rendering performed") #Down;
@@ -1017,13 +1171,20 @@
 f3d_test(NAME TestInteractionConsoleInvalidCommand DATA f3d.glb INTERACTION UI) #Escape;foo;Return
 f3d_test(NAME TestInteractionConsoleInvalidOption DATA f3d.glb INTERACTION UI) #Escape;toggle foo;Return
 f3d_test(NAME TestInteractionConsoleClear DATA dragon.vtu INTERACTION UI) #Escape;e;Escape;printt;BackSpace;_scee;Left;n;Right;_info;Return;clear;Return
-f3d_test(NAME TestInteractionConsoleTypingSceneInfo DATA f3d.glb INTERACTION UI) #Escape;e;Escape;printt;BackSpace;_scee;Left;Right;_info;Return
+f3d_test(NAME TestInteractionConsoleTypingSceneInfo RESOLUTION 400,300 DATA f3d.glb INTERACTION UI) #Escape;e;Escape;printt;BackSpace;_scee;Left;Right;_info;Return
 f3d_test(NAME TestInteractionConsoleReload DATA f3d.glb INTERACTION UI) #Escape;reload_current_file_group;Return
 f3d_test(NAME TestInteractionConsoleLoadNext DATA f3d.glb cow.vtp INTERACTION UI) #Escape;load_next_file_group;Return
 f3d_test(NAME TestInteractionConsoleRender ARGS --coloring-by-cells DATA waveletMaterial.vti INTERACTION UI) #Escape;toggle_volume_rendering;Return
 f3d_test(NAME TestInteractionConsoleCamera DATA f3d.glb INTERACTION UI) #Escape;set_camera top;Return
 f3d_test(NAME TestInteractionConsoleScrollbar DATA f3d.glb INTERACTION UI) #Escape;a;Enter;Grab scrollbar
 f3d_test(NAME TestInteractionConsoleEmptyCommand DATA f3d.glb INTERACTION UI) #Escape;Enter
+f3d_test(NAME TestInteractionConsoleOverCheatSheet DATA f3d.glb INTERACTION UI) #h;Escape;Enter
+f3d_test(NAME TestInteractionCorrectCameraForVolumeSwitch ARGS --no-config -v DATA dragon.vtu INTERACTION UI) #v
+
+# Cycling through gaussian splatting require > 9.3 to not warn
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240203)
+  f3d_test(NAME TestInteractionConfigFileAndCommand DATA multi ARGS -o CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json INTERACTION UI LONG_TIMEOUT) #OOX;Right;N;Right;Right;Right
+endif()
 
 # Need SSIM comparison for some reason
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240729)
@@ -1031,9 +1192,37 @@
   f3d_test(NAME TestInteractionConsoleAutoCompleteNoMatch DATA BoxAnimated.gltf INTERACTION UI) #Escape;zz;Tab
   f3d_test(NAME TestInteractionConsoleAutoCompleteCandidates DATA BoxAnimated.gltf INTERACTION UI) #Escape;Tab;to;Tab
   f3d_test(NAME TestInteractionConsoleAutoCompleteHide DATA BoxAnimated.gltf INTERACTION UI) #Escape;Tab;to;Tab;Esc;Esc;Esc
+  f3d_test(NAME TestInteractionConsoleAutoCompleteToggleOptions DATA cow.vtp INTERACTION UI) #Escape;to;Tab;space;m;Tab;p;Tab;e;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteSetOptions ARGS --point-sprites=gaussian DATA cow.vtp INTERACTION UI RESOLUTION 400,300) #Escape;se;Tab;space;m;Tab;p;Tab;t;Tab;Tab;s;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteSetDisplayCompl DATA cow.vtp INTERACTION UI RESOLUTION 400,300) #Escape;se;Tab;space;m;Tab;p;Tab;t;Tab;Tab;Tab
+  f3d_test(NAME TestInteractionConsoleAutoCompleteReaderOptions DATA BoxAnimated.gltf INTERACTION UI) #Escape;set_r;Tab;Q;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteColoring DATA BoxAnimated.gltf INTERACTION UI) #Escape;cy;Tab;;c;Tab;a;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteCamera DATA BoxAnimated.gltf INTERACTION UI) #Escape;se;Tab;_c;Tab;i;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteHelp DATA BoxAnimated.gltf INTERACTION UI) #Escape;h;Tab;h;Tab;Enter
+  f3d_test(NAME TestInteractionConsoleAutoCompleteTokenizeError DATA BoxAnimated.gltf INTERACTION UI NO_BASELINE REGEXP "unable to tokenize") #Escape;";Tab;
+
   f3d_test(NAME TestInteractionConsoleHistory DATA BoxAnimated.gltf INTERACTION UI) #Escape;ro;Tab;45;Enter;set_c;Tab;top;Enter;Up;Up;Enter;Up;Up;Up;Up;Down;Down;Down;Down
   f3d_test(NAME TestInteractionConsoleHistoryEmpty DATA BoxAnimated.gltf INTERACTION UI)#Escape;Up;Down
   f3d_test(NAME TestInteractionConsoleHistoryHide DATA BoxAnimated.gltf INTERACTION UI)#Escape;a;Enter;b;Enter;Up;Escape;Escape;Escape;Up
+
+  file(COPY "${F3D_SOURCE_DIR}/testing/data/cow.vtp" DESTINATION "${CMAKE_BINARY_DIR}/Testing/data/") # Capital T, to avoid confusion on Windows and in-sources build
+  # First file (`.d`) doesn't exist and is there to test multi args
+  f3d_test(NAME TestInteractionConsoleAutoCompleteFilesystem INTERACTION UI) #Escape;ad;Tab;_f;Tab; ;.;Tab;d ..;Tab;T;Tab;i;Tab;d;Tab;Tab;Enter;Esc
+  if(WIN32 OR APPLE)
+    f3d_test(NAME TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF INTERACTION UI NO_DATA_FORCE_RENDER WORKING_DIR ${F3D_SOURCE_DIR}/testing/data/AutoCompletion) #Escape;add_files F;Tab
+    f3d_test(NAME TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF INTERACTION UI NO_DATA_FORCE_RENDER WORKING_DIR ${F3D_SOURCE_DIR}/testing/data/AutoCompletion) #Escape;add_files f;Tab
+  else()
+    f3d_test(NAME TestInteractionConsoleAutoCompleteDirCaseSensitive INTERACTION UI NO_DATA_FORCE_RENDER WORKING_DIR ${F3D_SOURCE_DIR}/testing/data/AutoCompletion) #Escape;add_files F;Tab
+  endif()
+
+  if(NOT WIN32)
+    # Does not work on windows as it uses hard coded slash
+    f3d_test(NAME TestInteractionConsoleAutoCompleteFilesystemError INTERACTION UI NO_BASELINE LONG_TIMEOUT REGEXP "File name too long") #Escape;ad;Tab;_f;Tab; ;Tab;300*a/a;Tab;Esc
+  endif()
+endif()
+
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250513)
+  f3d_test(NAME TestInteractionAxesGridToggle INTERACTION DATA suzanne.ply THRESHOLD 0.08)#Shift+x
 endif()
 
 f3d_test(NAME TestInteractionMinimalConsoleOpen DATA f3d.glb INTERACTION UI) #Colon
@@ -1043,10 +1232,17 @@
 f3d_test(NAME TestInteractionMinimalConsoleInvalidCommand DATA f3d.glb INTERACTION UI) #Colon;foo;Return
 f3d_test(NAME TestInteractionMinimalConsoleResize DATA f3d.glb INTERACTION UI) #Colon;foo;Return;Colon
 f3d_test(NAME TestInteractionMinimalConsoleBadgeRemains DATA f3d.glb INTERACTION UI) #Colon;foo;Return;Colon;Escape
+f3d_test(NAME TestInteractionMinimalConsoleOverCheatSheet DATA f3d.glb INTERACTION UI) #h;:
+f3d_test(NAME TestInteractionMinimalConsoleOverCheatSheetAndFilename DATA f3d.glb INTERACTION UI) #h;n;:
 
 if (F3D_MODULE_TINYFILEDIALOGS)
-f3d_test(NAME TestInteractionFileOpen INTERACTION NO_BASELINE ENV CTEST_OPEN_DIALOG_FILE=${F3D_SOURCE_DIR}/testing/data/cow.vtp)
+  f3d_test(NAME TestInteractionFileOpen INTERACTION NO_BASELINE ENV CTEST_OPEN_DIALOG_FILE=${F3D_SOURCE_DIR}/testing/data/cow.vtp)
 endif ()
+
+if(F3D_MODULE_DMON)
+  f3d_test(NAME TestInteractionVerboseWatchUnwatch ARGS --watch --verbose DATA cow.vtp multi/dragon.vtu NO_BASELINE INTERACTION REGEXP "Stopped watching") #Right
+endif()
+
 # Test camera preserving/resetting when switching files in interaction mode
 f3d_test(NAME TestInteractionSwitchFileNoCameraKeeping DATA cow.vtp cowlow.vtp INTERACTION)
 f3d_test(NAME TestInteractionSwitchFilePrevCameraKeeping DATA cow.vtp cowlow.vtp INTERACTION)
@@ -1064,33 +1260,41 @@
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250507)
   f3d_test(NAME TestInteractionAnimationCycleAnimationSingle DATA soldier_animations.mdl INTERACTION) #WWWWWWWWWWWW;Space;Space;
 endif()
-
-f3d_test(NAME TestInteractionDropFiles ARGS -n INTERACTION_CONFIGURE UI)#X;DropEvent cow.vtp;DropEvent dragon.vtu suzanne.stl;
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestInteractionDropFiles ARGS -n INTERACTION_CONFIGURE UI)#X;DropEvent cow.vtp;DropEvent dragon.vtu suzanne.stl;
+  f3d_test(NAME TestInteractionDropSameFiles ARGS -x INTERACTION_CONFIGURE UI) #DropEvent cow.vtp;#DropEvent dragon.vtu;#DropEvent cow.vtp#DropEvent cow.vtp;
+endif()
 f3d_test(NAME TestInteractionMultiFileDrop ARGS --multi-file-mode=all -e INTERACTION_CONFIGURE) #DropEvent mb_1_0.vtp mb_2_0.vtp
-f3d_test(NAME TestInteractionDropSameFiles ARGS -x INTERACTION_CONFIGURE) #DropEvent cow.vtp;#DropEvent dragon.vtu;#DropEvent cow.vtp#DropEvent cow.vtp;
 
 # A proper test for this is not possible because of the double quotes
 f3d_test(NAME TestInteractionDropFileWithQuotes ARGS -n INTERACTION REGEXP "\"'`Quotes\"'`.stl does not exist" NO_BASELINE)#X;DropEvent "'`Quotes"'`.stl";
 
 if(WIN32)
-  # Windows specific drop test, using backslashes
-  f3d_test(NAME TestInteractionDropFileBackslashes ARGS -n INTERACTION_CONFIGURE)#X;DropEvent path\to\cow.vtp;
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+    # Windows specific drop test, using backslashes
+    f3d_test(NAME TestInteractionDropFileBackslashes ARGS -n INTERACTION_CONFIGURE)#X;DropEvent path\to\cow.vtp;
+  endif()
 endif()
 
-# HDRI test needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9767
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221220)
-  f3d_test(NAME TestInteractionDropHDRI INTERACTION_CONFIGURE LONG_TIMEOUT)#X;DropEvent dragon.vtu;DropEvent palermo.hdr;
-  f3d_test(NAME TestInteractionDropHDRIInvert INTERACTION_CONFIGURE LONG_TIMEOUT)#X;DropEvent palermo.hdr;DropEvent dragon.vtu;
-  f3d_test(NAME TestInteractionDropHDRIMulti INTERACTION_CONFIGURE LONG_TIMEOUT)#X;DropEvent dragon.vtu palermo.hdr;
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  # Interaction HDRI tests
+  f3d_test(NAME TestInteractionDropHDRI INTERACTION_CONFIGURE LONG_TIMEOUT UI)#X;DropEvent dragon.vtu;DropEvent palermo.hdr;
+  f3d_test(NAME TestInteractionDropHDRIInvert INTERACTION_CONFIGURE LONG_TIMEOUT UI)#X;DropEvent palermo.hdr;DropEvent dragon.vtu;
+  f3d_test(NAME TestInteractionDropHDRIMulti INTERACTION_CONFIGURE LONG_TIMEOUT UI)#X;DropEvent dragon.vtu palermo.hdr;
+endif()
+# Test modified drops, this test rendering is impacted by https://github.com/f3d-app/f3d/issues/1558
+# Empty drop is for coverage
+f3d_test(NAME TestInteractionDropHDRIModifiers INTERACTION_CONFIGURE LONG_TIMEOUT)#CTRL+DropEvent f3d.tif;SHIFT+DropEvent;SHIFT+DropEvent palermo.tif;SYYYY
 
-  # Test modified drops, this test rendering is impacted by https://github.com/f3d-app/f3d/issues/1558
-  # Empty drop is for coverage
-  f3d_test(NAME TestInteractionDropHDRIModifiers INTERACTION_CONFIGURE LONG_TIMEOUT)#CTRL+DropEvent f3d.tif;SHIFT+DropEvent;SHIFT+DropEvent palermo.tif;SYYYY
+# Test interactive animation and dropping HDRI
+f3d_test(NAME TestInteractionAnimationDropHDRI DATA InterpolationTest.glb ARGS --animation-indices=-1 --animation-progress INTERACTION_CONFIGURE LONG_TIMEOUT)#Space;DropEvent palermo.hdr;Space;
 
-  # Test interactive animation and dropping HDRI
-  f3d_test(NAME TestInteractionAnimationDropHDRI DATA InterpolationTest.glb ARGS --animation-indices=-1 --animation-progress INTERACTION_CONFIGURE LONG_TIMEOUT)#Space;DropEvent palermo.hdr;Space;
-
-  if(F3D_MODULE_EXR)
+if(F3D_MODULE_EXR)
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
     f3d_test(NAME TestInteractionDropHDRIExr INTERACTION_CONFIGURE LONG_TIMEOUT)#X;DropEvent kloofendal.exr;DropEvent dragon.vtu;
   endif()
 endif()
@@ -1112,15 +1316,18 @@
 f3d_test(NAME TestCommandScriptAzimuth DATA dragon.vtu SCRIPT TestCommandScriptAzimuth.txt) # azimuth_camera 90;toggle ui.scalar_bar;print_scene_info;increase_light_intensity
 f3d_test(NAME TestCommandScriptInvalidCommand DATA dragon.vtu SCRIPT TestCommandScriptInvalid.txt REGEXP "Command: \"INVALID_COMMAND_1\" is not recognized, ignoring" NO_BASELINE) # INVALID_COMMAND_1
 f3d_test(NAME TestCommandScriptMissingFile DATA dragon.vtu SCRIPT TestCommandScriptMissingFile.txt REGEXP "Unable to open command script file" NO_BASELINE)
-f3d_test(NAME TestCommandScriptPrintScene DATA dragon.vtu SCRIPT TestCommandScriptPrintScene.txt REGEXP "Camera position: 2.23745,3.83305,507.598" NO_BASELINE) # print_scene_info
+f3d_test(NAME TestCommandScriptPrintScene DATA dragon.vtu SCRIPT TestCommandScriptPrintScene.txt REGEXP "Camera position: 2.23745, 3.83305, 507.598" NO_BASELINE) # print_scene_info
 f3d_test(NAME TestCommandScriptPrintColoring DATA dragon.vtu SCRIPT TestCommandScriptPrintColoring.txt REGEXP "Not coloring" NO_BASELINE) # print_coloring_info
 f3d_test(NAME TestCommandScriptPrintMesh DATA dragon.vtu SCRIPT TestCommandScriptPrintMesh.txt REGEXP "Number of points: 13268" NO_BASELINE) # print_mesh_info
 f3d_test(NAME TestCommandScriptPrintOptions DATA dragon.vtu SCRIPT TestCommandScriptPrintOptions.txt REGEXP "interactor.invert_zoom: false" NO_BASELINE) # print_options_info
 f3d_test(NAME TestCommandScriptAlias DATA dragon.vtu SCRIPT TestCommandScriptAlias.txt --reference=${F3D_SOURCE_DIR}/testing/baselines/TestCommandScriptAlias.png) # alias myrotate roll_camera 90;myrotate
 f3d_test(NAME TestCommandScriptReset DATA dragon.vtu suzanne.stl ARGS --edges SCRIPT TestCommandScriptReset.txt) # reset render.show_edges; load_next_file_group;
-f3d_test(NAME TestParseOptionalBoolExtraArg DATA dragon.vtu SCRIPT TestParseOptionalBoolExtraArg.txt REGEXP "Command: load_previous_file_group takes at most 1 argument, got 2 arguments instead." NO_BASELINE) # load_previous_file_group true extra
-f3d_test(NAME TestRemoveFileGroups DATA dragon.vtu SCRIPT TestRemoveFileGroups.txt NO_DATA_FORCE_RENDER UI) # remove_file_groups
+f3d_test(NAME TestCommandScriptParseOptionalBoolExtraArg DATA dragon.vtu SCRIPT TestCommandScriptParseOptionalBoolExtraArg.txt REGEXP "Command: load_previous_file_group takes at most 1 argument, got 2 arguments instead." NO_BASELINE) # load_previous_file_group true extra
+f3d_test(NAME TestCommandScriptRemoveCurrentFileGroup DATA cow.vtp dragon.vtu SCRIPT TestCommandScriptRemoveCurrentFileGroup.txt) # remove_current_file_group
+f3d_test(NAME TestCommandScriptRemoveFileGroups DATA dragon.vtu SCRIPT TestCommandScriptRemoveFileGroups.txt NO_DATA_FORCE_RENDER UI) # remove_file_groups
 f3d_test(NAME TestCommandScriptInvalidReaderOptions DATA dragon.vtu SCRIPT TestCommandScriptInvalidReaderOptions.txt REGEXP "point to an inexistent option, ignoring" NO_BASELINE) # set_reader_option invalid value
+f3d_test(NAME TestCommandScriptHelp DATA dragon.vtu SCRIPT TestCommandScriptHelp.txt REGEX "set a libf3d option" NO_BASELINE) # help set
+f3d_test(NAME TestCommandScriptHelpInvalid DATA dragon.vtu SCRIPT TestCommandScriptHelpInvalid.txt REGEX "is not a recognized command" NO_BASELINE) # help invalid
 
 # Window position test
 f3d_test(NAME TestPosition DATA dragon.vtu ARGS --position=100,100 NO_BASELINE)
@@ -1188,7 +1395,7 @@
 f3d_test(NAME TestVerboseGrid DATA suzanne.ply ARGS -g --verbose REGEXP "Grid origin set to" NO_BASELINE)
 
 # Test bounding box no render output
-f3d_test(NAME TestNoRenderBBox DATA suzanne.ply NO_RENDER REGEXP "Scene bounding box: -1.32819,1.32819,-0.971822,0.939236,-0.778266,0.822441")
+f3d_test(NAME TestNoRenderBBox DATA suzanne.ply NO_RENDER REGEXP "Scene bounding box: -1.32819 ≤ x ≤ 1.32819, -0.971822 ≤ y ≤ 0.939236, -0.778266 ≤ z ≤ 0.822441")
 
 # Test Scalars coloring verbose output
 f3d_test(NAME TestVerboseScalars DATA suzanne.ply ARGS -s --verbose REGEXP "Coloring using point array named Normals, Magnitude." NO_BASELINE)
@@ -1212,7 +1419,7 @@
 f3d_test(NAME TestInvalidBackface DATA backface.vtp ARGS --backface-type=invalid REGEXP "is not a valid backface type, assuming it is not set" NO_BASELINE)
 
 # Test non existent file, do not create nonExistentFile.vtp
-f3d_test(NAME TestVerboseNonExistentFile DATA nonExistentFile.vtp REGEXP "File .*nonExistentFile.vtp does not exist" NO_RENDER)
+f3d_test(NAME TestVerboseNonExistentFile DATA nonExistentFile.vtp REGEXP ".*nonExistentFile.vtp does not exist" NO_RENDER)
 
 # Test non existent font file, do not create nonExistentFile.ttf
 f3d_test(NAME TestVerboseNonExistentFont DATA suzanne.ply ARGS -n --font-file=${F3D_SOURCE_DIR}/testing/data/nonExistentFile.ttf REGEXP "Cannot find \".*nonExistentFile.ttf\" font file" NO_BASELINE)
@@ -1327,7 +1534,10 @@
   set(GLOBSTAR_EXPR "**/*stl")
 endif()
 configure_file("${F3D_SOURCE_DIR}/testing/configs/glob.json.in" "${CMAKE_BINARY_DIR}/glob.json")
-f3d_test(NAME TestMatchFirstConfigGlobOptionBlock DATA dragon.vtu CONFIG ${CMAKE_BINARY_DIR}/glob.json)
+# Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+  f3d_test(NAME TestMatchFirstConfigGlobOptionBlock DATA dragon.vtu CONFIG ${CMAKE_BINARY_DIR}/glob.json UI)
+endif()
 f3d_test(NAME TestMatchSecondConfigGlobOptionBlock DATA suzanne.stl CONFIG ${CMAKE_BINARY_DIR}/glob.json)
 
 # Test exact matching
@@ -1375,7 +1585,7 @@
 endif()
 
 # Test bindings-list display
-f3d_test(NAME TestBindingsList ARGS --list-bindings REGEXP "Any.5        Toggle Orthographic Projection")
+f3d_test(NAME TestBindingsList ARGS --list-bindings REGEXP "Any.5        Orthographic Projection")
 
 # Test rendering backends
 # For some reason the sanitizer detects leaks because of EGL and OSMesa
@@ -1449,7 +1659,10 @@
   set_tests_properties(f3d::TestHOMEOutputTemplate PROPERTIES ENVIRONMENT "HOME=${CMAKE_BINARY_DIR}")
 
   f3d_test(NAME TestHOMEInput ARGS --input=~/testing/data/suzanne.stl ENV "HOME=${F3D_SOURCE_DIR}")
-  f3d_test(NAME TestHOMEConfig DATA suzanne.stl CONFIG ~/testing/configs/complex.json ENV "HOME=${F3D_SOURCE_DIR}")
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+    f3d_test(NAME TestHOMEConfig DATA suzanne.stl CONFIG ~/testing/configs/complex.json ENV "HOME=${F3D_SOURCE_DIR}" UI)
+  endif()
   f3d_test(NAME TestHOMEPlugin ARGS --load-plugins=~/testing/data/invalid.so REGEXP "Cannot open the library" NO_BASELINE ENV "HOME=${F3D_SOURCE_DIR}")
   f3d_test(NAME TestHOMEInteractionRecord DATA cow.vtp ARGS --interaction-test-record=~/Testing/Temporary/TestHOMEInteractionRecord.log NO_BASELINE ENV "HOME=${CMAKE_BINARY_DIR}")
   f3d_test(NAME TestHOMEInteractionPlay DATA cow.vtp ARGS --interaction-test-play=~/Testing/Temporary/TestHOMEInteractionRecord.log DEPENDS TestHOMEInteractionRecord NO_BASELINE ENV "HOME=${CMAKE_BINARY_DIR}")
@@ -1458,10 +1671,10 @@
   f3d_test(NAME TestHOMECommandScript DATA suzanne.ply ARGS --command-script=~/testing/scripts/TestHOMECommandScript.txt REGEXP "Camera focal point" NO_BASELINE ENV "HOME=${F3D_SOURCE_DIR}")
   f3d_test(NAME TestHOMEFontFile DATA suzanne.stl ARGS -n --font-file=~/testing/data/Crosterian.ttf ENV "HOME=${F3D_SOURCE_DIR}" UI)
   f3d_test(NAME TestHOMETexture DATA suzanne.ply ARGS --texture-matcap=~/testing/data/skin.png ENV "HOME=${F3D_SOURCE_DIR}")
-
-  if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221220)
-    f3d_test(NAME TestHOMEHDRI DATA suzanne.stl ARGS --hdri-file=~/testing/data/palermo_park_1k.hdr --hdri-ambient --hdri-skybox ENV "HOME=${F3D_SOURCE_DIR}" LONG_TIMEOUT)
-    f3d_test(NAME TestHOMEInteractionDropHDRICollapse INTERACTION_CONFIGURE ENV "HOME=${F3D_SOURCE_DIR}" LONG_TIMEOUT)#X;DropEvent dragon.vtu;DropEvent palermo.hdr;
+  f3d_test(NAME TestHOMEHDRI DATA suzanne.stl ARGS --hdri-file=~/testing/data/palermo_park_1k.hdr --hdri-ambient --hdri-skybox ENV "HOME=${F3D_SOURCE_DIR}" LONG_TIMEOUT)
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+    f3d_test(NAME TestHOMEInteractionDropHDRICollapse INTERACTION_CONFIGURE ENV "HOME=${F3D_SOURCE_DIR}" LONG_TIMEOUT UI)#X;DropEvent dragon.vtu;DropEvent palermo.hdr;
   endif()
 
   STRING(RANDOM LENGTH 257 ALPHABET "x" _f3d_test_invalid_folder)
@@ -1543,8 +1756,7 @@
 endif()
 
 # Watch testing require GLX rendering on Linux
-# SIGTERM is not caught with VTK 9.2.6 for some reason
-if(F3D_MODULE_DMON AND (WIN32 OR APPLE OR (F3D_TESTING_ENABLE_GLX_TESTS AND VTK_VERSION VERSION_GREATER_EQUAL 9.3.0)))
+if(F3D_MODULE_DMON AND (WIN32 OR APPLE OR F3D_TESTING_ENABLE_GLX_TESTS))
   if(UNIX)
     set(_f3d_os_script_ext "sh")
     set(_f3d_os_script_exec "")
@@ -1554,5 +1766,5 @@
   endif()
   # Custom bash/pwsh test for testing watch option
   add_test (NAME f3d::TestWatch COMMAND ${_f3d_os_script_exec} ${CMAKE_CURRENT_SOURCE_DIR}/test_watch.${_f3d_os_script_ext} $<TARGET_FILE:f3d> ${F3D_SOURCE_DIR}/testing/data ${CMAKE_BINARY_DIR}/Testing/Temporary)
-  set_tests_properties(f3d::TestWatch PROPERTIES RUN_SERIAL TRUE TIMEOUT 30)
+  set_tests_properties(f3d::TestWatch PROPERTIES RUN_SERIAL TRUE TIMEOUT 90)
 endif()
diff -Nru f3d-3.2.0+dfsg/application/testing/test_watch.ps1 f3d-3.4.1/application/testing/test_watch.ps1
--- f3d-3.2.0+dfsg/application/testing/test_watch.ps1	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/testing/test_watch.ps1	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,6 @@
-# Test the watch feature by opening a file
-# rewriting it and checking the the file has been
-# automatically reloaded
+# Test the watch feature by opening an innexistent file
+# rewriting it as invalid, rewriting as lower resolution
+# and checking the the file has been automatically reloaded
 
 $f3d_cmd = $args[0]
 $data_dir = $args[1]
@@ -8,16 +8,21 @@
 
 $hires_data = "$data_dir/cow.vtp"
 $lowres_data = "$data_dir/cowlow.vtp"
+$invalid_data = "$data_dir/invalid.vtp"
 $reloaded_data = "$tmp_dir/cow.vtp"
 
-Copy-Item $hires_data -Destination $reloaded_data
+Remove-Item $reloaded_data
 
 $log = "$tmp_dir/output.log"
 $id = (Start-Process -FilePath $f3d_cmd -ArgumentList "--watch --verbose $reloaded_data" -RedirectStandardOutput $log -PassThru).Id
 
-Start-Sleep -Seconds 3
+Start-Sleep -Seconds 1
+Copy-Item $hires_data -Destination $reloaded_data
+Start-Sleep -Seconds 1
+Copy-Item $invalid_data -Destination $reloaded_data
+Start-Sleep -Seconds 1
 Copy-Item $lowres_data -Destination $reloaded_data
-Start-Sleep -Seconds 3
+Start-Sleep -Seconds 1
 
 $str = Select-String $log -Pattern "Number of points: 634"
 if ($str -ne $null) {
diff -Nru f3d-3.2.0+dfsg/application/testing/test_watch.sh f3d-3.4.1/application/testing/test_watch.sh
--- f3d-3.2.0+dfsg/application/testing/test_watch.sh	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/application/testing/test_watch.sh	2026-01-08 08:41:55.000000000 +0200
@@ -1,8 +1,8 @@
 #!/bin/bash
 
-# Test the watch feature by opening a file
-# rewriting it and checking the the file has been
-# automatically reloaded
+# Test the watch feature by opening an innexistent file
+# rewriting it as invalid, rewriting as lower resolution
+# and checking the the file has been automatically reloaded
 
 set -euo pipefail
 f3d_cmd=$1
@@ -11,9 +11,10 @@
 
 hires_data=$data_dir/cow.vtp
 lowres_data=$data_dir/cowlow.vtp
+invalid_data=$data_dir/invalid.vtp
 reloaded_data=$tmp_dir/cow.vtp
 
-cp $hires_data $reloaded_data
+rm -rf $reloaded_data
 
 log=$tmp_dir/output.log
 $f3d_cmd --watch --verbose $reloaded_data > $log &
@@ -25,8 +26,12 @@
 }
 trap "cleanup" EXIT
 
-sleep 3
+sleep 1
+cp $hires_data $reloaded_data
+sleep 1
+cp $invalid_data $reloaded_data
+sleep 1
 cp $lowres_data $reloaded_data
-sleep 3
+sleep 1
 
 grep -q "Number of points: 634" $log
diff -Nru f3d-3.2.0+dfsg/assets/main.js f3d-3.4.1/assets/main.js
--- f3d-3.2.0+dfsg/assets/main.js	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/assets/main.js	1970-01-01 02:00:00.000000000 +0200
@@ -1,115 +0,0 @@
-function onload(event) {
-  if (window.location.pathname.endsWith("/INSTALLATION.html")) {
-    enhance_install_page();
-  }
-}
-
-function enhance_install_page() {
-  function open_mac_warning_popup() {
-    const popup = document.createElement("div");
-    popup.classList = "popup-container bg-grey-dk-200";
-    popup.innerHTML = `
-        <h2>Warning</h2>
-        <p>MacOS package is not signed, if macOS complains that <b>the file is invalid, damaged or corrupted</b>, see the <a href="/doc/user/LIMITATIONS_AND_TROUBLESHOOTING.html#macos">troubleshooting section</a> for a workaround.</p>
-    `;
-
-    const button = document.createElement("button");
-    button.classList = "btn btn-primary";
-    button.innerHTML = "Ok";
-    button.addEventListener("click", function () {
-      popup.remove();
-    });
-    popup.append(button);
-
-    document.body.append(popup);
-  }
-
-  function retrieve_downloads(target_platform_re) {
-    var downloads = [];
-    for (const table of document.querySelectorAll("table")) {
-      const th = document.querySelector("th");
-      if (th) {
-        if (/Platform/i.test(th.innerText)) {
-          for (const table_row of table.querySelectorAll("tr")) {
-            const table_cols = table_row.querySelectorAll("td");
-            if (table_cols.length > 1) {
-              const link = table_cols[1].querySelector("a");
-              if (link) {
-                const platform = table_cols[0].innerText;
-
-                if (target_platform_re.test(platform))
-                  downloads.push([platform, link.innerText, link.href]);
-
-                if (/MacOS/i.test(platform))
-                  link.addEventListener("click", open_mac_warning_popup);
-              }
-            }
-          }
-          break; // stop after first relevant table
-        }
-      }
-    }
-    return downloads;
-  }
-
-  /* check OS: first check for `?os=foo` in the URL, otherwise guess form user agent */
-  const params = new URLSearchParams(window.location.search);
-  const current_os = params.get("os") || guess_client_OS();
-
-  if (current_os == null) return;
-
-  /* find download links matching the OS */
-  const target_platform_re = RegExp(`(${current_os})`, "i");
-  const downloads = retrieve_downloads(target_platform_re);
-
-  if (downloads.length < 1) return;
-
-  /* create a box below the first header to insert buttons */
-  const header = document.querySelector("h1");
-  if (header == null) return;
-  const div = document.createElement("div");
-  div.setAttribute("class", "install-buttons");
-  header.parentNode.insertBefore(div, header.nextSibling);
-
-  /* create download button for each matching link */
-  var i = 0;
-  for ([platform, filename, url] of downloads) {
-    const bolded_platform = platform.replace(target_platform_re, "<b>$1</b>");
-    const link = document.createElement("a");
-    link.setAttribute("href", url);
-    link.setAttribute("class", i++ ? "btn" : "btn btn-primary");
-    link.innerHTML = `<div>Get <b>F3D</b> for ${bolded_platform}</div><small>${filename}</small>`;
-    if (current_os === "MacOS")
-      link.addEventListener("click", open_mac_warning_popup);
-    div.append(link);
-  }
-  const note = document.createElement("div");
-  note.innerHTML = "<small>or see other available versions below</small>";
-  div.append(note);
-}
-
-function guess_client_OS() {
-  /* https://stackoverflow.com/a/38241481 */
-  const userAgent = window.navigator.userAgent;
-  const platform =
-    window.navigator?.userAgentData?.platform || window.navigator.platform;
-  const macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"];
-  const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
-  const iosPlatforms = ["iPhone", "iPad", "iPod"];
-
-  if (macosPlatforms.indexOf(platform) !== -1) {
-    return "MacOS";
-  } else if (iosPlatforms.indexOf(platform) !== -1) {
-    return "iOS";
-  } else if (windowsPlatforms.indexOf(platform) !== -1) {
-    return "Windows";
-  } else if (/Android/.test(userAgent)) {
-    return "Android";
-  } else if (/Linux/.test(platform)) {
-    return "Linux";
-  } else {
-    return null;
-  }
-}
-
-window.addEventListener("load", onload);
diff -Nru f3d-3.2.0+dfsg/c/camera_c_api.cxx f3d-3.4.1/c/camera_c_api.cxx
--- f3d-3.2.0+dfsg/c/camera_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/camera_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,285 @@
+#include "camera_c_api.h"
+#include "camera.h"
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_position(f3d_camera_t* camera, const f3d_point3_t pos)
+{
+  if (!camera || !pos)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  f3d::point3_t cpp_pos = { pos[0], pos[1], pos[2] };
+  cpp_camera->setPosition(cpp_pos);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_get_position(const f3d_camera_t* camera, f3d_point3_t pos)
+{
+  if (!camera || !pos)
+  {
+    return;
+  }
+
+  const f3d::camera* cpp_camera = reinterpret_cast<const f3d::camera*>(camera);
+  f3d::point3_t cpp_pos;
+  cpp_camera->getPosition(cpp_pos);
+  pos[0] = cpp_pos[0];
+  pos[1] = cpp_pos[1];
+  pos[2] = cpp_pos[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_focal_point(f3d_camera_t* camera, const f3d_point3_t focal_point)
+{
+  if (!camera || !focal_point)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  f3d::point3_t cpp_focal = { focal_point[0], focal_point[1], focal_point[2] };
+  cpp_camera->setFocalPoint(cpp_focal);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_get_focal_point(const f3d_camera_t* camera, f3d_point3_t focal_point)
+{
+  if (!camera || !focal_point)
+  {
+    return;
+  }
+
+  const f3d::camera* cpp_camera = reinterpret_cast<const f3d::camera*>(camera);
+  f3d::point3_t cpp_focal;
+  cpp_camera->getFocalPoint(cpp_focal);
+  focal_point[0] = cpp_focal[0];
+  focal_point[1] = cpp_focal[1];
+  focal_point[2] = cpp_focal[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_view_up(f3d_camera_t* camera, const f3d_vector3_t view_up)
+{
+  if (!camera || !view_up)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  f3d::vector3_t cpp_view_up = { view_up[0], view_up[1], view_up[2] };
+  cpp_camera->setViewUp(cpp_view_up);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_get_view_up(const f3d_camera_t* camera, f3d_vector3_t view_up)
+{
+  if (!camera || !view_up)
+  {
+    return;
+  }
+
+  const f3d::camera* cpp_camera = reinterpret_cast<const f3d::camera*>(camera);
+  f3d::vector3_t cpp_view_up;
+  cpp_camera->getViewUp(cpp_view_up);
+  view_up[0] = cpp_view_up[0];
+  view_up[1] = cpp_view_up[1];
+  view_up[2] = cpp_view_up[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_view_angle(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->setViewAngle(angle);
+}
+
+//----------------------------------------------------------------------------
+f3d_angle_deg_t f3d_camera_get_view_angle(const f3d_camera_t* camera)
+{
+  if (!camera)
+  {
+    return 0.0;
+  }
+
+  const f3d::camera* cpp_camera = reinterpret_cast<const f3d::camera*>(camera);
+  return cpp_camera->getViewAngle();
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_state(f3d_camera_t* camera, const f3d_camera_state_t* state)
+{
+  if (!camera || !state)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  f3d::camera_state_t cpp_state;
+  cpp_state.position = { state->position[0], state->position[1], state->position[2] };
+  cpp_state.focalPoint = { state->focal_point[0], state->focal_point[1], state->focal_point[2] };
+  cpp_state.viewUp = { state->view_up[0], state->view_up[1], state->view_up[2] };
+  cpp_state.viewAngle = state->view_angle;
+  cpp_camera->setState(cpp_state);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_get_state(const f3d_camera_t* camera, f3d_camera_state_t* state)
+{
+  if (!camera || !state)
+  {
+    return;
+  }
+
+  const f3d::camera* cpp_camera = reinterpret_cast<const f3d::camera*>(camera);
+  f3d::camera_state_t cpp_state;
+  cpp_camera->getState(cpp_state);
+  state->position[0] = cpp_state.position[0];
+  state->position[1] = cpp_state.position[1];
+  state->position[2] = cpp_state.position[2];
+  state->focal_point[0] = cpp_state.focalPoint[0];
+  state->focal_point[1] = cpp_state.focalPoint[1];
+  state->focal_point[2] = cpp_state.focalPoint[2];
+  state->view_up[0] = cpp_state.viewUp[0];
+  state->view_up[1] = cpp_state.viewUp[1];
+  state->view_up[2] = cpp_state.viewUp[2];
+  state->view_angle = cpp_state.viewAngle;
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_dolly(f3d_camera_t* camera, double val)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->dolly(val);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_pan(f3d_camera_t* camera, double right, double up, double forward)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->pan(right, up, forward);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_zoom(f3d_camera_t* camera, double factor)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->zoom(factor);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_roll(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->roll(angle);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_azimuth(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->azimuth(angle);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_yaw(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->yaw(angle);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_elevation(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->elevation(angle);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_pitch(f3d_camera_t* camera, f3d_angle_deg_t angle)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->pitch(angle);
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_set_current_as_default(f3d_camera_t* camera)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->setCurrentAsDefault();
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_reset_to_default(f3d_camera_t* camera)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->resetToDefault();
+}
+
+//----------------------------------------------------------------------------
+void f3d_camera_reset_to_bounds(f3d_camera_t* camera, double zoom_factor)
+{
+  if (!camera)
+  {
+    return;
+  }
+
+  f3d::camera* cpp_camera = reinterpret_cast<f3d::camera*>(camera);
+  cpp_camera->resetToBounds(zoom_factor);
+}
diff -Nru f3d-3.2.0+dfsg/c/camera_c_api.h f3d-3.4.1/c/camera_c_api.h
--- f3d-3.2.0+dfsg/c/camera_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/camera_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,204 @@
+#ifndef F3D_CAMERA_C_API_H
+#define F3D_CAMERA_C_API_H
+
+#include "export.h"
+#include "types_c_api.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Opaque handle to an f3d::camera object.
+   */
+  typedef struct f3d_camera_t f3d_camera_t;
+
+  /**
+   * @brief Structure containing all information to configure a camera.
+   */
+  typedef struct f3d_camera_state_t
+  {
+    f3d_point3_t position;
+    f3d_point3_t focal_point;
+    f3d_vector3_t view_up;
+    f3d_angle_deg_t view_angle;
+  } f3d_camera_state_t;
+
+  /**
+   * @brief Set the position of the camera.
+   *
+   * @param camera Camera handle.
+   * @param pos Position array [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_set_position(f3d_camera_t* camera, const f3d_point3_t pos);
+
+  /**
+   * @brief Get the position of the camera.
+   *
+   * @param camera Camera handle.
+   * @param pos Output position array [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_get_position(const f3d_camera_t* camera, f3d_point3_t pos);
+
+  /**
+   * @brief Set the focal point of the camera.
+   *
+   * @param camera Camera handle.
+   * @param focal_point Focal point array [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_set_focal_point(f3d_camera_t* camera, const f3d_point3_t focal_point);
+
+  /**
+   * @brief Get the focal point of the camera.
+   *
+   * @param camera Camera handle.
+   * @param focal_point Output focal point array [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_get_focal_point(const f3d_camera_t* camera, f3d_point3_t focal_point);
+
+  /**
+   * @brief Set the view up vector of the camera.
+   *
+   * @param camera Camera handle.
+   * @param view_up View up vector [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_set_view_up(f3d_camera_t* camera, const f3d_vector3_t view_up);
+
+  /**
+   * @brief Get the view up vector of the camera.
+   *
+   * @param camera Camera handle.
+   * @param view_up Output view up vector [x, y, z].
+   */
+  F3D_EXPORT void f3d_camera_get_view_up(const f3d_camera_t* camera, f3d_vector3_t view_up);
+
+  /**
+   * @brief Set the view angle in degrees of the camera.
+   *
+   * @param camera Camera handle.
+   * @param angle View angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_set_view_angle(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Get the view angle in degrees of the camera.
+   *
+   * @param camera Camera handle.
+   * @return View angle in degrees.
+   */
+  F3D_EXPORT f3d_angle_deg_t f3d_camera_get_view_angle(const f3d_camera_t* camera);
+
+  /**
+   * @brief Set the complete state of the camera.
+   *
+   * @param camera Camera handle.
+   * @param state Camera state structure.
+   */
+  F3D_EXPORT void f3d_camera_set_state(f3d_camera_t* camera, const f3d_camera_state_t* state);
+
+  /**
+   * @brief Get the complete state of the camera.
+   *
+   * @param camera Camera handle.
+   * @param state Output camera state structure.
+   */
+  F3D_EXPORT void f3d_camera_get_state(const f3d_camera_t* camera, f3d_camera_state_t* state);
+
+  /**
+   * @brief Divide the camera's distance from the focal point by the given value.
+   *
+   * @param camera Camera handle.
+   * @param val Value to divide distance by.
+   */
+  F3D_EXPORT void f3d_camera_dolly(f3d_camera_t* camera, double val);
+
+  /**
+   * @brief Move the camera along its horizontal, vertical, and forward axes.
+   *
+   * @param camera Camera handle.
+   * @param right Movement along the right axis.
+   * @param up Movement along the up axis.
+   * @param forward Movement along the forward axis.
+   */
+  F3D_EXPORT void f3d_camera_pan(f3d_camera_t* camera, double right, double up, double forward);
+
+  /**
+   * @brief Decrease the view angle (or the parallel scale in parallel mode) by the specified
+   * factor.
+   *
+   * @param camera Camera handle.
+   * @param factor Zoom factor.
+   */
+  F3D_EXPORT void f3d_camera_zoom(f3d_camera_t* camera, double factor);
+
+  /**
+   * @brief Rotate the camera about its forward axis.
+   *
+   * @param camera Camera handle.
+   * @param angle Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_roll(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Rotate the camera about its vertical axis, centered at the focal point.
+   *
+   * @param camera Camera handle.
+   * @param angle Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_azimuth(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Rotate the camera about its vertical axis, centered at the camera's position.
+   *
+   * @param camera Camera handle.
+   * @param angle Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_yaw(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Rotate the camera about its horizontal axis, centered at the focal point.
+   *
+   * @param camera Camera handle.
+   * @param angle Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_elevation(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Rotate the camera about its horizontal axis, centered at the camera's position.
+   *
+   * @param camera Camera handle.
+   * @param angle Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_camera_pitch(f3d_camera_t* camera, f3d_angle_deg_t angle);
+
+  /**
+   * @brief Store the current camera configuration as default.
+   *
+   * @param camera Camera handle.
+   */
+  F3D_EXPORT void f3d_camera_set_current_as_default(f3d_camera_t* camera);
+
+  /**
+   * @brief Reset the camera to the stored default camera configuration.
+   *
+   * @param camera Camera handle.
+   */
+  F3D_EXPORT void f3d_camera_reset_to_default(f3d_camera_t* camera);
+
+  /**
+   * @brief Reset the camera using the bounds of actors in the scene.
+   *
+   * Provided zoom_factor will be used to position the camera.
+   * A value of 1 corresponds to the bounds roughly aligned to the edges of the window.
+   *
+   * @param camera Camera handle.
+   * @param zoom_factor Zoom factor (default: 0.9).
+   */
+  F3D_EXPORT void f3d_camera_reset_to_bounds(f3d_camera_t* camera, double zoom_factor);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_CAMERA_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/CMakeLists.txt f3d-3.4.1/c/CMakeLists.txt
--- f3d-3.2.0+dfsg/c/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,111 @@
+cmake_minimum_required(VERSION 3.21)
+
+project(f3d_c_api)
+
+include(GNUInstallDirs)
+
+# List of source files
+set(F3D_C_SOURCE_FILES
+  ${CMAKE_CURRENT_SOURCE_DIR}/camera_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/context_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/engine_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/image_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/interactor_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/log_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/options_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/scene_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/types_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/utils_c_api.cxx
+  ${CMAKE_CURRENT_SOURCE_DIR}/window_c_api.cxx
+)
+
+# List of headers that will be installed
+set(F3D_C_PUBLIC_HEADERS
+  ${CMAKE_CURRENT_SOURCE_DIR}/camera_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/context_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/engine_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/image_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/interactor_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/log_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/options_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/scene_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/types_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/utils_c_api.h
+  ${CMAKE_CURRENT_SOURCE_DIR}/window_c_api.h
+)
+
+add_library(c_api ${F3D_C_SOURCE_FILES})
+set_target_properties(c_api PROPERTIES
+  OUTPUT_NAME "f3d_c_api"
+  CXX_VISIBILITY_PRESET hidden
+  CXX_STANDARD 17
+  CXX_STANDARD_REQUIRED ON
+  POSITION_INDEPENDENT_CODE ON
+)
+
+target_link_libraries(c_api PRIVATE libf3d)
+
+target_include_directories(c_api
+  PUBLIC
+  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+  $<BUILD_INTERFACE:${F3D_BINARY_DIR}/library/public>
+  INTERFACE
+  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
+  )
+
+# Required because we use libf3d own export.h
+target_compile_definitions(c_api PRIVATE libf3d_EXPORTS)
+
+target_compile_options(c_api PUBLIC ${f3d_sanitizer_compile_options})
+target_link_options(c_api PUBLIC ${f3d_sanitizer_link_options})
+
+if(APPLE)
+  set_target_properties(c_api PROPERTIES INSTALL_RPATH "@loader_path")
+elseif(UNIX)
+  set_target_properties(c_api PROPERTIES INSTALL_RPATH "$ORIGIN")
+endif()
+
+# testing
+if(BUILD_TESTING)
+  add_subdirectory(testing)
+endif()
+
+# installing
+if (BUILD_SHARED_LIBS)
+
+  # lib
+  install(TARGETS c_api
+    EXPORT f3dCApiTargets
+    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT c_api
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT c_api
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT c_api
+  )
+
+  # includes
+  install(FILES ${F3D_C_PUBLIC_HEADERS}
+    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/f3d"
+    COMPONENT c_api)
+
+  # cmake
+  export(TARGETS c_api
+    NAMESPACE f3d::
+    APPEND
+    FILE "${CMAKE_BINARY_DIR}/cmake/f3dCApiTargets.cmake")
+
+  install(EXPORT f3dCApiTargets
+    NAMESPACE f3d::
+    FILE f3dCApiTargets.cmake
+    DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/f3d"
+    COMPONENT sdk EXCLUDE_FROM_ALL
+  )
+
+  file(COPY "${F3D_SOURCE_DIR}/cmake/c_api-config.cmake"
+    DESTINATION "${CMAKE_BINARY_DIR}/cmake")
+
+  install(
+    FILES
+    "${F3D_SOURCE_DIR}/cmake/c_api-config.cmake"
+    DESTINATION
+    "${CMAKE_INSTALL_LIBDIR}/cmake/f3d"
+    COMPONENT sdk EXCLUDE_FROM_ALL)
+endif ()
diff -Nru f3d-3.2.0+dfsg/c/context_c_api.cxx f3d-3.4.1/c/context_c_api.cxx
--- f3d-3.2.0+dfsg/c/context_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/context_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,59 @@
+#include "context_c_api.h"
+#include "context.h"
+
+struct f3d_context
+{
+  f3d::context::function func;
+
+  explicit f3d_context(f3d::context::function&& f)
+    : func(std::move(f))
+  {
+  }
+};
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_glx()
+{
+  return new f3d_context_t(f3d::context::glx());
+}
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_wgl()
+{
+  return new f3d_context_t(f3d::context::wgl());
+}
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_cocoa()
+{
+  return new f3d_context_t(f3d::context::cocoa());
+}
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_egl()
+{
+  return new f3d_context_t(f3d::context::egl());
+}
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_osmesa()
+{
+  return new f3d_context_t(f3d::context::osmesa());
+}
+
+//----------------------------------------------------------------------------
+f3d_context_t* f3d_context_get_symbol(const char* lib, const char* func)
+{
+  if (!lib || !func)
+  {
+    return nullptr;
+  }
+
+  return new f3d_context_t(f3d::context::getSymbol(lib, func));
+}
+
+//----------------------------------------------------------------------------
+void f3d_context_delete(f3d_context_t* ctx)
+{
+  delete ctx;
+}
diff -Nru f3d-3.2.0+dfsg/c/context_c_api.h f3d-3.4.1/c/context_c_api.h
--- f3d-3.2.0+dfsg/c/context_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/context_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,88 @@
+#ifndef F3D_CONTEXT_C_API_H
+#define F3D_CONTEXT_C_API_H
+
+#include "export.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Function pointer type for OpenGL symbol resolution.
+   */
+  typedef void (*(*f3d_context_function_t)(const char*))();
+
+  /**
+   * @brief Opaque handle to a context object.
+   */
+  typedef struct f3d_context f3d_context_t;
+
+  /**
+   * @brief Create a GLX context.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_glx();
+
+  /**
+   * @brief Create a WGL context.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_wgl();
+
+  /**
+   * @brief Create a COCOA context.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_cocoa();
+
+  /**
+   * @brief Create an EGL context.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_egl();
+
+  /**
+   * @brief Create an OSMesa context.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_osmesa();
+
+  /**
+   * @brief Create a context from a library name and function name.
+   *
+   * The returned context must be deleted with f3d_context_delete().
+   *
+   * @param lib Library name.
+   * @param func Function name to resolve.
+   * @return Context handle.
+   */
+  F3D_EXPORT f3d_context_t* f3d_context_get_symbol(const char* lib, const char* func);
+
+  /**
+   * @brief Delete a context object.
+   *
+   * @param ctx Context handle.
+   */
+  F3D_EXPORT void f3d_context_delete(f3d_context_t* ctx);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_CONTEXT_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/engine_c_api.cxx f3d-3.4.1/c/engine_c_api.cxx
--- f3d-3.2.0+dfsg/c/engine_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/engine_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,497 @@
+#include "engine_c_api.h"
+#include "engine.h"
+#include "options.h"
+
+#include <cstring>
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create(int offscreen)
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::create(offscreen != 0));
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_none()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createNone());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_glx(int offscreen)
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createGLX(offscreen != 0));
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_wgl(int offscreen)
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createWGL(offscreen != 0));
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_egl()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createEGL());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_osmesa()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createOSMesa());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external(f3d_context_function_t get_proc_address)
+{
+  if (!get_proc_address)
+  {
+    return nullptr;
+  }
+
+  f3d::context::function func = [get_proc_address](const char* name) -> f3d::context::fptr
+  { return get_proc_address(name); };
+
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternal(func));
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external_glx()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternalGLX());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external_wgl()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternalWGL());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external_cocoa()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternalCOCOA());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external_egl()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternalEGL());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+f3d_engine_t* f3d_engine_create_external_osmesa()
+{
+  f3d::engine* eng = new f3d::engine(f3d::engine::createExternalOSMesa());
+  return reinterpret_cast<f3d_engine_t*>(eng);
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_delete(f3d_engine_t* engine)
+{
+  if (!engine)
+  {
+    return;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  delete cpp_engine;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_set_cache_path(f3d_engine_t* engine, const char* cache_path)
+{
+  if (!engine || !cache_path)
+  {
+    return;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  cpp_engine->setCachePath(cache_path);
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_set_options(f3d_engine_t* engine, f3d_options_t* options)
+{
+  if (!engine || !options)
+  {
+    return;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_engine->setOptions(*cpp_options);
+}
+
+//----------------------------------------------------------------------------
+f3d_options_t* f3d_engine_get_options(f3d_engine_t* engine)
+{
+  if (!engine)
+  {
+    return nullptr;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  f3d::options& opts = cpp_engine->getOptions();
+  return reinterpret_cast<f3d_options_t*>(&opts);
+}
+
+//----------------------------------------------------------------------------
+f3d_window_t* f3d_engine_get_window(f3d_engine_t* engine)
+{
+  if (!engine)
+  {
+    return nullptr;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  f3d::window& win = cpp_engine->getWindow();
+  return reinterpret_cast<f3d_window_t*>(&win);
+}
+
+//----------------------------------------------------------------------------
+f3d_scene_t* f3d_engine_get_scene(f3d_engine_t* engine)
+{
+  if (!engine)
+  {
+    return nullptr;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  f3d::scene& sce = cpp_engine->getScene();
+  return reinterpret_cast<f3d_scene_t*>(&sce);
+}
+
+//----------------------------------------------------------------------------
+f3d_interactor_t* f3d_engine_get_interactor(f3d_engine_t* engine)
+{
+  if (!engine)
+  {
+    return nullptr;
+  }
+
+  f3d::engine* cpp_engine = reinterpret_cast<f3d::engine*>(engine);
+  f3d::interactor& inter = cpp_engine->getInteractor();
+  return reinterpret_cast<f3d_interactor_t*>(&inter);
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_load_plugin(const char* path_or_name)
+{
+  if (!path_or_name)
+  {
+    return;
+  }
+
+  f3d::engine::loadPlugin(path_or_name);
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_autoload_plugins()
+{
+  f3d::engine::autoloadPlugins();
+}
+
+//----------------------------------------------------------------------------
+char** f3d_engine_get_plugins_list(const char* plugin_path)
+{
+  if (!plugin_path)
+  {
+    return nullptr;
+  }
+
+  std::vector<std::string> plugins = f3d::engine::getPluginsList(plugin_path);
+
+  if (plugins.empty())
+  {
+    return nullptr;
+  }
+
+  char** result = new char*[plugins.size() + 1];
+
+  for (size_t i = 0; i < plugins.size(); i++)
+  {
+    result[i] = new char[plugins[i].length() + 1];
+    std::strcpy(result[i], plugins[i].c_str());
+  }
+
+  result[plugins.size()] = nullptr;
+  return result;
+}
+
+//----------------------------------------------------------------------------
+char** f3d_engine_get_all_reader_option_names()
+{
+  std::vector<std::string> names = f3d::engine::getAllReaderOptionNames();
+
+  char** result = new char*[names.size() + 1];
+
+  for (size_t i = 0; i < names.size(); i++)
+  {
+    result[i] = new char[names[i].length() + 1];
+    std::strcpy(result[i], names[i].c_str());
+  }
+
+  result[names.size()] = nullptr;
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_set_reader_option(const char* name, const char* value)
+{
+  if (!name || !value)
+  {
+    return;
+  }
+
+  f3d::engine::setReaderOption(name, value);
+}
+
+//----------------------------------------------------------------------------
+f3d_backend_info_t* f3d_engine_get_rendering_backend_list(int* count)
+{
+  std::map<std::string, bool> backends = f3d::engine::getRenderingBackendList();
+
+  f3d_backend_info_t* result = new f3d_backend_info_t[backends.size() + 1];
+
+  size_t i = 0;
+  for (const auto& [name, available] : backends)
+  {
+    result[i].name = new char[name.length() + 1];
+    std::strcpy(result[i].name, name.c_str());
+    result[i].available = available ? 1 : 0;
+    i++;
+  }
+
+  result[backends.size()].name = nullptr;
+  result[backends.size()].available = 0;
+
+  if (count)
+  {
+    *count = static_cast<int>(backends.size());
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_free_backend_list(f3d_backend_info_t* backends)
+{
+  if (!backends)
+  {
+    return;
+  }
+
+  for (int i = 0; backends[i].name != nullptr; i++)
+  {
+    delete[] backends[i].name;
+  }
+
+  delete[] backends;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_free_string_array(char** array)
+{
+  if (!array)
+  {
+    return;
+  }
+
+  for (int i = 0; array[i] != nullptr; i++)
+  {
+    delete[] array[i];
+  }
+
+  delete[] array;
+}
+
+//----------------------------------------------------------------------------
+f3d_lib_info_t* f3d_engine_get_lib_info()
+{
+  f3d::engine::libInformation cpp_info = f3d::engine::getLibInfo();
+
+  f3d_lib_info_t* info = new f3d_lib_info_t;
+
+  info->version = new char[cpp_info.Version.length() + 1];
+  std::strcpy(info->version, cpp_info.Version.c_str());
+
+  info->version_full = new char[cpp_info.VersionFull.length() + 1];
+  std::strcpy(info->version_full, cpp_info.VersionFull.c_str());
+
+  info->build_date = new char[cpp_info.BuildDate.length() + 1];
+  std::strcpy(info->build_date, cpp_info.BuildDate.c_str());
+
+  info->build_system = new char[cpp_info.BuildSystem.length() + 1];
+  std::strcpy(info->build_system, cpp_info.BuildSystem.c_str());
+
+  info->compiler = new char[cpp_info.Compiler.length() + 1];
+  std::strcpy(info->compiler, cpp_info.Compiler.c_str());
+
+  info->vtk_version = new char[cpp_info.VTKVersion.length() + 1];
+  std::strcpy(info->vtk_version, cpp_info.VTKVersion.c_str());
+
+  info->license = new char[cpp_info.License.length() + 1];
+  std::strcpy(info->license, cpp_info.License.c_str());
+
+  info->modules = new f3d_module_info_t[cpp_info.Modules.size() + 1];
+  size_t i = 0;
+  for (const auto& [name, available] : cpp_info.Modules)
+  {
+    info->modules[i].name = new char[name.length() + 1];
+    std::strcpy(info->modules[i].name, name.c_str());
+    info->modules[i].available = available ? 1 : 0;
+    i++;
+  }
+  info->modules[cpp_info.Modules.size()].name = nullptr;
+  info->modules[cpp_info.Modules.size()].available = 0;
+
+  info->copyrights = new char*[cpp_info.Copyrights.size() + 1];
+  for (size_t j = 0; j < cpp_info.Copyrights.size(); j++)
+  {
+    info->copyrights[j] = new char[cpp_info.Copyrights[j].length() + 1];
+    std::strcpy(info->copyrights[j], cpp_info.Copyrights[j].c_str());
+  }
+  info->copyrights[cpp_info.Copyrights.size()] = nullptr;
+
+  return info;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_free_lib_info(f3d_lib_info_t* info)
+{
+  if (!info)
+  {
+    return;
+  }
+
+  delete[] info->version;
+  delete[] info->version_full;
+  delete[] info->build_date;
+  delete[] info->build_system;
+  delete[] info->compiler;
+  delete[] info->vtk_version;
+  delete[] info->license;
+
+  if (info->modules)
+  {
+    for (int i = 0; info->modules[i].name != nullptr; i++)
+    {
+      delete[] info->modules[i].name;
+    }
+    delete[] info->modules;
+  }
+
+  if (info->copyrights)
+  {
+    for (int i = 0; info->copyrights[i] != nullptr; i++)
+    {
+      delete[] info->copyrights[i];
+    }
+    delete[] info->copyrights;
+  }
+
+  delete info;
+}
+
+//----------------------------------------------------------------------------
+f3d_reader_info_t* f3d_engine_get_readers_info(int* count)
+{
+  std::vector<f3d::engine::readerInformation> cpp_readers = f3d::engine::getReadersInfo();
+
+  f3d_reader_info_t* readers = new f3d_reader_info_t[cpp_readers.size() + 1];
+
+  for (size_t i = 0; i < cpp_readers.size(); i++)
+  {
+    const auto& cpp_reader = cpp_readers[i];
+
+    readers[i].name = new char[cpp_reader.Name.length() + 1];
+    std::strcpy(readers[i].name, cpp_reader.Name.c_str());
+
+    readers[i].description = new char[cpp_reader.Description.length() + 1];
+    std::strcpy(readers[i].description, cpp_reader.Description.c_str());
+
+    readers[i].plugin_name = new char[cpp_reader.PluginName.length() + 1];
+    std::strcpy(readers[i].plugin_name, cpp_reader.PluginName.c_str());
+
+    readers[i].extensions = new char*[cpp_reader.Extensions.size() + 1];
+    for (size_t j = 0; j < cpp_reader.Extensions.size(); j++)
+    {
+      readers[i].extensions[j] = new char[cpp_reader.Extensions[j].length() + 1];
+      std::strcpy(readers[i].extensions[j], cpp_reader.Extensions[j].c_str());
+    }
+    readers[i].extensions[cpp_reader.Extensions.size()] = nullptr;
+
+    readers[i].mime_types = new char*[cpp_reader.MimeTypes.size() + 1];
+    for (size_t j = 0; j < cpp_reader.MimeTypes.size(); j++)
+    {
+      readers[i].mime_types[j] = new char[cpp_reader.MimeTypes[j].length() + 1];
+      std::strcpy(readers[i].mime_types[j], cpp_reader.MimeTypes[j].c_str());
+    }
+    readers[i].mime_types[cpp_reader.MimeTypes.size()] = nullptr;
+
+    readers[i].has_scene_reader = cpp_reader.HasSceneReader ? 1 : 0;
+    readers[i].has_geometry_reader = cpp_reader.HasGeometryReader ? 1 : 0;
+  }
+
+  readers[cpp_readers.size()].name = nullptr;
+  readers[cpp_readers.size()].description = nullptr;
+  readers[cpp_readers.size()].extensions = nullptr;
+  readers[cpp_readers.size()].mime_types = nullptr;
+  readers[cpp_readers.size()].plugin_name = nullptr;
+  readers[cpp_readers.size()].has_scene_reader = 0;
+  readers[cpp_readers.size()].has_geometry_reader = 0;
+
+  if (count)
+  {
+    *count = static_cast<int>(cpp_readers.size());
+  }
+
+  return readers;
+}
+
+//----------------------------------------------------------------------------
+void f3d_engine_free_readers_info(f3d_reader_info_t* readers)
+{
+  if (!readers)
+  {
+    return;
+  }
+
+  for (int i = 0; readers[i].name != nullptr; i++)
+  {
+    delete[] readers[i].name;
+    delete[] readers[i].description;
+    delete[] readers[i].plugin_name;
+
+    if (readers[i].extensions)
+    {
+      for (int j = 0; readers[i].extensions[j] != nullptr; j++)
+      {
+        delete[] readers[i].extensions[j];
+      }
+      delete[] readers[i].extensions;
+    }
+
+    if (readers[i].mime_types)
+    {
+      for (int j = 0; readers[i].mime_types[j] != nullptr; j++)
+      {
+        delete[] readers[i].mime_types[j];
+      }
+      delete[] readers[i].mime_types;
+    }
+  }
+
+  delete[] readers;
+}
diff -Nru f3d-3.2.0+dfsg/c/engine_c_api.h f3d-3.4.1/c/engine_c_api.h
--- f3d-3.2.0+dfsg/c/engine_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/engine_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,362 @@
+#ifndef F3D_ENGINE_C_API_H
+#define F3D_ENGINE_C_API_H
+
+#include "context_c_api.h"
+#include "export.h"
+#include "interactor_c_api.h"
+#include "scene_c_api.h"
+#include "window_c_api.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Opaque handle to an f3d::engine object.
+   */
+  typedef struct f3d_engine_t f3d_engine_t;
+
+  /**
+   * @brief Opaque handle to f3d::options object (managed by engine).
+   */
+  typedef struct f3d_options_t f3d_options_t;
+
+  /**
+   * @brief Structure representing a rendering backend with its availability.
+   */
+  typedef struct
+  {
+    char* name;    /**< Backend name (e.g., "GLX", "EGL", "WGL", etc.) */
+    int available; /**< Non-zero if backend is available, zero otherwise */
+  } f3d_backend_info_t;
+
+  /**
+   * @brief Structure representing a module with its availability.
+   */
+  typedef struct
+  {
+    char* name;    /**< Module name */
+    int available; /**< Non-zero if module is available, zero otherwise */
+  } f3d_module_info_t;
+
+  /**
+   * @brief Structure providing information about the libf3d.
+   */
+  typedef struct
+  {
+    char* version;              /**< Version string */
+    char* version_full;         /**< Full version string */
+    char* build_date;           /**< Build date */
+    char* build_system;         /**< Build system */
+    char* compiler;             /**< Compiler used */
+    f3d_module_info_t* modules; /**< NULL-terminated array of modules */
+    char* vtk_version;          /**< VTK version */
+    char** copyrights;          /**< NULL-terminated array of copyright strings */
+    char* license;              /**< License text */
+  } f3d_lib_info_t;
+
+  /**
+   * @brief Structure providing information about a reader.
+   */
+  typedef struct
+  {
+    char* name;              /**< Reader name */
+    char* description;       /**< Reader description */
+    char** extensions;       /**< NULL-terminated array of file extensions */
+    char** mime_types;       /**< NULL-terminated array of MIME types */
+    char* plugin_name;       /**< Plugin name */
+    int has_scene_reader;    /**< Non-zero if has scene reader */
+    int has_geometry_reader; /**< Non-zero if has geometry reader */
+  } f3d_reader_info_t;
+
+  ///@{ @name Engine factory methods
+  /**
+   * @brief Create an engine with an automatic window.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @param offscreen If non-zero, the window will be hidden.
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create(int offscreen);
+
+  /**
+   * @brief Create an engine with no window.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_none();
+
+  /**
+   * @brief Create an engine with a GLX window (Linux only).
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @param offscreen If non-zero, the window will be hidden.
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_glx(int offscreen);
+
+  /**
+   * @brief Create an engine with a WGL window (Windows only).
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @param offscreen If non-zero, the window will be hidden.
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_wgl(int offscreen);
+
+  /**
+   * @brief Create an engine with an offscreen EGL window.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_egl();
+
+  /**
+   * @brief Create an engine with an offscreen OSMesa window.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_osmesa();
+
+  /**
+   * @brief Create an engine with an external window.
+   *
+   * A context to retrieve OpenGL symbols is required.
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @param get_proc_address Function pointer for OpenGL symbol resolution.
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external(f3d_context_function_t get_proc_address);
+
+  /**
+   * @brief Create an engine with an external GLX context (Linux only).
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external_glx();
+
+  /**
+   * @brief Create an engine with an external WGL context (Windows only).
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external_wgl();
+
+  /**
+   * @brief Create an engine with an external COCOA context (macOS only).
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external_cocoa();
+
+  /**
+   * @brief Create an engine with an external EGL context.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external_egl();
+
+  /**
+   * @brief Create an engine with an external OSMesa context.
+   *
+   * The returned engine must be deleted with f3d_engine_delete().
+   *
+   * @return Engine handle.
+   */
+  F3D_EXPORT f3d_engine_t* f3d_engine_create_external_osmesa();
+  ///@}
+
+  /**
+   * @brief Destroy an engine and free associated resources.
+   *
+   * @param engine Engine handle.
+   */
+  F3D_EXPORT void f3d_engine_delete(f3d_engine_t* engine);
+
+  /**
+   * @brief Set the cache path directory.
+   *
+   * @param engine Engine handle.
+   * @param cache_path Cache path string.
+   */
+  F3D_EXPORT void f3d_engine_set_cache_path(f3d_engine_t* engine, const char* cache_path);
+
+  /**
+   * @brief Set options for the engine.
+   *
+   * This will copy the provided options into the engine.
+   *
+   * @param engine Engine handle.
+   * @param options Options handle to copy from.
+   */
+  F3D_EXPORT void f3d_engine_set_options(f3d_engine_t* engine, f3d_options_t* options);
+
+  /**
+   * @brief Get the options object from the engine.
+   *
+   * @param engine Engine handle.
+   * @return Options handle (not owned by caller, managed by engine).
+   */
+  F3D_EXPORT f3d_options_t* f3d_engine_get_options(f3d_engine_t* engine);
+
+  /**
+   * @brief Get the window from the engine.
+   *
+   * @param engine Engine handle.
+   * @return Window handle (not owned by caller).
+   */
+  F3D_EXPORT f3d_window_t* f3d_engine_get_window(f3d_engine_t* engine);
+
+  /**
+   * @brief Get the scene from the engine.
+   *
+   * @param engine Engine handle.
+   * @return Scene handle (not owned by caller).
+   */
+  F3D_EXPORT f3d_scene_t* f3d_engine_get_scene(f3d_engine_t* engine);
+
+  /**
+   * @brief Get the interactor from the engine.
+   *
+   * @param engine Engine handle.
+   * @return Interactor handle (not owned by caller).
+   */
+  F3D_EXPORT f3d_interactor_t* f3d_engine_get_interactor(f3d_engine_t* engine);
+
+  ///@{ @name Rendering backends
+  /**
+   * @brief List rendering backends supported by libf3d.
+   *
+   * Returns a map of backend names with boolean flags indicating availability.
+   * The returned array of key-value pairs is NULL-terminated and must be freed
+   * by the caller using f3d_engine_free_backend_list().
+   *
+   * @param count Pointer to store the number of backends (optional, can be NULL).
+   * @return NULL-terminated array of backend name/availability pairs.
+   */
+  F3D_EXPORT f3d_backend_info_t* f3d_engine_get_rendering_backend_list(int* count);
+
+  ///@{ @name Plugin management
+  /**
+   * @brief Load a plugin.
+   *
+   * @param path_or_name Plugin path or name.
+   */
+  F3D_EXPORT void f3d_engine_load_plugin(const char* path_or_name);
+
+  /**
+   * @brief Automatically load all static plugins.
+   */
+  F3D_EXPORT void f3d_engine_autoload_plugins();
+
+  /**
+   * @brief List plugins based on associated json files located in the given directory.
+   *
+   * Listed plugins can be loaded using f3d_engine_load_plugin function.
+   * The returned array is NULL-terminated and must be freed by the caller using
+   * f3d_engine_free_string_array().
+   *
+   * @param plugin_path Path to the directory containing plugin json files.
+   * @return NULL-terminated array of plugin name strings, or NULL if the directory doesn't exist.
+   */
+  F3D_EXPORT char** f3d_engine_get_plugins_list(const char* plugin_path);
+  ///@}
+
+  ///@{ @name Reader options
+  /**
+   * @brief Get all plugin option names that can be set using f3d_engine_set_reader_option.
+   *
+   * This vector can be expanded when loading plugins using f3d_engine_load_plugin.
+   * The returned array is NULL-terminated and must be freed by the caller using
+   * f3d_engine_free_string_array().
+   *
+   * @return NULL-terminated array of option name strings.
+   */
+  F3D_EXPORT char** f3d_engine_get_all_reader_option_names();
+
+  /**
+   * @brief Set a specific reader option.
+   *
+   * @param name Option name.
+   * @param value Option value.
+   */
+  F3D_EXPORT void f3d_engine_set_reader_option(const char* name, const char* value);
+  ///@}
+
+  /**
+   * @brief Free a backend list returned by f3d_engine_get_rendering_backend_list().
+   *
+   * @param backends Backend list to free.
+   */
+  F3D_EXPORT void f3d_engine_free_backend_list(f3d_backend_info_t* backends);
+  ///@}
+
+  ///@{ @name Library information
+  /**
+   * @brief Get information about the libf3d.
+   *
+   * The returned structure must be freed by the caller using f3d_engine_free_lib_info().
+   *
+   * @return Library information structure.
+   */
+  F3D_EXPORT f3d_lib_info_t* f3d_engine_get_lib_info();
+
+  /**
+   * @brief Free a lib info structure returned by f3d_engine_get_lib_info().
+   *
+   * @param info Lib info structure to free.
+   */
+  F3D_EXPORT void f3d_engine_free_lib_info(f3d_lib_info_t* info);
+
+  /**
+   * @brief Get information about the supported readers.
+   *
+   * The returned array is NULL-terminated and must be freed by the caller using
+   * f3d_engine_free_readers_info().
+   *
+   * @param count Pointer to store the number of readers (optional, can be NULL).
+   * @return NULL-terminated array of reader information structures.
+   */
+  F3D_EXPORT f3d_reader_info_t* f3d_engine_get_readers_info(int* count);
+
+  /**
+   * @brief Free a readers info array returned by f3d_engine_get_readers_info().
+   *
+   * @param readers Readers info array to free.
+   */
+  F3D_EXPORT void f3d_engine_free_readers_info(f3d_reader_info_t* readers);
+  ///@}
+
+  ///@{ @name Utility functions
+  /**
+   * @brief Free a NULL-terminated string array.
+   *
+   * @param array String array to free.
+   */
+  F3D_EXPORT void f3d_engine_free_string_array(char** array);
+  ///@}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_ENGINE_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/image_c_api.cxx f3d-3.4.1/c/image_c_api.cxx
--- f3d-3.2.0+dfsg/c/image_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/image_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,271 @@
+#include "image_c_api.h"
+#include "image.h"
+#include <algorithm>
+#include <cstdio>
+#include <cstring>
+#include <iostream>
+
+// Helper class to wrap FILE* as an ostream
+class FileStreamBuf : public std::streambuf
+{
+public:
+  explicit FileStreamBuf(FILE* file)
+    : file_(file)
+  {
+  }
+
+protected:
+  int_type overflow(int_type c) override
+  {
+    if (c != EOF)
+    {
+      if (std::fputc(c, file_) == EOF)
+      {
+        return EOF;
+      }
+    }
+    return c;
+  }
+
+  std::streamsize xsputn(const char* s, std::streamsize n) override
+  {
+    return std::fwrite(s, 1, n, file_);
+  }
+
+private:
+  FILE* file_;
+};
+
+//----------------------------------------------------------------------------
+f3d_image_t* f3d_image_new_empty()
+{
+  f3d::image* img = new f3d::image();
+  return reinterpret_cast<f3d_image_t*>(img);
+}
+
+//----------------------------------------------------------------------------
+f3d_image_t* f3d_image_new_params(unsigned int width, unsigned int height,
+  unsigned int channelCount, f3d_image_channel_type_t channelType)
+{
+  f3d::image* img =
+    new f3d::image(width, height, channelCount, static_cast<f3d::image::ChannelType>(channelType));
+  return reinterpret_cast<f3d_image_t*>(img);
+}
+
+//----------------------------------------------------------------------------
+f3d_image_t* f3d_image_new_path(const char* path)
+{
+  f3d::image* img = new f3d::image(path);
+  return reinterpret_cast<f3d_image_t*>(img);
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_delete(f3d_image_t* img)
+{
+  f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  delete cpp_img;
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_get_normalized_pixel(f3d_image_t* img, int x, int y, double* pixel)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  std::vector<double> d = cpp_img->getNormalizedPixel({ x, y });
+  std::copy(d.begin(), d.end(), pixel);
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_width(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return cpp_img->getWidth();
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_height(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return cpp_img->getHeight();
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_channel_count(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return cpp_img->getChannelCount();
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_channel_type(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return static_cast<f3d_image_channel_type_t>(cpp_img->getChannelType());
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_channel_type_size(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return cpp_img->getChannelTypeSize();
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_set_content(f3d_image_t* img, void* buffer)
+{
+  f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  cpp_img->setContent(buffer);
+}
+
+//----------------------------------------------------------------------------
+void* f3d_image_get_content(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  return cpp_img->getContent();
+}
+
+//----------------------------------------------------------------------------
+double f3d_image_compare(f3d_image_t* img, f3d_image_t* reference)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  const f3d::image* cpp_ref = reinterpret_cast<f3d::image*>(reference);
+  return cpp_img->compare(*cpp_ref);
+}
+
+//----------------------------------------------------------------------------
+int f3d_image_equals(f3d_image_t* img, f3d_image_t* reference)
+{
+  if (!img || !reference)
+  {
+    return 0;
+  }
+
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  const f3d::image* cpp_ref = reinterpret_cast<f3d::image*>(reference);
+  return *cpp_img == *cpp_ref ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_image_not_equals(f3d_image_t* img, f3d_image_t* reference)
+{
+  if (!img || !reference)
+  {
+    return 1;
+  }
+
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  const f3d::image* cpp_ref = reinterpret_cast<f3d::image*>(reference);
+  return *cpp_img != *cpp_ref ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_save(f3d_image_t* img, const char* path, f3d_image_save_format_t format)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  cpp_img->save(path, static_cast<f3d::image::SaveFormat>(format));
+}
+
+//----------------------------------------------------------------------------
+unsigned char* f3d_image_save_buffer(
+  f3d_image_t* img, f3d_image_save_format_t format, unsigned int* size)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  std::vector<unsigned char> buffer =
+    cpp_img->saveBuffer(static_cast<f3d::image::SaveFormat>(format));
+  unsigned char* c_buffer = new unsigned char[buffer.size()];
+  std::copy(buffer.begin(), buffer.end(), c_buffer);
+  *size = buffer.size();
+  return c_buffer;
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_free_buffer(unsigned char* buffer)
+{
+  delete[] buffer;
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_to_terminal_text(f3d_image_t* img, void* stream)
+{
+  if (!img || !stream)
+  {
+    return;
+  }
+
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  FILE* file = static_cast<FILE*>(stream);
+
+  FileStreamBuf buffer(file);
+  std::ostream os(&buffer);
+
+  cpp_img->toTerminalText(os);
+  os.flush();
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_image_to_terminal_text_string(f3d_image_t* img)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  static std::string result;
+  result = cpp_img->toTerminalText();
+  return result.c_str();
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_set_metadata(f3d_image_t* img, const char* key, const char* value)
+{
+  f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  cpp_img->setMetadata(key, value);
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_image_get_metadata(f3d_image_t* img, const char* key)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  static std::string result;
+  result = cpp_img->getMetadata(key);
+  return result.c_str();
+}
+
+//----------------------------------------------------------------------------
+char** f3d_image_all_metadata(f3d_image_t* img, unsigned int* count)
+{
+  const f3d::image* cpp_img = reinterpret_cast<f3d::image*>(img);
+  std::vector<std::string> metadata_keys = cpp_img->allMetadata();
+  *count = metadata_keys.size();
+  char** keys = new char*[metadata_keys.size()];
+  for (size_t i = 0; i < metadata_keys.size(); ++i)
+  {
+    keys[i] = new char[metadata_keys[i].size() + 1];
+    std::strcpy(keys[i], metadata_keys[i].c_str());
+  }
+  return keys;
+}
+
+//----------------------------------------------------------------------------
+void f3d_image_free_metadata_keys(char** keys, unsigned int count)
+{
+  for (unsigned int i = 0; i < count; ++i)
+  {
+    delete[] keys[i];
+  }
+  delete[] keys;
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_image_get_supported_formats_count()
+{
+  std::vector<std::string> formats = f3d::image::getSupportedFormats();
+  return formats.size();
+}
+
+//----------------------------------------------------------------------------
+const char** f3d_image_get_supported_formats()
+{
+  static std::vector<std::string> formats = f3d::image::getSupportedFormats();
+  static std::vector<const char*> c_formats;
+  c_formats.clear();
+  c_formats.resize(formats.size());
+  std::transform(formats.begin(), formats.end(), c_formats.begin(),
+    [](const std::string& s) { return s.c_str(); });
+  return c_formats.data();
+}
diff -Nru f3d-3.2.0+dfsg/c/image_c_api.h f3d-3.4.1/c/image_c_api.h
--- f3d-3.2.0+dfsg/c/image_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/image_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,266 @@
+#ifndef F3D_IMAGE_C_API_H
+#define F3D_IMAGE_C_API_H
+
+#include "export.h" // Ensure F3D_EXPORT is defined
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  typedef enum
+  {
+    PNG,
+    JPG,
+    TIF,
+    BMP
+  } f3d_image_save_format_t;
+
+  typedef enum
+  {
+    BYTE,
+    SHORT,
+    FLOAT
+  } f3d_image_channel_type_t;
+
+  /**
+   * @struct f3d_image_t
+   * @brief Forward declaration of the f3d_image structure
+   */
+  typedef struct f3d_image f3d_image_t;
+
+  /**
+   * @brief Create a new empty image object
+   *
+   * The returned image must be deleted with f3d_image_delete().
+   *
+   * @return Pointer to the newly created image object
+   */
+  F3D_EXPORT f3d_image_t* f3d_image_new_empty();
+
+  /**
+   * @brief Create a new image object with the given parameters
+   *
+   * The returned image must be deleted with f3d_image_delete().
+   *
+   * @return Pointer to the newly created image object
+   */
+  F3D_EXPORT f3d_image_t* f3d_image_new_params(unsigned int width, unsigned int height,
+    unsigned int channelCount, f3d_image_channel_type_t channelType);
+
+  /**
+   * @brief Create a new image object from a file path
+   *
+   * The returned image must be deleted with f3d_image_delete().
+   *
+   * @return Pointer to the newly created image object
+   */
+  F3D_EXPORT f3d_image_t* f3d_image_new_path(const char* path);
+
+  /**
+   * @brief Delete an image object
+   * @param img Pointer to the image object to be deleted
+   */
+  F3D_EXPORT void f3d_image_delete(f3d_image_t* img);
+
+  /**
+   * @brief Test if two images are equal
+   *
+   * @param img Pointer to the first image object
+   * @param reference Pointer to the second image object
+   * @return Non-zero if images are equal, zero otherwise
+   */
+  F3D_EXPORT int f3d_image_equals(f3d_image_t* img, f3d_image_t* reference);
+
+  /**
+   * @brief Test if two images are not equal
+   *
+   * @param img Pointer to the first image object
+   * @param reference Pointer to the second image object
+   * @return Non-zero if images are not equal, zero otherwise
+   */
+  F3D_EXPORT int f3d_image_not_equals(f3d_image_t* img, f3d_image_t* reference);
+
+  /**
+   * @brief Get the normalized pixel
+   * @param img Pointer to the image object to be deleted
+   * @param x horizontal pixel coordinate
+   * @param y vertical pixel coordinate
+   * @param pixel Pointer to a preallocated buffer of channel count size
+   */
+  F3D_EXPORT void f3d_image_get_normalized_pixel(f3d_image_t* img, int x, int y, double* pixel);
+
+  /**
+   * @brief Get the count of supported image formats
+   * @return Count of supported image formats
+   */
+  F3D_EXPORT unsigned int f3d_image_get_supported_formats_count();
+
+  /**
+   * @brief Get the list of supported image formats
+   *
+   * The returned array points to internal static storage and must NOT be freed.
+   * The pointer is valid until the next call to this function.
+   *
+   * @return Pointer to the array of supported image formats
+   */
+  F3D_EXPORT const char** f3d_image_get_supported_formats();
+
+  /**
+   * @brief Get the width of an image
+   * @param img Pointer to the image object
+   * @return Width of the image
+   */
+  F3D_EXPORT unsigned int f3d_image_get_width(f3d_image_t* img);
+
+  /**
+   * @brief Get the height of an image
+   * @param img Pointer to the image object
+   * @return Height of the image
+   */
+  F3D_EXPORT unsigned int f3d_image_get_height(f3d_image_t* img);
+
+  /**
+   * @brief Get the number of channels in an image
+   * @param img Pointer to the image object
+   * @return Number of channels in the image
+   */
+  F3D_EXPORT unsigned int f3d_image_get_channel_count(f3d_image_t* img);
+
+  /**
+   * @brief Get the type of channels in an image
+   * @param img Pointer to the image object
+   * @return Type of channels in the image
+   */
+  F3D_EXPORT unsigned int f3d_image_get_channel_type(f3d_image_t* img);
+
+  /**
+   * @brief Get the size of the channel type in an image
+   * @param img Pointer to the image object
+   * @return Size of the channel type in the image
+   */
+  F3D_EXPORT unsigned int f3d_image_get_channel_type_size(f3d_image_t* img);
+
+  /**
+   * @brief Set the content of an image from a buffer
+   * @param img Pointer to the image object
+   * @param buffer Pointer to the buffer containing the image content
+   */
+  F3D_EXPORT void f3d_image_set_content(f3d_image_t* img, void* buffer);
+
+  /**
+   * @brief Get the content of an image as a buffer
+   *
+   * The returned pointer is owned by the image and must NOT be freed.
+   * It is valid as long as the image exists and its content is not modified.
+   *
+   * @param img Pointer to the image object
+   * @return Pointer to the buffer containing the image content
+   */
+  F3D_EXPORT void* f3d_image_get_content(f3d_image_t* img);
+
+  /**
+   * @brief Compare two images
+   * @param img Pointer to the image object
+   * @param reference Pointer to the reference image object
+   * @return SSIM difference between the two images
+   */
+  F3D_EXPORT double f3d_image_compare(f3d_image_t* img, f3d_image_t* reference);
+
+  /**
+   * @brief Save an image to a file
+   * @param img Pointer to the image object
+   * @param path Path to the file where the image will be saved
+   * @param format Format in which the image will be saved
+   */
+  F3D_EXPORT void f3d_image_save(
+    f3d_image_t* img, const char* path, f3d_image_save_format_t format);
+
+  /**
+   * @brief Save an image to a buffer
+   *
+   * The returned buffer is heap-allocated and must be freed with f3d_image_free_buffer().
+   *
+   * @param img Pointer to the image object
+   * @param format Format in which the image will be saved
+   * @param size Pointer to store the size of the saved buffer
+   * @return Pointer to the buffer containing the saved image
+   */
+  F3D_EXPORT unsigned char* f3d_image_save_buffer(
+    f3d_image_t* img, f3d_image_save_format_t format, unsigned int* size);
+
+  /**
+   * @brief Free a buffer returned by f3d_image_save_buffer
+   * @param buffer Pointer to the buffer to free
+   */
+  F3D_EXPORT void f3d_image_free_buffer(unsigned char* buffer);
+
+  /**
+   * @brief Convert an image to colored text using ANSI escape sequences for terminal output
+   *
+   * Writes colored text to the provided file stream.
+   * This is the C equivalent of toTerminalText(std::ostream&).
+   *
+   * @param img Pointer to the image object
+   * @param stream File stream to write to (e.g., stdout, stderr, or file handle)
+   */
+  F3D_EXPORT void f3d_image_to_terminal_text(f3d_image_t* img, void* stream);
+
+  /**
+   * @brief Convert an image to a string representation for terminal output
+   *
+   * The returned string points to internal static storage and must NOT be freed.
+   * The pointer is valid until the next call to this function.
+   * This is the C equivalent of toTerminalText() that returns a std::string.
+   *
+   * @param img Pointer to the image object
+   * @return Pointer to the string representation of the image
+   */
+  F3D_EXPORT const char* f3d_image_to_terminal_text_string(f3d_image_t* img);
+
+  /**
+   * @brief Set metadata for an image
+   * @param img Pointer to the image object
+   * @param key Metadata key
+   * @param value Metadata value
+   */
+  F3D_EXPORT void f3d_image_set_metadata(f3d_image_t* img, const char* key, const char* value);
+
+  /**
+   * @brief Get metadata from an image
+   *
+   * The returned string points to internal static storage and must NOT be freed.
+   * The pointer is valid until the next call to this function.
+   *
+   * @param img Pointer to the image object
+   * @param key Metadata key
+   * @return Metadata value
+   */
+  F3D_EXPORT const char* f3d_image_get_metadata(f3d_image_t* img, const char* key);
+
+  /**
+   * @brief Get all metadata keys from an image
+   *
+   * The returned keys must be freed with f3d_image_free_metadata_keys.
+   *
+   * @param img Pointer to the image object
+   * @param count Pointer to store the count of metadata keys
+   * @return Pointer to the array of metadata keys
+   */
+  F3D_EXPORT char** f3d_image_all_metadata(f3d_image_t* img, unsigned int* count);
+
+  /**
+   * @brief Free metadata keys obtained from an image
+   *
+   * Used to free the return of f3d_image_all_metadata.
+   *
+   * @param keys Pointer to the array of metadata keys
+   * @param count Count of metadata keys
+   */
+  F3D_EXPORT void f3d_image_free_metadata_keys(char** keys, unsigned int count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_IMAGE_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/interactor_c_api.cxx f3d-3.4.1/c/interactor_c_api.cxx
--- f3d-3.2.0+dfsg/c/interactor_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/interactor_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,657 @@
+#include "interactor_c_api.h"
+#include "interactor.h"
+#include <algorithm>
+#include <cstring>
+#include <string>
+
+//----------------------------------------------------------------------------
+void f3d_interaction_bind_format(const f3d_interaction_bind_t* bind, char* output, int output_size)
+{
+  if (!bind || !output || output_size <= 0)
+  {
+    return;
+  }
+
+  f3d::interaction_bind_t cpp_bind;
+  cpp_bind.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(bind->mod);
+  cpp_bind.inter = bind->inter;
+
+  std::string formatted = cpp_bind.format();
+  std::strncpy(output, formatted.c_str(), output_size - 1);
+  output[output_size - 1] = '\0';
+}
+
+//----------------------------------------------------------------------------
+void f3d_interaction_bind_parse(const char* str, f3d_interaction_bind_t* bind)
+{
+  if (!str || !bind)
+  {
+    return;
+  }
+
+  f3d::interaction_bind_t cpp_bind = f3d::interaction_bind_t::parse(str);
+
+  bind->mod = static_cast<f3d_interaction_bind_modifier_keys_t>(cpp_bind.mod);
+  std::strncpy(bind->inter, cpp_bind.inter.c_str(), sizeof(bind->inter) - 1);
+  bind->inter[sizeof(bind->inter) - 1] = '\0';
+}
+
+//----------------------------------------------------------------------------
+int f3d_interaction_bind_less_than(
+  const f3d_interaction_bind_t* lhs, const f3d_interaction_bind_t* rhs)
+{
+  if (!lhs || !rhs)
+  {
+    return 0;
+  }
+
+  f3d::interaction_bind_t cpp_lhs;
+  cpp_lhs.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(lhs->mod);
+  cpp_lhs.inter = lhs->inter;
+
+  f3d::interaction_bind_t cpp_rhs;
+  cpp_rhs.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(rhs->mod);
+  cpp_rhs.inter = rhs->inter;
+
+  return cpp_lhs < cpp_rhs ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_interaction_bind_equals(
+  const f3d_interaction_bind_t* lhs, const f3d_interaction_bind_t* rhs)
+{
+  if (!lhs || !rhs)
+  {
+    return 0;
+  }
+
+  f3d::interaction_bind_t cpp_lhs;
+  cpp_lhs.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(lhs->mod);
+  cpp_lhs.inter = lhs->inter;
+
+  f3d::interaction_bind_t cpp_rhs;
+  cpp_rhs.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(rhs->mod);
+  cpp_rhs.inter = rhs->inter;
+
+  return cpp_lhs == cpp_rhs ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_toggle_animation(
+  f3d_interactor_t* interactor, f3d_interactor_animation_direction_t direction)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::AnimationDirection cpp_direction =
+    static_cast<f3d::interactor::AnimationDirection>(direction);
+  cpp_interactor->toggleAnimation(cpp_direction);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_start_animation(
+  f3d_interactor_t* interactor, f3d_interactor_animation_direction_t direction)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::AnimationDirection cpp_direction =
+    static_cast<f3d::interactor::AnimationDirection>(direction);
+  cpp_interactor->startAnimation(cpp_direction);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_stop_animation(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->stopAnimation();
+}
+
+//----------------------------------------------------------------------------
+int f3d_interactor_is_playing_animation(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return 0;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  return cpp_interactor->isPlayingAnimation() ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+f3d_interactor_animation_direction_t f3d_interactor_get_animation_direction(
+  f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return F3D_INTERACTOR_ANIMATION_FORWARD;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::AnimationDirection cpp_direction = cpp_interactor->getAnimationDirection();
+  return static_cast<f3d_interactor_animation_direction_t>(cpp_direction);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_enable_camera_movement(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->enableCameraMovement();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_disable_camera_movement(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->disableCameraMovement();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_mod_update(
+  f3d_interactor_t* interactor, f3d_interactor_input_modifier_t mod)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::InputModifier cpp_mod = static_cast<f3d::interactor::InputModifier>(mod);
+  cpp_interactor->triggerModUpdate(cpp_mod);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_mouse_button(f3d_interactor_t* interactor,
+  f3d_interactor_input_action_t action, f3d_interactor_mouse_button_t button)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::InputAction cpp_action = static_cast<f3d::interactor::InputAction>(action);
+  f3d::interactor::MouseButton cpp_button = static_cast<f3d::interactor::MouseButton>(button);
+  cpp_interactor->triggerMouseButton(cpp_action, cpp_button);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_mouse_position(f3d_interactor_t* interactor, double xpos, double ypos)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->triggerMousePosition(xpos, ypos);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_mouse_wheel(
+  f3d_interactor_t* interactor, f3d_interactor_wheel_direction_t direction)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::WheelDirection cpp_direction =
+    static_cast<f3d::interactor::WheelDirection>(direction);
+  cpp_interactor->triggerMouseWheel(cpp_direction);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_keyboard_key(
+  f3d_interactor_t* interactor, f3d_interactor_input_action_t action, const char* key_sym)
+{
+  if (!interactor || !key_sym)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interactor::InputAction cpp_action = static_cast<f3d::interactor::InputAction>(action);
+  cpp_interactor->triggerKeyboardKey(cpp_action, key_sym);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_text_character(f3d_interactor_t* interactor, unsigned int codepoint)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->triggerTextCharacter(codepoint);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_trigger_event_loop(f3d_interactor_t* interactor, double delta_time)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->triggerEventLoop(delta_time);
+}
+
+//----------------------------------------------------------------------------
+int f3d_interactor_play_interaction(
+  f3d_interactor_t* interactor, const char* file_path, double delta_time)
+{
+  if (!interactor || !file_path)
+  {
+    return 0;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  return cpp_interactor->playInteraction(file_path, delta_time) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_interactor_record_interaction(f3d_interactor_t* interactor, const char* file_path)
+{
+  if (!interactor || !file_path)
+  {
+    return 0;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  return cpp_interactor->recordInteraction(file_path) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_start_with_callback(f3d_interactor_t* interactor, double delta_time,
+  f3d_interactor_callback_t callback, void* user_data)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+
+  if (callback)
+  {
+    cpp_interactor->start(delta_time, [callback, user_data]() { callback(user_data); });
+  }
+  else
+  {
+    cpp_interactor->start(delta_time, nullptr);
+  }
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_start(f3d_interactor_t* interactor, double delta_time)
+{
+  f3d_interactor_start_with_callback(interactor, delta_time, nullptr, nullptr);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_stop(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->stop();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_request_render(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->requestRender();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_request_stop(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->requestStop();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_init_commands(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->initCommands();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_add_command(f3d_interactor_t* interactor, const char* action,
+  f3d_interactor_command_callback_t callback, void* user_data)
+{
+  if (!interactor || !action || !callback)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+
+  auto cpp_callback = [callback, user_data](const std::vector<std::string>& args)
+  {
+    std::vector<const char*> c_args;
+    c_args.resize(args.size());
+    std::transform(
+      args.begin(), args.end(), c_args.begin(), [](const std::string& s) { return s.c_str(); });
+    callback(c_args.data(), static_cast<int>(c_args.size()), user_data);
+  };
+
+  cpp_interactor->addCommand(action, cpp_callback);
+}
+
+//----------------------------------------------------------------------------
+char** f3d_interactor_get_command_actions(f3d_interactor_t* interactor, int* count)
+{
+  if (!interactor || !count)
+  {
+    if (count)
+    {
+      *count = 0;
+    }
+    return nullptr;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  std::vector<std::string> actions = cpp_interactor->getCommandActions();
+
+  *count = static_cast<int>(actions.size());
+  if (actions.empty())
+  {
+    return nullptr;
+  }
+
+  char** result = new char*[actions.size()];
+
+  for (size_t i = 0; i < actions.size(); ++i)
+  {
+    result[i] = new char[actions[i].length() + 1];
+    std::strcpy(result[i], actions[i].c_str());
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_remove_command(f3d_interactor_t* interactor, const char* action)
+{
+  if (!interactor || !action)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->removeCommand(action);
+}
+
+//----------------------------------------------------------------------------
+int f3d_interactor_trigger_command(
+  f3d_interactor_t* interactor, const char* command, int keep_comments)
+{
+  if (!interactor || !command)
+  {
+    return 0;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  return cpp_interactor->triggerCommand(command, keep_comments != 0) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_init_bindings(f3d_interactor_t* interactor)
+{
+  if (!interactor)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  cpp_interactor->initBindings();
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_add_binding(f3d_interactor_t* interactor, const f3d_interaction_bind_t* bind,
+  const char** commands, int command_count, const char* group)
+{
+  if (!interactor || !bind || !commands || command_count <= 0)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+
+  f3d::interaction_bind_t cpp_bind;
+  cpp_bind.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(bind->mod);
+  cpp_bind.inter = bind->inter;
+
+  std::vector<std::string> cpp_commands;
+  cpp_commands.reserve(command_count);
+  for (int i = 0; i < command_count; ++i)
+  {
+    cpp_commands.push_back(commands[i]);
+  }
+
+  std::string cpp_group = group ? group : "";
+
+  cpp_interactor->addBinding(cpp_bind, cpp_commands, cpp_group);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_remove_binding(f3d_interactor_t* interactor, const f3d_interaction_bind_t* bind)
+{
+  if (!interactor || !bind)
+  {
+    return;
+  }
+
+  f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interaction_bind_t cpp_bind;
+  cpp_bind.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(bind->mod);
+  cpp_bind.inter = bind->inter;
+  cpp_interactor->removeBinding(cpp_bind);
+}
+
+//----------------------------------------------------------------------------
+char** f3d_interactor_get_bind_groups(f3d_interactor_t* interactor, int* count)
+{
+  if (!interactor || !count)
+  {
+    if (count)
+    {
+      *count = 0;
+    }
+    return nullptr;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  std::vector<std::string> groups = cpp_interactor->getBindGroups();
+
+  *count = static_cast<int>(groups.size());
+  if (groups.empty())
+  {
+    return nullptr;
+  }
+
+  char** result = new char*[groups.size()];
+
+  for (size_t i = 0; i < groups.size(); ++i)
+  {
+    result[i] = new char[groups[i].length() + 1];
+    std::strcpy(result[i], groups[i].c_str());
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+f3d_interaction_bind_t* f3d_interactor_get_binds_for_group(
+  f3d_interactor_t* interactor, const char* group, int* count)
+{
+  if (!interactor || !group || !count)
+  {
+    if (count)
+    {
+      *count = 0;
+    }
+    return nullptr;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  std::vector<f3d::interaction_bind_t> binds = cpp_interactor->getBindsForGroup(group);
+
+  *count = static_cast<int>(binds.size());
+  if (binds.empty())
+  {
+    return nullptr;
+  }
+
+  f3d_interaction_bind_t* result = new f3d_interaction_bind_t[binds.size()];
+
+  for (size_t i = 0; i < binds.size(); ++i)
+  {
+    result[i].mod = static_cast<f3d_interaction_bind_modifier_keys_t>(binds[i].mod);
+    std::strncpy(result[i].inter, binds[i].inter.c_str(), sizeof(result[i].inter) - 1);
+    result[i].inter[sizeof(result[i].inter) - 1] = '\0';
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+f3d_interaction_bind_t* f3d_interactor_get_binds(f3d_interactor_t* interactor, int* count)
+{
+  if (!interactor || !count)
+  {
+    if (count)
+    {
+      *count = 0;
+    }
+    return nullptr;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  std::vector<f3d::interaction_bind_t> binds = cpp_interactor->getBinds();
+
+  *count = static_cast<int>(binds.size());
+  if (binds.empty())
+  {
+    return nullptr;
+  }
+
+  f3d_interaction_bind_t* result = new f3d_interaction_bind_t[binds.size()];
+
+  for (size_t i = 0; i < binds.size(); ++i)
+  {
+    result[i].mod = static_cast<f3d_interaction_bind_modifier_keys_t>(binds[i].mod);
+    std::strncpy(result[i].inter, binds[i].inter.c_str(), sizeof(result[i].inter) - 1);
+    result[i].inter[sizeof(result[i].inter) - 1] = '\0';
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_get_binding_documentation(f3d_interactor_t* interactor,
+  const f3d_interaction_bind_t* bind, f3d_binding_documentation_t* doc)
+{
+  if (!interactor || !bind || !doc)
+  {
+    return;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interaction_bind_t cpp_bind;
+  cpp_bind.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(bind->mod);
+  cpp_bind.inter = bind->inter;
+
+  auto [doc_str, value_str] = cpp_interactor->getBindingDocumentation(cpp_bind);
+
+  std::strncpy(doc->doc, doc_str.c_str(), sizeof(doc->doc) - 1);
+  doc->doc[sizeof(doc->doc) - 1] = '\0';
+
+  std::strncpy(doc->value, value_str.c_str(), sizeof(doc->value) - 1);
+  doc->value[sizeof(doc->value) - 1] = '\0';
+}
+
+//----------------------------------------------------------------------------
+f3d_interactor_binding_type_t f3d_interactor_get_binding_type(
+  f3d_interactor_t* interactor, const f3d_interaction_bind_t* bind)
+{
+  if (!interactor || !bind)
+  {
+    return F3D_INTERACTOR_BINDING_OTHER;
+  }
+
+  const f3d::interactor* cpp_interactor = reinterpret_cast<f3d::interactor*>(interactor);
+  f3d::interaction_bind_t cpp_bind;
+  cpp_bind.mod = static_cast<f3d::interaction_bind_t::ModifierKeys>(bind->mod);
+  cpp_bind.inter = bind->inter;
+  f3d::interactor::BindingType cpp_type = cpp_interactor->getBindingType(cpp_bind);
+  return static_cast<f3d_interactor_binding_type_t>(cpp_type);
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_free_string_array(char** array, int count)
+{
+  if (!array)
+  {
+    return;
+  }
+
+  for (int i = 0; i < count; ++i)
+  {
+    delete[] array[i];
+  }
+  delete[] array;
+}
+
+//----------------------------------------------------------------------------
+void f3d_interactor_free_bind_array(f3d_interaction_bind_t* array)
+{
+  delete[] array;
+}
diff -Nru f3d-3.2.0+dfsg/c/interactor_c_api.h f3d-3.4.1/c/interactor_c_api.h
--- f3d-3.2.0+dfsg/c/interactor_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/interactor_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,504 @@
+#ifndef F3D_INTERACTOR_C_API_H
+#define F3D_INTERACTOR_C_API_H
+
+#include "export.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Opaque handle to an f3d::interactor object.
+   */
+  typedef struct f3d_interactor_t f3d_interactor_t;
+
+  /**
+   * @brief Enumeration of supported modifier key combinations.
+   */
+  typedef enum f3d_interaction_bind_modifier_keys_t
+  {
+    F3D_INTERACTION_BIND_ANY = 0x80,
+    F3D_INTERACTION_BIND_NONE = 0x0,
+    F3D_INTERACTION_BIND_CTRL = 0x1,
+    F3D_INTERACTION_BIND_SHIFT = 0x2,
+    F3D_INTERACTION_BIND_CTRL_SHIFT = 0x3
+  } f3d_interaction_bind_modifier_keys_t;
+
+  /**
+   * @brief Structure representing an interaction binding.
+   */
+  typedef struct f3d_interaction_bind_t
+  {
+    f3d_interaction_bind_modifier_keys_t mod;
+    char inter[256]; // String for the interaction
+  } f3d_interaction_bind_t;
+
+  /**
+   * @brief Format an interaction bind into a string.
+   *
+   * Formats the bind into a string like "A", "Any+Question", "Shift+L", etc.
+   * The output buffer must be at least 512 bytes.
+   *
+   * @param bind Interaction bind to format.
+   * @param output Output buffer to store the formatted string.
+   * @param output_size Size of the output buffer.
+   */
+  F3D_EXPORT void f3d_interaction_bind_format(
+    const f3d_interaction_bind_t* bind, char* output, int output_size);
+
+  /**
+   * @brief Parse a string into an interaction bind.
+   *
+   * Creates an interaction bind from a string like "A", "Ctrl+A", "Shift+B", etc.
+   *
+   * @param str String to parse.
+   * @param bind Output parameter for the parsed bind.
+   */
+  F3D_EXPORT void f3d_interaction_bind_parse(const char* str, f3d_interaction_bind_t* bind);
+
+  /**
+   * @brief Compare two interaction binds for less-than ordering.
+   *
+   * Compares modifier and interaction string for ordering.
+   * Useful for storing binds in sorted data structures.
+   *
+   * @param lhs Left-hand side bind.
+   * @param rhs Right-hand side bind.
+   * @return 1 if lhs < rhs, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_interaction_bind_less_than(
+    const f3d_interaction_bind_t* lhs, const f3d_interaction_bind_t* rhs);
+
+  /**
+   * @brief Compare two interaction binds for equality.
+   *
+   * Compares both modifier and interaction string for equality.
+   *
+   * @param lhs Left-hand side bind.
+   * @param rhs Right-hand side bind.
+   * @return 1 if binds are equal, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_interaction_bind_equals(
+    const f3d_interaction_bind_t* lhs, const f3d_interaction_bind_t* rhs);
+
+  /**
+   * @brief Enumeration of binding types.
+   */
+  typedef enum f3d_interactor_binding_type_t
+  {
+    F3D_INTERACTOR_BINDING_CYCLIC = 0,
+    F3D_INTERACTOR_BINDING_NUMERICAL = 1,
+    F3D_INTERACTOR_BINDING_TOGGLE = 2,
+    F3D_INTERACTOR_BINDING_OTHER = 3
+  } f3d_interactor_binding_type_t;
+
+  /**
+   * @brief Enumeration of supported mouse buttons.
+   */
+  typedef enum f3d_interactor_mouse_button_t
+  {
+    F3D_INTERACTOR_MOUSE_LEFT,
+    F3D_INTERACTOR_MOUSE_RIGHT,
+    F3D_INTERACTOR_MOUSE_MIDDLE
+  } f3d_interactor_mouse_button_t;
+
+  /**
+   * @brief Enumeration of supported mouse wheel directions.
+   */
+  typedef enum f3d_interactor_wheel_direction_t
+  {
+    F3D_INTERACTOR_WHEEL_FORWARD,
+    F3D_INTERACTOR_WHEEL_BACKWARD,
+    F3D_INTERACTOR_WHEEL_LEFT,
+    F3D_INTERACTOR_WHEEL_RIGHT
+  } f3d_interactor_wheel_direction_t;
+
+  /**
+   * @brief Enumeration of supported input actions.
+   */
+  typedef enum f3d_interactor_input_action_t
+  {
+    F3D_INTERACTOR_INPUT_PRESS,
+    F3D_INTERACTOR_INPUT_RELEASE
+  } f3d_interactor_input_action_t;
+
+  /**
+   * @brief Enumeration of supported input modifiers.
+   */
+  typedef enum f3d_interactor_input_modifier_t
+  {
+    F3D_INTERACTOR_INPUT_NONE,
+    F3D_INTERACTOR_INPUT_CTRL,
+    F3D_INTERACTOR_INPUT_SHIFT,
+    F3D_INTERACTOR_INPUT_CTRL_SHIFT
+  } f3d_interactor_input_modifier_t;
+
+  /**
+   * @brief Enumeration of animation direction.
+   */
+  typedef enum f3d_interactor_animation_direction_t
+  {
+    F3D_INTERACTOR_ANIMATION_FORWARD,
+    F3D_INTERACTOR_ANIMATION_BACKWARD
+  } f3d_interactor_animation_direction_t;
+
+  ///@{ @name Commands
+  /**
+   * @brief Initialize commands (remove existing and add defaults).
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_init_commands(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Callback signature for command execution.
+   */
+  typedef void (*f3d_interactor_command_callback_t)(
+    const char** args, int arg_count, void* user_data);
+
+  /**
+   * @brief Add a command with the provided action.
+   *
+   * @param interactor Interactor handle.
+   * @param action Action string.
+   * @param callback Command callback function.
+   * @param user_data Optional user data passed to callback.
+   */
+  F3D_EXPORT void f3d_interactor_add_command(f3d_interactor_t* interactor, const char* action,
+    f3d_interactor_command_callback_t callback, void* user_data);
+
+  /**
+   * @brief Remove a command for the provided action.
+   *
+   * @param interactor Interactor handle.
+   * @param action Action string.
+   */
+  F3D_EXPORT void f3d_interactor_remove_command(f3d_interactor_t* interactor, const char* action);
+
+  /**
+   * @brief Get all command actions.
+   *
+   * @param interactor Interactor handle.
+   * @param count Output parameter for number of actions.
+   * @return Array of action strings. Caller must free the array with
+   *         f3d_interactor_free_string_array().
+   */
+  F3D_EXPORT char** f3d_interactor_get_command_actions(f3d_interactor_t* interactor, int* count);
+
+  /**
+   * @brief Trigger a command.
+   *
+   * @param interactor Interactor handle.
+   * @param command Command string.
+   * @param keep_comments If non-zero, comments with # are supported.
+   * @return 1 if command succeeded, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_interactor_trigger_command(
+    f3d_interactor_t* interactor, const char* command, int keep_comments);
+  ///@}
+
+  ///@{ @name Bindings
+  /**
+   * @brief Initialize bindings (remove existing and add defaults).
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_init_bindings(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Add a binding for the provided bind.
+   *
+   * @param interactor Interactor handle.
+   * @param bind Interaction bind.
+   * @param commands Array of command strings.
+   * @param command_count Number of commands.
+   * @param group Optional group name (can be NULL).
+   */
+  F3D_EXPORT void f3d_interactor_add_binding(f3d_interactor_t* interactor,
+    const f3d_interaction_bind_t* bind, const char** commands, int command_count,
+    const char* group);
+
+  /**
+   * @brief Remove a binding for the provided bind.
+   *
+   * @param interactor Interactor handle.
+   * @param bind Interaction bind.
+   */
+  F3D_EXPORT void f3d_interactor_remove_binding(
+    f3d_interactor_t* interactor, const f3d_interaction_bind_t* bind);
+
+  /**
+   * @brief Get all bind groups.
+   *
+   * @param interactor Interactor handle.
+   * @param count Output parameter for number of groups.
+   * @return Array of group strings. Caller must free the array with
+   *         f3d_interactor_free_string_array().
+   */
+  F3D_EXPORT char** f3d_interactor_get_bind_groups(f3d_interactor_t* interactor, int* count);
+
+  /**
+   * @brief Get all binds for a specific group.
+   *
+   * @param interactor Interactor handle.
+   * @param group Group name.
+   * @param count Output parameter for number of binds.
+   * @return Array of binds. Caller must free the array with
+   *         f3d_interactor_free_bind_array().
+   */
+  F3D_EXPORT f3d_interaction_bind_t* f3d_interactor_get_binds_for_group(
+    f3d_interactor_t* interactor, const char* group, int* count);
+
+  /**
+   * @brief Get all binds.
+   *
+   * @param interactor Interactor handle.
+   * @param count Output parameter for number of binds.
+   * @return Array of binds. Caller must free the array with
+   *         f3d_interactor_free_bind_array().
+   */
+  F3D_EXPORT f3d_interaction_bind_t* f3d_interactor_get_binds(
+    f3d_interactor_t* interactor, int* count);
+
+  /**
+   * @brief Structure containing binding documentation.
+   */
+  typedef struct f3d_binding_documentation_t
+  {
+    char doc[512];   // Documentation string
+    char value[256]; // Current value string
+  } f3d_binding_documentation_t;
+
+  /**
+   * @brief Get documentation for a binding.
+   *
+   * @param interactor Interactor handle.
+   * @param bind Interaction bind.
+   * @param doc Output parameter for documentation.
+   */
+  F3D_EXPORT void f3d_interactor_get_binding_documentation(f3d_interactor_t* interactor,
+    const f3d_interaction_bind_t* bind, f3d_binding_documentation_t* doc);
+
+  /**
+   * @brief Get the type of a binding.
+   *
+   * @param interactor Interactor handle.
+   * @param bind Interaction bind.
+   * @return Binding type.
+   */
+  F3D_EXPORT f3d_interactor_binding_type_t f3d_interactor_get_binding_type(
+    f3d_interactor_t* interactor, const f3d_interaction_bind_t* bind);
+  ///@}
+
+  ///@{ @name Animation
+  /**
+   * @brief Toggle the animation.
+   *
+   * @param interactor Interactor handle.
+   * @param direction Animation direction.
+   */
+  F3D_EXPORT void f3d_interactor_toggle_animation(
+    f3d_interactor_t* interactor, f3d_interactor_animation_direction_t direction);
+
+  /**
+   * @brief Start the animation.
+   *
+   * @param interactor Interactor handle.
+   * @param direction Animation direction.
+   */
+  F3D_EXPORT void f3d_interactor_start_animation(
+    f3d_interactor_t* interactor, f3d_interactor_animation_direction_t direction);
+
+  /**
+   * @brief Stop the animation.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_stop_animation(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Check if animation is currently playing.
+   *
+   * @param interactor Interactor handle.
+   * @return 1 if animation is playing, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_interactor_is_playing_animation(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Get the current animation direction.
+   *
+   * @param interactor Interactor handle.
+   * @return Current animation direction.
+   */
+  F3D_EXPORT f3d_interactor_animation_direction_t f3d_interactor_get_animation_direction(
+    f3d_interactor_t* interactor);
+  ///@}
+
+  ///@{ @name Movement
+  /**
+   * @brief Enable camera movement.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_enable_camera_movement(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Disable camera movement.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_disable_camera_movement(f3d_interactor_t* interactor);
+  ///@}
+
+  ///@{ @name Forwarding input events
+  /**
+   * @brief Trigger a modifier update.
+   *
+   * @param interactor Interactor handle.
+   * @param mod Input modifier.
+   */
+  F3D_EXPORT void f3d_interactor_trigger_mod_update(
+    f3d_interactor_t* interactor, f3d_interactor_input_modifier_t mod);
+
+  /**
+   * @brief Trigger a mouse button event.
+   *
+   * @param interactor Interactor handle.
+   * @param action Input action (press or release).
+   * @param button Mouse button.
+   */
+  F3D_EXPORT void f3d_interactor_trigger_mouse_button(f3d_interactor_t* interactor,
+    f3d_interactor_input_action_t action, f3d_interactor_mouse_button_t button);
+
+  /**
+   * @brief Trigger a mouse position event.
+   *
+   * @param interactor Interactor handle.
+   * @param xpos X position in window coordinates (pixels).
+   * @param ypos Y position in window coordinates (pixels).
+   */
+  F3D_EXPORT void f3d_interactor_trigger_mouse_position(
+    f3d_interactor_t* interactor, double xpos, double ypos);
+
+  /**
+   * @brief Trigger a mouse wheel event.
+   *
+   * @param interactor Interactor handle.
+   * @param direction Wheel direction.
+   */
+  F3D_EXPORT void f3d_interactor_trigger_mouse_wheel(
+    f3d_interactor_t* interactor, f3d_interactor_wheel_direction_t direction);
+
+  /**
+   * @brief Trigger a keyboard key event.
+   *
+   * @param interactor Interactor handle.
+   * @param action Input action (press or release).
+   * @param key_sym Key symbol string.
+   */
+  F3D_EXPORT void f3d_interactor_trigger_keyboard_key(
+    f3d_interactor_t* interactor, f3d_interactor_input_action_t action, const char* key_sym);
+
+  /**
+   * @brief Trigger a text character input event.
+   *
+   * @param interactor Interactor handle.
+   * @param codepoint Unicode codepoint of the character.
+   */
+  F3D_EXPORT void f3d_interactor_trigger_text_character(
+    f3d_interactor_t* interactor, unsigned int codepoint);
+  ///@}
+
+  /**
+   * @brief Manually trigger the event loop.
+   *
+   * @param interactor Interactor handle.
+   * @param delta_time Time step in seconds (must be positive).
+   */
+  F3D_EXPORT void f3d_interactor_trigger_event_loop(
+    f3d_interactor_t* interactor, double delta_time);
+
+  /**
+   * @brief Play a VTK interaction file.
+   *
+   * @param interactor Interactor handle.
+   * @param file_path Path to the interaction file.
+   * @param delta_time Time step in seconds (default: 1.0/30).
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_interactor_play_interaction(
+    f3d_interactor_t* interactor, const char* file_path, double delta_time);
+
+  /**
+   * @brief Record interaction to a VTK interaction file.
+   *
+   * @param interactor Interactor handle.
+   * @param file_path Path to save the interaction file.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_interactor_record_interaction(
+    f3d_interactor_t* interactor, const char* file_path);
+
+  /**
+   * @brief Start the interactor event loop.
+   *
+   * @param interactor Interactor handle.
+   * @param delta_time Time step in seconds.
+   */
+  F3D_EXPORT void f3d_interactor_start(f3d_interactor_t* interactor, double delta_time);
+
+  typedef void (*f3d_interactor_callback_t)(void* user_data);
+  /**
+   * @brief Start the interactor event loop.
+   *
+   * @param interactor Interactor handle.
+   * @param delta_time Time step in seconds.
+   * @param callback Optional user callback called at the start of each event-loop
+   *        iteration. May be NULL if no callback is desired.
+   * @param user_data Optional opaque pointer passed verbatim to callback.
+   */
+  F3D_EXPORT void f3d_interactor_start_with_callback(f3d_interactor_t* interactor,
+    double delta_time, f3d_interactor_callback_t callback, void* user_data);
+
+  /**
+   * @brief Stop the interactor.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_stop(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Request a render on the next event loop.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_request_render(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Request the interactor to stop on the next event loop.
+   *
+   * @param interactor Interactor handle.
+   */
+  F3D_EXPORT void f3d_interactor_request_stop(f3d_interactor_t* interactor);
+
+  /**
+   * @brief Free a string array returned by interactor functions.
+   *
+   * @param array String array to free.
+   * @param count Number of strings in the array.
+   */
+  F3D_EXPORT void f3d_interactor_free_string_array(char** array, int count);
+
+  /**
+   * @brief Free a bind array returned by interactor functions.
+   *
+   * @param array Bind array to free.
+   */
+  F3D_EXPORT void f3d_interactor_free_bind_array(f3d_interaction_bind_t* array);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_INTERACTOR_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/log_c_api.cxx f3d-3.4.1/c/log_c_api.cxx
--- f3d-3.2.0+dfsg/c/log_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/log_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,106 @@
+#include "log_c_api.h"
+#include "log.h"
+#include <string>
+
+static f3d_log_forward_fn_t g_log_callback = nullptr;
+
+//----------------------------------------------------------------------------
+static void cpp_log_forwarder(f3d::log::VerboseLevel level, const std::string& message)
+{
+  if (g_log_callback != nullptr)
+  {
+    f3d_log_verbose_level_t c_level = static_cast<f3d_log_verbose_level_t>(level);
+    g_log_callback(c_level, message.c_str());
+  }
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_set_verbose_level(f3d_log_verbose_level_t level, int force_std_err)
+{
+  f3d::log::VerboseLevel cpp_level = static_cast<f3d::log::VerboseLevel>(level);
+  f3d::log::setVerboseLevel(cpp_level, force_std_err != 0);
+}
+
+//----------------------------------------------------------------------------
+f3d_log_verbose_level_t f3d_log_get_verbose_level(void)
+{
+  f3d::log::VerboseLevel cpp_level = f3d::log::getVerboseLevel();
+  return static_cast<f3d_log_verbose_level_t>(cpp_level);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_set_use_coloring(int use)
+{
+  f3d::log::setUseColoring(use != 0);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_print(f3d_log_verbose_level_t level, const char* message)
+{
+  if (!message)
+  {
+    return;
+  }
+
+  f3d::log::VerboseLevel cpp_level = static_cast<f3d::log::VerboseLevel>(level);
+  f3d::log::print(cpp_level, message);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_debug(const char* message)
+{
+  if (!message)
+  {
+    return;
+  }
+
+  f3d::log::debug(message);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_info(const char* message)
+{
+  if (!message)
+  {
+    return;
+  }
+
+  f3d::log::info(message);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_warn(const char* message)
+{
+  if (!message)
+  {
+    return;
+  }
+
+  f3d::log::warn(message);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_error(const char* message)
+{
+  if (!message)
+  {
+    return;
+  }
+
+  f3d::log::error(message);
+}
+
+//----------------------------------------------------------------------------
+void f3d_log_forward(f3d_log_forward_fn_t callback)
+{
+  g_log_callback = callback;
+
+  if (callback != nullptr)
+  {
+    f3d::log::forward(cpp_log_forwarder);
+  }
+  else
+  {
+    f3d::log::forward(nullptr);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/c/log_c_api.h f3d-3.4.1/c/log_c_api.h
--- f3d-3.2.0+dfsg/c/log_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/log_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,107 @@
+#ifndef F3D_LOG_C_API_H
+#define F3D_LOG_C_API_H
+
+#include "export.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Enumeration of verbose levels.
+   */
+  typedef enum f3d_log_verbose_level_t
+  {
+    F3D_LOG_DEBUG = 0,
+    F3D_LOG_INFO,
+    F3D_LOG_WARN,
+    F3D_LOG_ERROR,
+    F3D_LOG_QUIET
+  } f3d_log_verbose_level_t;
+
+  /**
+   * @brief Log a message at the specified verbose level.
+   *
+   * @param level The verbose level for the message.
+   * @param message The message string.
+   */
+  F3D_EXPORT void f3d_log_print(f3d_log_verbose_level_t level, const char* message);
+
+  /**
+   * @brief Log a debug message.
+   *
+   * @param message The message string.
+   */
+  F3D_EXPORT void f3d_log_debug(const char* message);
+
+  /**
+   * @brief Log an info message.
+   *
+   * @param message The message string.
+   */
+  F3D_EXPORT void f3d_log_info(const char* message);
+
+  /**
+   * @brief Log a warning message.
+   *
+   * @param message The message string.
+   */
+  F3D_EXPORT void f3d_log_warn(const char* message);
+
+  /**
+   * @brief Log an error message.
+   *
+   * @param message The message string.
+   */
+  F3D_EXPORT void f3d_log_error(const char* message);
+
+  /**
+   * @brief Callback function type for log forwarding.
+   *
+   * @param level The verbose level of the log message.
+   * @param message The log message string.
+   */
+  typedef void (*f3d_log_forward_fn_t)(f3d_log_verbose_level_t level, const char* message);
+
+  /**
+   * @brief Set the coloring usage, if applicable (e.g., console output).
+   *
+   * @param use If non-zero, coloring will be used.
+   */
+  F3D_EXPORT void f3d_log_set_use_coloring(int use);
+
+  /**
+   * @brief Set the verbose level.
+   *
+   * By default, only warnings and errors are written to stderr, debug and info are written to
+   * stdout. If force_std_err is non-zero, all messages including debug and info are written to
+   * stderr.
+   *
+   * @param level The verbose level to set.
+   * @param force_std_err If non-zero, all messages are written to stderr.
+   */
+  F3D_EXPORT void f3d_log_set_verbose_level(f3d_log_verbose_level_t level, int force_std_err);
+
+  /**
+   * @brief Get the current verbose level.
+   *
+   * @return The current verbose level.
+   */
+  F3D_EXPORT f3d_log_verbose_level_t f3d_log_get_verbose_level(void);
+
+  /**
+   * @brief Set a callback function to forward log messages.
+   *
+   * The callback will be invoked with the level and the message string whenever a message is
+   * logged, regardless of the verbose level. Set to NULL to disable forwarding.
+   *
+   * @param callback The callback function, or NULL to disable forwarding.
+   */
+  F3D_EXPORT void f3d_log_forward(f3d_log_forward_fn_t callback);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_LOG_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/options_c_api.cxx f3d-3.4.1/c/options_c_api.cxx
--- f3d-3.2.0+dfsg/c/options_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/options_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,527 @@
+#include "options_c_api.h"
+#include "options.h"
+
+#include <cstring>
+#include <string>
+#include <vector>
+
+//----------------------------------------------------------------------------
+f3d_options_t* f3d_options_create()
+{
+  f3d::options* cpp_options = new f3d::options();
+  return reinterpret_cast<f3d_options_t*>(cpp_options);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_delete(f3d_options_t* options)
+{
+  if (!options)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  delete cpp_options;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_bool(f3d_options_t* options, const char* name, int value)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->set(name, static_cast<bool>(value));
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_int(f3d_options_t* options, const char* name, int value)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->set(name, value);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_double(f3d_options_t* options, const char* name, double value)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->set(name, value);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_string(f3d_options_t* options, const char* name, const char* value)
+{
+  if (!options || !name || !value)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->set(name, std::string(value));
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_double_vector(
+  f3d_options_t* options, const char* name, const double* values, size_t count)
+{
+  if (!options || !name || !values)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  std::vector<double> vec(values, values + count);
+  cpp_options->set(name, vec);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_int_vector(
+  f3d_options_t* options, const char* name, const int* values, size_t count)
+{
+  if (!options || !name || !values)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  std::vector<int> vec(values, values + count);
+  cpp_options->set(name, vec);
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_get_as_bool(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return 0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  return std::get<bool>(cpp_options->get(name)) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_get_as_int(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return 0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  return std::get<int>(cpp_options->get(name));
+}
+
+//----------------------------------------------------------------------------
+double f3d_options_get_as_double(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return 0.0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  return std::get<double>(cpp_options->get(name));
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_get_as_string(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return nullptr;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  const std::string str = std::get<std::string>(cpp_options->get(name));
+  char* result = new char[str.length() + 1];
+  std::strcpy(result, str.c_str());
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_get_as_double_vector(
+  const f3d_options_t* options, const char* name, double* values, size_t* count)
+{
+  if (!options || !name || !values || !count)
+  {
+    return;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  const std::vector<double> vec = std::get<std::vector<double>>(cpp_options->get(name));
+  *count = vec.size();
+  std::copy(vec.begin(), vec.end(), values);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_get_as_int_vector(
+  const f3d_options_t* options, const char* name, int* values, size_t* count)
+{
+  if (!options || !name || !values || !count)
+  {
+    return;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  const std::vector<int> vec = std::get<std::vector<int>>(cpp_options->get(name));
+  *count = vec.size();
+  std::copy(vec.begin(), vec.end(), values);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_toggle(f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->toggle(name);
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_is_same(const f3d_options_t* options, const f3d_options_t* other, const char* name)
+{
+  if (!options || !other || !name)
+  {
+    return 0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  const f3d::options* cpp_other = reinterpret_cast<const f3d::options*>(other);
+  return cpp_options->isSame(*cpp_other, name) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_has_value(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return 0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  return cpp_options->hasValue(name) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_copy(f3d_options_t* options, const f3d_options_t* other, const char* name)
+{
+  if (!options || !other || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  const f3d::options* cpp_other = reinterpret_cast<const f3d::options*>(other);
+  cpp_options->copy(*cpp_other, name);
+}
+
+//----------------------------------------------------------------------------
+char** f3d_options_get_all_names(size_t* count)
+{
+  if (!count)
+  {
+    return nullptr;
+  }
+
+  std::vector<std::string> names = f3d::options::getAllNames();
+  *count = names.size();
+
+  char** result = new char*[names.size()];
+  for (size_t i = 0; i < names.size(); i++)
+  {
+    result[i] = new char[names[i].length() + 1];
+    std::strcpy(result[i], names[i].c_str());
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+char** f3d_options_get_names(const f3d_options_t* options, size_t* count)
+{
+  if (!options || !count)
+  {
+    return nullptr;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  std::vector<std::string> names = cpp_options->getNames();
+  *count = names.size();
+
+  char** result = new char*[names.size()];
+  for (size_t i = 0; i < names.size(); i++)
+  {
+    result[i] = new char[names[i].length() + 1];
+    std::strcpy(result[i], names[i].c_str());
+  }
+
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_free_names(char** names, size_t count)
+{
+  if (!names)
+  {
+    return;
+  }
+
+  for (size_t i = 0; i < count; i++)
+  {
+    delete[] names[i];
+  }
+  delete[] names;
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_is_optional(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return 0;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  return cpp_options->isOptional(name) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_reset(f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->reset(name);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_remove_value(f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->removeValue(name);
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_get_as_string_representation(const f3d_options_t* options, const char* name)
+{
+  if (!options || !name)
+  {
+    return nullptr;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  std::string str = cpp_options->getAsString(name);
+  char* result = new char[str.length() + 1];
+  std::strcpy(result, str.c_str());
+  return result;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_set_as_string_representation(
+  f3d_options_t* options, const char* name, const char* str)
+{
+  if (!options || !name || !str)
+  {
+    return;
+  }
+
+  f3d::options* cpp_options = reinterpret_cast<f3d::options*>(options);
+  cpp_options->setAsString(name, str);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_free_string(const char* str)
+{
+  if (!str)
+  {
+    return;
+  }
+
+  delete[] str;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_get_closest_option(
+  const f3d_options_t* options, const char* option, char** closest, unsigned int* distance)
+{
+  if (!options || !option || !closest || !distance)
+  {
+    return;
+  }
+
+  const f3d::options* cpp_options = reinterpret_cast<const f3d::options*>(options);
+  auto result = cpp_options->getClosestOption(option);
+
+  char* result_str = new char[result.first.length() + 1];
+  std::strcpy(result_str, result.first.c_str());
+  *closest = result_str;
+  *distance = result.second;
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_parse_bool(const char* str)
+{
+  if (!str)
+  {
+    return 0;
+  }
+
+  return f3d::options::parse<bool>(str) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+int f3d_options_parse_int(const char* str)
+{
+  if (!str)
+  {
+    return 0;
+  }
+
+  return f3d::options::parse<int>(str);
+}
+
+//----------------------------------------------------------------------------
+double f3d_options_parse_double(const char* str)
+{
+  if (!str)
+  {
+    return 0.0;
+  }
+
+  return f3d::options::parse<double>(str);
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_parse_string(const char* str)
+{
+  if (!str)
+  {
+    return nullptr;
+  }
+
+  std::string result = f3d::options::parse<std::string>(str);
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_parse_double_vector(const char* str, double* values, size_t* count)
+{
+  if (!str || !values || !count)
+  {
+    return;
+  }
+
+  std::vector<double> vec = f3d::options::parse<std::vector<double>>(str);
+  *count = vec.size();
+  std::copy(vec.begin(), vec.end(), values);
+}
+
+//----------------------------------------------------------------------------
+void f3d_options_parse_int_vector(const char* str, int* values, size_t* count)
+{
+  if (!str || !values || !count)
+  {
+    return;
+  }
+
+  std::vector<int> vec = f3d::options::parse<std::vector<int>>(str);
+  *count = vec.size();
+  std::copy(vec.begin(), vec.end(), values);
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_bool(int value)
+{
+  std::string result = f3d::options::format(static_cast<bool>(value));
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_int(int value)
+{
+  std::string result = f3d::options::format(value);
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_double(double value)
+{
+  std::string result = f3d::options::format(value);
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_string(const char* value)
+{
+  if (!value)
+  {
+    return nullptr;
+  }
+
+  std::string result = f3d::options::format(std::string(value));
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_double_vector(const double* values, size_t count)
+{
+  if (!values)
+  {
+    return nullptr;
+  }
+
+  std::vector<double> vec(values, values + count);
+  std::string result = f3d::options::format(vec);
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
+
+//----------------------------------------------------------------------------
+const char* f3d_options_format_int_vector(const int* values, size_t count)
+{
+  if (!values)
+  {
+    return nullptr;
+  }
+
+  std::vector<int> vec(values, values + count);
+  std::string result = f3d::options::format(vec);
+  char* result_str = new char[result.length() + 1];
+  std::strcpy(result_str, result.c_str());
+  return result_str;
+}
diff -Nru f3d-3.2.0+dfsg/c/options_c_api.h f3d-3.4.1/c/options_c_api.h
--- f3d-3.2.0+dfsg/c/options_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/options_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,405 @@
+#ifndef F3D_OPTIONS_C_API_H
+#define F3D_OPTIONS_C_API_H
+
+#include "export.h"
+#include "types_c_api.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  typedef struct f3d_options_t f3d_options_t;
+
+  ///@{ @name Options lifecycle
+  /**
+   * @brief Create a new options object.
+   *
+   * The returned options object must be freed with f3d_options_delete().
+   *
+   * @return Options handle.
+   */
+  F3D_EXPORT f3d_options_t* f3d_options_create();
+
+  /**
+   * @brief Delete an options object.
+   *
+   * @param options Options handle to delete.
+   */
+  F3D_EXPORT void f3d_options_delete(f3d_options_t* options);
+  ///@}
+
+  ///@{ @name Option setters
+  /**
+   * @brief Set an option value as a boolean.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param value Boolean value (0 for false, non-zero for true).
+   */
+  F3D_EXPORT void f3d_options_set_as_bool(f3d_options_t* options, const char* name, int value);
+
+  /**
+   * @brief Set an option value as an integer.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param value Integer value.
+   */
+  F3D_EXPORT void f3d_options_set_as_int(f3d_options_t* options, const char* name, int value);
+
+  /**
+   * @brief Set an option value as a double.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param value Double value.
+   */
+  F3D_EXPORT void f3d_options_set_as_double(f3d_options_t* options, const char* name, double value);
+
+  /**
+   * @brief Set an option value as a string.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param value String value.
+   */
+  F3D_EXPORT void f3d_options_set_as_string(
+    f3d_options_t* options, const char* name, const char* value);
+
+  /**
+   * @brief Set an option value as a double vector.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param values Array of double values.
+   * @param count Number of values in the array.
+   */
+  F3D_EXPORT void f3d_options_set_as_double_vector(
+    f3d_options_t* options, const char* name, const double* values, size_t count);
+
+  /**
+   * @brief Set an option value as an integer vector.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param values Array of integer values.
+   * @param count Number of values in the array.
+   */
+  F3D_EXPORT void f3d_options_set_as_int_vector(
+    f3d_options_t* options, const char* name, const int* values, size_t count);
+  ///@}
+
+  ///@{ @name Option getters
+  /**
+   * @brief Get an option value as a boolean.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return Boolean value (0 for false, non-zero for true).
+   */
+  F3D_EXPORT int f3d_options_get_as_bool(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Get an option value as an integer.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return Integer value.
+   */
+  F3D_EXPORT int f3d_options_get_as_int(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Get an option value as a double.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return Double value.
+   */
+  F3D_EXPORT double f3d_options_get_as_double(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Get an option value as a string.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_options_free_string().
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return String value.
+   */
+  F3D_EXPORT const char* f3d_options_get_as_string(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Get an option value as a string representation.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_options_free_string().
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return String representation of the option value.
+   */
+  F3D_EXPORT const char* f3d_options_get_as_string_representation(
+    const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Set an option value from a string representation.
+   *
+   * Parses the string and sets the option to the appropriate type.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param str String representation of the value.
+   */
+  F3D_EXPORT void f3d_options_set_as_string_representation(
+    f3d_options_t* options, const char* name, const char* str);
+
+  /**
+   * @brief Free a string returned by an options function.
+   *
+   * Use this to free strings returned by f3d_options_get_as_string()
+   * and f3d_options_get_as_string_representation().
+   *
+   * @param str String to free.
+   */
+  F3D_EXPORT void f3d_options_free_string(const char* str);
+
+  /**
+   * @brief Get an option value as a double vector.
+   *
+   * The caller must provide a pre-allocated array large enough to hold the values.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param values Pre-allocated array to store the double values.
+   * @param count Pointer to store the number of values retrieved.
+   */
+  F3D_EXPORT void f3d_options_get_as_double_vector(
+    const f3d_options_t* options, const char* name, double* values, size_t* count);
+
+  /**
+   * @brief Get an option value as an integer vector.
+   *
+   * The caller must provide a pre-allocated array large enough to hold the values.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @param values Pre-allocated array to store the integer values.
+   * @param count Pointer to store the number of values retrieved.
+   */
+  F3D_EXPORT void f3d_options_get_as_int_vector(
+    const f3d_options_t* options, const char* name, int* values, size_t* count);
+  ///@}
+
+  ///@{ @name Option manipulation
+  /**
+   * @brief Toggle a boolean option value.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   */
+  F3D_EXPORT void f3d_options_toggle(f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Check if an option value is the same in two options objects.
+   *
+   * @param options Options handle.
+   * @param other Other options handle to compare with.
+   * @param name Option name.
+   * @return 1 if the values are the same, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_options_is_same(
+    const f3d_options_t* options, const f3d_options_t* other, const char* name);
+
+  /**
+   * @brief Check if an option has a value set.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return 1 if the option has a value, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_options_has_value(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Copy an option value from another options object.
+   *
+   * @param options Destination options handle.
+   * @param other Source options handle to copy from.
+   * @param name Option name.
+   */
+  F3D_EXPORT void f3d_options_copy(
+    f3d_options_t* options, const f3d_options_t* other, const char* name);
+  ///@}
+
+  /**
+   * @brief Get all option names.
+   *
+   * The returned array is heap-allocated and must be freed with f3d_options_free_names().
+   *
+   * @param count Pointer to store the count of names.
+   * @return Array of option names.
+   */
+  F3D_EXPORT char** f3d_options_get_all_names(size_t* count);
+
+  /**
+   * @brief Get option names that have values.
+   *
+   * The returned array is heap-allocated and must be freed with f3d_options_free_names().
+   *
+   * @param options Options handle.
+   * @param count Pointer to store the count of names.
+   * @return Array of option names.
+   */
+  F3D_EXPORT char** f3d_options_get_names(const f3d_options_t* options, size_t* count);
+
+  /**
+   * @brief Get the closest option name and its Levenshtein distance.
+   *
+   * @param options Options handle.
+   * @param option Option name to match.
+   * @param closest Output parameter for the closest option name. Caller must free with
+   *                f3d_options_free_string().
+   * @param distance Output parameter for the Levenshtein distance.
+   */
+  F3D_EXPORT void f3d_options_get_closest_option(
+    const f3d_options_t* options, const char* option, char** closest, unsigned int* distance);
+
+  /**
+   * @brief Free an array of option names.
+   *
+   * @param names Array of names to free.
+   * @param count Number of names in the array.
+   */
+  F3D_EXPORT void f3d_options_free_names(char** names, size_t count);
+
+  /**
+   * @brief Check if an option is optional.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   * @return 1 if the option is optional, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_options_is_optional(const f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Reset an option to its default value.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   */
+  F3D_EXPORT void f3d_options_reset(f3d_options_t* options, const char* name);
+
+  /**
+   * @brief Remove an option value if it is optional.
+   *
+   * @param options Options handle.
+   * @param name Option name.
+   */
+  F3D_EXPORT void f3d_options_remove_value(f3d_options_t* options, const char* name);
+
+  ///@{ @name Parsing and formatting
+  /**
+   * @brief Parse a string as a boolean.
+   *
+   * @param str String to parse.
+   * @return 1 for true, 0 for false.
+   */
+  F3D_EXPORT int f3d_options_parse_bool(const char* str);
+
+  /**
+   * @brief Parse a string as an integer.
+   *
+   * @param str String to parse.
+   * @return Parsed integer value.
+   */
+  F3D_EXPORT int f3d_options_parse_int(const char* str);
+
+  /**
+   * @brief Parse a string as a double.
+   *
+   * @param str String to parse.
+   * @return Parsed double value.
+   */
+  F3D_EXPORT double f3d_options_parse_double(const char* str);
+
+  /**
+   * @brief Parse a string as a string (returns a copy).
+   *
+   * @param str String to parse.
+   * @return Parsed string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_parse_string(const char* str);
+
+  /**
+   * @brief Parse a string as a double vector.
+   *
+   * @param str String to parse.
+   * @param values Pre-allocated array to store the double values.
+   * @param count Pointer to store the number of values retrieved.
+   */
+  F3D_EXPORT void f3d_options_parse_double_vector(const char* str, double* values, size_t* count);
+
+  /**
+   * @brief Parse a string as an integer vector.
+   *
+   * @param str String to parse.
+   * @param values Pre-allocated array to store the integer values.
+   * @param count Pointer to store the number of values retrieved.
+   */
+  F3D_EXPORT void f3d_options_parse_int_vector(const char* str, int* values, size_t* count);
+
+  /**
+   * @brief Format a boolean as a string.
+   *
+   * @param value Boolean value.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_bool(int value);
+
+  /**
+   * @brief Format an integer as a string.
+   *
+   * @param value Integer value.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_int(int value);
+
+  /**
+   * @brief Format a double as a string.
+   *
+   * @param value Double value.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_double(double value);
+
+  /**
+   * @brief Format a string (returns a copy).
+   *
+   * @param value String value.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_string(const char* value);
+
+  /**
+   * @brief Format a double vector as a string.
+   *
+   * @param values Array of double values.
+   * @param count Number of values in the array.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_double_vector(const double* values, size_t count);
+
+  /**
+   * @brief Format an integer vector as a string.
+   *
+   * @param values Array of integer values.
+   * @param count Number of values in the array.
+   * @return Formatted string. Caller must free with f3d_options_free_string().
+   */
+  F3D_EXPORT const char* f3d_options_format_int_vector(const int* values, size_t count);
+  ///@}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_OPTIONS_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/scene_c_api.cxx f3d-3.4.1/c/scene_c_api.cxx
--- f3d-3.2.0+dfsg/c/scene_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/scene_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,315 @@
+#include "scene_c_api.h"
+#include "scene.h"
+#include "types.h"
+
+#include <filesystem>
+#include <vector>
+
+namespace
+{
+//----------------------------------------------------------------------------
+f3d::light_state_t to_cpp_light_state(const f3d_light_state_t* c_state)
+{
+  f3d::light_state_t cpp_state;
+
+  switch (c_state->type)
+  {
+    case F3D_LIGHT_TYPE_HEADLIGHT:
+      cpp_state.type = f3d::light_type::HEADLIGHT;
+      break;
+    case F3D_LIGHT_TYPE_CAMERA_LIGHT:
+      cpp_state.type = f3d::light_type::CAMERA_LIGHT;
+      break;
+    case F3D_LIGHT_TYPE_SCENE_LIGHT:
+      cpp_state.type = f3d::light_type::SCENE_LIGHT;
+      break;
+  }
+
+  cpp_state.position[0] = c_state->position[0];
+  cpp_state.position[1] = c_state->position[1];
+  cpp_state.position[2] = c_state->position[2];
+
+  cpp_state.color[0] = c_state->color.data[0];
+  cpp_state.color[1] = c_state->color.data[1];
+  cpp_state.color[2] = c_state->color.data[2];
+
+  cpp_state.direction[0] = c_state->direction[0];
+  cpp_state.direction[1] = c_state->direction[1];
+  cpp_state.direction[2] = c_state->direction[2];
+
+  cpp_state.positionalLight = c_state->positional_light != 0;
+  cpp_state.intensity = c_state->intensity;
+  cpp_state.switchState = c_state->switch_state != 0;
+
+  return cpp_state;
+}
+
+//----------------------------------------------------------------------------
+f3d_light_state_t* to_c_light_state(const f3d::light_state_t& cpp_state)
+{
+  f3d_light_state_t* c_state = new f3d_light_state_t;
+
+  switch (cpp_state.type)
+  {
+    case f3d::light_type::HEADLIGHT:
+      c_state->type = F3D_LIGHT_TYPE_HEADLIGHT;
+      break;
+    case f3d::light_type::CAMERA_LIGHT:
+      c_state->type = F3D_LIGHT_TYPE_CAMERA_LIGHT;
+      break;
+    case f3d::light_type::SCENE_LIGHT:
+      c_state->type = F3D_LIGHT_TYPE_SCENE_LIGHT;
+      break;
+  }
+
+  c_state->position[0] = cpp_state.position[0];
+  c_state->position[1] = cpp_state.position[1];
+  c_state->position[2] = cpp_state.position[2];
+
+  c_state->color.data[0] = cpp_state.color[0];
+  c_state->color.data[1] = cpp_state.color[1];
+  c_state->color.data[2] = cpp_state.color[2];
+
+  c_state->direction[0] = cpp_state.direction[0];
+  c_state->direction[1] = cpp_state.direction[1];
+  c_state->direction[2] = cpp_state.direction[2];
+
+  c_state->positional_light = cpp_state.positionalLight ? 1 : 0;
+  c_state->intensity = cpp_state.intensity;
+  c_state->switch_state = cpp_state.switchState ? 1 : 0;
+
+  return c_state;
+}
+
+//----------------------------------------------------------------------------
+f3d::mesh_t to_cpp_mesh(const f3d_mesh_t* c_mesh)
+{
+  f3d::mesh_t cpp_mesh;
+
+  if (c_mesh->points && c_mesh->points_count > 0)
+  {
+    cpp_mesh.points.assign(c_mesh->points, c_mesh->points + c_mesh->points_count);
+  }
+
+  if (c_mesh->normals && c_mesh->normals_count > 0)
+  {
+    cpp_mesh.normals.assign(c_mesh->normals, c_mesh->normals + c_mesh->normals_count);
+  }
+
+  if (c_mesh->texture_coordinates && c_mesh->texture_coordinates_count > 0)
+  {
+    cpp_mesh.texture_coordinates.assign(
+      c_mesh->texture_coordinates, c_mesh->texture_coordinates + c_mesh->texture_coordinates_count);
+  }
+
+  if (c_mesh->face_sides && c_mesh->face_sides_count > 0)
+  {
+    cpp_mesh.face_sides.assign(c_mesh->face_sides, c_mesh->face_sides + c_mesh->face_sides_count);
+  }
+
+  if (c_mesh->face_indices && c_mesh->face_indices_count > 0)
+  {
+    cpp_mesh.face_indices.assign(
+      c_mesh->face_indices, c_mesh->face_indices + c_mesh->face_indices_count);
+  }
+
+  return cpp_mesh;
+}
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_supports(f3d_scene_t* scene, const char* file_path)
+{
+  if (!scene || !file_path)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  return cpp_scene->supports(std::filesystem::path(file_path)) ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+void f3d_scene_clear(f3d_scene_t* scene)
+{
+  if (!scene)
+  {
+    return;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  cpp_scene->clear();
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_add(f3d_scene_t* scene, const char* file_path)
+{
+  if (!scene || !file_path)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  cpp_scene->add(std::filesystem::path(file_path));
+  return 1;
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_add_multiple(f3d_scene_t* scene, const char** file_paths, size_t count)
+{
+  if (!scene || !file_paths)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  std::vector<std::filesystem::path> paths;
+  paths.reserve(count);
+
+  for (size_t i = 0; i < count; ++i)
+  {
+    if (file_paths[i])
+    {
+      paths.emplace_back(file_paths[i]);
+    }
+  }
+
+  cpp_scene->add(paths);
+  return 1;
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_add_mesh(f3d_scene_t* scene, const f3d_mesh_t* mesh)
+{
+  if (!scene || !mesh)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  f3d::mesh_t cpp_mesh = to_cpp_mesh(mesh);
+  cpp_scene->add(cpp_mesh);
+  return 1;
+}
+
+//----------------------------------------------------------------------------
+void f3d_scene_load_animation_time(f3d_scene_t* scene, double time_value)
+{
+  if (!scene)
+  {
+    return;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  cpp_scene->loadAnimationTime(time_value);
+}
+
+//----------------------------------------------------------------------------
+void f3d_scene_animation_time_range(f3d_scene_t* scene, double* min_time, double* max_time)
+{
+  if (!scene)
+  {
+    return;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  auto range = cpp_scene->animationTimeRange();
+
+  if (min_time)
+  {
+    *min_time = range.first;
+  }
+  if (max_time)
+  {
+    *max_time = range.second;
+  }
+}
+
+//----------------------------------------------------------------------------
+unsigned int f3d_scene_available_animations(const f3d_scene_t* scene)
+{
+  if (!scene)
+  {
+    return 0;
+  }
+
+  const f3d::scene* cpp_scene = reinterpret_cast<const f3d::scene*>(scene);
+  return cpp_scene->availableAnimations();
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_add_light(const f3d_scene_t* scene, const f3d_light_state_t* light_state)
+{
+  if (!scene || !light_state)
+  {
+    return -1;
+  }
+
+  const f3d::scene* cpp_scene = reinterpret_cast<const f3d::scene*>(scene);
+  f3d::light_state_t cpp_state = to_cpp_light_state(light_state);
+  return cpp_scene->addLight(cpp_state);
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_remove_light(f3d_scene_t* scene, int index)
+{
+  if (!scene)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  cpp_scene->removeLight(index);
+  return 1;
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_update_light(f3d_scene_t* scene, int index, const f3d_light_state_t* light_state)
+{
+  if (!scene || !light_state)
+  {
+    return 0;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  f3d::light_state_t cpp_state = to_cpp_light_state(light_state);
+  cpp_scene->updateLight(index, cpp_state);
+  return 1;
+}
+
+//----------------------------------------------------------------------------
+f3d_light_state_t* f3d_scene_get_light(const f3d_scene_t* scene, int index)
+{
+  if (!scene)
+  {
+    return nullptr;
+  }
+
+  const f3d::scene* cpp_scene = reinterpret_cast<const f3d::scene*>(scene);
+  f3d::light_state_t cpp_state = cpp_scene->getLight(index);
+  return to_c_light_state(cpp_state);
+}
+
+//----------------------------------------------------------------------------
+int f3d_scene_get_light_count(const f3d_scene_t* scene)
+{
+  if (!scene)
+  {
+    return 0;
+  }
+
+  const f3d::scene* cpp_scene = reinterpret_cast<const f3d::scene*>(scene);
+  return cpp_scene->getLightCount();
+}
+
+//----------------------------------------------------------------------------
+void f3d_scene_remove_all_lights(f3d_scene_t* scene)
+{
+  if (!scene)
+  {
+    return;
+  }
+
+  f3d::scene* cpp_scene = reinterpret_cast<f3d::scene*>(scene);
+  cpp_scene->removeAllLights();
+}
diff -Nru f3d-3.2.0+dfsg/c/scene_c_api.h f3d-3.4.1/c/scene_c_api.h
--- f3d-3.2.0+dfsg/c/scene_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/scene_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,150 @@
+#ifndef F3D_SCENE_C_API_H
+#define F3D_SCENE_C_API_H
+
+#include "export.h"
+#include "types_c_api.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Opaque handle to an f3d::scene object.
+   */
+  typedef struct f3d_scene_t f3d_scene_t;
+
+  /**
+   * @brief Add and load a file into the scene.
+   *
+   * @param scene Scene handle.
+   * @param file_path File path to add.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_scene_add(f3d_scene_t* scene, const char* file_path);
+
+  /**
+   * @brief Add and load multiple files into the scene.
+   *
+   * @param scene Scene handle.
+   * @param file_paths Array of file paths.
+   * @param count Number of file paths in the array.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_scene_add_multiple(f3d_scene_t* scene, const char** file_paths, size_t count);
+
+  /**
+   * @brief Add and load a mesh into the scene.
+   *
+   * @param scene Scene handle.
+   * @param mesh Mesh structure.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_scene_add_mesh(f3d_scene_t* scene, const f3d_mesh_t* mesh);
+
+  /**
+   * @brief Clear the scene of all added files.
+   *
+   * @param scene Scene handle.
+   */
+  F3D_EXPORT void f3d_scene_clear(f3d_scene_t* scene);
+
+  /**
+   * @brief Add a light based on a light state.
+   *
+   * @param scene Scene handle.
+   * @param light_state Light state structure.
+   * @return Index of the added light.
+   */
+  F3D_EXPORT int f3d_scene_add_light(
+    const f3d_scene_t* scene, const f3d_light_state_t* light_state);
+
+  /**
+   * @brief Get the number of lights.
+   *
+   * @param scene Scene handle.
+   * @return Number of lights in the scene.
+   */
+  F3D_EXPORT int f3d_scene_get_light_count(const f3d_scene_t* scene);
+
+  /**
+   * @brief Get the light state at provided index.
+   *
+   * The returned light_state is heap-allocated and must be freed with
+   * f3d_light_state_free().
+   *
+   * @param scene Scene handle.
+   * @param index Index of the light.
+   * @return Light state.
+   */
+  F3D_EXPORT f3d_light_state_t* f3d_scene_get_light(const f3d_scene_t* scene, int index);
+
+  /**
+   * @brief Update a light at provided index with the provided light state.
+   *
+   * @param scene Scene handle.
+   * @param index Index of the light to update.
+   * @param light_state New light state.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_scene_update_light(
+    f3d_scene_t* scene, int index, const f3d_light_state_t* light_state);
+
+  /**
+   * @brief Remove a light at provided index.
+   *
+   * @param scene Scene handle.
+   * @param index Index of the light to remove.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_scene_remove_light(f3d_scene_t* scene, int index);
+
+  /**
+   * @brief Remove all lights from the scene.
+   *
+   * @param scene Scene handle.
+   */
+  F3D_EXPORT void f3d_scene_remove_all_lights(f3d_scene_t* scene);
+
+  /**
+   * @brief Check if a file path is supported by the scene.
+   *
+   * @param scene Scene handle.
+   * @param file_path File path to check.
+   * @return 1 if supported, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_scene_supports(f3d_scene_t* scene, const char* file_path);
+
+  /**
+   * @brief Load added files at provided time value if they contain any animation.
+   *
+   * @param scene Scene handle.
+   * @param time_value Time value to load.
+   */
+  F3D_EXPORT void f3d_scene_load_animation_time(f3d_scene_t* scene, double time_value);
+
+  /**
+   * @brief Get animation time range of currently added files.
+   *
+   * @param scene Scene handle.
+   * @param min_time Pointer to store minimum time.
+   * @param max_time Pointer to store maximum time.
+   */
+  F3D_EXPORT void f3d_scene_animation_time_range(
+    f3d_scene_t* scene, double* min_time, double* max_time);
+
+  /**
+   * @brief Return the number of animations available in the currently loaded files.
+   *
+   * @param scene Scene handle.
+   * @return Number of available animations.
+   */
+  F3D_EXPORT unsigned int f3d_scene_available_animations(const f3d_scene_t* scene);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_SCENE_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/testing/CMakeLists.txt f3d-3.4.1/c/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/c/testing/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,31 @@
+list(APPEND f3d_c_api_tests_list
+  test_camera_c_api.c
+  test_engine_c_api.c
+  test_image_c_api.c
+  test_interactor_c_api.c
+  test_log_c_api.c
+  test_options_c_api.c
+  test_scene_c_api.c
+  test_types_c_api.c
+  test_utils_c_api.c
+  test_window_c_api.c
+)
+
+set(CMAKE_TESTDRIVER_EXTRA_INCLUDES "")
+set(CMAKE_TESTDRIVER_ARGVC_FUNCTION "")
+set(CMAKE_TESTDRIVER_BEFORE_TESTMAIN "")
+set(CMAKE_TESTDRIVER_AFTER_TESTMAIN "")
+create_test_sourcelist(_f3d_c_api_driver_src f3d_c_api_tests.c ${f3d_c_api_tests_list})
+
+# add the executable
+add_executable(f3d_c_api_tests ${_f3d_c_api_driver_src})
+
+target_link_libraries(f3d_c_api_tests PRIVATE c_api)
+target_compile_definitions(f3d_c_api_tests PRIVATE F3D_TESTING_DATA_DIR="${F3D_SOURCE_DIR}/testing/data/")
+
+# Add all the ADD_TEST for each test
+foreach (test ${f3d_c_api_tests_list})
+  get_filename_component (TName ${test} NAME_WE)
+  add_test(NAME c_api::${TName} COMMAND f3d_c_api_tests ${TName} "${F3D_SOURCE_DIR}/testing/" "${CMAKE_BINARY_DIR}/Testing/Temporary/" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
+  set_tests_properties(c_api::${TName} PROPERTIES TIMEOUT 30)
+endforeach ()
diff -Nru f3d-3.2.0+dfsg/c/testing/test_camera_c_api.c f3d-3.4.1/c/testing/test_camera_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_camera_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_camera_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,81 @@
+#include <camera_c_api.h>
+#include <engine_c_api.h>
+#include <window_c_api.h>
+
+#include <stdio.h>
+
+int test_camera_c_api()
+{
+  f3d_engine_t* engine = f3d_engine_create(1);
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  f3d_window_t* window = f3d_engine_get_window(engine);
+  if (!window)
+  {
+    puts("[ERROR] Failed to get window");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_camera_t* camera = f3d_window_get_camera(window);
+  if (!camera)
+  {
+    puts("[ERROR] Failed to get camera");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_point3_t pos = { 1.0, 2.0, 3.0 };
+  f3d_camera_set_position(camera, pos);
+  f3d_point3_t get_pos;
+  f3d_camera_get_position(camera, get_pos);
+
+  f3d_point3_t focal = { 0.0, 0.0, 0.0 };
+  f3d_camera_set_focal_point(camera, focal);
+  f3d_point3_t get_focal;
+  f3d_camera_get_focal_point(camera, get_focal);
+
+  f3d_vector3_t view_up = { 0.0, 1.0, 0.0 };
+  f3d_camera_set_view_up(camera, view_up);
+  f3d_vector3_t get_view_up;
+  f3d_camera_get_view_up(camera, get_view_up);
+
+  f3d_camera_set_view_angle(camera, 30.0);
+  f3d_angle_deg_t angle = f3d_camera_get_view_angle(camera);
+  (void)angle;
+
+  f3d_camera_state_t state = { 0 };
+  state.position[0] = 5.0;
+  state.position[1] = 5.0;
+  state.position[2] = 5.0;
+  state.focal_point[0] = 0.0;
+  state.focal_point[1] = 0.0;
+  state.focal_point[2] = 0.0;
+  state.view_up[0] = 0.0;
+  state.view_up[1] = 1.0;
+  state.view_up[2] = 0.0;
+  state.view_angle = 45.0;
+  f3d_camera_set_state(camera, &state);
+  f3d_camera_state_t get_state;
+  f3d_camera_get_state(camera, &get_state);
+
+  f3d_camera_dolly(camera, 1.5);
+  f3d_camera_pan(camera, 0.1, 0.2, 0.3);
+  f3d_camera_zoom(camera, 0.9);
+  f3d_camera_roll(camera, 10.0);
+  f3d_camera_azimuth(camera, 15.0);
+  f3d_camera_yaw(camera, 20.0);
+  f3d_camera_elevation(camera, 25.0);
+  f3d_camera_pitch(camera, 30.0);
+
+  f3d_camera_set_current_as_default(camera);
+  f3d_camera_reset_to_default(camera);
+  f3d_camera_reset_to_bounds(camera, 0.9);
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_engine_c_api.c f3d-3.4.1/c/testing/test_engine_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_engine_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_engine_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,93 @@
+#include <engine_c_api.h>
+#include <interactor_c_api.h>
+#include <scene_c_api.h>
+#include <window_c_api.h>
+
+#include <stdio.h>
+
+int test_engine_c_api()
+{
+  f3d_engine_t* engine = f3d_engine_create(1);
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  const f3d_scene_t* scene = f3d_engine_get_scene(engine);
+  if (!scene)
+  {
+    puts("[ERROR] get_scene() should return a valid scene");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  const f3d_options_t* options = f3d_engine_get_options(engine);
+  if (!options)
+  {
+    puts("[ERROR] get_options() should return valid options");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_window_t* window = f3d_engine_get_window(engine);
+  (void)window;
+
+  f3d_interactor_t* interactor = f3d_engine_get_interactor(engine);
+  (void)interactor;
+
+  f3d_engine_set_cache_path(engine, "/tmp/f3d_test_cache");
+
+  f3d_engine_autoload_plugins();
+  f3d_engine_load_plugin("native");
+
+  char** plugins_list = f3d_engine_get_plugins_list("inexistent");
+  (void)plugins_list;
+
+  f3d_engine_set_reader_option("QuakeMDL.skin_index", "0");
+
+  char** option_names = f3d_engine_get_all_reader_option_names();
+  f3d_engine_free_string_array(option_names);
+
+  f3d_backend_info_t* backends = f3d_engine_get_rendering_backend_list(NULL);
+  f3d_engine_free_backend_list(backends);
+
+  f3d_lib_info_t* lib_info = f3d_engine_get_lib_info();
+  if (!lib_info)
+  {
+    puts("[ERROR] get_lib_info() should return valid info");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+  if (!lib_info->version || !lib_info->vtk_version)
+  {
+    puts("[ERROR] lib_info should have version and vtk_version");
+    f3d_engine_free_lib_info(lib_info);
+    f3d_engine_delete(engine);
+    return 1;
+  }
+  f3d_engine_free_lib_info(lib_info);
+
+  int reader_count = 0;
+  f3d_reader_info_t* readers = f3d_engine_get_readers_info(&reader_count);
+  if (!readers || reader_count == 0)
+  {
+    puts("[ERROR] get_readers_info() should return at least one reader");
+    f3d_engine_free_readers_info(readers);
+    f3d_engine_delete(engine);
+    return 1;
+  }
+  f3d_engine_free_readers_info(readers);
+
+  f3d_options_t* new_options = f3d_engine_get_options(engine);
+  f3d_engine_set_options(engine, new_options);
+
+  f3d_engine_t* engine_none = f3d_engine_create_none();
+  if (engine_none)
+  {
+    f3d_engine_delete(engine_none);
+  }
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_image_c_api.c f3d-3.4.1/c/testing/test_image_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_image_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_image_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,100 @@
+#include <image_c_api.h>
+
+#include <stdio.h>
+
+int test_image_c_api()
+{
+  f3d_image_t* img_empty = f3d_image_new_empty();
+  if (img_empty)
+  {
+    f3d_image_delete(img_empty);
+  }
+
+  f3d_image_t* img = f3d_image_new_params(800, 600, 3, BYTE);
+  if (!img)
+  {
+    puts("[ERROR] Failed to create image");
+    return 1;
+  }
+
+  unsigned int width = f3d_image_get_width(img);
+  (void)width;
+  unsigned int height = f3d_image_get_height(img);
+  (void)height;
+  unsigned int channels = f3d_image_get_channel_count(img);
+  (void)channels;
+  unsigned int type = f3d_image_get_channel_type(img);
+  (void)type;
+  unsigned int type_size = f3d_image_get_channel_type_size(img);
+  (void)type_size;
+
+  void* content = f3d_image_get_content(img);
+  if (content)
+  {
+    f3d_image_set_content(img, content);
+  }
+
+  double pixel[3];
+  f3d_image_get_normalized_pixel(img, 0, 0, pixel);
+
+  f3d_image_set_metadata(img, "Author", "TestUser");
+  const char* author = f3d_image_get_metadata(img, "Author");
+  (void)author;
+
+  unsigned int count;
+  char** metadata_keys = f3d_image_all_metadata(img, &count);
+  if (metadata_keys)
+  {
+    f3d_image_free_metadata_keys(metadata_keys, count);
+  }
+
+  f3d_image_t* ref_img = f3d_image_new_params(800, 600, 3, BYTE);
+  if (ref_img)
+  {
+    double error = f3d_image_compare(img, ref_img);
+    (void)error;
+
+    int is_equal = f3d_image_equals(img, ref_img);
+    (void)is_equal;
+
+    int is_not_equal = f3d_image_not_equals(img, ref_img);
+    (void)is_not_equal;
+
+    f3d_image_delete(ref_img);
+  }
+
+  unsigned int buffer_size;
+  unsigned char* buffer = f3d_image_save_buffer(img, PNG, &buffer_size);
+  if (buffer)
+  {
+    f3d_image_free_buffer(buffer);
+  }
+
+  const char* tmp_path = "/tmp/f3d_test_image.png";
+  f3d_image_save(img, tmp_path, PNG);
+
+  f3d_image_t* img_from_file = f3d_image_new_path(tmp_path);
+  if (img_from_file)
+  {
+    f3d_image_delete(img_from_file);
+  }
+
+  const char* text = f3d_image_to_terminal_text_string(img);
+  (void)text;
+
+  f3d_image_to_terminal_text(img, stdout);
+
+  unsigned int format_count = f3d_image_get_supported_formats_count();
+  (void)format_count;
+  const char** formats = f3d_image_get_supported_formats();
+  (void)formats;
+
+  f3d_image_t* img2 = f3d_image_new_params(100, 100, 4, BYTE);
+  if (img2)
+  {
+    f3d_image_delete(img2);
+  }
+
+  f3d_image_delete(img);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_interactor_c_api.c f3d-3.4.1/c/testing/test_interactor_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_interactor_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_interactor_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,142 @@
+#include <engine_c_api.h>
+#include <interactor_c_api.h>
+#include <window_c_api.h>
+
+#include <stdio.h>
+#include <string.h>
+
+static void stop_callback(void* user_data)
+{
+  f3d_interactor_t* interactor = (f3d_interactor_t*)user_data;
+  f3d_interactor_request_stop(interactor);
+}
+
+int test_interactor_c_api()
+{
+  f3d_engine_t* engine = f3d_engine_create(1);
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  f3d_window_t* window = f3d_engine_get_window(engine);
+  if (window)
+  {
+    f3d_window_render(window);
+  }
+
+  f3d_interactor_t* interactor = f3d_engine_get_interactor(engine);
+  if (!interactor)
+  {
+    puts("[ERROR] Failed to get interactor");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_interactor_toggle_animation(interactor, F3D_INTERACTOR_ANIMATION_FORWARD);
+  f3d_interactor_start_animation(interactor, F3D_INTERACTOR_ANIMATION_BACKWARD);
+  int playing = f3d_interactor_is_playing_animation(interactor);
+  (void)playing;
+  f3d_interactor_animation_direction_t direction =
+    f3d_interactor_get_animation_direction(interactor);
+  (void)direction;
+  f3d_interactor_stop_animation(interactor);
+
+  f3d_interactor_enable_camera_movement(interactor);
+  f3d_interactor_disable_camera_movement(interactor);
+
+  f3d_interactor_trigger_mod_update(interactor, F3D_INTERACTOR_INPUT_CTRL);
+  f3d_interactor_trigger_mouse_button(
+    interactor, F3D_INTERACTOR_INPUT_PRESS, F3D_INTERACTOR_MOUSE_LEFT);
+  f3d_interactor_trigger_mouse_button(
+    interactor, F3D_INTERACTOR_INPUT_RELEASE, F3D_INTERACTOR_MOUSE_RIGHT);
+  f3d_interactor_trigger_mouse_position(interactor, 100.0, 200.0);
+  f3d_interactor_trigger_mouse_wheel(interactor, F3D_INTERACTOR_WHEEL_FORWARD);
+  f3d_interactor_trigger_mouse_wheel(interactor, F3D_INTERACTOR_WHEEL_BACKWARD);
+  f3d_interactor_trigger_keyboard_key(interactor, F3D_INTERACTOR_INPUT_PRESS, "a");
+  f3d_interactor_trigger_keyboard_key(interactor, F3D_INTERACTOR_INPUT_RELEASE, "b");
+  f3d_interactor_trigger_text_character(interactor, 65);
+
+  f3d_interactor_trigger_event_loop(interactor, 0.016);
+
+  f3d_interactor_play_interaction(interactor, "/nonexistent.log", 1.0 / 30.0);
+  f3d_interactor_record_interaction(interactor, "/tmp/test_interaction.log");
+
+  f3d_interactor_request_render(interactor);
+  f3d_interactor_request_stop(interactor);
+
+  f3d_interactor_init_commands(interactor);
+  f3d_interactor_remove_command(interactor, "test_action");
+  f3d_interactor_trigger_command(interactor, "print Test", 0);
+  f3d_interactor_trigger_command(interactor, "print Test # comment", 1);
+
+  f3d_interactor_init_bindings(interactor);
+
+  f3d_interactor_add_command(interactor, "test_action", NULL, NULL);
+  int action_count = 0;
+  char** actions = f3d_interactor_get_command_actions(interactor, &action_count);
+  if (actions)
+  {
+    f3d_interactor_free_string_array(actions, action_count);
+  }
+
+  f3d_interaction_bind_t bind;
+  bind.mod = F3D_INTERACTION_BIND_NONE;
+  snprintf(bind.inter, sizeof(bind.inter), "t");
+
+  char formatted[512];
+  f3d_interaction_bind_format(&bind, formatted, sizeof(formatted));
+
+  f3d_interaction_bind_t ctrl_bind;
+  ctrl_bind.mod = F3D_INTERACTION_BIND_CTRL;
+  snprintf(ctrl_bind.inter, sizeof(ctrl_bind.inter), "A");
+  f3d_interaction_bind_format(&ctrl_bind, formatted, sizeof(formatted));
+
+  f3d_interaction_bind_t parsed_bind;
+  f3d_interaction_bind_parse("Shift+B", &parsed_bind);
+
+  int equals1 = f3d_interaction_bind_equals(&ctrl_bind, &parsed_bind);
+  (void)equals1;
+
+  int less1 = f3d_interaction_bind_less_than(&ctrl_bind, &parsed_bind);
+  (void)less1;
+
+  const char* test_commands[] = { "test_action" };
+  f3d_interactor_add_binding(interactor, &bind, test_commands, 1, "test_group");
+
+  int group_count = 0;
+  char** groups = f3d_interactor_get_bind_groups(interactor, &group_count);
+  if (groups)
+  {
+    f3d_interactor_free_string_array(groups, group_count);
+  }
+
+  int bind_count = 0;
+  f3d_interaction_bind_t* binds_for_group =
+    f3d_interactor_get_binds_for_group(interactor, "test_group", &bind_count);
+  if (binds_for_group)
+  {
+    f3d_interactor_free_bind_array(binds_for_group);
+  }
+
+  int all_bind_count = 0;
+  f3d_interaction_bind_t* all_binds = f3d_interactor_get_binds(interactor, &all_bind_count);
+  if (all_binds)
+  {
+    f3d_interactor_free_bind_array(all_binds);
+  }
+
+  f3d_binding_documentation_t doc;
+  f3d_interactor_get_binding_documentation(interactor, &bind, &doc);
+
+  f3d_interactor_binding_type_t binding_type = f3d_interactor_get_binding_type(interactor, &bind);
+  (void)binding_type;
+
+  f3d_interactor_remove_binding(interactor, &bind);
+
+  f3d_interactor_start_with_callback(interactor, 0.01, stop_callback, interactor);
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_log_c_api.c f3d-3.4.1/c/testing/test_log_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_log_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_log_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,88 @@
+#include <log_c_api.h>
+
+#include <stdio.h>
+#include <string.h>
+
+static int g_callback_count = 0;
+static f3d_log_verbose_level_t g_last_level = F3D_LOG_INFO;
+static char g_last_message[256] = { 0 };
+
+static void test_log_callback(f3d_log_verbose_level_t level, const char* message)
+{
+  g_callback_count++;
+  g_last_level = level;
+  if (message)
+  {
+    strncpy(g_last_message, message, sizeof(g_last_message) - 1);
+    g_last_message[sizeof(g_last_message) - 1] = '\0';
+  }
+}
+
+int test_log_c_api()
+{
+  int failed = 0;
+
+  f3d_log_verbose_level_t initial_level = f3d_log_get_verbose_level();
+
+  f3d_log_set_verbose_level(F3D_LOG_DEBUG, 0);
+  if (f3d_log_get_verbose_level() != F3D_LOG_DEBUG)
+  {
+    puts("[ERROR] Expected F3D_LOG_DEBUG");
+    failed++;
+  }
+
+  f3d_log_set_verbose_level(F3D_LOG_QUIET, 0);
+  if (f3d_log_get_verbose_level() != F3D_LOG_QUIET)
+  {
+    puts("[ERROR] Expected F3D_LOG_QUIET");
+    failed++;
+  }
+
+  f3d_log_set_verbose_level(initial_level, 0);
+
+  f3d_log_set_use_coloring(1);
+  f3d_log_set_use_coloring(0);
+
+  f3d_log_verbose_level_t saved_level = f3d_log_get_verbose_level();
+  f3d_log_set_verbose_level(F3D_LOG_QUIET, 0);
+
+  f3d_log_print(F3D_LOG_DEBUG, "Test debug message");
+  f3d_log_print(F3D_LOG_INFO, "Test info message");
+  f3d_log_print(F3D_LOG_WARN, "Test warning message");
+  f3d_log_print(F3D_LOG_ERROR, "Test error message");
+
+  f3d_log_set_verbose_level(saved_level, 0);
+
+  g_callback_count = 0;
+  g_last_level = F3D_LOG_INFO;
+  memset(g_last_message, 0, sizeof(g_last_message));
+
+  f3d_log_forward(test_log_callback);
+  f3d_log_print(F3D_LOG_WARN, "Test callback message");
+
+  if (g_callback_count != 1)
+  {
+    puts("[ERROR] Callback should have been called once");
+    failed++;
+  }
+
+  if (g_last_level != F3D_LOG_WARN)
+  {
+    puts("[ERROR] Expected callback level F3D_LOG_WARN");
+    failed++;
+  }
+
+  if (strcmp(g_last_message, "Test callback message") != 0)
+  {
+    puts("[ERROR] Expected callback message 'Test callback message'");
+    failed++;
+  }
+
+  f3d_log_debug(NULL);
+  f3d_log_warn(NULL);
+  f3d_log_info(NULL);
+  f3d_log_error(NULL);
+  f3d_log_forward(NULL);
+
+  return failed;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_options_c_api.c f3d-3.4.1/c/testing/test_options_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_options_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_options_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,178 @@
+#include <engine_c_api.h>
+#include <options_c_api.h>
+
+#include <stdio.h>
+
+int test_options_c_api()
+{
+  f3d_options_t* standalone_options = f3d_options_create();
+  f3d_options_delete(standalone_options);
+
+  f3d_engine_t* engine = f3d_engine_create_none();
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  f3d_options_t* options = f3d_engine_get_options(engine);
+  if (!options)
+  {
+    puts("[ERROR] Failed to get options");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_options_set_as_bool(options, "model.scivis.cells", 1);
+  f3d_options_set_as_int(options, "model.scivis.component", 2);
+  f3d_options_set_as_double(options, "render.line_width", 3.5);
+  f3d_options_set_as_string(options, "render.effect.final_shader", "test.glsl");
+
+  const double vec_values[] = { 1.0, 2.0, 3.0 };
+  f3d_options_set_as_double_vector(options, "render.background.color", vec_values, 3);
+
+  const int int_vec_values[] = { 1, 2 };
+  f3d_options_set_as_int_vector(options, "scene.animation.indices", int_vec_values, 2);
+
+  int bool_val = f3d_options_get_as_bool(options, "model.scivis.cells");
+  (void)bool_val;
+
+  int int_val = f3d_options_get_as_int(options, "model.scivis.component");
+  (void)int_val;
+
+  double double_val = f3d_options_get_as_double(options, "render.line_width");
+  (void)double_val;
+
+  const char* str_val = f3d_options_get_as_string(options, "render.effect.final_shader");
+  if (str_val)
+  {
+    f3d_options_free_string(str_val);
+  }
+
+  double out_vec[3];
+  size_t out_count;
+  f3d_options_get_as_double_vector(options, "render.background.color", out_vec, &out_count);
+
+  int out_int_vec[3];
+  size_t out_int_count;
+  f3d_options_get_as_int_vector(options, "scene.animation.indices", out_int_vec, &out_int_count);
+
+  f3d_options_toggle(options, "model.scivis.cells");
+
+  f3d_engine_t* engine2 = f3d_engine_create_none();
+  if (engine2)
+  {
+    f3d_options_t* options2 = f3d_engine_get_options(engine2);
+    if (options2)
+    {
+      int same = f3d_options_is_same(options, options2, "model.scivis.cells");
+      (void)same;
+
+      f3d_options_copy(options2, options, "model.scivis.cells");
+    }
+    f3d_engine_delete(engine2);
+  }
+
+  int has_value = f3d_options_has_value(options, "model.scivis.cells");
+  (void)has_value;
+
+  size_t all_count;
+  char** all_names = f3d_options_get_all_names(&all_count);
+  if (all_names)
+  {
+    f3d_options_free_names(all_names, all_count);
+  }
+
+  size_t names_count;
+  char** names = f3d_options_get_names(options, &names_count);
+  if (names)
+  {
+    f3d_options_free_names(names, names_count);
+  }
+
+  int is_optional = f3d_options_is_optional(options, "render.show_edges");
+  (void)is_optional;
+
+  f3d_options_reset(options, "model.scivis.cells");
+  f3d_options_remove_value(options, "render.show_edges");
+
+  const char* str_repr = f3d_options_get_as_string_representation(options, "render.line_width");
+  if (str_repr)
+  {
+    f3d_options_free_string(str_repr);
+  }
+
+  f3d_options_set_as_string_representation(options, "render.line_width", "5.0");
+
+  char* closest = NULL;
+  unsigned int distance = 0;
+  f3d_options_get_closest_option(options, "render.line_wdth", &closest, &distance);
+  if (closest)
+  {
+    f3d_options_free_string(closest);
+  }
+
+  int parsed_bool = f3d_options_parse_bool("true");
+  (void)parsed_bool;
+
+  int parsed_int = f3d_options_parse_int("42");
+  (void)parsed_int;
+
+  double parsed_double = f3d_options_parse_double("3.14");
+  (void)parsed_double;
+
+  const char* parsed_string = f3d_options_parse_string("test");
+  if (parsed_string)
+  {
+    f3d_options_free_string(parsed_string);
+  }
+
+  double parsed_dvec[3];
+  size_t parsed_dvec_count;
+  f3d_options_parse_double_vector("1.0,2.0,3.0", parsed_dvec, &parsed_dvec_count);
+
+  int parsed_ivec[3];
+  size_t parsed_ivec_count;
+  f3d_options_parse_int_vector("1,2,3", parsed_ivec, &parsed_ivec_count);
+
+  const char* fmt_bool = f3d_options_format_bool(1);
+  if (fmt_bool)
+  {
+    f3d_options_free_string(fmt_bool);
+  }
+
+  const char* fmt_int = f3d_options_format_int(42);
+  if (fmt_int)
+  {
+    f3d_options_free_string(fmt_int);
+  }
+
+  const char* fmt_double = f3d_options_format_double(3.14);
+  if (fmt_double)
+  {
+    f3d_options_free_string(fmt_double);
+  }
+
+  const char* fmt_string = f3d_options_format_string("test");
+  if (fmt_string)
+  {
+    f3d_options_free_string(fmt_string);
+  }
+
+  const double fmt_dvec[] = { 1.0, 2.0, 3.0 };
+  const char* fmt_dvec_str = f3d_options_format_double_vector(fmt_dvec, 3);
+  if (fmt_dvec_str)
+  {
+    f3d_options_free_string(fmt_dvec_str);
+  }
+
+  const int fmt_ivec[] = { 1, 2, 3 };
+  const char* fmt_ivec_str = f3d_options_format_int_vector(fmt_ivec, 3);
+  if (fmt_ivec_str)
+  {
+    f3d_options_free_string(fmt_ivec_str);
+  }
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_scene_c_api.c f3d-3.4.1/c/testing/test_scene_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_scene_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_scene_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,108 @@
+#include <engine_c_api.h>
+#include <scene_c_api.h>
+#include <types_c_api.h>
+#include <utils_c_api.h>
+
+#include <stdio.h>
+
+int test_scene_c_api()
+{
+  f3d_engine_autoload_plugins();
+
+  f3d_engine_t* engine = f3d_engine_create(1);
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  f3d_scene_t* scene = f3d_engine_get_scene(engine);
+  if (!scene)
+  {
+    puts("[ERROR] Failed to get scene");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  int supported = f3d_scene_supports(scene, "test.obj");
+  (void)supported;
+
+  int add_result = f3d_scene_add(scene, F3D_TESTING_DATA_DIR "cow.vtp");
+  (void)add_result;
+
+  const char* files[] = { F3D_TESTING_DATA_DIR "cow.vtp", F3D_TESTING_DATA_DIR "suzanne.obj" };
+  int add_multiple_result = f3d_scene_add_multiple(scene, files, 2);
+  (void)add_multiple_result;
+
+  f3d_scene_clear(scene);
+
+  float points[] = { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.0f };
+  unsigned int face_sides[] = { 3 };
+  unsigned int face_indices[] = { 0, 1, 2 };
+
+  f3d_mesh_t mesh = { 0 };
+  mesh.points = points;
+  mesh.points_count = 9;
+  mesh.face_sides = face_sides;
+  mesh.face_sides_count = 1;
+  mesh.face_indices = face_indices;
+  mesh.face_indices_count = 3;
+
+  char* error_msg = NULL;
+  int valid = f3d_mesh_is_valid(&mesh, &error_msg);
+  (void)valid;
+  if (error_msg)
+  {
+    f3d_utils_string_free(error_msg);
+  }
+
+  f3d_scene_add_mesh(scene, &mesh);
+
+  f3d_scene_load_animation_time(scene, 0.5);
+  double min_time, max_time;
+  f3d_scene_animation_time_range(scene, &min_time, &max_time);
+  unsigned int anim_count = f3d_scene_available_animations(scene);
+  (void)anim_count;
+
+  f3d_light_state_t light_state = { 0 };
+  light_state.type = F3D_LIGHT_TYPE_HEADLIGHT;
+  light_state.intensity = 1.0;
+  light_state.switch_state = 1;
+
+  int light_idx = f3d_scene_add_light(scene, &light_state);
+  (void)light_idx;
+
+  int light_count = f3d_scene_get_light_count(scene);
+  (void)light_count;
+
+  if (light_idx >= 0)
+  {
+    f3d_light_state_t* get_light = f3d_scene_get_light(scene, light_idx);
+    if (get_light)
+    {
+      f3d_light_state_free(get_light);
+    }
+
+    f3d_light_state_t update_light = light_state;
+    update_light.intensity = 2.0;
+    f3d_scene_update_light(scene, light_idx, &update_light);
+
+    f3d_scene_remove_light(scene, light_idx);
+  }
+
+  f3d_light_state_t light1 = { 0 };
+  light1.type = F3D_LIGHT_TYPE_HEADLIGHT;
+  light1.intensity = 1.0;
+
+  f3d_light_state_t light2 = { 0 };
+  light2.type = F3D_LIGHT_TYPE_CAMERA_LIGHT;
+  light2.intensity = 1.0;
+
+  int equal = f3d_light_state_equal(&light1, &light2);
+  (void)equal;
+
+  f3d_scene_remove_all_lights(scene);
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_types_c_api.c f3d-3.4.1/c/testing/test_types_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_types_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_types_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,36 @@
+#include <types_c_api.h>
+
+#include <stdio.h>
+
+int test_types_c_api()
+{
+  f3d_color_t color;
+  f3d_color_set(&color, 1.0, 0.5, 0.25);
+
+  double r = f3d_color_r(&color);
+  (void)r;
+  double g = f3d_color_g(&color);
+  (void)g;
+  double b = f3d_color_b(&color);
+  (void)b;
+
+  f3d_direction_t direction;
+  f3d_direction_set(&direction, 1.0, 0.0, 0.0);
+
+  double x = f3d_direction_x(&direction);
+  (void)x;
+  double y = f3d_direction_y(&direction);
+  (void)y;
+  double z = f3d_direction_z(&direction);
+  (void)z;
+
+  f3d_transform2d_t transform;
+  f3d_transform2d_create(&transform, 1.0, 1.0, 10.0, 20.0, 45.0);
+
+  f3d_colormap_t colormap;
+  colormap.data = NULL;
+  colormap.count = 0;
+  f3d_colormap_free(&colormap);
+
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_utils_c_api.c f3d-3.4.1/c/testing/test_utils_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_utils_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_utils_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,78 @@
+#include <utils_c_api.h>
+
+#include <stdio.h>
+#include <string.h>
+
+int test_utils_c_api()
+{
+  int failed = 0;
+
+  if (f3d_utils_text_distance("kitten", "sitting") != 3)
+  {
+    puts("[ERROR] text_distance(\"kitten\", \"sitting\") != 3");
+    failed++;
+  }
+
+  if (f3d_utils_text_distance("same", "same") != 0)
+  {
+    puts("[ERROR] text_distance(\"same\", \"same\") != 0");
+    failed++;
+  }
+
+  const char* cmd = "one two \"three four\" # comment";
+  size_t tok_count = 0;
+  char** tokens = f3d_utils_tokenize(cmd, 1, &tok_count);
+
+  if (!(tokens && tok_count == 3 && strcmp(tokens[0], "one") == 0 &&
+        strcmp(tokens[1], "two") == 0 && strcmp(tokens[2], "three four") == 0))
+  {
+    puts("[ERROR] tokenize() without comments failed");
+    failed++;
+  }
+
+  f3d_utils_tokens_free(tokens, tok_count);
+
+  char* collapsed = f3d_utils_collapse_path(".", NULL);
+  if (!collapsed || collapsed[0] == '\0')
+  {
+    puts("[ERROR] collapse_path returned NULL/empty");
+    failed++;
+  }
+  f3d_utils_string_free(collapsed);
+
+  char* regex = f3d_utils_glob_to_regex("*.txt", '/');
+  if (!regex || regex[0] == '\0')
+  {
+    puts("[ERROR] glob_to_regex returned NULL/empty");
+    failed++;
+  }
+  f3d_utils_string_free(regex);
+
+  char* env_val = f3d_utils_get_env("PATH");
+  if (!env_val)
+  {
+    puts("[ERROR] get_env(\"PATH\") returned NULL");
+    failed++;
+  }
+  f3d_utils_string_free(env_val);
+
+#ifdef _WIN32
+  char* kf = f3d_utils_get_known_folder(F3D_UTILS_KNOWN_FOLDER_ROAMINGAPPDATA);
+  if (!kf || kf[0] == '\0')
+  {
+    puts("[ERROR] get_known_folder returned NULL/empty on Windows");
+    failed++;
+  }
+  f3d_utils_string_free(kf);
+#else
+  char* kf = f3d_utils_get_known_folder(F3D_UTILS_KNOWN_FOLDER_ROAMINGAPPDATA);
+  if (kf != NULL)
+  {
+    puts("[ERROR] get_known_folder should return NULL on non-Windows");
+    f3d_utils_string_free(kf);
+    failed++;
+  }
+#endif
+
+  return failed;
+}
diff -Nru f3d-3.2.0+dfsg/c/testing/test_window_c_api.c f3d-3.4.1/c/testing/test_window_c_api.c
--- f3d-3.2.0+dfsg/c/testing/test_window_c_api.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/testing/test_window_c_api.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,64 @@
+#include <engine_c_api.h>
+#include <image_c_api.h>
+#include <window_c_api.h>
+
+#include <stdio.h>
+
+int test_window_c_api()
+{
+  f3d_engine_t* engine = f3d_engine_create(1);
+  if (!engine)
+  {
+    puts("[ERROR] Failed to create engine");
+    return 1;
+  }
+
+  f3d_window_t* window = f3d_engine_get_window(engine);
+  if (!window)
+  {
+    puts("[ERROR] Failed to get window");
+    f3d_engine_delete(engine);
+    return 1;
+  }
+
+  f3d_window_type_t type = f3d_window_get_type(window);
+  (void)type;
+
+  int offscreen = f3d_window_is_offscreen(window);
+  (void)offscreen;
+
+  f3d_camera_t* camera = f3d_window_get_camera(window);
+  (void)camera;
+
+  f3d_window_render(window);
+
+  f3d_image_t* img = f3d_window_render_to_image(window, 0);
+  if (img)
+  {
+    f3d_image_delete(img);
+  }
+
+  f3d_window_set_size(window, 800, 600);
+  int width = f3d_window_get_width(window);
+  (void)width;
+  int height = f3d_window_get_height(window);
+  (void)height;
+
+  f3d_window_set_position(window, 100, 100);
+
+  unsigned char icon_data[] = { 0xFF, 0xFF, 0xFF, 0xFF };
+  f3d_window_set_icon(window, icon_data, sizeof(icon_data));
+
+  f3d_window_set_window_name(window, "Test Window");
+
+  f3d_point3_t display_point = { 400.0, 300.0, 0.0 };
+  f3d_point3_t world_point;
+  f3d_window_get_world_from_display(window, display_point, world_point);
+
+  f3d_point3_t test_world = { 0.0, 0.0, 0.0 };
+  f3d_point3_t display_out;
+  f3d_window_get_display_from_world(window, test_world, display_out);
+
+  f3d_engine_delete(engine);
+  return 0;
+}
diff -Nru f3d-3.2.0+dfsg/c/types_c_api.cxx f3d-3.4.1/c/types_c_api.cxx
--- f3d-3.2.0+dfsg/c/types_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/types_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,257 @@
+#include "types_c_api.h"
+#include "types.h"
+#include <cmath>
+#include <cstring>
+
+// f3d_color_t functions
+//----------------------------------------------------------------------------
+double f3d_color_r(const f3d_color_t* color)
+{
+  if (!color)
+  {
+    return 0.0;
+  }
+  return color->data[0];
+}
+
+//----------------------------------------------------------------------------
+double f3d_color_g(const f3d_color_t* color)
+{
+  if (!color)
+  {
+    return 0.0;
+  }
+  return color->data[1];
+}
+
+//----------------------------------------------------------------------------
+double f3d_color_b(const f3d_color_t* color)
+{
+  if (!color)
+  {
+    return 0.0;
+  }
+  return color->data[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_color_set(f3d_color_t* color, double r, double g, double b)
+{
+  if (!color)
+  {
+    return;
+  }
+  color->data[0] = r;
+  color->data[1] = g;
+  color->data[2] = b;
+}
+
+// f3d_direction_t functions
+//----------------------------------------------------------------------------
+double f3d_direction_x(const f3d_direction_t* dir)
+{
+  if (!dir)
+  {
+    return 0.0;
+  }
+  return dir->data[0];
+}
+
+//----------------------------------------------------------------------------
+double f3d_direction_y(const f3d_direction_t* dir)
+{
+  if (!dir)
+  {
+    return 0.0;
+  }
+  return dir->data[1];
+}
+
+//----------------------------------------------------------------------------
+double f3d_direction_z(const f3d_direction_t* dir)
+{
+  if (!dir)
+  {
+    return 0.0;
+  }
+  return dir->data[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_direction_set(f3d_direction_t* dir, double x, double y, double z)
+{
+  if (!dir)
+  {
+    return;
+  }
+  dir->data[0] = x;
+  dir->data[1] = y;
+  dir->data[2] = z;
+}
+
+// f3d_transform2d_t functions
+//----------------------------------------------------------------------------
+void f3d_transform2d_create(f3d_transform2d_t* transform, double scale_x, double scale_y,
+  double translate_x, double translate_y, f3d_angle_deg_t angle_deg)
+{
+  if (!transform)
+  {
+    return;
+  }
+
+  try
+  {
+    f3d::double_array_t<2> scale({ scale_x, scale_y });
+    f3d::double_array_t<2> translate({ translate_x, translate_y });
+    f3d::transform2d_t cpp_transform(scale, translate, angle_deg);
+
+    for (int i = 0; i < 9; ++i)
+    {
+      transform->data[i] = cpp_transform[i];
+    }
+  }
+  catch (...)
+  {
+    // Initialize to identity on error
+    for (int i = 0; i < 9; ++i)
+    {
+      transform->data[i] = (i % 4 == 0) ? 1.0 : 0.0;
+    }
+  }
+}
+
+// f3d_colormap_t functions
+//----------------------------------------------------------------------------
+void f3d_colormap_free(f3d_colormap_t* colormap)
+{
+  if (!colormap)
+  {
+    return;
+  }
+  delete[] colormap->data;
+  colormap->data = nullptr;
+  colormap->count = 0;
+}
+
+// f3d_mesh_t functions
+//----------------------------------------------------------------------------
+int f3d_mesh_is_valid(const f3d_mesh_t* mesh, char** error_message)
+{
+  if (!mesh)
+  {
+    if (error_message)
+    {
+      const char* msg = "Mesh pointer is NULL";
+      *error_message = new char[std::strlen(msg) + 1];
+      std::strcpy(*error_message, msg);
+    }
+    return 0;
+  }
+
+  try
+  {
+    f3d::mesh_t cpp_mesh;
+
+    if (mesh->points && mesh->points_count > 0)
+    {
+      cpp_mesh.points.assign(mesh->points, mesh->points + mesh->points_count);
+    }
+
+    if (mesh->normals && mesh->normals_count > 0)
+    {
+      cpp_mesh.normals.assign(mesh->normals, mesh->normals + mesh->normals_count);
+    }
+
+    if (mesh->texture_coordinates && mesh->texture_coordinates_count > 0)
+    {
+      cpp_mesh.texture_coordinates.assign(
+        mesh->texture_coordinates, mesh->texture_coordinates + mesh->texture_coordinates_count);
+    }
+
+    if (mesh->face_sides && mesh->face_sides_count > 0)
+    {
+      cpp_mesh.face_sides.assign(mesh->face_sides, mesh->face_sides + mesh->face_sides_count);
+    }
+
+    if (mesh->face_indices && mesh->face_indices_count > 0)
+    {
+      cpp_mesh.face_indices.assign(
+        mesh->face_indices, mesh->face_indices + mesh->face_indices_count);
+    }
+
+    // Check validity
+    auto [valid, msg] = cpp_mesh.isValid();
+
+    if (!valid && error_message)
+    {
+      *error_message = new char[msg.size() + 1];
+      std::strcpy(*error_message, msg.c_str());
+    }
+
+    return valid ? 1 : 0;
+  }
+  catch (...)
+  {
+    if (error_message)
+    {
+      const char* msg = "Exception during mesh validation";
+      *error_message = new char[std::strlen(msg) + 1];
+      std::strcpy(*error_message, msg);
+    }
+    return 0;
+  }
+}
+
+// f3d_light_state_t functions
+//----------------------------------------------------------------------------
+void f3d_light_state_free(f3d_light_state_t* light_state)
+{
+  delete light_state;
+}
+
+//----------------------------------------------------------------------------
+int f3d_light_state_equal(const f3d_light_state_t* a, const f3d_light_state_t* b)
+{
+  if (!a || !b)
+  {
+    return 0;
+  }
+
+  if (a->type != b->type)
+  {
+    return 0;
+  }
+
+  for (int i = 0; i < 3; ++i)
+  {
+    if (a->position[i] != b->position[i])
+    {
+      return 0;
+    }
+    if (a->color.data[i] != b->color.data[i])
+    {
+      return 0;
+    }
+    if (a->direction[i] != b->direction[i])
+    {
+      return 0;
+    }
+  }
+
+  if (a->positional_light != b->positional_light)
+  {
+    return 0;
+  }
+
+  if (a->intensity != b->intensity)
+  {
+    return 0;
+  }
+
+  if (a->switch_state != b->switch_state)
+  {
+    return 0;
+  }
+
+  return 1;
+}
diff -Nru f3d-3.2.0+dfsg/c/types_c_api.h f3d-3.4.1/c/types_c_api.h
--- f3d-3.2.0+dfsg/c/types_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/types_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,207 @@
+#ifndef F3D_TYPES_C_API_H
+#define F3D_TYPES_C_API_H
+
+#include "export.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Describe a 3D point.
+   */
+  typedef double f3d_point3_t[3];
+
+  /**
+   * @brief Describe a 3D vector.
+   */
+  typedef double f3d_vector3_t[3];
+
+  /**
+   * @brief Describe an angle in degrees.
+   */
+  typedef double f3d_angle_deg_t;
+
+  /**
+   * @brief Describe a ratio.
+   */
+  typedef struct f3d_ratio_t
+  {
+    double value;
+  } f3d_ratio_t;
+
+  /**
+   * @brief Describe a RGB color.
+   */
+  typedef struct f3d_color_t
+  {
+    double data[3];
+  } f3d_color_t;
+
+  /**
+   * @brief Get the red component of a color.
+   */
+  F3D_EXPORT double f3d_color_r(const f3d_color_t* color);
+
+  /**
+   * @brief Get the green component of a color.
+   */
+  F3D_EXPORT double f3d_color_g(const f3d_color_t* color);
+
+  /**
+   * @brief Get the blue component of a color.
+   */
+  F3D_EXPORT double f3d_color_b(const f3d_color_t* color);
+
+  /**
+   * @brief Set color components.
+   */
+  F3D_EXPORT void f3d_color_set(f3d_color_t* color, double r, double g, double b);
+
+  /**
+   * @brief Describe a 3D direction.
+   */
+  typedef struct f3d_direction_t
+  {
+    double data[3];
+  } f3d_direction_t;
+
+  /**
+   * @brief Get the x component of a direction.
+   */
+  F3D_EXPORT double f3d_direction_x(const f3d_direction_t* dir);
+
+  /**
+   * @brief Get the y component of a direction.
+   */
+  F3D_EXPORT double f3d_direction_y(const f3d_direction_t* dir);
+
+  /**
+   * @brief Get the z component of a direction.
+   */
+  F3D_EXPORT double f3d_direction_z(const f3d_direction_t* dir);
+
+  /**
+   * @brief Set direction components.
+   */
+  F3D_EXPORT void f3d_direction_set(f3d_direction_t* dir, double x, double y, double z);
+
+  /**
+   * @brief Store a 3x3 transform matrix as a sequence of 9 double values.
+   */
+  typedef struct f3d_transform2d_t
+  {
+    double data[9];
+  } f3d_transform2d_t;
+
+  /**
+   * @brief Create a 2D transform from scale, translate and angle.
+   *
+   * @param transform Transform structure to fill.
+   * @param scale_x Scale factor in x.
+   * @param scale_y Scale factor in y.
+   * @param translate_x Translation in x.
+   * @param translate_y Translation in y.
+   * @param angle_deg Rotation angle in degrees.
+   */
+  F3D_EXPORT void f3d_transform2d_create(f3d_transform2d_t* transform, double scale_x,
+    double scale_y, double translate_x, double translate_y, f3d_angle_deg_t angle_deg);
+
+  /**
+   * @brief Describe a colormap.
+   */
+  typedef struct f3d_colormap_t
+  {
+    double* data;
+    size_t count;
+  } f3d_colormap_t;
+
+  /**
+   * @brief Free a colormap structure.
+   *
+   * @param colormap Colormap to free.
+   */
+  F3D_EXPORT void f3d_colormap_free(f3d_colormap_t* colormap);
+
+  /**
+   * @brief Describe a 3D surfacic mesh.
+   */
+  typedef struct f3d_mesh_t
+  {
+    float* points;
+    size_t points_count;
+
+    float* normals;
+    size_t normals_count;
+
+    float* texture_coordinates;
+    size_t texture_coordinates_count;
+
+    unsigned int* face_sides;
+    size_t face_sides_count;
+
+    unsigned int* face_indices;
+    size_t face_indices_count;
+  } f3d_mesh_t;
+
+  /**
+   * @brief Check validity of a mesh.
+   *
+   * The returned error message string is heap-allocated and must be freed with
+   * f3d_utils_string_free().
+   *
+   * @param mesh Mesh to validate.
+   * @param error_message Pointer to receive error message if invalid.
+   * @return 1 if valid, 0 if invalid.
+   */
+  F3D_EXPORT int f3d_mesh_is_valid(const f3d_mesh_t* mesh, char** error_message);
+
+  /**
+   * @brief Enumeration of light types.
+   */
+  typedef enum f3d_light_type_t
+  {
+    F3D_LIGHT_TYPE_HEADLIGHT = 1,
+    F3D_LIGHT_TYPE_CAMERA_LIGHT = 2,
+    F3D_LIGHT_TYPE_SCENE_LIGHT = 3
+  } f3d_light_type_t;
+
+  /**
+   * @brief Structure describing the state of a light.
+   */
+  typedef struct f3d_light_state_t
+  {
+    f3d_light_type_t type;
+    f3d_point3_t position;
+    f3d_color_t color;
+    f3d_vector3_t direction;
+    int positional_light;
+    double intensity;
+    int switch_state;
+  } f3d_light_state_t;
+
+  /**
+   * @brief Free a light state structure.
+   *
+   * @param light_state Light state to free.
+   */
+  F3D_EXPORT void f3d_light_state_free(f3d_light_state_t* light_state);
+
+  /**
+   * @brief Compare two light states for equality.
+   *
+   * @param a First light state.
+   * @param b Second light state.
+   * @return 1 if equal, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_light_state_equal(const f3d_light_state_t* a, const f3d_light_state_t* b);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_TYPES_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/utils_c_api.cxx f3d-3.4.1/c/utils_c_api.cxx
--- f3d-3.2.0+dfsg/c/utils_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/utils_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,134 @@
+#include "utils_c_api.h"
+#include "utils.h"
+#include <cstring>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+namespace
+{
+//----------------------------------------------------------------------------
+char* f3d_utils_strdup(const std::string& s)
+{
+  char* r = new char[s.size() + 1];
+  std::memcpy(r, s.c_str(), s.size() + 1);
+  return r;
+}
+
+//----------------------------------------------------------------------------
+void f3d_utils_internal_tokens_free(char** tokens, size_t count)
+{
+  if (!tokens)
+  {
+    return;
+  }
+  for (size_t i = 0; i < count; ++i)
+  {
+    delete[] tokens[i];
+  }
+  delete[] tokens;
+}
+} // namespace
+
+//----------------------------------------------------------------------------
+unsigned int f3d_utils_text_distance(const char* str_a, const char* str_b)
+{
+  std::string a = str_a ? str_a : "";
+  std::string b = str_b ? str_b : "";
+  return f3d::utils::textDistance(a, b);
+}
+
+//----------------------------------------------------------------------------
+char** f3d_utils_tokenize(const char* str, int keep_comments, size_t* out_count)
+{
+  if (!str)
+  {
+    if (out_count)
+    {
+      *out_count = 0;
+    }
+    return nullptr;
+  }
+
+  std::vector<std::string> vec = f3d::utils::tokenize(str, keep_comments != 0);
+
+  size_t n = vec.size();
+  char** out = new char*[n];
+  for (size_t i = 0; i < n; ++i)
+  {
+    out[i] = f3d_utils_strdup(vec[i]);
+  }
+
+  if (out_count)
+  {
+    *out_count = n;
+  }
+
+  return out;
+}
+
+//----------------------------------------------------------------------------
+void f3d_utils_tokens_free(char** tokens, size_t count)
+{
+  f3d_utils_internal_tokens_free(tokens, count);
+}
+
+//----------------------------------------------------------------------------
+char* f3d_utils_collapse_path(const char* path, const char* base_directory)
+{
+  std::filesystem::path p(path);
+  std::filesystem::path base = base_directory ? base_directory : "";
+  auto collapsed = f3d::utils::collapsePath(p, base);
+  return f3d_utils_strdup(collapsed.string());
+}
+
+//----------------------------------------------------------------------------
+char* f3d_utils_glob_to_regex(const char* glob, char path_separator)
+{
+  std::string regex = f3d::utils::globToRegex(glob, path_separator);
+  return f3d_utils_strdup(regex);
+}
+
+//----------------------------------------------------------------------------
+char* f3d_utils_get_env(const char* env)
+{
+  auto opt = f3d::utils::getEnv(std::string(env));
+  if (!opt.has_value())
+  {
+    return nullptr;
+  }
+  return f3d_utils_strdup(*opt);
+}
+
+//----------------------------------------------------------------------------
+char* f3d_utils_get_known_folder(f3d_utils_known_folder_t known_folder)
+{
+  f3d::utils::KnownFolder kf;
+  switch (known_folder)
+  {
+    case F3D_UTILS_KNOWN_FOLDER_ROAMINGAPPDATA:
+      kf = f3d::utils::KnownFolder::ROAMINGAPPDATA;
+      break;
+    case F3D_UTILS_KNOWN_FOLDER_LOCALAPPDATA:
+      kf = f3d::utils::KnownFolder::LOCALAPPDATA;
+      break;
+    case F3D_UTILS_KNOWN_FOLDER_PICTURES:
+      kf = f3d::utils::KnownFolder::PICTURES;
+      break;
+    default:
+      return nullptr;
+  }
+
+  auto opt = f3d::utils::getKnownFolder(kf);
+  if (!opt.has_value())
+  {
+    return nullptr;
+  }
+  return f3d_utils_strdup(*opt);
+}
+
+//----------------------------------------------------------------------------
+void f3d_utils_string_free(char* str)
+{
+  delete[] str;
+}
diff -Nru f3d-3.2.0+dfsg/c/utils_c_api.h f3d-3.4.1/c/utils_c_api.h
--- f3d-3.2.0+dfsg/c/utils_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/utils_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,109 @@
+#ifndef F3D_UTILS_C_API_H
+#define F3D_UTILS_C_API_H
+
+#include "export.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Enumeration of supported Windows known folders.
+   */
+  typedef enum f3d_utils_known_folder_t
+  {
+    F3D_UTILS_KNOWN_FOLDER_ROAMINGAPPDATA,
+    F3D_UTILS_KNOWN_FOLDER_LOCALAPPDATA,
+    F3D_UTILS_KNOWN_FOLDER_PICTURES
+  } f3d_utils_known_folder_t;
+
+  /**
+   * @brief Compute the Levenshtein distance between two strings.
+   *
+   * @param str_a First string.
+   * @param str_b Second string.
+   * @return The Levenshtein distance between the two strings.
+   */
+  F3D_EXPORT unsigned int f3d_utils_text_distance(const char* str_a, const char* str_b);
+
+  /**
+   * @brief Tokenize a string using the same logic as bash.
+   *
+   * The returned array and strings are heap-allocated and must be freed by calling
+   * f3d_utils_tokens_free().
+   *
+   * @param str Input string to tokenize.
+   * @param keep_comments Non-zero to keep comments, zero to treat '#' as a normal character.
+   * @param out_count Pointer to receive the number of tokens.
+   * @return Array of C strings.
+   */
+  F3D_EXPORT char** f3d_utils_tokenize(const char* str, int keep_comments, size_t* out_count);
+
+  /**
+   * @brief Free an array of tokens allocated by f3d_utils_tokenize().
+   *
+   * @param tokens Array of tokens.
+   * @param count Number of tokens in the array.
+   */
+  F3D_EXPORT void f3d_utils_tokens_free(char** tokens, size_t count);
+
+  /**
+   * @brief Collapse a filesystem path.
+   *
+   * Expands '~' to the home directory, makes the path absolute using base_directory
+   * or the current directory, and normalizes '..' components.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_utils_string_free().
+   *
+   * @param path Input path.
+   * @param base_directory Base directory for relative paths.
+   * @return Collapsed absolute path string.
+   */
+  F3D_EXPORT char* f3d_utils_collapse_path(const char* path, const char* base_directory);
+
+  /**
+   * @brief Converts a glob expression to a regular expression.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_utils_string_free().
+   *
+   * @param glob Glob expression.
+   * @param path_separator Path separator character.
+   * @return Regular expression string.
+   */
+  F3D_EXPORT char* f3d_utils_glob_to_regex(const char* glob, char path_separator);
+
+  /**
+   * @brief Get the value of an environment variable.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_utils_string_free().
+   *
+   * @param env Environment variable name.
+   * @return Value of the environment variable.
+   */
+  F3D_EXPORT char* f3d_utils_get_env(const char* env);
+
+  /**
+   * @brief Get a Windows known folder.
+   *
+   * The returned string is heap-allocated and must be freed with f3d_utils_string_free().
+   *
+   * @param known_folder Known folder identifier.
+   * @return Folder path.
+   */
+  F3D_EXPORT char* f3d_utils_get_known_folder(f3d_utils_known_folder_t known_folder);
+
+  /**
+   * @brief Free a string returned by any f3d_utils_* function.
+   *
+   * @param str String to free.
+   */
+  F3D_EXPORT void f3d_utils_string_free(char* str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_UTILS_C_API_H
diff -Nru f3d-3.2.0+dfsg/c/window_c_api.cxx f3d-3.4.1/c/window_c_api.cxx
--- f3d-3.2.0+dfsg/c/window_c_api.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/window_c_api.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,174 @@
+#include "window_c_api.h"
+#include "image.h"
+#include "window.h"
+
+//----------------------------------------------------------------------------
+f3d_window_type_t f3d_window_get_type(f3d_window_t* window)
+{
+  if (!window)
+  {
+    return F3D_WINDOW_UNKNOWN;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  f3d::window::Type type = cpp_window->getType();
+  return static_cast<f3d_window_type_t>(type);
+}
+
+//----------------------------------------------------------------------------
+int f3d_window_is_offscreen(f3d_window_t* window)
+{
+  if (!window)
+  {
+    return 0;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  return cpp_window->isOffscreen() ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+f3d_camera_t* f3d_window_get_camera(f3d_window_t* window)
+{
+  if (!window)
+  {
+    return nullptr;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  f3d::camera& camera = cpp_window->getCamera();
+  return reinterpret_cast<f3d_camera_t*>(&camera);
+}
+
+//----------------------------------------------------------------------------
+int f3d_window_render(f3d_window_t* window)
+{
+  if (!window)
+  {
+    return 0;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  return cpp_window->render() ? 1 : 0;
+}
+
+//----------------------------------------------------------------------------
+f3d_image_t* f3d_window_render_to_image(f3d_window_t* window, int no_background)
+{
+  if (!window)
+  {
+    return nullptr;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  f3d::image img = cpp_window->renderToImage(no_background != 0);
+
+  f3d::image* heap_img = new f3d::image(std::move(img));
+  return reinterpret_cast<f3d_image_t*>(heap_img);
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_set_size(f3d_window_t* window, int width, int height)
+{
+  if (!window)
+  {
+    return;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  cpp_window->setSize(width, height);
+}
+
+//----------------------------------------------------------------------------
+int f3d_window_get_width(const f3d_window_t* window)
+{
+  if (!window)
+  {
+    return 0;
+  }
+
+  const f3d::window* cpp_window = reinterpret_cast<const f3d::window*>(window);
+  return cpp_window->getWidth();
+}
+
+//----------------------------------------------------------------------------
+int f3d_window_get_height(const f3d_window_t* window)
+{
+  if (!window)
+  {
+    return 0;
+  }
+
+  const f3d::window* cpp_window = reinterpret_cast<const f3d::window*>(window);
+  return cpp_window->getHeight();
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_set_position(f3d_window_t* window, int x, int y)
+{
+  if (!window)
+  {
+    return;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  cpp_window->setPosition(x, y);
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_set_icon(f3d_window_t* window, const unsigned char* icon, size_t icon_size)
+{
+  if (!window || !icon)
+  {
+    return;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  cpp_window->setIcon(icon, icon_size);
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_set_window_name(f3d_window_t* window, const char* window_name)
+{
+  if (!window || !window_name)
+  {
+    return;
+  }
+
+  f3d::window* cpp_window = reinterpret_cast<f3d::window*>(window);
+  cpp_window->setWindowName(window_name);
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_get_world_from_display(
+  const f3d_window_t* window, const f3d_point3_t display_point, f3d_point3_t world_point)
+{
+  if (!window || !display_point || !world_point)
+  {
+    return;
+  }
+
+  const f3d::window* cpp_window = reinterpret_cast<const f3d::window*>(window);
+  f3d::point3_t cpp_display_point = { display_point[0], display_point[1], display_point[2] };
+  f3d::point3_t cpp_world_point = cpp_window->getWorldFromDisplay(cpp_display_point);
+  world_point[0] = cpp_world_point[0];
+  world_point[1] = cpp_world_point[1];
+  world_point[2] = cpp_world_point[2];
+}
+
+//----------------------------------------------------------------------------
+void f3d_window_get_display_from_world(
+  const f3d_window_t* window, const f3d_point3_t world_point, f3d_point3_t display_point)
+{
+  if (!window || !world_point || !display_point)
+  {
+    return;
+  }
+
+  const f3d::window* cpp_window = reinterpret_cast<const f3d::window*>(window);
+  f3d::point3_t cpp_world_point = { world_point[0], world_point[1], world_point[2] };
+  f3d::point3_t cpp_display_point = cpp_window->getDisplayFromWorld(cpp_world_point);
+  display_point[0] = cpp_display_point[0];
+  display_point[1] = cpp_display_point[1];
+  display_point[2] = cpp_display_point[2];
+}
diff -Nru f3d-3.2.0+dfsg/c/window_c_api.h f3d-3.4.1/c/window_c_api.h
--- f3d-3.2.0+dfsg/c/window_c_api.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/c/window_c_api.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,158 @@
+#ifndef F3D_WINDOW_C_API_H
+#define F3D_WINDOW_C_API_H
+
+#include "camera_c_api.h"
+#include "export.h"
+#include "image_c_api.h"
+#include "types_c_api.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * @brief Opaque handle to an f3d::window object.
+   */
+  typedef struct f3d_window_t f3d_window_t;
+
+  /**
+   * @brief Enumeration of supported window types.
+   */
+  typedef enum f3d_window_type_t
+  {
+    F3D_WINDOW_NONE,
+    F3D_WINDOW_EXTERNAL,
+    F3D_WINDOW_GLX,
+    F3D_WINDOW_WGL,
+    F3D_WINDOW_COCOA,
+    F3D_WINDOW_EGL,
+    F3D_WINDOW_OSMESA,
+    F3D_WINDOW_WASM,
+    F3D_WINDOW_UNKNOWN
+  } f3d_window_type_t;
+
+  /**
+   * @brief Get the type of the window.
+   *
+   * @param window Window handle.
+   * @return The window type.
+   */
+  F3D_EXPORT f3d_window_type_t f3d_window_get_type(f3d_window_t* window);
+
+  /**
+   * @brief Check if the window is offscreen.
+   *
+   * @param window Window handle.
+   * @return 1 if offscreen, 0 otherwise.
+   */
+  F3D_EXPORT int f3d_window_is_offscreen(f3d_window_t* window);
+
+  /**
+   * @brief Get the camera provided by the window.
+   *
+   * @param window Window handle.
+   * @return Camera handle.
+   */
+  F3D_EXPORT f3d_camera_t* f3d_window_get_camera(f3d_window_t* window);
+
+  /**
+   * @brief Perform a render of the window to the screen.
+   *
+   * All dynamic options are updated if needed.
+   *
+   * @param window Window handle.
+   * @return 1 on success, 0 on failure.
+   */
+  F3D_EXPORT int f3d_window_render(f3d_window_t* window);
+
+  /**
+   * @brief Perform a render of the window to the screen and save the result in an image.
+   *
+   * The image is of ChannelType BYTE and 3 or 4 components (RGB or RGBA).
+   * Set no_background to non-zero to have a transparent background.
+   * The caller must free the returned image with f3d_image_delete().
+   *
+   * @param window Window handle.
+   * @param no_background If non-zero, renders with a transparent background.
+   * @return Image handle containing the rendered result, or NULL on failure.
+   */
+  F3D_EXPORT f3d_image_t* f3d_window_render_to_image(f3d_window_t* window, int no_background);
+
+  /**
+   * @brief Set the size of the window.
+   *
+   * @param window Window handle.
+   * @param width Window width in pixels.
+   * @param height Window height in pixels.
+   */
+  F3D_EXPORT void f3d_window_set_size(f3d_window_t* window, int width, int height);
+
+  /**
+   * @brief Get the width of the window.
+   *
+   * @param window Window handle.
+   * @return Window width in pixels.
+   */
+  F3D_EXPORT int f3d_window_get_width(const f3d_window_t* window);
+
+  /**
+   * @brief Get the height of the window.
+   *
+   * @param window Window handle.
+   * @return Window height in pixels.
+   */
+  F3D_EXPORT int f3d_window_get_height(const f3d_window_t* window);
+
+  /**
+   * @brief Set the position of the window.
+   *
+   * @param window Window handle.
+   * @param x X position in pixels.
+   * @param y Y position in pixels.
+   */
+  F3D_EXPORT void f3d_window_set_position(f3d_window_t* window, int x, int y);
+
+  /**
+   * @brief Set the icon to be shown by a window manager.
+   *
+   * @param window Window handle.
+   * @param icon Icon data as unsigned char array.
+   * @param icon_size Size of icon data in bytes.
+   */
+  F3D_EXPORT void f3d_window_set_icon(
+    f3d_window_t* window, const unsigned char* icon, size_t icon_size);
+
+  /**
+   * @brief Set the window name to be shown by a window manager.
+   *
+   * @param window Window handle.
+   * @param window_name Window name string.
+   */
+  F3D_EXPORT void f3d_window_set_window_name(f3d_window_t* window, const char* window_name);
+
+  /**
+   * @brief Convert a point in display coordinate to world coordinate.
+   *
+   * @param window Window handle.
+   * @param display_point Display coordinate point [x, y, z].
+   * @param world_point Output world coordinate point [x, y, z].
+   */
+  F3D_EXPORT void f3d_window_get_world_from_display(
+    const f3d_window_t* window, const f3d_point3_t display_point, f3d_point3_t world_point);
+
+  /**
+   * @brief Convert a point in world coordinate to display coordinate.
+   *
+   * @param window Window handle.
+   * @param world_point World coordinate point [x, y, z].
+   * @param display_point Output display coordinate point [x, y, z].
+   */
+  F3D_EXPORT void f3d_window_get_display_from_world(
+    const f3d_window_t* window, const f3d_point3_t world_point, f3d_point3_t display_point);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // F3D_WINDOW_C_API_H
diff -Nru f3d-3.2.0+dfsg/.clang-tidy f3d-3.4.1/.clang-tidy
--- f3d-3.2.0+dfsg/.clang-tidy	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.clang-tidy	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-Checks: '-*,performance-*,modernize-*,-modernize-use-trailing-return-type,-modernize-use-nodiscard,-modernize-avoid-c-arrays,-modernize-use-auto,-modernize-avoid-bind,-performance-no-int-to-ptr'
+Checks: '-*,performance-*,modernize-*,-modernize-use-trailing-return-type,-modernize-use-nodiscard,-modernize-avoid-c-arrays,-modernize-use-auto,-modernize-avoid-bind,-performance-no-int-to-ptr,-modernize-use-emplace'
 WarningsAsErrors: '*'
 UseColor: true
 FormatStyle: none
diff -Nru f3d-3.2.0+dfsg/cmake/c_api-config.cmake f3d-3.4.1/cmake/c_api-config.cmake
--- f3d-3.2.0+dfsg/cmake/c_api-config.cmake	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/cmake/c_api-config.cmake	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,10 @@
+if (NOT TARGET f3d::libf3d)
+  include("${CMAKE_CURRENT_LIST_DIR}/library-config.cmake")
+endif ()
+
+include("${CMAKE_CURRENT_LIST_DIR}/f3dCApiTargets.cmake")
+
+set_and_check(f3d_C_API_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include")
+
+message(STATUS "Found f3d c_api component")
+set(f3d_c_api_FOUND TRUE)
diff -Nru f3d-3.2.0+dfsg/cmake/f3dConfig.cmake.in f3d-3.4.1/cmake/f3dConfig.cmake.in
--- f3d-3.2.0+dfsg/cmake/f3dConfig.cmake.in	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/cmake/f3dConfig.cmake.in	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,7 @@
 #
 # The following variables will be defined:
 #
+#   f3d_BINDINGS_C                 Will be enabled if F3D was built with c bindings
 #   f3d_BINDINGS_JAVA              Will be enabled if F3D was built with java bindings
 #   f3d_BINDINGS_PYTHON            Will be enabled if F3D was built with python bindings
 #   f3d_BUILD_APPLICATION          Will be enabled if F3D application was built
@@ -9,6 +10,7 @@
 #   f3d_MODULE_EXR                 Will be enabled if F3D was built with OpenEXR images support
 #   f3d_MODULE_RAYTRACING          Will be enabled if F3D was built with raytracing support
 #   f3d_MODULE_UI                  Will be enabled if F3D was built with ImGui support
+#   f3d_MODULE_WEBP                Will be enabled if F3D was built with WebP images support
 #   f3d_PLUGINS_INSTALL_DIR        Path to the location to install plugins so that F3D can find them
 #
 # It is also possible to look for optional components.
@@ -46,6 +48,8 @@
 set(f3d_MODULE_RAYTRACING "@F3D_MODULE_RAYTRACING@")
 set(f3d_MODULE_EXR "@F3D_MODULE_EXR@")
 set(f3d_MODULE_UI "@F3D_MODULE_UI@")
+set(f3d_MODULE_WEBP "@F3D_MODULE_WEBP@")
+set(f3d_BINDINGS_C "@F3D_BINDINGS_C@")
 set(f3d_BINDINGS_PYTHON "@F3D_BINDINGS_PYTHON@")
 set(f3d_BINDINGS_JAVA "@F3D_BINDINGS_JAVA@")
 set(f3d_CONFIG_DIR "@f3d_config_dir@")
diff -Nru f3d-3.2.0+dfsg/cmake/f3dPlugin.cmake f3d-3.4.1/cmake/f3dPlugin.cmake
--- f3d-3.2.0+dfsg/cmake/f3dPlugin.cmake	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/cmake/f3dPlugin.cmake	2026-01-08 08:41:55.000000000 +0200
@@ -204,7 +204,7 @@
 macro(f3d_plugin_build)
   cmake_parse_arguments(F3D_PLUGIN "FREEDESKTOP;FORCE_STATIC" "NAME;DESCRIPTION;VERSION" "VTK_MODULES;ADDITIONAL_RPATHS;MIMETYPE_XML_FILES;CONFIGURATION_DIRS" ${ARGN})
 
-  find_package(VTK 9.2.6 REQUIRED COMPONENTS
+  find_package(VTK 9.3.0 REQUIRED COMPONENTS
                CommonCore CommonExecutionModel IOImport
                ${F3D_PLUGIN_VTK_MODULES})
 
@@ -255,6 +255,7 @@
 
     vtk_module_build(
       MODULES ${modules}
+      ENABLE_WRAPPING OFF
       INSTALL_HEADERS OFF
       PACKAGE "f3d_${F3D_PLUGIN_NAME}_vtkext_private")
 
diff -Nru f3d-3.2.0+dfsg/cmake/pluginsdk-config.cmake f3d-3.4.1/cmake/pluginsdk-config.cmake
--- f3d-3.2.0+dfsg/cmake/pluginsdk-config.cmake	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/cmake/pluginsdk-config.cmake	2026-01-08 08:41:55.000000000 +0200
@@ -3,7 +3,7 @@
   find_package(X11)
 endif()
 find_package(OpenGL)
-find_package(VTK 9.2.6 REQUIRED COMPONENTS CommonCore CommonExecutionModel IOImport)
+find_package(VTK 9.3.0 REQUIRED COMPONENTS CommonCore CommonExecutionModel IOImport)
 include("${CMAKE_CURRENT_LIST_DIR}/f3dPlugin.cmake")
 include("${CMAKE_CURRENT_LIST_DIR}/../f3d_vtkext/f3d_vtkext-targets.cmake")
 
diff -Nru f3d-3.2.0+dfsg/CMakeLists.txt f3d-3.4.1/CMakeLists.txt
--- f3d-3.2.0+dfsg/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -30,8 +30,8 @@
 
 # Handle F3D_VERSION
 set(F3D_MAJOR_VERSION 3)
-set(F3D_MINOR_VERSION 2)
-set(F3D_PATCH_VERSION 0)
+set(F3D_MINOR_VERSION 4)
+set(F3D_PATCH_VERSION 1)
 set(F3D_VERSION ${F3D_MAJOR_VERSION}.${F3D_MINOR_VERSION}.${F3D_PATCH_VERSION})
 
 set(F3D_PATCH_VERSION_EXTRA "")
@@ -52,36 +52,10 @@
 option(F3D_EXCLUDE_DEPRECATED "Exclude deprecated functions and options" OFF)
 mark_as_advanced(F3D_EXCLUDE_DEPRECATED)
 
-# Modules
-option(F3D_MODULE_RAYTRACING "Raytracing module" OFF)
-option(F3D_MODULE_EXR "OpenEXR images module" OFF)
-option(F3D_MODULE_UI "ImGui widgets module" ON)
-option(F3D_MODULE_DMON "dmon (watch) module" ON)
-option(F3D_MODULE_TINYFILEDIALOGS "tinyfiledialogs module" ON)
-
-# Use externals
-option(F3D_USE_EXTERNAL_CXXOPTS "Use external cxxopts dependency" OFF)
-option(F3D_USE_EXTERNAL_NLOHMANN_JSON "Use external nlohmann_json dependency" OFF)
-cmake_dependent_option(F3D_USE_EXTERNAL_DMON "Use external dmon dependency" OFF "F3D_MODULE_DMON" OFF)
-cmake_dependent_option(F3D_USE_EXTERNAL_IMGUI "Use external imgui dependency" OFF "F3D_MODULE_UI" OFF)
-
-if (F3D_USE_EXTERNAL_CXXOPTS)
-  find_package(cxxopts REQUIRED)
-endif ()
-if (F3D_USE_EXTERNAL_NLOHMANN_JSON)
-  find_package(nlohmann_json REQUIRED)
-endif ()
-if (F3D_USE_EXTERNAL_DMON)
-  find_package(dmon REQUIRED)
-endif ()
-if (F3D_USE_EXTERNAL_IMGUI)
-  find_package(imgui REQUIRED)
-endif ()
-
 # VTK dependency
 # Optional components should list VTK modules
 # needed by plugins and optional modules
-find_package(VTK 9.2.6 REQUIRED
+find_package(VTK 9.3.0 REQUIRED
   COMPONENTS
     CommonCore
     CommonDataModel
@@ -93,10 +67,10 @@
     InteractionStyle
     InteractionWidgets
     IOCityGML
+    IOLegacy
     IOGeometry
     IOImage
     IOImport
-    IOParallel
     IOPLY
     IOXML
     RenderingAnnotation
@@ -104,13 +78,13 @@
     RenderingOpenGL2
     RenderingVolumeOpenGL2
     TestingCore
-    jsoncpp
   OPTIONAL_COMPONENTS
     opengl
     IOExodus
     IOHDF
     IONetCDF
     IOOpenVDB
+    RenderingGridAxes
     RenderingRayTracing)
 message(STATUS "VTK ${VTK_VERSION} found")
 
@@ -119,6 +93,33 @@
   find_package(VTK REQUIRED COMPONENTS opengl)
 endif()
 
+# Modules
+option(F3D_MODULE_RAYTRACING "Raytracing module" OFF)
+option(F3D_MODULE_EXR "OpenEXR images module" OFF)
+option(F3D_MODULE_WEBP "WebP images module" OFF)
+option(F3D_MODULE_UI "ImGui widgets module" ON)
+option(F3D_MODULE_DMON "dmon (watch) module" ON)
+option(F3D_MODULE_TINYFILEDIALOGS "tinyfiledialogs module" ON)
+
+# Use externals
+option(F3D_USE_EXTERNAL_CXXOPTS "Use external cxxopts dependency" OFF)
+option(F3D_USE_EXTERNAL_NLOHMANN_JSON "Use external nlohmann_json dependency" OFF)
+cmake_dependent_option(F3D_USE_EXTERNAL_DMON "Use external dmon dependency" OFF "F3D_MODULE_DMON" OFF)
+cmake_dependent_option(F3D_USE_EXTERNAL_IMGUI "Use external imgui dependency" OFF "F3D_MODULE_UI" OFF)
+
+if (F3D_USE_EXTERNAL_CXXOPTS)
+  find_package(cxxopts REQUIRED)
+endif ()
+if (F3D_USE_EXTERNAL_NLOHMANN_JSON)
+  find_package(nlohmann_json REQUIRED)
+endif ()
+if (F3D_USE_EXTERNAL_DMON)
+  find_package(dmon REQUIRED)
+endif ()
+if (F3D_USE_EXTERNAL_IMGUI)
+  find_package(imgui REQUIRED)
+endif ()
+
 # Shared options between application and library
 include(GNUInstallDirs)
 cmake_dependent_option(F3D_WINDOWS_BUILD_CONSOLE_APPLICATION "Build a supplemental f3d-console application" OFF "WIN32" OFF)
@@ -136,9 +137,11 @@
 set(F3D_STRICT_BUILD OFF CACHE BOOL "Use strict warnings and errors flags for building F3D")
 mark_as_advanced(F3D_STRICT_BUILD)
 set(f3d_strict_build_compile_options "")
+set(f3d_strict_build_link_options "")
 if(F3D_STRICT_BUILD)
   if(MSVC)
     set(f3d_strict_build_compile_options /W4 /WX)
+    set(f3d_strict_build_link_options /WX)
   else()
     set(f3d_strict_build_compile_options -Wall -Wextra -Wshadow -Woverloaded-virtual -Wno-deprecated -Wno-strict-overflow -Wno-array-bounds -Wunreachable-code -Wno-missing-field-initializers -Wno-unused-parameter -Wredundant-decls -Wpointer-arith -Werror)
     if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@@ -188,6 +191,7 @@
 
 ## F3D_STRICT_BUILD
 list(APPEND f3d_compile_options_private ${f3d_strict_build_compile_options})
+list(APPEND f3d_link_options_public ${f3d_strict_build_link_options})
 
 ## Coverage
 list(APPEND f3d_compile_options_public ${f3d_coverage_compile_options})
@@ -265,6 +269,12 @@
   add_subdirectory(winshellext)
 endif()
 
+# C bindings
+option(F3D_BINDINGS_C "Create C bindings" OFF)
+if(F3D_BINDINGS_C)
+  add_subdirectory(c)
+endif()
+
 # Python bindings
 option(F3D_BINDINGS_PYTHON "Create Python bindings" OFF)
 if(F3D_BINDINGS_PYTHON)
@@ -293,7 +303,7 @@
   DESTINATION ${F3D_LIC_DIR} COMPONENT licenses)
 
 if (F3D_BUILD_APPLICATION)
-  install(FILES doc/THIRD_PARTY_LICENSES.md
+  install(FILES THIRD_PARTY_LICENSES.md
     DESTINATION ${F3D_LIC_DIR} COMPONENT licenses)
 endif ()
 
@@ -313,12 +323,14 @@
   message(STATUS "${f3d_var}: ${${f3d_var}}")
 endfunction()
 
+f3d_report_variable(F3D_BINDINGS_C)
 f3d_report_variable(F3D_BINDINGS_JAVA)
 f3d_report_variable(F3D_BINDINGS_PYTHON)
 f3d_report_variable(F3D_BUILD_APPLICATION)
 f3d_report_variable(F3D_MODULE_EXR)
 f3d_report_variable(F3D_MODULE_RAYTRACING)
 f3d_report_variable(F3D_MODULE_UI)
+f3d_report_variable(F3D_MODULE_WEBP)
 f3d_report_variable(F3D_PLUGIN_BUILD_ALEMBIC)
 f3d_report_variable(F3D_PLUGIN_BUILD_ASSIMP)
 f3d_report_variable(F3D_PLUGIN_BUILD_DRACO)
diff -Nru f3d-3.2.0+dfsg/CNAME f3d-3.4.1/CNAME
--- f3d-3.2.0+dfsg/CNAME	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/CNAME	1970-01-01 02:00:00.000000000 +0200
@@ -1 +0,0 @@
-f3d.app
\ No newline at end of file
diff -Nru f3d-3.2.0+dfsg/CODE_OF_CONDUCT.md f3d-3.4.1/CODE_OF_CONDUCT.md
--- f3d-3.2.0+dfsg/CODE_OF_CONDUCT.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/CODE_OF_CONDUCT.md	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-# F3D Code of Conduct
+# Code of Conduct
 
 ## Our Pledge
 
@@ -52,7 +52,7 @@
 decisions when appropriate.
 
 In case of any issues please reach out, either to any moderators or maintainers
-on [discord] or at <contact@f3d.app>.
+on [discord] or at [contact@f3d.app](mailto:contact@f3d.app).
 
 ## Scope
 
diff -Nru f3d-3.2.0+dfsg/_config.yml f3d-3.4.1/_config.yml
--- f3d-3.2.0+dfsg/_config.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/_config.yml	1970-01-01 02:00:00.000000000 +0200
@@ -1,340 +0,0 @@
-remote_theme: just-the-docs/just-the-docs
-plugins:
-  - jekyll-optional-front-matter
-  - jekyll-relative-links
-  - jekyll-remote-theme
-  - jekyll-seo-tag
-  - jekyll-titles-from-headings
-relative_links:
-  enabled: true
-  collections: true
-include:
-  - README.md
-  - LICENSE.md
-  - _licenses.md
-  - CONTRIBUTING.md
-  - CODE_OF_CONDUCT.md
-exclude:
-  - CMakeLists.txt
-  - codecov.yml
-  - application
-  - cmake
-  - examples
-  - java
-  - library
-  - plugins
-  - python
-  - testing
-  - winshellext
-title: F3D
-tagline: Fast and minimalist 3D viewer
-description: F3D - Fast and minimalist 3D viewer
-url: https://f3d.app/
-logo: /resources/logotype.svg
-color_scheme: dark
-
-# Set default front matter values
-defaults:
-  - scope:
-      path: ""
-    values:
-      image: # Set a opengraph image from the gallery for all pages
-        path: https://media.githubusercontent.com/media/f3d-app/f3d-media/refs/heads/typical_graph/media/typical_opengraph.png
-        height: 1200
-        width: 630
-      description: "F3D - Fast and minimalist 3D viewer"
-      has_toc: false
-
-  - scope:
-      path: "README.md"
-    values:
-      title: Home
-      nav_order: 0
-      permalink: /
-      layout: default
-
-  - scope:
-      path: "doc/*"
-    values:
-      layout: default
-
-  - scope:
-      path: "doc/GALLERY.md"
-    values:
-      nav_order: 1
-
-  # user doc
-  - scope:
-      path: "doc/user/README_USER.md"
-    values:
-      nav_order: 2
-      has_children: true
-
-  - scope:
-      path: "doc/user/QUICKSTART.md"
-    values:
-      title: Quickstart
-      parent: User Documentation
-      nav_order: 0
-
-  - scope:
-      path: "doc/user/INSTALLATION.md"
-    values:
-      parent: User Documentation
-      nav_order: 1
-
-  - scope:
-      path: "doc/user/SUPPORTED_FORMATS.md"
-    values:
-      title: Supported File Formats
-      parent: User Documentation
-      nav_order: 2
-
-  - scope:
-      path: "doc/user/OPTIONS.md"
-    values:
-      title: Options
-      parent: User Documentation
-      nav_order: 3
-
-  - scope:
-      path: "doc/user/INTERACTIONS.md"
-    values:
-      parent: User Documentation
-      nav_order: 4
-
-  - scope:
-      path: "doc/user/ANIMATIONS.md"
-    values:
-      parent: User Documentation
-      nav_order: 5
-
-  - scope:
-      path: "doc/user/CONFIGURATION_FILE.md"
-    values:
-      parent: User Documentation
-      nav_order: 6
-  - scope:
-      path: "doc/user/COMMANDS.md"
-    values:
-      parent: User Documentation
-      nav_order: 7
-
-  - scope:
-      path: "doc/user/PARSING.md"
-    values:
-      parent: User Documentation
-      nav_order: 8
-
-  - scope:
-      path: "doc/user/COLOR_MAPS.md"
-    values:
-      parent: User Documentation
-      nav_order: 9
-
-  - scope:
-      path: "doc/user/FINAL_SHADER.md"
-    values:
-      parent: User Documentation
-      nav_order: 10
-
-  - scope:
-      path: "doc/user/DESKTOP_INTEGRATION.md"
-    values:
-      parent: User Documentation
-      nav_order: 11
-
-  - scope:
-      path: "doc/user/PLUGINS.md"
-    values:
-      parent: User Documentation
-      nav_order: 12
-
-  - scope:
-      path: "doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md"
-    values:
-      title: Limitations and Troubleshooting
-      parent: User Documentation
-      nav_order: 13
-
-  - scope:
-      path: "doc/user/SPONSORING.md"
-    values:
-      title: Sponsoring
-      parent: User Documentation
-      nav_order: 14
-
-  # libf3d doc
-  - scope:
-      path: "doc/libf3d/README_LIBF3D.md"
-    values:
-      nav_order: 3
-      has_children: true
-
-  - scope:
-      path: "doc/libf3d/OVERVIEW.md"
-    values:
-      title: Overview
-      parent: libf3d Documentation
-      nav_order: 0
-
-  - scope:
-      path: "doc/libf3d/CLASSES.md"
-    values:
-      parent: libf3d Documentation
-      nav_order: 1
-
-  - scope:
-      path: "doc/libf3d/OPTIONS.md"
-    values:
-      title: Options
-      parent: libf3d Documentation
-      nav_order: 2
-
-  - scope:
-      path: "doc/libf3d/LANGUAGE_BINDINGS.md"
-    values:
-      parent: libf3d Documentation
-      nav_order: 3
-
-  - scope:
-      path: "doc/libf3d/PLUGINS.md"
-    values:
-      parent: libf3d Documentation
-      nav_order: 4
-
-  # dev doc
-  - scope:
-      path: "doc/dev/README_DEV.md"
-    values:
-      nav_order: 4
-      has_children: true
-
-  - scope:
-      path: "doc/dev/GETTING_STARTED.md"
-    values:
-      title: Getting Started
-      parent: Developer Documentation
-      nav_order: 0
-
-  - scope:
-      path: "doc/dev/BUILD.md"
-    values:
-      title: Build
-      parent: Developer Documentation
-      nav_order: 1
-
-  - scope:
-      path: "doc/dev/TESTING.md"
-    values:
-      parent: Developer Documentation
-      nav_order: 2
-
-  - scope:
-      path: "CONTRIBUTING.md"
-    values:
-      title: Contribute
-      parent: Developer Documentation
-      nav_order: 3
-
-  - scope:
-      path: "doc/dev/GENERATE.md"
-    values:
-      title: Generate
-      parent: Developer Documentation
-      nav_order: 4
-
-  - scope:
-      path: "doc/dev/ARCHITECTURE.md"
-    values:
-      title: Architecture
-      parent: Developer Documentation
-      nav_order: 5
-
-  - scope:
-      path: "doc/dev/CODING_STYLE.md"
-    values:
-      title: Coding style
-      parent: Developer Documentation
-      nav_order: 6
-
-  - scope:
-      path: "doc/dev/ROADMAPS_AND_RELEASES.md"
-    values:
-      title: Roadmaps and release cycle
-      parent: Developer Documentation
-      nav_order: 7
-
-  - scope:
-      path: "doc/dev/GOVERNANCE.md"
-    values:
-      title: Governance
-      parent: Developer Documentation
-      nav_order: 8
-
-  - scope:
-      path: "doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md"
-    values:
-      title: Maintainers and Contributors
-      parent: Developer Documentation
-      nav_order: 9
-
-  - scope:
-      path: "CODE_OF_CONDUCT.md"
-    values:
-      title: Code of conduct
-      parent: Developer Documentation
-      nav_order: 10
-
-  - scope:
-      path: "doc/dev/BUILD_WASM.md"
-    values:
-      title: Build (WebAssembly)
-      parent: Developer Documentation
-      nav_order: 11
-
-  # Licenses doc
-  # _licenses.md uses front matter
-  - scope:
-      path: "LICENSE.md"
-    values:
-      title: License
-      parent: Licenses
-      nav_order: 0
-      layout: default
-
-  - scope:
-      path: "doc/THIRD_PARTY_LICENSES.md"
-    values:
-      title: Third Party Licenses
-      parent: Licenses
-      nav_order: 1
-
-  - scope:
-      path: "doc/CHANGELOG.md"
-    values:
-      nav_order: 6
-
-# Aux links for the upper right navigation
-aux_links:
-  '<img src="https://user-images.githubusercontent.com/3129530/197903476-11d9c41d-73b0-461f-93e8-c8c00c680e6e.png" width="30px">':
-    - "//github.com/f3d-app/f3d"
-
-# Makes Aux links open in a new tab.
-aux_links_new_tab: true
-
-# External navigation links
-# TODO: Create a dedicated javascript page instead: #481
-nav_external_links:
-  - title: Download F3D
-    url: https://f3d.app/doc/user/INSTALLATION.html
-  - title: GitHub repository
-    url: https://github.com/f3d-app/f3d
-  - title: Web viewer
-    url: https://f3d.app/web
-  - title: libf3d API documentation
-    url: https://f3d.app/doc/libf3d/doxygen/
-
-# Google Analytics Tracking
-ga_tracking: G-ZCWEYB9KVG
-ga_tracking_anonymize_ip: true
diff -Nru f3d-3.2.0+dfsg/CONTRIBUTING.md f3d-3.4.1/CONTRIBUTING.md
--- f3d-3.2.0+dfsg/CONTRIBUTING.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/CONTRIBUTING.md	2026-01-08 08:41:55.000000000 +0200
@@ -22,8 +22,10 @@
 ## How to Get Started With Development
 
 To contribute to F3D as a developer, first you may want to try and build F3D for yourself.
-If you are already familiar with software compilation, you can take a look at our [build guide](doc/dev/BUILD.md).
-If not, you may want to look at our [getting started guide](doc/dev/GETTING_STARTED.md), that provide steps to compile F3D.
+If you are already familiar with software compilation, you can take a look at our [build guide](doc/dev/04-BUILD.md).
+If not, you may want to look at our [getting started guide](doc/dev/03-GETTING_STARTED.md), that provide steps to compile F3D.
+You may also want to take a look into the [F3D mentoring program](doc/dev/12-MENTORING.md) in order to get help with all the
+contributing steps.
 
 Once you are able to build F3D, you may want to take a look at the open [issues](https://github.com/f3d-app/f3d/issues)!
 If you are a beginner, you may want to look at ["good first issues"](https://github.com/f3d-app/f3d/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
@@ -31,39 +33,17 @@
 If an issue is interesting to you and is not yet assigned, then you should _comment on the issue_ and ask for any help or clarification needed.
 F3D maintainers will see your comment, assign the issue to you and provide guidance as needed.
 
-To get involved more deeply, please take a look at the [roadmaps](doc/dev/ROADMAPS_AND_RELEASES.md) for the upcoming releases.
+To get involved more deeply, please take a look at the [roadmaps](doc/dev/09-ROADMAPS_AND_RELEASES.md) for the upcoming releases.
 
 It is also recommended to reach out on [Discord](https://discord.f3d.app) to simplify communication, but it is not required.
 
-You may also want to understand the overall [architecture](doc/dev/ARCHITECTURE.md) of the F3D project.
+You may also want to understand the overall [architecture](doc/dev/07-ARCHITECTURE.md) of the F3D project.
 
 You can then fix the issue or implement the feature on your side and contribute it to the F3D repository by following the workflow described below.
 
 Of course, if you are already using F3D and want to improve it for your specific needs, because you want a feature or found a bug,
 that is definitely possible. Feel free to reach out for guidance by opening an issue or asking on [Discord](https://discord.f3d.app).
 
-## Bounties
-
-The [F3D-APP Foundation](https://annuaire-entreprises.data.gouv.fr/entreprise/f3d-app-foundation-941551624), as a non-profit institution, would like to redistribute some donations to the contributors, for specific issues identified by maintainers as important for the project.
-Such issues have been identified with the ["bounty"](https://github.com/f3d-app/f3d/issues?q=is%3Aissue%20state%3Aopen%20label%3Abounty) label. The amount of money of their bounties is fairly small (up to a few hundred euros).
-
-In order to be eligible for the bounty you need to:
-
-- Be a [returning contributor](doc/dev/GOVERNANCE.md#12-returning-contributors)
-- Be able to provide an IBAN from a country located in the European Union where the money will be donated to
-
-If you are not eligible or are not interested by the donation, this is perfectly fine, you can still work on the issue, but there will be no donation.
-If you are eligible and interested, please comment on the issue and Maintainers will reach out to you privately to discuss the details and share the exact amount of the bounty
-for this particular issue. We will also decide together a targeted deadline for completion of the work.
-Maintainers will then vote to decide if you are a good fit for a specific issue and share the result of the vote with you.
-
-When working on a bounty, some form of communication is expected once a week, failure to do so may result on the issue being unassigned by maintainers opening it up to other contributors.
-You should then work on this issue as you would any other issue.
-
-When the maintainer close the issue, the donation will take place. This can sometimes requires multiple pull requests being merged.
-
-Please note the [F3D-APP foundation](https://annuaire-entreprises.data.gouv.fr/entreprise/f3d-app-foundation-941551624) is handling the donations.
-
 ## F3D Development Workflow
 
 F3D uses [GitLab Flow](https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/). In a few words, here is how to contribute:
@@ -76,28 +56,34 @@
 - Maintainers and returning contributors will provide help to interpret the CI and fix issues that may arise.
 - Your PR will be reviewed by maintainers and returning contributors, please take their feedback into account and resolve discussions when adresssed.
 - Once the PR is approved and CI comes back clean, a maintainer will merge your pull request in the master branch.
-- The master now contains your changes and will be present in the next minor/major release, any documentation related changes are visible in <https://f3d.app>!
+- The master now contains your changes and will be present in the next minor/major release, any documentation related changes are visible on [the website](https://f3d.app)!
 
 ## Continuous Integration
 
 F3D has pretty extensive continuous integration trying to cover all usecases for F3D.
-It means that if the change in your pull request breaks continuous integration, it will not be merged until it passes successfully.
+It means that if the change in your pull request breaks continuous integration, it will not be merged until the full CI passes successfully.
 It also means that adding a new feature or behavior means adding an associated test.
 Make sure to check the results for yourself and ask for help if needed.
 
-To run the CI, just check the CI related checkboxes in your pull request and push anything to your branch.
+To run the CI, just add a comment like this in your PR:
+
+- `\ci fast`: Style checks and a fast linux job
+- `\ci main`: Cross platform CI that cover most usecases, including coverage, contains `ci:fast`.
+- `\ci full`: Complete CI, required before merge, contains `ci:main`.
+
 After this, the CI will always be run every time you push to your branch.
+To remove a label, use the same syntax with a `-` before the label, eg: `\ci -fast`.
 
 F3D continuous integration will also check the coverage as it is a good way to evaluate if new features are being tested or not.
-When adding code to F3D, always try to cover it by adding/modifying [tests](doc/dev/TESTING.md).
+When adding code to F3D, always try to cover it by adding/modifying [tests](doc/dev/05-TESTING.md).
 
 F3D continuous integration also checks formatting using clang-format and other tools and will inform you if changes needs to be made.
-Some [formatting rules](doc/dev/CODING_STYLE.md) are not enforced by clang-format and will be checked during the review process.
+Some [formatting rules](doc/dev/08-CODING_STYLE.md) are not enforced by clang-format and will be checked during the review process.
 
 When making changes to the `libf3d` public API, continuous integration will warn about making related changes to the bindings.
 This is required in order to merge the pull request.
 
-When making changes to the `default_versions.json` file, continuous integration will warn about updating the timestamp in the same file.
+When making changes to the `default_versions.json` file, continuous integration will warn about updating the docker timestamp in the same file.
 This is required in order to merge the pull request.
 
 The continuous integration is just code, as the rest of F3D is. If you want to fix or improve it, you are very welcome to!
diff -Nru f3d-3.2.0+dfsg/.cppcheck.supp f3d-3.4.1/.cppcheck.supp
--- f3d-3.2.0+dfsg/.cppcheck.supp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.cppcheck.supp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,6 @@
 // remove some checks
+duplInheritedMember  // VTK design
+missingIncludeSystem // cppcheck seems confused about this
 missingInclude
 unusedFunction
 unknownMacro
@@ -12,14 +14,30 @@
 *:external/imgui/*.h
 *:external/imgui/*.cpp
 
+// generated files
+*:*vtkextTests.cxx
+*:*vtkextPrivateTests.cxx
+*:*vtkextNativeTests.cxx
+*:*VTKExtensionsPluginAlembic.cxx
+*:*VTKExtensionsPluginAssimp.cxx
+*:*vtkextDracoTests.cxx
+*:*vtkextOCCTTests.cxx
+*:*VTKExtensionsPluginUSD.cxx
+*:*libf3dSDKTests.cxx
+
 // specific checks
+knownConditionTrueFalse:vtkext/private/module/vtkF3DPointSplatMapper.cxx
 knownConditionTrueFalse:library/testing/TestSDKImage.cxx
 knownConditionTrueFalse:library/testing/TestSDKImageDeprecated.cxx
 noExplicitConstructor:library/public/types.h
 preprocessorErrorDirective:library/src/engine.cxx
 preprocessorErrorDirective:plugins/draco/module/vtkF3DDracoReader.cxx
 unknownMacro:library/VTKExtensions/Applicative/vtkF3DObjectFactory.cxx
-unusedVariable:*factory.cxx*
 constParameter:library/src/image.cxx
 invalidPointerCast:plugins/native/module/vtkF3DSplatReader.cxx
 invalidPointerCast:plugins/draco/module/vtkF3DGLTFDocumentLoader.cxx
+unusedStructMember:plugins/native/module/vtkF3DQuakeMDLImporter.cxx
+unusedStructMember:plugins/native/module/vtkF3DSPZReader.cxx
+constParameterPointer:vtkext/private/module/vtkF3DMetaImporter.cxx
+constParameterPointer:plugins/assimp/module/vtkF3DAssimpImporter.cxx
+constParameterPointer:plugins/alembic/module/vtkF3DAlembicReader.cxx
diff -Nru f3d-3.2.0+dfsg/debian/changelog f3d-3.4.1/debian/changelog
--- f3d-3.2.0+dfsg/debian/changelog	2025-10-12 12:21:48.000000000 +0300
+++ f3d-3.4.1/debian/changelog	2026-07-17 12:56:16.000000000 +0300
@@ -1,3 +1,12 @@
+f3d (3.4.1-0.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * New upstream release.
+    - Compatible with VTK 9.6. (Closes: #1142176)
+  * Patch for older ImGui.
+
+ -- Adrian Bunk <bunk@debian.org>  Fri, 17 Jul 2026 12:56:16 +0300
+
 f3d (3.2.0+dfsg-3) unstable; urgency=medium
 
   * Team upload
diff -Nru f3d-3.2.0+dfsg/debian/patches/older-imgui.patch f3d-3.4.1/debian/patches/older-imgui.patch
--- f3d-3.2.0+dfsg/debian/patches/older-imgui.patch	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/debian/patches/older-imgui.patch	2026-07-17 12:56:16.000000000 +0300
@@ -0,0 +1,13 @@
+Description: Patch for older ImGui
+Author: Adrian Bunk <bunk@debian.org>
+
+--- f3d-3.4.1.orig/vtkext/private/module/vtkF3DImguiActor.cxx
++++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiActor.cxx
+@@ -349,7 +349,6 @@ void vtkF3DImguiActor::Initialize(vtkOpe
+   style->FramePadding = ImVec2(4, 2);
+   style->FrameRounding = 2.f;
+   style->GrabRounding = 4.0f;
+-  style->ScrollbarPadding = 2.f;
+   style->WindowBorderSize = 0.f;
+   style->WindowPadding = ImVec2(10, 10);
+   style->WindowRounding = 8.f;
diff -Nru f3d-3.2.0+dfsg/debian/patches/series f3d-3.4.1/debian/patches/series
--- f3d-3.2.0+dfsg/debian/patches/series	2025-10-12 12:19:47.000000000 +0300
+++ f3d-3.4.1/debian/patches/series	2026-07-17 12:56:16.000000000 +0300
@@ -1 +1,2 @@
 no_lfs_check.patch
+older-imgui.patch
diff -Nru f3d-3.2.0+dfsg/doc/CHANGELOG.md f3d-3.4.1/doc/CHANGELOG.md
--- f3d-3.2.0+dfsg/doc/CHANGELOG.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/CHANGELOG.md	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,113 @@
 # Changelog
 
+## v3.3.0
+
+For F3D users:
+
+- Created a F3D web application based on WebAssembly: https://f3d.app/viewer
+- **BEHAVIOR CHANGE**: Added a CLI option `--recursive-dir-add` to control if directory should be added recursively, off by default
+- Complete rework of the cheatsheet with for a nicer, cleaner look
+- [Linux] Added support for using F3D offscreen without GLX lib being present on the system
+- Added support for identifying non-supported files and not loading them at all
+- Added support for skipping unsupported files when loading multiple files
+- Added `--remove-empty-file-groups` to control if scene without files should be removed
+- Added `--watch` support for yet-to-exists files
+- Added a `remove_current_file_group` command
+- Added `--axes-grid` CLI option to show grid axes
+- Added an bind `Ctrl+X` to control the visibility of grid axes
+- Added proper parsing for transform2D options
+- Added `--multi-file-regex` CLI option to group files based on their names
+- Added autocompletion in interactive console
+- Added a `help` command to print information about commands
+- Added support for SPZ v3 format
+- Added `--backdrop-opacity` to control the UI backdrop opacity
+- Added support for .webp image and textures
+- Added support for GroupSkin for .mdl format
+- Added temporal anti aliasing support `--antialiasing-mode=taa`
+- Improve dropzone visuals
+- Improved `print_scene_info` output
+- Improved UI consistency
+- Improved rendering performance slightly
+- Reworked the website from scratch which now support documentation versioning
+- Removed support for comments in interactive console for better color parsing
+- Fixed and improved many documentations issues
+- Fixed an issue with `--watch` when file is failing to load
+- Fixed missing frame in .mdl animations
+- Fixed a potential crash when graphic libraries are not found
+- Fixed an issue with the scalar bar not being continuous
+
+For libf3d users:
+
+- Created a npm package to implement libf3d app for the web
+- Added event forwarding in order to implement interactions easily with _external_ engines
+- Added `ui.drop_zone.custom_binds` option to control the bindings visible in drop zone
+- Added `ui.backdrop.opacity` to control the UI backdrop opacity
+
+For F3D packagers:
+
+- F3D does not use libglx directly anymore when using VTK > 9.5
+- F3D v3.3.0 will be the last version of F3D supporting vtk v9.2.6, VTK v9.3.1 will be required for the next release
+
+## v3.2.0
+
+For F3D users:
+
+- Added support for VTKHDF format, including AMR datasets
+- Added support for SPZ and PLY gaussian splatting format
+- Added a `--base-ior` option to control index of refraction
+- Added a `--multi-file-mode=dir` option mode to create file groups based on the folder organization
+- Added spherical harmonics support for gaussian splatting (SPZ and PLY)
+- Added a QuakeMDL reader option: `-DQuakeMDL.skin_index` to select skin to load
+- Added support for reading EXR texture in USD format
+- Added support for selection multiple animations using `--animation-indices=1,2,3`
+- Added a `--colormap-discretization` option to control scalar rendering discretization
+- Added glob support to configuration file
+- Added a `--textures-transform` option to set a transform on textures
+- Added listing of more exodus extensions `.e`, `.g` and `.exo`
+- Added a minimal console, open by pressing `:`
+- Added `increase_opacity` and `decrease_opacity` commands and bindings (`Ctrl+P` and `Shift+P`)
+- Added a `--force-reader` option to select the reader to use, even with the wrong extension
+- Added a binding to switch verbose level by pressing `Shift+V`
+- Added netcdf format support
+- Added `elevation_camera` and `azimuth_camera` commands
+- Added a `elevation_camera` binding (`2` and `8`)
+- Improved documentation
+- Improved logging
+- Improved camera management on special locations like looking straight down
+- Changed UI style to fit with new F3D design
+- Fixed multiple issues with coloring while skinning or morphing
+- Fixed an issue with normals in the QuakeMDL importer
+- Fixed a wobbling issue when rotating the camera
+- Fixed an issue where `--verbose=quiet` was not respected
+- Fixed multiple UI issues
+- Fixed an infamous crash on Windows when username has a special character in it
+- Fixed an issue where configuration file loading order was not respected
+- Fixed an issue where optional option were not kept when changing file group
+- Fixed a crash when using `Ctrl+O` but then selecting no files
+- Fixed an issue with colinear camera and Up vector
+- Deprecated `--animation-index` option
+
+For libf3d users:
+
+- Added `scene.animation.indices` to select multiple animations
+- Added `model.material.base_ior` to control index of refraction
+- Added `model.scivis.discretization` to control scalar rendering discretization
+- Added `model.textures_transform` to set a transform on textures
+- Added `scene.force_reader` to force a specific reader to be used
+- Added streaming support to Splat reader (not exposed yet)
+- Improved camera API and default values
+- Improved python stubs
+- Fixed `help(f3d)`
+- Deprecated `scene.animation.index` option
+
+For F3D packagers:
+
+- Fixed a missing include in vtkF3DQuakeMDLImporter
+- Fixed a configuration issue against assimp 6.0
+- Removed `f3d_PREFIX_DIR` and rely on `PACKAGE_PREFIX_DIR` instead
+- Renamed `F3D_PLUGIN_BUILD_EXODUS` into `F3D_PLUGIN_BUILD_HDF`
+- Added a few new VTK module dependencies
+
 ## v3.1.0
 
 For F3D users:
@@ -64,7 +172,7 @@
 
 For F3D users:
 
-- BREAKING CHANGE: [Configuration files](https://f3d.app/doc/user/CONFIGURATION_FILE.html) have changed syntax
+- BREAKING CHANGE: [Configuration files](https://f3d.app/docs/next/user/CONFIGURATION_FILE) have changed syntax
 - BREAKING CHANGE: Removed all deprecated options
 - BREAKING CHANGE: `--scalars` have been split into `--scalar-coloring` and `--coloring-array` CLI options
 - BREAKING CHANGE: `=` must be used for CLI Options, `--option value` is not supported anymore
@@ -75,7 +183,7 @@
 - BREAKING CHANGE: `--animation-frame-rate` has been replaced by `--frame-rate`
 - BREAKING CHANGE: Renamed `--bg-color` into `--background-color`
 - BREAKING CHANGE: Renamed `--comp` into `--coloring-component`
-- BREAKING CHANGE: Removed the `?` interaction to dump info, use [commands](https://f3d.app/doc/user/COMMANDS.html) instead
+- BREAKING CHANGE: Removed the `?` interaction to dump info, use [commands](https://f3d.app/docs/next/user/COMMANDS) instead
 - BREAKING CHANGE: Renamed `--progress` into `--loading-progress`
 - BREAKING CHANGE: Renamed `--cells` into `--coloring-by-cells`
 - BREAKING CHANGE: Renamed `--range` into `--coloring-range`
@@ -88,19 +196,19 @@
 - BREAKING CHANGE: F3D binary release is now built with GCC9
 - Changed the logo!
 - Reworked the UI using ImGui
-- Added support for specifying interactions in config files, [see doc](https://f3d.app/doc/user/CONFIGURATION_FILE.html#bindings)
+- Added support for specifying interactions in config files, [see doc](https://f3d.app/docs/next/user/CONFIGURATION_FILE#bindings)
 - Added a `--command-script` CLI option
 - Added a tiny file dialog, just press `Ctrl+O`
 - Added support for armature visualization with `--armature` CLI option (glTF only)
 - Added support for keeping camera position when switching files, just press `Ctrl+Arrow`
-- Added a ImGui based console to input [commands](https://f3d.app/doc/user/COMMANDS.html) by pressing `Esc`
+- Added a ImGui based console to input [commands](https://f3d.app/docs/next/user/COMMANDS) by pressing `Esc`
 - Added a "Save minimal screenshot" interaction by pressing `Shift+F12`
 - Added error reporting when loading a file failed for some reason
 - Added missing config file for .brep files
 - Added support for .xbf files
 - Added `--input` CLI option
 - Added support for libf3d option in configuration files
-- Added support for parsing option based on their type, see [the doc](https://f3d.app/doc/user/PARSING.html)
+- Added support for parsing option based on their type, see [the doc](https://f3d.app/docs/next/user/PARSING)
 - Added a `--rendering-backend` option to select the rendering backend, including headless
 - Added an interaction to rotate camera 90 degrees by pressing 4/6
 - Added an interaction to drop only HDRI (Shift+Drop) and an interaction to drop only 3D files (Ctrl+Drop)
@@ -181,14 +289,14 @@
 
 For F3D users:
 
-- Added a Webassembly version available online: https://f3d.app/web
+- Added a Webassembly version available online: https://f3d.app/viewer
 - Added a "neutral" tone mapping feature
 - Added an screenshot feature when pressing F12, with its dedicated option, `--screenshot-filename`
 - Added a `--final-shader` option to customize post-processing.
 - Added a `--grid-color` option to set the color of the grid
 - Added a `--animation-progress` option to control if the animation progress bar should be shown
 - Added a `--backface-type` option to control backface visibility
-- Added a concept of filename [template](https://f3d.app/doc/user/OPTIONS.html#filename-templating) for saving screenshots and outputs
+- Added a concept of filename [template](https://f3d.app/docs/next/user/OPTIONS#filename-templating) for saving screenshots and outputs
 - Added native menus for macOS
 - Improved documentation all around
 - Reworked config file logic to avoid duplicating of the default config
@@ -244,7 +352,7 @@
 - Added a new option `model.point-sprites.type` used to specify how to display points (only if `model.point-sprites.enable` is true)
 - Added a `image::toTerminalText` method to print image to terminal text
 - Added an `image` metadata API
-- Added a `vtkext` module for plugin developer, see the [documentation](https://f3d.app/doc/libf3d/PLUGINS.html)
+- Added a `vtkext` module for plugin developer, see the [documentation](https://f3d.app/docs/next/libf3d/PLUGINS)
 - Added a new f3d component to find_package of, `pluginsdk`, require to build plugins against F3D
 
 For F3D packagers:
diff -Nru f3d-3.2.0+dfsg/doc/dev/03-GETTING_STARTED.md f3d-3.4.1/doc/dev/03-GETTING_STARTED.md
--- f3d-3.2.0+dfsg/doc/dev/03-GETTING_STARTED.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/03-GETTING_STARTED.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,251 @@
+# Getting Started
+
+This is a getting started guide, aimed to people that are not used
+to C++ programming, configuring and compiling. If you are already used
+to such processes, you may be looking for our [build guide](04-BUILD.md).
+
+You may also want to take a look into the [mentoring program](12-MENTORING.md).
+
+This guide will help you to compile your own version of F3D with required dependencies.
+Optional dependencies or covered in the build guide.
+
+- If you are using [Linux](#linux)
+- If you are using [Windows](#windows)
+- If you are using [macOS](#macos)
+
+## Linux
+
+Note: The following steps were tested with Ubuntu 23 and ArchLinux but
+should work for other OSes as stated, as long as listed packages are available.
+
+### Install dependencies
+
+First make sure your system is up-to-date,
+then install the required dependencies using the terminal.
+
+#### Debian/Ubuntu/Mint
+
+```
+sudo apt update
+sudo apt upgrade
+sudo apt install build-essential git git-lfs cmake libvtk9-dev
+```
+
+Note: Ubuntu 24.04 / Debian 12 provides VTK version 9.1 but f3d requires VTK 9.3.0 at minimum, so building and installing vtk from source is suggested. Look at Windows build instruction for inspiration if needed.
+
+#### Fedora/Centos/RedHat
+
+```
+sudo yum update
+sudo dnf install make automake gcc gcc-c++ kernel-devel git git-lfs cmake vtk-devel
+```
+
+#### Arch Linux
+
+```
+sudo pacman -Syu
+sudo pacman -S base-devel git git-lfs cmake vtk nlohmann-json hdf5 netcdf fmt verdict openmpi glew ospray libxcursor
+```
+
+### Compile F3D
+
+Open a terminal.
+
+```
+mkdir f3d
+cd f3d
+git clone https://github.com/f3d-app/f3d src
+mkdir build
+cd build
+cmake ../src
+make
+```
+
+You can then simply run F3D from the command line:
+
+```
+./bin/f3d
+```
+
+### Running the test suite (optional)
+
+```
+cmake -DBUILD_TESTING=ON ../src
+make
+ctest
+```
+
+## Windows
+
+### Visual Studio with internal CMake/VCPKG integration
+
+On very recent Visual Studio versions, it's possible to rely on the internal CMake to automatically configure the project
+and the internal VCPKG to automatically build the dependencies.
+
+#### Install Dependencies
+
+- Download and install a recent version of [Visual Studio 2022 Community Edition][visual-studio]
+
+> [!WARNING]
+> This process has been tested successfully on 17.12 but is known to fail on 17.8
+
+#### Open the project
+
+- Open Visual Studio
+- Select `Clone a repository`
+- Specify the repository address: `https://github.com/f3d-app/f3d.git` and the local directory of your choice
+
+> [!WARNING]
+> The first run will take a while (up to 1 hour) because VTK is compiled twice (in Debug and Release mode)
+
+#### Build and the project
+
+- When the configuration is finished, press `F7` to compile F3D
+- Select `f3d.exe (bin\f3d.exe)` in the list of the target and to run F3D
+
+### Command line only
+
+The following steps were tested with Visual Studio Community 2022 using Windows 10, but should also work for other versions.
+This guide is relying a lot on terminal command in order to provide easy steps to follow.
+Alternatively, it is possible to use Visual Studio directly by opening .sln files and building in Visual Studio.
+
+#### Install Dependencies
+
+- Download and install [git bash for windows][gitforwindows]
+- Download and install [cmake][cmake-download]
+- Download and install [Visual Studio 2022 Community Edition][visual-studio]
+
+#### Retrieve VTK and F3D sources
+
+- Open git bash
+
+```sh
+cd C:
+mkdir dev
+cd dev
+mkdir vtk
+cd vtk
+git clone --depth 1 --branch v9.5.2 https://gitlab.kitware.com/vtk/vtk.git src
+mkdir build
+mkdir install
+cd ..
+mkdir f3d
+cd f3d
+git clone https://github.com/f3d-app/f3d.git src
+mkdir build
+```
+
+Note: Resulting directory architecture should look like this:
+
+```
+dev
+|-- f3d
+|   |-- build
+|   `-- src
+`-- vtk
+    |-- build
+    |-- install
+    `-- src
+```
+
+#### Build and install VTK in a dedicated directory
+
+- Open x64 Native Tools Command Prompt for VS 2022
+
+```sh
+cd C:\dev\vtk\build
+cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=../install ../src
+cmake --build . -j 16 --config Debug
+cmake --install . --config Debug
+```
+
+Note: These commands will take a while.
+
+#### Build F3D
+
+- Open x64 Native Tools Command Prompt for VS 2022
+
+```sh
+cd C:\dev\f3d\build
+cmake -DVTK_DIR=C:/dev/vtk/install/lib/cmake/vtk-9.5 ../src
+cmake --build . --config Debug
+```
+
+#### Run
+
+- Open a new file manager window
+- Double click on the `C:\dev\f3d\build\bin_Debug\f3d.exe` executable
+
+### Running the test suite (optional)
+
+- Open x64 Native Tools Command Prompt for VS 2022
+
+```
+cd C:\dev\f3d\build
+cmake -DBUILD_TESTING=ON .
+cmake --build . --config Debug
+ctest -C Debug
+```
+
+## macOS
+
+Note: The following steps were tested with macOS Catalina using Xcode 12.4, but should also work for other versions
+
+### Install Xcode
+
+There are different ways to install Xcode, you can use the AppStore or download it
+from apple developer website and then extract and install it.
+
+### Install Homebrew
+
+Open a terminal.
+
+```
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> ~/.zprofile
+eval "$(/opt/homebrew/bin/brew shellenv)"
+```
+
+### Install dependencies
+
+Open a terminal.
+
+```
+brew install git git-lfs cmake curl vtk
+git lfs install
+```
+
+Note: Homebrew is downloading, compiling and installing all vtk dependencies.
+This command may take a very long time.
+
+### Compile F3D
+
+Open a terminal.
+
+```
+mkdir f3d
+cd f3d
+git clone https://github.com/f3d-app/f3d src
+mkdir build
+cd build
+cmake ../src
+make
+```
+
+You can then simply run F3D from the command line:
+
+```
+./bin/f3d.app/Contents/MacOS/f3d
+```
+
+### Running the test suite (optional)
+
+```
+cmake -DBUILD_TESTING=ON ../src
+make
+ctest
+```
+
+[cmake-download]: https://cmake.org/download
+[gitforwindows]: https://gitforwindows.org/
+[visual-studio]: https://visualstudio.microsoft.com/vs/community/
diff -Nru f3d-3.2.0+dfsg/doc/dev/04-BUILD.md f3d-3.4.1/doc/dev/04-BUILD.md
--- f3d-3.2.0+dfsg/doc/dev/04-BUILD.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/04-BUILD.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,105 @@
+# Building
+
+> [!NOTE]
+> For WebAssembly cross-compilation, follow the dedicated guide [here](13-BUILD_WASM.md).
+
+F3D uses a CMake based build system, so building F3D just requires installing
+needed dependencies, configuring and building. If you are not used to such processes
+please take a look at our [getting started guide](03-GETTING_STARTED.md).
+
+## Dependencies
+
+- [CMake](https://cmake.org) >= 3.1.
+- [VTK](https://vtk.org) >= 9.3.0 (9.5.2 recommended).
+- A C++17 compiler.
+- A CMake-compatible build system (Visual Studio, XCode, Ninja, Make, etc.).
+- Optionally, [Assimp](https://www.assimp.org/) >= 5.4.0 (6.0.2 recommended).
+- Optionally, Open CASCADE [OCCT](https://dev.opencascade.org/) >= 7.6.3 (7.9.1 recommended).
+- Optionally, [Alembic](http://www.alembic.io/) >= 1.8.5.
+- Optionally, [OpenUSD](https://openusd.org/release/index.html) >= 24.08 (25.05.01 recommended).
+- Optionally, [OpenVDB](https://www.openvdb.org/download/) >= 12.0.0, enable `IOOpenVDB` module during VTK configuration.
+- Optionally, [OSPray](https://www.ospray.org/) == v2.12.0, enable `RenderingRayTracing` module during VTK configuration.
+- Optionally, [Draco](https://google.github.io/draco/) >= 1.5.6.
+- Optionally, [Python](https://www.python.org/) >= 3.10 and [pybind11](https://github.com/pybind/pybind11) >= 2.9.2.
+- Optionally, [Java](https://www.java.com) >= 17.
+- Optionally, [OpenEXR](https://openexr.com/en/latest/) >= 3.0.1.
+
+F3D is tested continuously against versions recommended by the [VFX reference platform](https://vfxplatform.com) defined for **CY2025**
+
+## VTK compatibility
+
+As stated in the dependencies, F3D is compatible with VTK >= 9.3.0, however, some features may not be available. We suggest using VTK 9.5.2 with RenderingRayTracing, IOExodus, IOHDF, IONetCDF and IOOpenVDB modules enabled in order to get as many features as possible in F3D.
+
+## Configuration and building
+
+Configure and generate the project with CMake,
+then build the software using your build system.
+
+Here is some CMake options of interest:
+
+- `F3D_BUILD_APPLICATION`: Build the F3D executable.
+- `BUILD_TESTING`: Enable the [tests](05-TESTING.md).
+- `F3D_MACOS_BUNDLE`: On macOS, build a `.app` bundle.
+- `F3D_WINDOWS_BUILD_SHELL_THUMBNAILS_EXTENSION`: On Windows, build the shell thumbnails extension.
+- `F3D_WINDOWS_BUILD_CONSOLE_APPLICATION`: On Windows, build a supplemental Win32 console application.
+- `F3D_PLUGINS_STATIC_BUILD`: Build all plugins as static library (embedded into `libf3d`) and automatically loaded by the application. Incompatible with `F3D_MACOS_BUNDLE`.
+- `BUILD_SHARED_LIBS`: Build the libf3d and all plugins as static library (embedded into `f3d` executable). The `library` and `plugin_sdk` component will not be installed.
+
+Some modules, plugins and language bindings depending on external libraries can be optionally enabled with the following CMake variables:
+
+- `F3D_MODULE_RAYTRACING`: Support for raytracing rendering. Requires that VTK has been built with `OSPRay` and `RenderingRayTracing` turned on. Disabled by default.
+- `F3D_MODULE_EXR`: Support for OpenEXR images. Requires `OpenEXR`. Disabled by default.
+- `F3D_MODULE_UI`: Support for ImGui widgets. Uses provided ImGui. Enabled by default.
+- `F3D_MODULE_WEBP`: Support for WebP images. Requires `libwebp`. Disabled by default.
+- `F3D_PLUGIN_BUILD_HDF`: Support for VTKHDF (.vtkhdf), ExodusII (.ex2), and NetCDF (.nc) file formats. Requires that VTK has been built with `IOHDF`, `IOExodus`, and `IONetCDF` modules (and `hdf5`). Enabled by default.
+- `F3D_PLUGIN_BUILD_OCCT`: Support for STEP, IGES, BREP, and XBF file formats. Requires `OpenCASCADE`. Disabled by default.
+- `F3D_PLUGIN_BUILD_ASSIMP`: Support for FBX, DAE, OFF, DXF, X and 3MF file formats. Requires `Assimp`. Disabled by default.
+- `F3D_PLUGIN_BUILD_ALEMBIC`: Support for ABC file format. Requires `Alembic`. Disabled by default.
+- `F3D_PLUGIN_BUILD_DRACO`: Support for DRC file format. Requires `Draco`. Disabled by default.
+- `F3D_PLUGIN_BUILD_USD`: Support for USD file format. Requires `OpenUSD`. Disabled by default.
+- `F3D_PLUGIN_BUILD_VDB`: Support for VDB file format. Requires that VTK has been built with `IOOpenVDB` module (and `OpenVDB`). Disabled by default.
+- `F3D_BINDINGS_PYTHON`: Generate python bindings (requires `Python` and `pybind11`). Disabled by default.
+- `F3D_BINDINGS_PYTHON_GENERATE_STUBS`: Generate python stubs (requires `Python` and `pybind11_stubgen`). Disabled by default.
+- `F3D_BINDINGS_JAVA`: Generate java bindings (requires `Java` and `JNI`). Disabled by default.
+
+Some dependencies are provided internally, eg: ImGui, dmon and others. Use `F3D_USE_EXTERNAL_*` to use an external version of these libraries.
+
+## VCPKG
+
+It's possible to use VCPKG to automatically build dependencies. A manifest file `vcpkg.json` is available at your convenience. Please read [VCPKG documentation](https://vcpkg.io/en/getting-started.html).
+Basically, just install VCPKG, and configure F3D using `-DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake` option.
+Only VTK is added to the manifest file and additional dependencies must be added manually in this file.
+
+## Installing
+
+Installation can be done through CMake, by running the following command:
+
+```
+cmake --install ${your_build_dir}
+```
+
+Individual components can also be installed by specifying the component name:
+
+```
+cmake --install ${your_build_dir} --component ${component_name}
+```
+
+Here is the list of all the components:
+
+| Name            | Installed by default | Operating system | Description                                                                                                                 |
+| --------------- | -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `application`   | YES                  | ALL              | F3D application.                                                                                                            |
+| `configuration` | NO                   | ALL              | Default configuration files, `config` and `thumbnail`.                                                                      |
+| `library`       | YES                  | ALL              | libf3d library binaries.                                                                                                    |
+| `plugin`        | YES                  | ALL              | libf3d plugins.                                                                                                             |
+| `dependencies`  | NO                   | ALL              | libf3d runtime dependencies. Can be used to create a self-contained and relocatable package. System libraries are excluded. |
+| `sdk`           | NO                   | ALL              | libf3d SDK (headers and CMake config files) for `library` and `application` find_package components.                        |
+| `plugin_sdk`    | NO                   | ALL              | libf3d plugin SDK (headers and CMake config files including macros) for `pluginsdk` find_package components.                |
+| `licenses`      | YES                  | ALL              | F3D and third party licenses.                                                                                               |
+| `documentation` | YES                  | Linux            | `man` documentation.                                                                                                        |
+| `shellext`      | YES                  | Windows/Linux    | Desktop integration.                                                                                                        |
+| `python`        | YES                  | ALL              | Python bindings.                                                                                                            |
+| `java`          | YES                  | ALL              | Java bindings.                                                                                                              |
+| `mimetypes`     | NO                   | Linux            | Plugins mimetype XML files for integration with Freedesktop.                                                                |
+| `assets`        | YES                  | Linux            | Assets for integration with Freedesktop.                                                                                    |
+| `colormaps`     | NO                   | ALL              | Color maps presets, see [documentation](../user/09-COLOR_MAPS.md)                                                           |
diff -Nru f3d-3.2.0+dfsg/doc/dev/05-TESTING.md f3d-3.4.1/doc/dev/05-TESTING.md
--- f3d-3.2.0+dfsg/doc/dev/05-TESTING.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/05-TESTING.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,161 @@
+# Testing
+
+F3D has an extensive suite of tests that can be run locally, either to validate your build or because you are contributing to F3D and want to add/modify a test.
+
+## CMake Options
+
+There are a few CMake options to configure the F3D testing framework:
+
+- `BUILD_TESTING`: Enable the test framework, off by default. Requires [git LFS](https://git-lfs.com/) if repository is cloned.
+- `F3D_TESTING_ENABLE_RENDERING_TESTS`: An option to enable/disable test that require rendering capabilities, on by default.
+- `F3D_TESTING_ENABLE_LONG_TIMEOUT_TESTS`: Certain tests can take some time to run, off by default, requires rendering tests.
+- `F3D_TESTING_FORCE_RENDERING_BACKEND`: Configure the rendering backend to use. Can be `auto` (default), `glx`, `wgl`, `egl` or `osmesa`.
+- `F3D_TESTING_ENABLE_GLX_TESTS`: Enable tests requiring a X11 server running on Linux.
+- `F3D_TESTING_ENABLE_OSMESA_TESTS`: Enable tests requiring OSMesa dependency.
+- `F3D_TESTING_ENABLE_EGL_TESTS`: Enable tests requiring EGL dependency.
+- `F3D_TESTING_ENABLE_EXTERNAL_GLFW`: Enable libf3d tests requiring GLFW dependency.
+- `F3D_TESTING_ENABLE_EXTERNAL_QT`: Enable libf3d tests requiring QT dependency.
+
+## Running the tests
+
+To run all tests, build and then run `ctest` from the build directory:
+
+```
+cmake --build .
+ctest
+```
+
+To run a specific test, use the `ctest -R <testname>` option:
+
+```
+ctest -R PLY
+```
+
+## Testing architecture
+
+There are multiple layers of tests to ensure that testing covers all aspects of the application. The layers of the application are
+
+- Application layer
+- Library layer
+- Bindings layer
+- VTK Extension layer
+
+When contributing to F3D, it is necessary that new code is covered by a test in at least one layer above. Additional tests can be created in other layers, depending on the change.
+
+### Application layer
+
+All application tests are initiated via the [CTest](https://cmake.org/cmake/help/book/mastering-cmake/chapter/Testing%20With%20CMake%20and%20CTest.html) testing framework.
+CTest runs `f3d` with the `--output` and `--reference` arguments which renders an output image and compares it to a reference image. F3D compares the differences between the 2 images and compute a difference value and compare it to a threshold to check if a test passes or not.
+
+All aspects of the application test framework are handled in `application/testing/CMakeLists.txt`.
+
+Usually, adding a test is as simple as adding a line like this one:
+
+```
+f3d_test(NAME TestName DATA datafile.ext ARGS --args-to-test)
+```
+
+where
+
+- `NAME` should be the name of the test, which must be unique
+- `DATA` should be a file in `testing/data` directory, though adding a new file is possible
+- `ARGS` should be the F3D options to pass to the f3d executable, if any
+
+Once the new test has been added, configure and build F3D, then run the test (`-VV` for verbose output):
+
+```
+ctest -R TestName -VV
+```
+
+The test will run and fail but an image output will be generated in the build directory, namely `Testing/Temporary/TestName.png`.
+Visually check that the generated file looks as expected, then add it to the F3D sources in `testing/baselines`.
+Rerun the test, it should now pass.
+
+There is many other keywords in the `f3d_test` macro, here is a non exhaustive list:
+
+- `TONE_MAPPING`: Tests that uses tone mapping so they can be disabled with old VTK version
+- `LONG_TIMEOUT`: Tests that takes a long time to run, so they can be disabled on weaker CI machines
+- `INTERACTION`: Interaction test, see below
+- `INTERACTION_CONFIGURE`: A special kind of interaction tests that require configuring the interaction log using CMake, eg drag and drop tests.
+- `NO_BASELINE`: Test that have no baselines and do not perform a rendering comparison at the end, usually combined with `REGEXP`
+- `NO_RENDER`: Tests that do not require rendering capability at all, usually combined with `REGEXP`
+- `NO_OUTPUT`: Tests that do not have an automatic `--output`, may still use `BASELINE` or define `--output` manually
+- `WILL_FAIL`: Tests that should fail in order to pass
+- `NO_DATA_FORCE_RENDER`: Tests that do not open any data yet require a rendering tests at the end, rely on an environment variable
+- `UI`: Tests that show the ImGui UI, hence require it to be present in order to be enabled
+
+### Recovering baselines from CI
+
+Occasionally you may need to recover a baseline from the CI. If this is required, create a PR and let the test run and fail on CI. Check the actions run summary on Github and download the appropriate `baseline` archive. Extract the archive and navigate to the `build/Testing/Temporary/TestName.png`. Visually check that the generated file looks as expected, then add it to the F3D sources in `testing/baselines`.
+
+### Creating Interaction tests
+
+Sometimes you may contribute changes that affect how the end user interacts with F3D, example, toggling orthographic projection on/off or zooming in/out of the rendered image. These human interactions are simulated by interaction tests. F3D has the functionality to record human interactions such as mouse wheel scrolls, mouse movements as well as keypresses to a file. This functionality is enabled by running
+
+```
+f3d --interaction-test-record ./TestName.log
+```
+
+where
+
+- `TestName` should be the name of the test case.
+
+While recording the interaction, care should be taken to perform the minimum number of events to simulate the interaction. For example, if you want to simulate the number `5` being pressed, there should be no mouse events or other keypress events during the recording of the interaction. The interaction file is saved when `f3d` is exited.
+
+Another interaction to avoid the one that quit F3D as it will have unexpected behavior when being replayed. Instead, quit the F3D application by using your window manager or Ctrl+C in the terminal.
+
+You can verify that your interaction file is correct by playing back the interaction
+
+```
+f3d --interaction-test-play ./TestName.log
+```
+
+Interactions that are not needed for the test can be removed manually. After verifying that the interaction file works as expected, copy it to `./testing/recordings`. The name of the interaction file should be the same as the test name. The interaction test case can then be created by adding
+
+```
+f3d_test(NAME TestName DATA datafile.ext INTERACTION)
+```
+
+to `application/testing/CMakeLists.txt`
+
+where
+
+- `INTERACTION` signifies that this is an interaction test
+
+The steps to running the test are the same as above.
+
+### Library layer
+
+When for some reason adding a test in the application layer is not possible, it is possible
+to add a C++ test in the library layer. These tests are simple C++ methods that should return
+`EXIT_SUCCESS` or `EXIT_FAILURE`.
+
+Library test cases are handled in `library/testing`.
+
+To add a test, create a new `TestSDKName.cxx` file containing a `int TestSDKName(int argc, char* argv[])` method,
+then implement your test in C++ using the [libf3d](../libf3d/01-OVERVIEW.md) API.
+Then add you new file to `src/library/testing/CMakeLists.txt`.
+
+It is supported to read file as input and perform image comparison against baselines as an output, see other tests as examples.
+
+### Bindings layer
+
+The libf3d supports multiple bindings, including Python, Java and Javascript.
+
+When improving/modifying these bindings, it is necessary to also improve/modify the bindings tests accordingly.
+Please take a look into `<java/python/webassembly>/testing` for examples to follow.
+
+### VTKExtensions layer
+
+When for some reason adding a test in the application or library layer is not possible, it is possible
+to add a C++ test in the VTKExtensions layer. These tests are simple C++ methods that should return
+`EXIT_SUCESS` or `EXIT_FAILURE`.
+
+Everything is handled in `library/VTKExtensions/ModuleName/Testing`.
+
+To add a test, first identify which VTKExtensions module you need to add a test into,
+then create a new `TestName.cxx` file containing a `int TestName(int argc, char* argv[])` method,
+then implement your test in C++ using VTK and F3D VTKExtensions modules.
+Then add you new file to `library/VTKExtensions/ModuleName/Testing/CMakeLists.txt`.
+
+It is supported to read file as input if needed, see other tests as examples.
diff -Nru f3d-3.2.0+dfsg/doc/dev/06-TOOLING.md f3d-3.4.1/doc/dev/06-TOOLING.md
--- f3d-3.2.0+dfsg/doc/dev/06-TOOLING.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/06-TOOLING.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,42 @@
+# Tooling
+
+## How to generate the full coverage report
+
+Requires `gcovr` program and `gcc` toolchain.
+
+1. Build with `F3D_COVERAGE` option enabled.
+2. Run all tests, `xdotool` needs to be installed. This will generate many `.gcda` files.
+3. Generate the report with: `gcovr -r /path/to/sources --html --html-details -o coverage.html`.
+
+## How to build and test with sanitizer
+
+Requires `clang` toolchain.
+
+1. Build with `F3D_SANITIZER` option to any of the possible values.
+2. `export LSAN_OPTIONS=suppressions=/path/to/f3d/.lsan.supp:use_tls=0`.
+3. `export TSAN_OPTIONS=suppressions=/path/to/f3d/.tsan.supp`.
+4. Run all tests.
+
+## How to locally generate and run the website
+
+1. Install `npm`
+2. Clone https://github.com/f3d-app/f3d-website
+3. Install needed npm packages: `npm install`
+4. Build and serve the website: `npm run start`
+
+Please note the search bar is not working locally, this is expected.
+
+## How to generate the doc for latest master
+
+1. Generate and run as described above
+2. Update the doc: `npm run update-doc`
+3. Refresh the website to see the new doc
+
+## How to update the doc for a new release
+
+1. Generate and run as described above
+2. Update the doc for release branch: `npm run update-doc release`
+3. Add a new versioned doc: `npm run docusaurus docs:version X.Y`
+4. Add `X.Y` in the `docsVersionDropdown` in `docusaurus.config.ts`
+5. Rebuild the website: `npm run start`
+6. Refresh the website to see the new versioned doc
diff -Nru f3d-3.2.0+dfsg/doc/dev/07-ARCHITECTURE.md f3d-3.4.1/doc/dev/07-ARCHITECTURE.md
--- f3d-3.2.0+dfsg/doc/dev/07-ARCHITECTURE.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/07-ARCHITECTURE.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,96 @@
+# Architecture
+
+F3D is structured in different parts, interacting with each others and with F3D dependencies.
+This architecture is reflected by the directories organisation.
+
+- **application**: the code of the F3D application itself, see below
+- cmake: cmake macros and functions, used by the CMake build system
+- doc: this very documentation
+- examples: examples usage of the libf3d and plugin framework in C++, C, python, java and javascript
+- external: dependencies that are included directly in the code
+- **library**: the [libf3d](../libf3d/01-OVERVIEW.md) itself, see below
+- **plugins**: all the [plugins](../libf3d/05-PLUGINS.md) providing different readers, see below
+- resources: all non code, non doc, like icon, configs and such
+- testing: all testing related resources, does not contain the test themselves
+- **vtkext**: extensions to VTK and related tests, see below
+- c: [c bindings](../libf3d/04-LANGUAGE_BINDINGS.md#c) and tests
+- python: [python bindings](../libf3d/04-LANGUAGE_BINDINGS.md#python) and tests
+- java: [java bindings](../libf3d/04-LANGUAGE_BINDINGS.md#java-experimental) and tests
+- webassembly: [webassembly/javascript bindings](../libf3d/04-LANGUAGE_BINDINGS.md#javascript) and tests
+- winshellext: shell extension for Windows, provide [thumbnails for Windows](../user/11-DESKTOP_INTEGRATION.md#windows)
+
+Here is diagram explaining how some of these parts interact together:
+
+```
+   ┌────────────────┐        ┌───────────────┐
+   │                │        │               │
+   │  application   │◄──uses─┤  winshellext  │
+   │                │        │               │ ┌──────────┐
+   └───────┬────────┘        └───────────────┘ │          │
+           │                              ┌────┤  python  │
+           │                              │    │          │
+           │                              │    └──────────┘
+       depends on                         │    ┌──────────┐
+           │                              │    │          │
+           │                              ├────┤   java   │
+           │         ┌───────────┐        │    │          │
+           └────────►│           │        │    └──────────┘
+                     │  library  │◄─wraps─┤    ┌──────────┐
+      ┌───loads──────┤           │        │    │          │
+      │              └─────┬─────┘        ├────┤   wasm   │
+      ▼                    │              │    │          │
+┌───────────┐              │              │    └──────────┘
+│           │          depends on         │    ┌──────────┐
+│  plugins  │              │              │    │          │
+│           │              ▼              └────┤    c     │
+└─────┬─────┘     ┌──────────────────┐         │          │
+      │           │      vtkext      │         └──────────┘
+      │           ├────────┬─────────┤
+     depends─on──►│ public │ private │
+                  └────────┴─────────┘
+```
+
+## vtkext
+
+`vtkext` contains two [VTK modules](https://docs.vtk.org/en/latest/api/cmake/ModuleSystem.html) that are used extensively in the libf3d. The public one is used in the plugins.
+
+`public` is a VTK module that contains classes and utilities that can be installed as part of the `plugin_sdk` and used by plugins, including externals plugins. `vtkF3DImporter` is a class
+that is specifically made for plugin developers to inherit their importers from. The documentation of this module can be found [here](https://f3d.app/docs/next/category/vtkext-api-reference).
+
+`private` is a VTK module that contains many classes and utilities used by the libf3d to provide all features of F3D, especially the rendering, interactions and UI.
+A notable class is `vtkF3DRenderer` that is responsible to actually add the different actors in the 3D scene.
+
+Each of these modules also contains [tests](05-TESTING.md#vtkextensions-layer) in the `Testing` directory.
+
+## plugins
+
+`plugins` contains [libf3d plugins](../libf3d/05-PLUGINS.md) that are provided by default in the F3D packages. Each of these plugins correspond to a specific dependency and are named accordingly. Each of these plugins provide access to specific readers for specific formats. Without plugins, F3D and the libf3d would not be able to open any file. These plugins can be loaded statically or dynamically, which makes the dependencies truly optional if needed.
+
+## library
+
+`library` contains the code of the libf3d. It is a C++ library with a very limited API surface and larger, private, implementation.
+Most classes in the libf3d are split in two. A public part that contains mostly the public API, and a private part, suffixed "\_impl", that implements that public API
+and also contains hidden methods used to communicate between classes, especially in regards to VTK symbols.
+
+Logically, it is structured in 3 parts, `public` which contains the public API header files and are all installed, `private` which contains the implementation classes headers files and `src` that contains the source files of all the classes, public and private.
+
+There is also a dedicated `testing` directory which contains the [unit and functional testing](05-TESTING.md#library-layer) of the libf3d.
+
+It also contains the `options.json` file, which is the file used to generate all [options](../libf3d/03-OPTIONS.md) code.
+
+## application
+
+`application` contains the code of the F3D application itself. It relies of course on the libf3d to implement all the applicative logic.
+The most important class in the `F3DStarter` which contains most of the top logic on the application. `F3DOptionsTools` is also notable as it handles most of
+the command line options logic.
+
+There is also a dedicated `testing` directory which contains all of the [applicative testing](05-TESTING.md#application-layer) of the F3D application as well as many functional testing of the libf3d.
+
+## Other f3d-app repositories
+
+Although almost everything is contained in the [f3d-app/f3d](https://github.com/f3d-app/f3d) repository, other repositories in the [f3d-app](https://github.com/f3d-app) organisation are handling certains specific tasks in the F3D ecosystem.
+
+- The [f3d-superbuild](https://github.com/f3d-app/f3d-superbuild) handles the packaging and the creation of the binaries provided in the [releases page](https://github.com/f3d-app/f3d/releases).
+- [f3d-website](https://github.com/f3d-app/f3d-website) contains the website generated from our documentation and deployed [here](https://f3d.app) as well as our [webviewer](https://f3d.app/viewer).
+- A collection of actions: [sccache-setup](https://github.com/f3d-app/sccache-setup-action), [lfs-data-cache](https://github.com/f3d-app/lfs-data-cache-action) and [install-mesa-windows](https://github.com/f3d-app/install-mesa-windows-action) used by the CI of F3D
+- A collection of [docker files](https://github.com/f3d-app/f3d-docker-images) used for generating docker images used by the CI of F3D
diff -Nru f3d-3.2.0+dfsg/doc/dev/08-CODING_STYLE.md f3d-3.4.1/doc/dev/08-CODING_STYLE.md
--- f3d-3.2.0+dfsg/doc/dev/08-CODING_STYLE.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/08-CODING_STYLE.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,87 @@
+# Coding Style
+
+F3D use different coding styles in each component, however there are some common rules
+
+## C++
+
+### Common rules
+
+Overall syntax:
+
+- CamelCase.
+- Avoid using `using` for namespaces.
+- Initialize variables in header when possible.
+- Local variables starts with a lower case char.
+- Class members starts with a upper case char.
+- Indents are two spaces.
+- One space between instruction and parenthesis.
+- Always add curly brace after instruction.
+- Curly brace one a new line, aligned with instruction.
+- Add `//----------------------------------------------------------------------------` before any method in implementation.
+
+Example:
+
+```cpp
+//----------------------------------------------------------------------------
+void class::method()
+{
+  if (condition)
+  {
+    std::vector<int> myIntVector = { 13 };
+  }
+}
+```
+
+Includes:
+
+- Organized by category: `F3DApplication`, `libf3d`, `VTKExtensions`, `other deps`, `std`, `system`.
+- Sorted inside category.
+
+### F3D Application rules
+
+- Class starts with `F3D`
+- Method starts with an uppercase char.
+
+### libf3d rules
+
+- Class starts with a lower case char.
+- Method starts with an lower case char.
+
+### VTKExtensions rules
+
+- Follow VTK rules
+- Method starts with an uppercase char.
+- Class starts with `vtkF3D` if inheriting from vtkObject.
+- Class starts with `F3D` if not inheriting from vtkObject.
+
+### Automatic formatting
+
+Some of the rules above are enforced using clang-format thanks to a `.clang-format` file.
+The continuous integration checks it whenever code is pushed to a pull request.
+To fix it locally, you can use:
+
+- single file: `clang-format -i /path/to/file.ext`
+- all files: `shopt -s globstar; clang-format -i **/*.{h,cxx}`
+
+Please note there can be small discrepancy between the CI and local run of clang-format depending
+on the version in use. You may need to fix these manually.
+
+## Python
+
+All python code is simply formatted using [Black style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html).
+
+The continuous integration checks the formatting of python code using `black`.
+You can fix it locally by running:
+
+- single file: `black /path/to/file.py`
+- all files: `black --include '(\.py|\.py\.in)' .`
+
+## Markdown and others
+
+Markdown, JavaScript, JSON, HTML and YAML files are formatted using [Prettier](https://prettier.io/docs/).
+
+The continuous integration checks the formatting of all these files using `prettier`.
+You can fix them locally by running:
+
+- single file: `prettier -w /path/to/file.ext`
+- all files: `shopt -s dotglob;shopt -s globstar; prettier -w **/*.{ts,tsx,js,json,md,html,yml}`
diff -Nru f3d-3.2.0+dfsg/doc/dev/09-ROADMAPS_AND_RELEASES.md f3d-3.4.1/doc/dev/09-ROADMAPS_AND_RELEASES.md
--- f3d-3.2.0+dfsg/doc/dev/09-ROADMAPS_AND_RELEASES.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/09-ROADMAPS_AND_RELEASES.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,24 @@
+# Roadmaps and Release Cycles
+
+F3D is a community driven project and we try to provide as much information as we can about the upcoming features
+and release expectations. Please keep in mind while we may try to fulfill what was planned, some features may not be available
+as planned and releases can be delayed.
+
+Roadmaps are available on their dedicated issue with their associated project board and milestone.
+The first draft of the roadmap of each release will be discussed publicly on [discord](https://discord.f3d.app), then they are bound to evolve over time
+while the development happens.
+
+If you want to get involved, please feel free to join the [discord](https://discord.f3d.app), comment on the issues and contribute to the issues that matters to you!
+
+## 3.4.0
+
+Out next minor release is planned for January 2026
+
+- [Issue](https://github.com/f3d-app/f3d/issues/2565)
+- [Milestone](https://github.com/f3d-app/f3d/milestone/14)
+- [Board](https://github.com/orgs/f3d-app/projects/2)
+
+## Release cycle
+
+F3D release cycle is one minor release every three months, in January, April, July and October.
+A minor release may be skipped before a major release.
diff -Nru f3d-3.2.0+dfsg/doc/dev/10-GOVERNANCE.md f3d-3.4.1/doc/dev/10-GOVERNANCE.md
--- f3d-3.2.0+dfsg/doc/dev/10-GOVERNANCE.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/10-GOVERNANCE.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,94 @@
+# Governance Policy
+
+This document provides the governance policy for F3D (the "Project"). [Every person](./11-MAINTAINERS_AND_CONTRIBUTORS.md) responsible for its maintenance (the "Maintainers") agrees to this policy and to abide by all Project policies, including the [code of conduct](../../CODE_OF_CONDUCT.md) by adding their name to the [MAINTAINERS_AND_CONTRIBUTORS.md](./11-MAINTAINERS_AND_CONTRIBUTORS.md) file.
+
+## 1. Roles
+
+This Project includes the following roles.
+
+### 1.1. Maintainers
+
+Maintainers are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are responsible of upholding the [Code of Conduct](../../CODE_OF_CONDUCT.md). Maintainers are also responsible for determining consensus for technical or community matters. The Project may add or remove Maintainers by affirmative vote of 2/3 of all Maintainers. Maintainers are de-facto moderators. Maintainers are part of the f3d-app github organization with a "owner" role and listed in the [dedicated](11-MAINTAINERS_AND_CONTRIBUTORS.md) file.
+
+Maintainers is an important role and as such, will be evaluated by the community yearly and exit interview may be conducted in case of a Maintainer leaving the role.
+
+Any Returning Contributor interested to become a Maintainer should just reach out to existing Maintainers for consideration.
+
+### 1.1. Moderators
+
+Moderators are responsible of upholding the [Code of Conduct](../../CODE_OF_CONDUCT.md). Moderators are responsible for determining consensus for community matters. The Project may add or remove Maintainers by affirmative vote of 2/3 of all Maintainers.
+
+Anyone interested to become a moderators should just reach out to existing Maintainers for consideration.
+
+### 1.2. Returning Contributors
+
+Returning Contributors are those who have made multiple contributions to the Project over multiple release and want to be acknowledged as such. Returning Contributors are also very much welcome to share their ideas and feedbacks about the development and maintenance of F3D. Returning contributors may be asked by others about their contributions, provide reviews on code they are familiar with, provide technical insight during discussions and take part in any related activities they wish to.
+
+To become a Returning Contributor, one need to have merged at least two pull requests into the F3D repositories over the course of multiple releases.
+Returning Contributors are part of the f3d-app github organization with a "member" role and are listed in the [dedicated](./11-MAINTAINERS_AND_CONTRIBUTORS.md) file.
+
+Usually Maintainers will reach out to potential Returning Contributors after new releases but in case it was missed, one should reach out to Maintainers for consideration.
+Returning contributors also usually takes care of [mentoring Mentees](./12-MENTORING.md).
+
+Maintainers may decide to list a returning contributor as inactive after a year without contributions, in that case they will be removed from the f3d-app github organization until active again.
+
+### 1.3. Contributors
+
+Contributors are those who have made or want to make contributions to the Project.
+
+### 1.4. Mentees
+
+Mentees are contributors that are enrolled in the [mentoring program](./12-MENTORING.md) of F3D.
+It is open to anyone willing to learn more about open source contribution to F3D and be helped by Maintainers and Contributors.
+
+### 1.5. Sponsor
+
+Sponsor are users that are currently sponsoring the project. They are acknowledged as specified in our [donation page](https://f3d.app/thanks).
+
+### 1.6 Industry sponsor
+
+Industry sponsor are company that are currently sponsoring the project. They are acknowledged as specified in our [donation page](https://f3d.app/thanks).
+
+## 2. Decisions
+
+### 2.1 Consensus-Based Decision Making
+
+The Project makes decisions through the consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of several factors, including the dominant view of the Contributors and the nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements using issues comments.
+
+### 2.2. Appeal Process
+
+Decisions may be appealed by opening an issue and that appeal will be considered by the Maintainers in good faith, who will respond in writing within a reasonable time.
+
+## 3. How We Work
+
+### 3.1. Openness
+
+Participation is open to anyone who is directly and materially affected by the activity in question. There shall be no undue financial barriers to participation.
+
+### 3.2. Balance
+
+The development process should balance the interests of Contributors and other stakeholders. Contributors from diverse interest categories shall be sought with the objective of achieving balance.
+
+### 3.3. Coordination and Harmonization
+
+Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in the Project.
+
+### 3.4. Consideration of Views and Objections
+
+Prompt consideration shall be given to the written views and objections of all Contributors.
+
+### 3.5. Written procedures
+
+This governance document and other materials documenting the Project development process shall be available to any interested person.
+
+## 4. No Confidentiality
+
+Information disclosed in connection with any Project activity, including but not limited to meetings, contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary.
+
+## 5. Amendments
+
+Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers.
+
+---
+
+Modified from of MVG-0.1-beta GOVERNANCE.md. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff -Nru f3d-3.2.0+dfsg/doc/dev/11-MAINTAINERS_AND_CONTRIBUTORS.md f3d-3.4.1/doc/dev/11-MAINTAINERS_AND_CONTRIBUTORS.md
--- f3d-3.2.0+dfsg/doc/dev/11-MAINTAINERS_AND_CONTRIBUTORS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/11-MAINTAINERS_AND_CONTRIBUTORS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,34 @@
+# Maintainers and Contributors
+
+This document lists the Maintainers and Returning Contributors of the Project.
+
+## Maintainers
+
+Maintainers may add themselves once approved by the existing Maintainers as described in the [Governance document](./10-GOVERNANCE.md). By adding your name to this list you are agreeing to abide by and uphold the Project governance documents and to abide by all of the Organization's policies, including the [code of conduct](../../CODE_OF_CONDUCT.md).
+
+| **Name/Alias**   | **Handle** |
+| ---------------- | ---------- |
+| Mathieu Westphal | mwestphal  |
+| Michael Migliore | Meakk      |
+
+## Returning Contributors
+
+Returning Contributors should add themselves once [requirements](./10-GOVERNANCE.md#12-returning-contributors) have been checked by existing Maintainers.
+Maintainers may decide to list a returning contributor as inactive after a year without contributions.
+Contributors with a single contribution or that may not want to are not listed here but can be found in the git history.
+
+| **Name/Alias**                  | **Handle**     | **Active** |
+| ------------------------------- | -------------- | ---------- |
+| jrushlow                        | 19jrushlow     | Yes        |
+| ssmncrf                         | samoncrief     | Yes        |
+| Evgenii Startcev (PhilHellmuth) | exbluesbreaker | Yes        |
+| Louis Gombert                   | lgt2x          | Yes        |
+| Artem                           | 0xfedcafe      | Yes        |
+| Nig3l                           | Ni-g-3l        | Yes        |
+| Luke                            | lknknm         | Yes        |
+| Larry Skuse                     | skusel         | Yes        |
+| Medyan Naser                    | Medyan-Naser   | Yes        |
+
+---
+
+Modified from MVG-0.1-beta MAINTAINERS.md. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff -Nru f3d-3.2.0+dfsg/doc/dev/12-MENTORING.md f3d-3.4.1/doc/dev/12-MENTORING.md
--- f3d-3.2.0+dfsg/doc/dev/12-MENTORING.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/12-MENTORING.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,62 @@
+# Mentoring
+
+In order to help new contributors that may want or need mentoring in order to contribute to F3D,
+F3D has a mentoring program. Here we explain how it works as well as provide resources that could not be listed elsewhere.
+
+## Process
+
+If you want to contribute to F3D but have no experience with open source contributions or need help with technical subjects,
+then you may want to enroll in the mentoring program!
+
+Just join our [discord](https://discord.f3d.app) and select the "mentee" role, then a mentor will reach out and guide you.
+Please note there is no expectation of knowledge, skill or ability. You just need to want to contribute to F3D and willing to learn.
+Usually it looks like this:
+
+- You join the discord as Mentee.
+- A maintainer/moderator welcomes you and guide you to a dedicated channel.
+- Your mentor ask you about what you want to do and what are your skills in order to adapt the mentoring.
+- Your mentor points you to resources to get started, usually with compiling the software.
+- Once you are able to build and use F3D, your mentor asks you to look at ["good-first-issue"](https://github.com/f3d-app/f3d/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
+- You pick an _unassigned_ issue
+- Your mentor explain the issue in details to you to get your started on it
+- While you progress on the issue, your mentor will guide you with technical issue and contribution steps
+
+## Important points
+
+- [Discord](https://discord.f3d.app) is needed for mentoring.
+- Please talk with your mentor in the dedicated channel, not in private messages.
+- Please ask any questions you may have, mentoring is about learning!
+- Once again, please ask questions and make sure you understand everything, we love to share about how the project works!
+- Your mentor will ask you to open a pull request to show some code. Please do that instead of sharing a branch, it is much easier for mentors.
+- The contribution guide mention running tests, do not worry about tests for now.
+- The contribution guide mention running CI, do not worry about CI for now.
+- If you use AI as a dev tool, mentors will considered it your own code and you should be able to explain what it does.
+
+## Resources
+
+Generic Resources:
+
+- [How to ask questions the smart way](http://www.catb.org/esr/faqs/smart-questions.html)
+- [CMake practical guide](https://crascit.com/professional-cmake/)
+- [LearnCPP](https://www.learncpp.com/)
+- [LeanOpenGL](https://learnopengl.com/)
+- [VTK documentation](https://docs.vtk.org/en/latest/)
+- [ImGui documentation](https://github.com/ocornut/imgui/wiki/)
+
+F3D pull request examples:
+
+- [PR that makes a small doc change](https://github.com/f3d-app/f3d/pull/2450)
+- [PR that fixes a simple issue](https://github.com/f3d-app/f3d/pull/2397)
+- [PR that adds a CLI option](https://github.com/f3d-app/f3d/pull/2402)
+- [PR that adds a CLI and libf3d option](https://github.com/f3d-app/f3d/pull/2181)
+- [PR that adds a new interaction binding](https://github.com/f3d-app/f3d/pull/2404)
+- [PR that adds a new reader](https://github.com/f3d-app/f3d/pull/1579)
+- [PR that adds a new plugin](https://github.com/f3d-app/f3d/pull/1128)
+
+## Mentors
+
+Anyone can be a mentor!
+
+That being said, usually mentors are contributors with a bit of experience with the project and with open source contributions.
+So, when a Contributor becomes a [Returning Contributor](./10-GOVERNANCE.md#12-returning-contributors), they can also decide to select the `mentor`
+role on discord, which means that they will be informed when a Mentee needs mentorings. There is no obligations and no expectations.
diff -Nru f3d-3.2.0+dfsg/doc/dev/13-BUILD_WASM.md f3d-3.4.1/doc/dev/13-BUILD_WASM.md
--- f3d-3.2.0+dfsg/doc/dev/13-BUILD_WASM.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/dev/13-BUILD_WASM.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,52 @@
+# Build WebAssembly
+
+F3D can be built in WebAssembly using emscripten in order to embed it into a web browser.
+It is used by our [web viewer](https://f3d.app/viewer) and covers most of the [libf3d public API](../libf3d/04-LANGUAGE_BINDINGS.md).
+A simple example is available [here](https://github.com/f3d-app/f3d/blob/master/examples/libf3d/web).
+
+This guide is describing how to build VTK and F3D with emscripten using Docker on Linux or Windows.
+
+# Building
+
+While it's possible to setup an emscripten cross-compiling toolchain locally, it's easier to use Docker and the provided npm scripts.
+
+## Preparing the build
+
+Install `Docker` and `npm` locally.
+
+## Building F3D
+
+From the root of the repository run the following command:
+
+```sh
+npm run build
+```
+
+By default, the latest Docker image is pulled automatically, but you can force a specific Docker image by setting the environment variable `F3D_DOCKER_TIMESTAMP` to a timestamp (see `.github/workflows/versions.json` file)
+It's also possible to use `podman` instead of `docker`, please set the `F3D_DOCKER_CLIENT` environment variable to `podman`.
+On completion, a directory `dist` is created containing the artifacts (`f3d.js` and `f3d.wasm`).
+`assimp`, `occt` and `draco` plugins are automatically included.
+
+# Run tests
+
+From the root of the repository, after the build step, run the following command:
+
+```sh
+npm test
+```
+
+# Integrating
+
+It's possible to generate a local package to use in other javascript projects.
+Run the following command:
+
+```sh
+npm pack
+```
+
+It will build and generate a `f3d-vX.X.X.tgz` file.
+This file can be imported into your project:
+
+```sh
+npm install f3d-vX.X.X.tgz
+```
diff -Nru f3d-3.2.0+dfsg/doc/dev/ARCHITECTURE.md f3d-3.4.1/doc/dev/ARCHITECTURE.md
--- f3d-3.2.0+dfsg/doc/dev/ARCHITECTURE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/ARCHITECTURE.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,95 +0,0 @@
-# F3D Architecture
-
-F3D is structured in different parts, interacting with each others and with F3D dependencies.
-This architecture is reflected by the directories organisation.
-
-- **application**: the code of the F3D application itself, see below
-- cmake: cmake macros and functions, used by the CMake build system
-- doc: this very documentation
-- examples: examples usage of the libf3d and plugin framework in python and C++
-- external: dependencies that are included directly in the code
-- java: [java bindings](../libf3d/LANGUAGE_BINDINGS.md#java-experimental) and associated tests.
-- **library**: the [libf3d](../libf3d/README_LIBF3D.md) itself, see below
-- **plugins**: all the [plugins](../libf3d/PLUGINS.md) providing different readers, see below
-- python: [python bindings](../libf3d/LANGUAGE_BINDINGS.md#python) and tests
-- resources: all non code, non doc, like icon, configs and such
-- testing: all testing related resources, does not contain the test themselves
-- **vtkext**: extensions to VTK and related tests, see below
-- webassembly: [webassembly/javascript bindings](../libf3d/LANGUAGE_BINDINGS.md#javascript-experimental) and [F3DWeb](https://f3d.app/web/) application code
-- winshellext: shell extension for Windows, provide [thumbnails for Windows](../user/DESKTOP_INTEGRATION.md#windows)
-
-Here is diagram explaining how some of these parts interact together:
-
-```
-   ┌────────────────┐              ┌───────────────┐
-   │                │              │               │
-   │  application   │◄────uses─────│  winshellext  │
-   │                │              │               │
-   └────────────────┘              └───────────────┘
-           │
-           │                                   ┌──────────┐
-           │                                   │          │
-       depends on                     ┌─wraps──│  python  │
-           │                          │        │          │
-           │                          │        └──────────┘
-           │         ┌───────────┐    │        ┌──────────┐
-           └────────►│           │◄───┘        │          │
-                     │  library  │◄─────wraps──│   java   │
-      ┌───loads──────│           │◄───┐        │          │
-      │              └───────────┘    │        └──────────┘
-      ▼                    │          │        ┌──────────┐
-┌───────────┐              │          │        │          │
-│           │          depends on     └─wraps──│   wasm   │
-│  plugins  │              │                   │          │
-│           │              ▼                   └──────────┘
-└───────────┘     ┌──────────────────┐
-      │           │      vtkext      │
-      │           ├────────┬─────────┤
-     depends─on──►│ public │ private │
-                  └────────┴─────────┘
-```
-
-## vtkext
-
-`vtkext` contains two [VTK modules](https://docs.vtk.org/en/latest/api/cmake/ModuleSystem.html) that are used extensively in the libf3d. The public one is used in the plugins.
-
-`public` is a VTK module that contains classes and utilities that can be installed as part of the `plugin_sdk` and used by plugins, including externals plugins. `vtkF3DImporter` is a class
-that is specifically made for plugin developers to inherit their importers from. The documentation of this module can be found [here](https://f3d.app/doc/libf3d/vtkext_doxygen/).
-
-`private` is a VTK module that contains many classes and utilities used by the libf3d to provide all features of F3D, especially the rendering, interactions and UI.
-A notable class is `vtkF3DRenderer` that is responsible to actually add the different actors in the 3D scene.
-
-Each of these modules also contains [tests](TESTING.md#vtkextensions-layer) in the `Testing` directory.
-
-## plugins
-
-`plugins` contains [libf3d plugins](../libf3d/PLUGINS.md) that are provided by default in the F3D packages. Each of these plugins correspond to a specific dependency and are named accordingly. Each of these plugins provide access to specific readers for specific formats. Without plugins, F3D and the libf3d would not be able to open any file. These plugins can be loaded statically or dynamically, which makes the dependencies truly optional if needed.
-
-## library
-
-`library` contains the code of the libf3d. It is a C++ library with a very limited API surface and larger, private, implementation.
-Most classes in the libf3d are split in two. A public part that contains mostly the public API, and a private part, suffixed "\_impl", that implements that public API
-and also contains hidden methods used to communicate between classes, especially in regards to VTK symbols.
-
-Logically, it is structured in 3 parts, `public` which contains the public API header files and are all installed, `private` which contains the implementation classes headers files and `src` that contains the source files of all the classes, public and private.
-
-There is also a dedicated `testing` directory which contains the [unit and functional testing](TESTING.md#library-layer) of the libf3d.
-
-It also contains the `options.json` file, which is the file used to generate all [options](../libf3d/OPTIONS.md) code.
-
-## application
-
-`application` contains the code of the F3D application itself. It relies of course on the libf3d to implement all the applicative logic.
-The most important class in the `F3DStarter` which contains most of the top logic on the application. `F3DOptionsTools` is also notable as it handles most of
-the command line options logic.
-
-There is also a dedicated `testing` directory which contains all of the [applicative testing](TESTING.md#application-layer) of the F3D application as well as many functional testing of the libf3d.
-
-## Other f3d-app repositories
-
-Although almost everything is contained in the [f3d-app/f3d](https://github.com/f3d-app/f3d) repository, other repositories in the [f3d-app](https://github.com/f3d-app) organisation are handling certains specific tasks in the F3D ecosystem.
-
-- The [f3d-superbuild](https://github.com/f3d-app/f3d-superbuild) handles the packaging and the creation of the binaries provided in the [releases page](https://github.com/f3d-app/f3d/releases).
-- [f3d-media](https://github.com/f3d-app/f3d-media) backups all images and video used in this documentation
-- A collection of actions: [sccache-setup](https://github.com/f3d-app/sccache-setup-action), [lfs-data-cache](https://github.com/f3d-app/lfs-data-cache-action) and [install-mesa-windows](https://github.com/f3d-app/install-mesa-windows-action) used by the CI of F3D
-- A collection of [docker files](https://github.com/f3d-app/f3d-docker-images) used for generating docker images used by the CI of F3D
diff -Nru f3d-3.2.0+dfsg/doc/dev/BUILD.md f3d-3.4.1/doc/dev/BUILD.md
--- f3d-3.2.0+dfsg/doc/dev/BUILD.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/BUILD.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,100 +0,0 @@
-# Build guide
-
-F3D uses a CMake based build system, so building F3D just requires installing
-needed dependencies, configuring and building. If you are not used to such processes
-please take a look at our [getting started guide](GETTING_STARTED.md).
-
-## Dependencies
-
-- [CMake](https://cmake.org) >= 3.1.
-- [VTK](https://vtk.org) >= 9.2.6 (9.5.0 recommended).
-- A C++17 compiler.
-- A CMake-compatible build system (Visual Studio, XCode, Ninja, Make, etc.).
-- Optionally, [Assimp](https://www.assimp.org/) >= 5.4.0 (6.0.2 recommended).
-- Optionally, Open CASCADE [OCCT](https://dev.opencascade.org/) >= 7.6.3.
-- Optionally, [Alembic](http://www.alembic.io/) >= 1.8.5.
-- Optionally, [OpenUSD](https://openusd.org/release/index.html) >= 24.08.
-- Optionally, [OpenVDB](https://www.openvdb.org/download/) >= 12.0.0, enable `IOOpenVDB` module during VTK configuration.
-- Optionally, [Draco](https://google.github.io/draco/) >= 1.5.6.
-- Optionally, [Python](https://www.python.org/) >= 3.9 and [pybind11](https://github.com/pybind/pybind11) >= 2.9.2.
-- Optionally, [Java](https://www.java.com) >= 17.
-- Optionally, [OpenEXR](https://openexr.com/en/latest/) >= 3.0.1.
-
-F3D is tested continuously against versions recommended by the [VFX reference platform](https://vfxplatform.com) defined for **CY2025**
-
-## VTK compatibility
-
-As stated in the dependencies, F3D is compatible with VTK >= 9.2.6, however, some features may not be available. We suggest using VTK 9.5.0 with RenderingRayTracing, IOExodus, IOHDF, IONetCDF and IOOpenVDB modules enabled in order to get as many features as possible in F3D.
-
-## Configuration and building
-
-Configure and generate the project with CMake,
-then build the software using your build system.
-
-Here is some CMake options of interest:
-
-- `F3D_BUILD_APPLICATION`: Build the F3D executable.
-- `BUILD_TESTING`: Enable the [tests](TESTING.md).
-- `F3D_MACOS_BUNDLE`: On macOS, build a `.app` bundle.
-- `F3D_WINDOWS_BUILD_SHELL_THUMBNAILS_EXTENSION`: On Windows, build the shell thumbnails extension.
-- `F3D_WINDOWS_BUILD_CONSOLE_APPLICATION`: On Windows, build a supplemental Win32 console application.
-- `F3D_PLUGINS_STATIC_BUILD`: Build all plugins as static library (embedded into `libf3d`) and automatically loaded by the application. Incompatible with `F3D_MACOS_BUNDLE`.
-- `BUILD_SHARED_LIBS`: Build the libf3d and all plugins as static library (embedded into `f3d` executable). The `library` and `plugin_sdk` component will not be installed.
-
-Some modules, plugins and language bindings depending on external libraries can be optionally enabled with the following CMake variables:
-
-- `F3D_MODULE_RAYTRACING`: Support for raytracing rendering. Requires that VTK has been built with `OSPRay` and `VTK_MODULE_ENABLE_VTK_RenderingRayTracing` turned on. Disabled by default.
-- `F3D_MODULE_EXR`: Support for OpenEXR images. Requires `OpenEXR`. Disabled by default.
-- `F3D_MODULE_UI`: Support for ImGui widgets. Uses provided ImGui. Enabled by default.
-- `F3D_PLUGIN_BUILD_HDF`: Support for VTKHDF (.vtkhdf), ExodusII (.ex2), and NetCDF (.nc) file formats. Requires that VTK has been built with `IOHDF`, `IOExodus`, and `IONetCDF` modules (and `hdf5`). Enabled by default.
-- `F3D_PLUGIN_BUILD_OCCT`: Support for STEP, IGES, BREP, and XBF file formats. Requires `OpenCASCADE`. Disabled by default.
-- `F3D_PLUGIN_BUILD_ASSIMP`: Support for FBX, DAE, OFF, DXF, X and 3MF file formats. Requires `Assimp`. Disabled by default.
-- `F3D_PLUGIN_BUILD_ALEMBIC`: Support for ABC file format. Requires `Alembic`. Disabled by default.
-- `F3D_PLUGIN_BUILD_DRACO`: Support for DRC file format. Requires `Draco`. Disabled by default.
-- `F3D_PLUGIN_BUILD_USD`: Support for USD file format. Requires `OpenUSD`. Disabled by default.
-- `F3D_PLUGIN_BUILD_VDB`: Support for VDB file format. Requires that VTK has been built with `IOOpenVDB` module (and `OpenVDB`). Disabled by default.
-- `F3D_BINDINGS_PYTHON`: Generate python bindings (requires `Python` and `pybind11`). Disabled by default.
-- `F3D_BINDINGS_PYTHON_GENERATE_STUBS`: Generate python stubs (requires `Python` and `pybind11_stubgen`). Disabled by default.
-- `F3D_BINDINGS_JAVA`: Generate java bindings (requires `Java` and `JNI`). Disabled by default.
-
-Some dependencies are provided internally, eg: ImGui, dmon and others. Use `F3D_USE_EXTERNAL_*` to use an external version of these libraries.
-
-## VCPKG
-
-It's possible to use VCPKG to automatically build dependencies. A manifest file `vcpkg.json` is available at your convenience. Please read [VCPKG documentation](https://vcpkg.io/en/getting-started.html).  
-Basically, just install VCPKG, and configure F3D using `-DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake` option.  
-Only VTK is added to the manifest file and additional dependencies must be added manually in this file.
-
-## Installing
-
-Installation can be done through CMake, by running the following command:
-
-```
-cmake --install ${your_build_dir}
-```
-
-Individual components can also be installed by specifying the component name:
-
-```
-cmake --install ${your_build_dir} --component ${component_name}
-```
-
-Here is the list of all the components:
-
-| Name            | Installed by default | Operating system | Description                                                                                                                 |
-| --------------- | -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| `application`   | YES                  | ALL              | F3D application.                                                                                                            |
-| `configuration` | NO                   | ALL              | Default configuration files, `config` and `thumbnail`.                                                                      |
-| `library`       | YES                  | ALL              | libf3d library binaries.                                                                                                    |
-| `plugin`        | YES                  | ALL              | libf3d plugins.                                                                                                             |
-| `dependencies`  | NO                   | ALL              | libf3d runtime dependencies. Can be used to create a self-contained and relocatable package. System libraries are excluded. |
-| `sdk`           | NO                   | ALL              | libf3d SDK (headers and CMake config files) for `library` and `application` find_package components.                        |
-| `plugin_sdk`    | NO                   | ALL              | libf3d plugin SDK (headers and CMake config files including macros) for `pluginsdk` find_package components.                |
-| `licenses`      | YES                  | ALL              | F3D and third party licenses.                                                                                               |
-| `documentation` | YES                  | Linux            | `man` documentation.                                                                                                        |
-| `shellext`      | YES                  | Windows/Linux    | Desktop integration.                                                                                                        |
-| `python`        | YES                  | ALL              | Python bindings.                                                                                                            |
-| `java`          | YES                  | ALL              | Java bindings.                                                                                                              |
-| `mimetypes`     | NO                   | Linux            | Plugins mimetype XML files for integration with Freedesktop.                                                                |
-| `assets`        | YES                  | Linux            | Assets for integration with Freedesktop.                                                                                    |
-| `colormaps`     | NO                   | ALL              | Color maps presets, see [documentation](../user/COLOR_MAPS.md)                                                              |
diff -Nru f3d-3.2.0+dfsg/doc/dev/BUILD_WASM.md f3d-3.4.1/doc/dev/BUILD_WASM.md
--- f3d-3.2.0+dfsg/doc/dev/BUILD_WASM.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/BUILD_WASM.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,43 +0,0 @@
-# Introduction
-
-F3D can be built in WebAssembly using emscripten in order to embed it into a web browser.
-It is still experimental and only a small fraction of the libf3d public API is bound.
-An example can be seen at https://f3d.app/web
-
-This guide is describing how to build VTK and F3D with emscripten using Docker on Linux or Windows.
-
-# Building
-
-While it's possible to setup an emscripten cross-compiling toolchain locally, it's easier to use Docker and the provided npm scripts.
-
-## Preparing the build
-
-Install `Docker` and `npm` locally.
-
-## Building F3D
-
-Go to the `webassembly` folder and run the following command to build F3D:
-
-```sh
-npm run build:deb # or build:rel for optimized build
-```
-
-On completion, a folder `webassembly/dist` is created containing the artifacts.
-
-# Testing it locally
-
-Five different files should be located in `webassembly/dist` folder:
-
-- `f3d.js`: javascript library
-- `f3d.wasm`: webassembly binaries
-- `f3d.data`: virtual filesystem containing the file specified in `F3D_WASM_DATA_FILE`
-- `index.html`: example file setting up the web page
-- `favicon.ico`: icon used to display the logo in the browser tab
-
-Again, use a npm command to test locally by running a HTTP server:
-
-```sh
-npm run serve
-```
-
-Going to http://localhost:3000 should display the web page and load F3D web app.
diff -Nru f3d-3.2.0+dfsg/doc/dev/CODING_STYLE.md f3d-3.4.1/doc/dev/CODING_STYLE.md
--- f3d-3.2.0+dfsg/doc/dev/CODING_STYLE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/CODING_STYLE.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,87 +0,0 @@
-# Coding Style
-
-F3D use different coding styles in each component, however there are some common rules
-
-## C++
-
-### Common rules
-
-Overall syntax:
-
-- CamelCase.
-- Avoid using `using` for namespaces.
-- Initialize variables in header when possible.
-- Local variables starts with a lower case char.
-- Class members starts with a upper case char.
-- Indents are two spaces.
-- One space between instruction and parenthesis.
-- Always add curly brace after instruction.
-- Curly brace one a new line, aligned with instruction.
-- Add `//----------------------------------------------------------------------------` before any method in implementation.
-
-Example:
-
-```cpp
-//----------------------------------------------------------------------------
-void class::method()
-{
-  if (condition)
-  {
-    std::vector<int> myIntVector = { 13 };
-  }
-}
-```
-
-Includes:
-
-- Organized by category: `F3DApplication`, `libf3d`, `VTKExtensions`, `other deps`, `std`, `system`.
-- Sorted inside category.
-
-### F3D Application rules
-
-- Class starts with `F3D`
-- Method starts with an uppercase char.
-
-### libf3d rules
-
-- Class starts with a lower case char.
-- Method starts with an lower case char.
-
-### VTKExtensions rules
-
-- Follow VTK rules
-- Method starts with an uppercase char.
-- Class starts with `vtkF3D` if inheriting from vtkObject.
-- Class starts with `F3D` if not inheriting from vtkObject.
-
-### Automatic formatting
-
-Some of the rules above are enforced using clang-format thanks to a `.clang-format` file.
-The continuous integration checks it whenever code is pushed to a pull request.
-To fix it locally, you can use:
-
-- single file: `clang-format -i /path/to/file.ext`
-- all files: `shopt -s globstar; clang-format -i **/*.{h,cxx}`
-
-Please note there can be small discrepancy between the CI and local run of clang-format depending
-on the version in use. You may need to fix these manually.
-
-## Python
-
-All python code is simply formatted using [Black style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html).
-
-The continuous integration checks the formatting of python code using `black`.
-You can fix it locally by running:
-
-- single file: `black /path/to/file.py`
-- all files: `black --include '(\.py|\.py\.in)' .`
-
-## Markdown and others
-
-Markdown, JavaScript, JSON, HTML and YAML files are formatted using [Prettier](https://prettier.io/docs/).
-
-The continuous integration checks the formatting of all these files using `prettier`.
-You can fix them locally by running:
-
-- single file: `prettier -w /path/to/file.ext`
-- all files: `shopt -s dotglob;shopt -s globstar; prettier -w **/*.{js,json,md,html,yml}`
diff -Nru f3d-3.2.0+dfsg/doc/dev/GENERATE.md f3d-3.4.1/doc/dev/GENERATE.md
--- f3d-3.2.0+dfsg/doc/dev/GENERATE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/GENERATE.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,32 +0,0 @@
-# How to generate the full coverage report
-
-Requires `gcovr` program and `gcc` toolchain.
-
-1. Build with `F3D_COVERAGE` option enabled.
-2. Run all tests, `xdotool` needs to be installed. This will generate many `.gcda` files.
-3. Generate the report with: `gcovr -r /path/to/sources --html --html-details -o coverage.html`.
-
-# How to build and test with sanitizer
-
-Requires `clang` toolchain.
-
-1. Build with `F3D_SANITIZER` option to any of the possible values.
-2. `export LSAN_OPTIONS=suppressions=/path/to/f3d/.lsan.supp:use_tls=0`.
-3. `export TSAN_OPTIONS=suppressions=/path/to/f3d/.tsan.supp`.
-4. Run all tests.
-
-# How to locally generate the Jekyll based website
-
-1. Install `ruby` and make sure ruby binaries directory is added to your `PATH`
-2. Install Jekyll and all dependencies: `gem install jekyll jekyll-remote-theme jekyll-relative-links jekyll-seo-tag jekyll-optional-front-matter jekyll-titles-from-headings jekyll-include-cache`
-3. Run Jekyll locally: `jekyll server`
-4. Open http://127.0.0.1:4000/ in a browser
-
-Please note the favicon and search bar are not working locally, this is expected.
-
-# How to locally generate the doxygen documentation
-
-1. Install Python, pip and doxygen
-2. `pip install sphinx sphinx-book-theme exhale`
-3. `sphinx-build -M html ./library/doxygen /path/to/build`
-4. `/path/to/build` directory now contains a HTML website
diff -Nru f3d-3.2.0+dfsg/doc/dev/GETTING_STARTED.md f3d-3.4.1/doc/dev/GETTING_STARTED.md
--- f3d-3.2.0+dfsg/doc/dev/GETTING_STARTED.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/GETTING_STARTED.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,259 +0,0 @@
-# Getting started guide
-
-This is a getting started guide, aimed to people that are not used
-to C++ programming, configuring and compiling. If you are already used
-to such processes, you may be looking for our [build guide](BUILD.md).
-
-This guide will help you to compile your own version of F3D with required dependencies.
-Optional dependencies or covered in the build guide.
-
-- If you are using [Linux](#linux)
-- If you are using [Windows](#windows)
-- If you are using [macOS](#macos)
-
-## Linux
-
-Note: The following steps were tested with Ubuntu 23 and ArchLinux but
-should work for other OSes as stated, as long as listed packages are available.
-
-### Install dependencies
-
-First make sure your system is up-to-date,
-then install the required dependencies using the terminal.
-
-#### Debian/Ubuntu/Mint
-
-```
-sudo apt update
-sudo apt upgrade
-sudo apt install build-essential git git-lfs cmake libvtk9-dev
-```
-
-Note: Ubuntu 24.04 / Debian 12 provides VTK version 9.1 but f3d requires VTK 9.2.6 at minimum, so building and installing vtk from source is suggested. Look at Windows build instruction for inspiration if needed.
-
-#### Fedora/Centos/RedHat
-
-```
-sudo yum update
-sudo dnf install make automake gcc gcc-c++ kernel-devel git git-lfs cmake vtk-devel
-```
-
-#### Arch Linux
-
-```
-sudo pacman -Syu
-sudo pacman -S base-devel git git-lfs cmake vtk nlohmann-json hdf5 netcdf fmt verdict openmpi glew ospray libxcursor
-```
-
-### Compile F3D
-
-Open a terminal.
-
-```
-mkdir f3d
-cd f3d
-git clone https://github.com/f3d-app/f3d src
-mkdir build
-cd build
-cmake ../src
-make
-```
-
-You can then simply run F3D from the command line:
-
-```
-./bin/f3d
-```
-
-### Running the test suite (optional)
-
-```
-cmake -DBUILD_TESTING=ON ../src
-make
-ctest
-```
-
-## Windows
-
-### Visual Studio with internal CMake/VCPKG integration
-
-On very recent Visual Studio versions, it's possible to rely on the internal CMake to automatically configure the project
-and the internal VCPKG to automatically build the dependencies.
-
-#### Install Dependencies
-
-- Download and install a recent version of [Visual Studio 2022 Community Edition][visual-studio]
-
-> [!WARNING]
-> This process has been tested successfully on 17.12 but is known to fail on 17.8
-
-#### Open the project
-
-- Open Visual Studio
-- Select `Clone a repository`
-- Specify the repository address: `https://github.com/f3d-app/f3d.git` and the local folder of your choice
-
-> [!WARNING]
-> The first run will take a while (up to 1 hour) because VTK is compiled twice (in Debug and Release mode)
-
-#### Build and the project
-
-- When the configuration is finished, press `F7` to compile F3D
-- Select `f3d.exe (bin\f3d.exe)` in the list of the target and to run F3D
-
-### Command line only
-
-The following steps were tested with Visual Studio Community 2022 using Windows 10, but should also work for other versions.
-This guide is relying a lot on terminal command in order to provide easy steps to follow.
-Alternatively, it is possible to use Visual Studio directly by opening .sln files and building in Visual Studio.
-
-#### Install Dependencies
-
-- Download and install [git bash for windows][gitforwindows]
-- Download and install [cmake][cmake-download]
-- Download and install [Visual Studio 2022 Community Edition][visual-studio]
-
-#### Retrieve VTK and F3D sources
-
-- Open git bash
-
-```sh
-cd C:
-mkdir dev
-cd dev
-mkdir vtk
-cd vtk
-git clone --depth 1 --branch v9.5.0 https://gitlab.kitware.com/vtk/vtk.git src
-mkdir build
-mkdir install
-cd ..
-mkdir f3d
-cd f3d
-git clone https://github.com/f3d-app/f3d.git src
-mkdir build
-```
-
-Note: Resulting folder architecture should look like this:
-
-```
-dev
-|-- f3d
-|   |-- build
-|   `-- src
-`-- vtk
-    |-- build
-    |-- install
-    `-- src
-```
-
-#### Build and install VTK in a dedicated folder
-
-- Open x64 Native Tools Command Prompt for VS 2022
-
-```sh
-cd C:\dev\vtk\build
-cmake -DCMAKE_INSTALL_PREFIX=../install ../src
-cmake --build . -j 16 --config Debug
-cmake --install . --config Debug
-```
-
-Note: These commands will take a while.
-
-#### Add VTK libraries to the PATH
-
-- Search for "Environment variables" in Windows menu and open associated control panel
-- Click on "Environment variables.." button
-- Double click on "Path"
-- Click on "New" and write "C:\dev\vtk\install\bin"
-- Press Ok
-- Press Ok
-- Press Ok
-
-#### Build F3D
-
-- Open x64 Native Tools Command Prompt for VS 2022
-
-```sh
-cd C:\dev\f3d\build
-cmake -DVTK_DIR=C:/dev/vtk/install/lib/cmake/vtk-9.5 ../src
-cmake --build . --config Debug
-```
-
-#### Run
-
-- Open a new file manager window
-- Double click on the `C:\dev\f3d\build\bin_Debug\f3d.exe` executable
-
-### Running the test suite (optional)
-
-- Open x64 Native Tools Command Prompt for VS 2022
-
-```
-cd C:\dev\f3d\build
-cmake -DBUILD_TESTING=ON .
-cmake --build . --config Debug
-ctest -C Debug
-```
-
-## macOS
-
-Note: The following steps were tested with macOS Catalina using Xcode 12.4, but should also work for other versions
-
-### Install Xcode
-
-There are different ways to install Xcode, you can use the AppStore or download it
-from apple developer website and then extract and install it.
-
-### Install Homebrew
-
-Open a terminal.
-
-```
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> ~/.zprofile
-eval "$(/opt/homebrew/bin/brew shellenv)"
-```
-
-### Install dependencies
-
-Open a terminal.
-
-```
-brew install git git-lfs cmake curl vtk
-git lfs install
-```
-
-Note: Homebrew is downloading, compiling and installing all vtk dependencies.
-This command may take a very long time.
-
-### Compile F3D
-
-Open a terminal.
-
-```
-mkdir f3d
-cd f3d
-git clone https://github.com/f3d-app/f3d src
-mkdir build
-cd build
-cmake ../src
-make
-```
-
-You can then simply run F3D from the command line:
-
-```
-./bin/f3d.app/Contents/MacOS/f3d
-```
-
-### Running the test suite (optional)
-
-```
-cmake -DBUILD_TESTING=ON ../src
-make
-ctest
-```
-
-[cmake-download]: https://cmake.org/download
-[gitforwindows]: https://gitforwindows.org/
-[visual-studio]: https://visualstudio.microsoft.com/vs/community/
diff -Nru f3d-3.2.0+dfsg/doc/dev/GOVERNANCE.md f3d-3.4.1/doc/dev/GOVERNANCE.md
--- f3d-3.2.0+dfsg/doc/dev/GOVERNANCE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/GOVERNANCE.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,93 +0,0 @@
-# Governance Policy
-
-This document provides the governance policy for F3D (the "Project"). [Every person](./MAINTAINERS_AND_CONTRIBUTORS.md) responsible for its maintenance (the "Maintainers") agrees to this policy and to abide by all Project policies, including the [code of conduct](../../CODE_OF_CONDUCT.md) by adding their name to the [MAINTAINERS_AND_CONTRIBUTORS.md](./MAINTAINERS_AND_CONTRIBUTORS.md) file.
-
-## 1. Roles
-
-This Project includes the following roles.
-
-### 1.1. Maintainers
-
-Maintainers are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are responsible of upholding the [Code of Conduct](../../CODE_OF_CONDUCT.md). Maintainers are also responsible for determining consensus for technical or community matters. The Project may add or remove Maintainers by affirmative vote of 2/3 of all Maintainers. Maintainers are de-facto moderators. Maintainers are part of the f3d-app github organization with a "owner" role and listed in the [dedicated](./MAINTAINERS_AND_CONTRIBUTORS.md) file.
-
-Maintainers is an important role and as such, will be evaluated by the community yearly and exit interview may be conducted in case of a Maintainer leaving the role.
-
-Any Returning Contributor interested to become a Maintainer should just reach out to existing Maintainers for consideration.
-
-### 1.1. Moderators
-
-Moderators are responsible of upholding the [Code of Conduct](../../CODE_OF_CONDUCT.md). Moderators are responsible for determining consensus for community matters. The Project may add or remove Maintainers by affirmative vote of 2/3 of all Maintainers.
-
-Anyone interested to become a moderators should just reach out to existing Maintainers for consideration.
-
-### 1.2. Returning Contributors
-
-Returning Contributors are those who have made multiple contributions to the Project and want to be acknowledged as such. Returning Contributors are also very much welcome to share their ideas and feedbacks about the development and maintenance of F3D. Returning contributors may be asked by others about their contributions, provide reviews on code they are familiar with, provide technical insight during discussions and take part in any related activities they wish to.
-
-Returning Contributors can also work on ["bounty" issues](../../CONTRIBUTING.md#bounties) in order to receive donations.
-To become a Returning Contributor, one need to have merged at least two pull requests into the F3D repositories. Returning Contributors are part of the f3d-app github organization with a "member" role and are listed in the [dedicated](./MAINTAINERS_AND_CONTRIBUTORS.md) file.
-
-Usually Maintainers will reach out to potential Returning Contributors but in case it was missed, one should reach out to Maintainers for consideration.
-
-Maintainers may decide to list a returning contributor as inactive after a year without contributions, in that case they will be removed from the f3d-app github organization until active again.
-
-### 1.3. Contributors
-
-Contributors are those who have made or want to make contributions to the Project.
-
-### 1.4. Mentees
-
-Mentees are contributors that are enrolled in the mentoring program of F3D. It is open to anyone willing to learn more about open source contribution to F3D
-and be helped by Maintainers and Contributors on [discord](https://discord.f3d.app).
-
-### 1.5. Sponsor
-
-Sponsor are users that are currently sponsoring the project. They are acknowledged as such in each release and on [discord](https://discord.f3d.app). Sponsoring can be done either one-time or monthly. One-time sponsor will only be considered sponsor until the next release. See the detail on our [sponsor page](https://github.com/sponsors/f3d-app).
-
-### 1.6 Industry sponsor
-
-Industry sponsor are company that are currently sponsoring the project. They are acknowledged as such in each release and in the [README.md](../../README.md) of the project with an appropriate logo. Potential industry sponsor should reach out to maintainers.
-
-## 2. Decisions
-
-### 2.1 Consensus-Based Decision Making
-
-The Project makes decisions through the consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of several factors, including the dominant view of the Contributors and the nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements using issues comments.
-
-### 2.2. Appeal Process
-
-Decisions may be appealed by opening an issue and that appeal will be considered by the Maintainers in good faith, who will respond in writing within a reasonable time.
-
-## 3. How We Work
-
-### 3.1. Openness
-
-Participation is open to anyone who is directly and materially affected by the activity in question. There shall be no undue financial barriers to participation.
-
-### 3.2. Balance
-
-The development process should balance the interests of Contributors and other stakeholders. Contributors from diverse interest categories shall be sought with the objective of achieving balance.
-
-### 3.3. Coordination and Harmonization
-
-Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in the Project.
-
-### 3.4. Consideration of Views and Objections
-
-Prompt consideration shall be given to the written views and objections of all Contributors.
-
-### 3.5. Written procedures
-
-This governance document and other materials documenting the Project development process shall be available to any interested person.
-
-## 4. No Confidentiality
-
-Information disclosed in connection with any Project activity, including but not limited to meetings, contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary.
-
-## 5. Amendments
-
-Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers.
-
----
-
-Modified from of MVG-0.1-beta GOVERNANCE.md. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff -Nru f3d-3.2.0+dfsg/doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md f3d-3.4.1/doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md
--- f3d-3.2.0+dfsg/doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/MAINTAINERS_AND_CONTRIBUTORS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,32 +0,0 @@
-# Maintainers and Contributors
-
-This document lists the Maintainers and Returning Contributors of the Project.
-
-## Maintainers
-
-Maintainers may add themselves once approved by the existing Maintainers as described in the [Governance document](./GOVERNANCE.md). By adding your name to this list you are agreeing to abide by and uphold the Project governance documents and to abide by all of the Organization's policies, including the [code of conduct](../../CODE_OF_CONDUCT.md).
-
-| **Name/Alias**   | **Handle** |
-| ---------------- | ---------- |
-| Mathieu Westphal | mwestphal  |
-| Michael Migliore | Meakk      |
-
-## Returning Contributors
-
-Returning Contributors should add themselves once [requirements](./GOVERNANCE.md#12-returning-contributors) have been checked by existing Maintainers.
-Maintainers may decide to list a returning contributor as inactive after a year without contributions.
-Contributors with a single contribution or that may not want to are not listed here but can be found in the git history.
-
-| **Name/Alias**                  | **Handle**     | **Active** |
-| ------------------------------- | -------------- | ---------- |
-| jrushlow                        | 19jrushlow     | Yes        |
-| ssmncrf                         | samoncrief     | Yes        |
-| Evgenii Startcev (PhilHellmuth) | exbluesbreaker | Yes        |
-| Louis Gombert                   | lgt2x          | Yes        |
-| Artem                           | 0xfedcafe      | Yes        |
-| Nig3l                           | Ni-g-3l        | Yes        |
-| Luke                            | lknknm         | Yes        |
-
----
-
-Modified from MVG-0.1-beta MAINTAINERS.md. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff -Nru f3d-3.2.0+dfsg/doc/dev/README_DEV.md f3d-3.4.1/doc/dev/README_DEV.md
--- f3d-3.2.0+dfsg/doc/dev/README_DEV.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/README_DEV.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,14 +0,0 @@
-# Developer Documentation
-
-- [How to get started with building F3D.](GETTING_STARTED.md)
-- [How to build F3D.](BUILD.md)
-- [How to test F3D.](TESTING.md)
-- [How to contribute to F3D.](../../CONTRIBUTING.md)
-- [How to Generate coverage and sanitizer report.](GENERATE.md)
-- [Overview of the architecture of F3D.](ARCHITECTURE.md)
-- [Coding Style.](CODING_STYLE.md)
-- [Roadmaps and release cycle.](ROADMAPS_AND_RELEASES.md)
-- [Governance.](GOVERNANCE.md)
-- [Maintainers and Contributors.](MAINTAINERS_AND_CONTRIBUTORS.md)
-- [Code of conduct.](../../CODE_OF_CONDUCT.md)
-- [How to build F3D for webassembly.](BUILD_WASM.md)
diff -Nru f3d-3.2.0+dfsg/doc/dev/ROADMAPS_AND_RELEASES.md f3d-3.4.1/doc/dev/ROADMAPS_AND_RELEASES.md
--- f3d-3.2.0+dfsg/doc/dev/ROADMAPS_AND_RELEASES.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/ROADMAPS_AND_RELEASES.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,24 +0,0 @@
-# Roadmaps and release cycles
-
-F3D is a community driven project and we try to provide as much information as we can about the upcoming features
-and release expectations. Please keep in mind while we may try to fulfill what was planned, some features may not be available
-as planned and releases can be delayed.
-
-Roadmaps are available on their dedicated issue with their associated project board and milestone.
-The first draft of the roadmap of each release will be discussed publicly on [discord](https://discord.f3d.app), then they are bound to evolve over time
-while the development happens.
-
-If you want to get involved, please feel free to join the [discord](https://discord.f3d.app), comment on the issues and contribute to the issues that matters to you!
-
-## 3.2.0
-
-Out next minor release is planned for July 2025
-
-- [Issue](https://github.com/f3d-app/f3d/issues/2157)
-- [Milestone](https://github.com/f3d-app/f3d/milestone/12)
-- [Board](https://github.com/orgs/f3d-app/projects/2)
-
-## Release cycle
-
-F3D release cycle is one minor release every three months, in January, April, July and October.
-A minor release may be skipped before a major release.
diff -Nru f3d-3.2.0+dfsg/doc/dev/TESTING.md f3d-3.4.1/doc/dev/TESTING.md
--- f3d-3.2.0+dfsg/doc/dev/TESTING.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/dev/TESTING.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,161 +0,0 @@
-# Testing
-
-F3D has an extensive suite of tests that can be run locally, either to validate your build or because you are contributing to F3D and want to add/modify a test.
-
-## CMake Options
-
-There are a few CMake options to configure the F3D testing framework:
-
-- `BUILD_TESTING`: Enable the test framework, off by default. Requires [git LFS](https://git-lfs.com/) if repository is cloned.
-- `F3D_TESTING_ENABLE_RENDERING_TESTS`: An option to enable/disable test that require rendering capabilities, on by default.
-- `F3D_TESTING_ENABLE_LONG_TIMEOUT_TESTS`: Certain tests can take some time to run, off by default, requires rendering tests.
-- `F3D_TESTING_FORCE_RENDERING_BACKEND`: Configure the rendering backend to use. Can be `auto` (default), `glx`, `wgl`, `egl` or `osmesa`.
-- `F3D_TESTING_ENABLE_GLX_TESTS`: Enable tests requiring a X11 server running on Linux.
-- `F3D_TESTING_ENABLE_OSMESA_TESTS`: Enable tests requiring OSMesa dependency.
-- `F3D_TESTING_ENABLE_EGL_TESTS`: Enable tests requiring EGL dependency.
-- `F3D_TESTING_ENABLE_EXTERNAL_GLFW`: Enable libf3d tests requiring GLFW dependency.
-- `F3D_TESTING_ENABLE_EXTERNAL_QT`: Enable libf3d tests requiring QT dependency.
-
-## Running the tests
-
-To run all tests, build and then run `ctest` from the build directory:
-
-```
-cmake --build .
-ctest
-```
-
-To run a specific test, use the `ctest -R <testname>` option:
-
-```
-ctest -R PLY
-```
-
-## Testing architecture
-
-There are multiple layers of tests to ensure that testing covers all aspects of the application. The layers of the application are
-
-- Application layer
-- Library layer
-- Bindings layer
-- VTK Extension layer
-
-When contributing to F3D, it is necessary that new code is covered by a test in at least one layer above. Additional tests can be created in other layers, depending on the change.
-
-### Application layer
-
-All application tests are initiated via the `CTest` (https://cmake.org/cmake/help/book/mastering-cmake/chapter/Testing%20With%20CMake%20and%20CTest.html) testing framework.
-`CTest` runs `f3d` with the `--output` and `--reference arguments which renders an output image and compares it to a reference image. F3D compares the differences between the 2 images and compute a difference value and compare it to a threshold to check if a test passes or not.
-
-All aspects of the application test framework are handled in `application/testing/CMakeLists.txt`.
-
-Usually, adding a test is as simple as adding a line like this one:
-
-```
-f3d_test(NAME TestName DATA datafile.ext ARGS --args-to-test)
-```
-
-where
-
-- `NAME` should be the name of the test, which must be unique
-- `DATA` should be a file in `testing/data` directory, though adding a new file is possible
-- `ARGS` should be the F3D options to pass to the f3d executable, if any
-
-Once the new test has been added, configure and build F3D, then run the test (`-VV` for verbose output):
-
-```
-ctest -R TestName -VV
-```
-
-The test will run and fail but an image output will be generated in the build directory, namely `Testing/Temporary/TestName.png`.
-Visually check that the generated file looks as expected, then add it to the F3D sources in `testing/baselines`.
-Rerun the test, it should now pass.
-
-There is many other keywords in the `f3d_test` macro, here is a non exhaustive list:
-
-- `TONE_MAPPING`: Tests that uses tone mapping so they can be disabled with old VTK version
-- `LONG_TIMEOUT`: Tests that takes a long time to run, so they can be disabled on weaker CI machines
-- `INTERACTION`: Interaction test, see below
-- `INTERACTION_CONFIGURE`: A special kind of interaction tests that require configuring the interaction log using CMake, eg drag and drop tests.
-- `NO_BASELINE`: Test that have no baselines and do not perform a rendering comparison at the end, usually combined with `REGEXP`
-- `NO_RENDER`: Tests that do not require rendering capability at all, usually combined with `REGEXP`
-- `NO_OUTPUT`: Tests that do not have an automatic `--output`, may still use `BASELINE` or define `--output` manually
-- `WILL_FAIL`: Tests that should fail in order to pass
-- `NO_DATA_FORCE_RENDER`: Tests that do not open any data yet require a rendering tests at the end, rely on an environment variable
-- `UI`: Tests that show the ImGui UI, hence require it to be present in order to be enabled
-
-### Recovering baselines from CI
-
-Occasionally you may need to recover a baseline from the CI. If this is required, create a PR and let the test run and fail on CI. Check the actions run summary on Github and download the appropriate `baseline` archive. Extract the archive and navigate to the `build/Testing/Temporary/TestName.png`. Visually check that the generated file looks as expected, then add it to the F3D sources in `testing/baselines`.
-
-### Creating Interaction tests
-
-Sometimes you may contribute changes that affect how the end user interacts with F3D, example, toggling orthographic projection on/off or zooming in/out of the rendered image. These human interactions are simulated by interaction tests. F3D has the functionality to record human interactions such as mouse wheel scrolls, mouse movements as well as keypresses to a file. This functionality is enabled by running
-
-```
-f3d --interaction-test-record ./TestName.log
-```
-
-where
-
-- `TestName` should be the name of the test case.
-
-While recording the interaction, care should be taken to perform the minimum number of events to simulate the interaction. For example, if you want to simulate the number `5` being pressed, there should be no mouse events or other keypress events during the recording of the interaction. The interaction file is saved when `f3d` is exited.
-
-Another interaction to avoid the one that quit F3D as it will have unexpected behavior when being replayed. Instead, quit the F3D application by using your window manager or Ctrl+C in the terminal.
-
-You can verify that your interaction file is correct by playing back the interaction
-
-```
-f3d --interaction-test-play ./TestName.log
-```
-
-Interactions that are not needed for the test can be removed manually. After verifying that the interaction file works as expected, copy it to `./testing/recordings`. The name of the interaction file should be the same as the test name. The interaction test case can then be created by adding
-
-```
-f3d_test(NAME TestName DATA datafile.ext INTERACTION)
-```
-
-to `application/testing/CMakeLists.txt`
-
-where
-
-- `INTERACTION` signifies that this is an interaction test
-
-The steps to running the test are the same as above.
-
-### Library layer
-
-When for some reason adding a test in the application layer is not possible, it is possible
-to add a C++ test in the library layer. These tests are simple C++ methods that should return
-`EXIT_SUCCESS` or `EXIT_FAILURE`.
-
-Library test cases are handled in `library/testing`.
-
-To add a test, create a new `TestSDKName.cxx` file containing a `int TestSDKName(int argc, char* argv[])` method,
-then implement your test in C++ using the [libf3d](../libf3d/README.md) API.
-Then add you new file to `src/library/testing/CMakeLists.txt`.
-
-It is supported to read file as input and perform image comparison against baselines as an output, see other tests as examples.
-
-### Bindings layer
-
-The libf3d supports multiple bindings, including Python, Java and Javascript.
-
-When improving/modifying these bindings, it is necessary to also improve/modify the bindings tests accordingly.
-Please take a look into `<java/python/webassembly>/testing` for examples to follow.
-
-### VTKExtensions layer
-
-When for some reason adding a test in the application or library layer is not possible, it is possible
-to add a C++ test in the VTKExtensions layer. These tests are simple C++ methods that should return
-`EXIT_SUCESS` or `EXIT_FAILURE`.
-
-Everything is handled in `library/VTKExtensions/ModuleName/Testing`.
-
-To add a test, first identify which VTKExtensions module you need to add a test into,
-then create a new `TestName.cxx` file containing a `int TestName(int argc, char* argv[])` method,
-then implement your test in C++ using VTK and F3D VTKExtensions modules.
-Then add you new file to `library/VTKExtensions/ModuleName/Testing/CMakeLists.txt`.
-
-It is supported to read file as input if needed, see other tests as examples.
diff -Nru f3d-3.2.0+dfsg/doc/GALLERY.md f3d-3.4.1/doc/GALLERY.md
--- f3d-3.2.0+dfsg/doc/GALLERY.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/GALLERY.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,74 +0,0 @@
-# Gallery
-
-Examples renderings with their associated command lines.
-Images and videos displayed below use public datasets, you can download them [here](https://drive.google.com/file/d/1hYGAHr67T7tBrUKFrCF_uc_jnvJaQF-A/view?usp=sharing).
-
-<video src='https://user-images.githubusercontent.com/3129530/194735274-395839e9-e2cb-4a47-83b9-5c6987640032.webm' autoplay="autoplay" loop="loop" width="700"></video>
-
-_Animated realistic rendering_: `f3d gearbox/scene.gltf --hdri-file=future_parking_2k.hdr -fjuqxtgas`
-
-<video src='https://user-images.githubusercontent.com/3129530/194734947-f34bc377-8ee4-472a-b130-30ce9e86facf.webm' autoplay="autoplay" loop="loop" width="700"></video>
-
-_Animated, skinned and morphed rendering_: `f3d dota/scene.gltf --hdri-file=future_parking_2k.hdr -fjuqxtgas`
-
-<video src='https://user-images.githubusercontent.com/3129530/194735269-b62216e6-33da-484c-9f0d-39befc8253bf.webm' autoplay="autoplay" loop="loop" width="700"></video>
-
-_Animated scientific visualization rendering_: `f3d can.ex2 -xtgans --up=+Z --scalar-coloring --coloring-array=VEL --animation-speed-factor=0.005`
-
-<video src='https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/counter.webm' autoplay="autoplay" loop="loop" width="700"></video>
-
-_3D Gaussians Splatting_: `f3d counter.splat --point-sprites-size=1 --point-sprites-type=gaussian -soynxz --up=-Y --camera-position=0,1,-5.2 --camera-focal-point=0,1,0`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735272-5bcd3e7c-a333-41f5-8066-9b0bec9885e8.png" width="700">
-
-_Direct scalars rendering of a point cloud_: `f3d Carola_PointCloud.ply --point-size=0 --coloring-component=-2 -sofj --coloring-array=RGBA --up=+Z --hdri-file=venice_sunset_8k.hdr`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735378-b8be4df2-e030-4d13-b12f-24806cbb385b.png" width="700">
-
-_Raytraced CAD assembly_: `f3d 202.vtp -xtgans -rd --raytracing-samples=10 --coloring-range=-2,9`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735377-127fa5d8-ece5-40c7-8176-672279ebdacc.png" width="700">
-
-_Volume rendering of a security bag scan_: `f3d backpack.vti -vmn --coloring-range=300,1000 --colormap=0,#000000,1,#ffffff`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735376-3a476643-00d6-4cfc-9a88-e0dd33658564.png" width="700">
-
-_Realistic rendering #1_: `f3d DamagedHelmet.glb --hdri-file=lebombo_4k.hdr -fjtuqap`
-
-<img src="https://media.githubusercontent.com/media/f3d-app/f3d-media/95b76c22d927bb24759bbe0246b6260121f2933b/media/typical.png" width="700">
-
-_Showcase of interactive widgets_: `f3d dragon.vtu -xtganse --coloring-component=0`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735370-4d1f067c-704c-4118-9d94-c1adf4039c39.png" width="700">
-
-_Metallic Rendering of a STEP file_: `f3d eta_asm.stp --hdri-file=future_parking_2k.hdr -fjuqxtga --up=+Z --metallic=1 --roughness=0.6 --color=0.98,0.90,0.59 --scalar-coloring=false`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735366-e79a8d45-8b76-4a65-9445-4a0ca1b63876.png" width="700">
-
-_Rendering of a FBX file_: `f3d zeldaPosed001.fbx --hdri-file=hikers_cave_2k.hdr -fjuqxtga --light-intensity=1.5`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735363-d2be23bb-90b6-4e90-941d-14b6dc24b476.png" width="700">
-
-_Realistic rendering #2_: `f3d FlightHelmet.glb --hdri-file=lebombo_4k.hdr -fjtuqap`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735361-7e827d8e-531a-4938-b608-18825a9574f8.png" width="700">
-
-_Visualization of a CFD velocity field_: `f3d single-pin.vtp -xtbgans --coloring-range=-2,8 --colormap=0,0.3,0.7,0,0.7,0,0.1,1,1,0.8,0.8,0`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735354-486999dd-3c57-4ab1-a9ad-b8e97bac0da0.png" width="700">
-
-_Volume rendering of a medical skull scan_: `f3d skull.vti -vxbt --coloring-range=40,200 --camera-direction=1,0,0`
-
-<img src="https://user-images.githubusercontent.com/3129530/194735348-7dd14922-f546-4f54-8bc2-b6602617c4cc.png" width="700">
-
-_Point cloud rendering using sprites_: `f3d pointCloud.vtp -o --point-size=0.2 --colormap=0,0,0.8,0,0.4,0.9,0,0,0.8,0.9,0.9,0,1,0.5,0.5,1 --up=+Z`
-
-## Acknowledgments
-
-- Bristleback DOTA Fan-Art by [Nikolay_Tsys](https://sketchfab.com/Tolst).
-- SY Carola (point cloud) by [Scottish Maritime Museum](https://sketchfab.com/ScottishMaritimeMuseum)
-- Gearbox Animation by [DZHUSI ØNE](https://sketchfab.com/dzhusione)
-- Watch movement by [Greg Brown](https://grabcad.com/greg.brown)
-- Zelda - Breath Of The Wild by [theStoff](https://sketchfab.com/theStoff)
-- Venice Sunset HDRI and Hiker’s Cave HDRI by [Greg Zaal](https://polyhaven.com/hdris?a=Greg%20Zaal)
-- Future Parking HDRI by [Sergej Majboroda](https://polyhaven.com/hdris?a=Sergej%20Majboroda)
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/01-OVERVIEW.md f3d-3.4.1/doc/libf3d/01-OVERVIEW.md
--- f3d-3.2.0+dfsg/doc/libf3d/01-OVERVIEW.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/libf3d/01-OVERVIEW.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,151 @@
+# libf3d
+
+libf3d is a BSD-licensed C++17 library to open and render 3D meshes. It is of course used by F3D.
+libf3d API is simple and easy to learn. Python bindings are provided through pybind11. Java bindings are also available.
+
+## Getting Started
+
+Rendering a file and starting the interaction is very easy:
+
+```cpp
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/scene.h>
+
+// Load VTK native readers
+f3d::engine::autoloadPlugins();
+
+// Create a f3d::engine
+f3d::engine eng = f3d::engine::create();
+
+// Add a file into a scene
+eng.getScene().add("path/to/file.ext");
+
+// Start rendering and interacting
+eng.getInteractor().start();
+```
+
+As well as loading multiple files:
+
+```cpp
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/scene.h>
+
+// Load VTK native readers
+f3d::engine::autoloadPlugins();
+
+// Create a f3d::engine
+f3d::engine eng = f3d::engine::create();
+
+// Load multiples geometries
+eng.getScene().add({"path/to/file.ext", "path/to/file2.ext"});
+
+// Start rendering and interacting
+eng.getInteractor().start();
+```
+
+It's also possible to load a geometry from memory buffers:
+
+```cpp
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/scene.h>
+
+// Create a f3d::engine
+f3d::engine eng = f3d::engine::create();
+
+// Create a single triangle
+f3d::mesh_t mesh = {};
+mesh.points = { 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f };
+mesh.face_sides = { 3 };
+mesh.face_indices = { 0, 1, 2 };
+eng.getScene().add(mesh);
+
+// Start rendering and interacting
+eng.getInteractor().start();
+```
+
+Manipulating the window directly can be done this way:
+
+```cpp
+#include <f3d/engine.h>
+#include <f3d/image.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+// Load VTK native readers
+f3d::engine::autoloadPlugins();
+
+// Create a f3d::engine with a offscreen window
+f3d::engine eng = f3d::engine::create(true);
+
+// Load a geometry
+eng.getScene().add("path/to/file.ext");
+
+// Set the window size and render to an image
+f3d::image img = eng.getWindow().setSize(300, 300).renderToImage();
+
+// Save the image to a file
+img.save("/path/to/img.png");
+```
+
+Changing some options can be done this way:
+
+```cpp
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+
+// Load VTK native readers
+f3d::engine::autoloadPlugins();
+
+// Create a f3d::engine
+f3d::engine eng = f3d::engine::create();
+
+// Recover the options and set the wanted value
+f3d::options& opt = eng.getOptions();
+opt.render.effect.ambient_occlusion = true;
+opt.render.effect.antialiasing.enable = true;
+
+// Standard libf3d usage
+eng.getScene().add("path/to/file.ext");
+eng.getInteractor().start();
+```
+
+Most options are dynamic, some are only taken into account when loading a file. See the [options](03-OPTIONS.md) documentation.
+
+Find more examples in the [examples directory](https://github.com/f3d-app/f3d/tree/master/examples),
+you can also find other usages in the [testing directory](https://github.com/f3d-app/f3d/tree/master/library/testing).
+
+For the complete documentation, please consult the [libf3d doxygen documentation.](https://f3d.app/docs/next/category/libf3d-api-reference).
+
+## Building against the libf3d
+
+> [!NOTE]
+> `libf3d` works best when built as a shared library, make sure it's the case. The `sdk` component must also be installed in order to be able to use CMake to find `libf3d`.
+> eg: `cmake --install <build_dir> --component sdk`
+
+It's as simple as linking against it like this in your CMakeLists.txt:
+
+```cmake
+find_package(f3d REQUIRED COMPONENTS library)
+[...]
+target_link_libraries(your_target f3d::libf3d)
+```
+
+## find_package COMPONENTS
+
+When using `find_package(f3d)` and `sdk` CMake component have been installed, you can require specific components:
+
+- `application`: access to f3d::f3d target
+- `library`: access to f3d::libf3d target and include dir
+- `plugin_sdk`: access to CMake macros to create plugins, the f3d::vtkext target and include dir
+
+see the installed `f3dConfig.cmake` file for more info.
+
+## Extending libf3d with plugins
+
+In order to add new supported file format to libf3d, you can create a plugin using CMake macros. Please follow instructions in the [F3D plugin SDK guide](05-PLUGINS.md).
+Also make sure the `plugin_sdk` component have been installed as well as the `sdk` component.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/02-CLASSES.md f3d-3.4.1/doc/libf3d/02-CLASSES.md
--- f3d-3.2.0+dfsg/doc/libf3d/02-CLASSES.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/libf3d/02-CLASSES.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,82 @@
+# Classes Introduction
+
+This is only an introduction to libf3d classes.
+For the complete documentation, please consult the [libf3d doxygen documentation.](https://f3d.app/docs/next/category/libf3d-api-reference).
+
+## Engine class
+
+The engine class is the main class that needs to be instantiated. All other classes instance are provided by the engine using getters, `getScene`, `getWindow`, `getInteractor`, `getOptions`.
+
+The engine factory lets you choose between the different types of OpenGL rendering backend.
+The generic `create()` is recommended in most cases and will use the best context possible available on your system.
+However, it's possible to force the rendering backend in some specific use cases:
+
+- `createGLX()`: force usage of GLX backend, works on Linux only and requires a X11 server to run.
+- `createWGL()`: force usage of WGL native backend on Windows.
+- `createEGL()`: force usage of EGL backend, recommended when doing offscreen rendering with a GPU available. Requires EGL library available. No interactor provided.
+- `createOSMesa()`: force usage of OSMesa backend (software rendering), recommended when doing offscreen rendering without any GPU. Requires OSMesa library available. No interactor provided.
+- `createNone()`: do not use any rendering. Useful to retrieve metadata only.
+- `createExternal()`: the user is responsible of the rendering stack. It lets the user integrate libf3d in other frameworks like Qt or GLFW. No interactor provided. See [Context](#context-class) documentation for more info.
+  An additional boolean argument is available to specify if offscreen rendering is requested when relevant on the selected rendering backend.
+
+A static function `loadPlugin` can also be called to load reader plugins. It must be called before loading any file. An internal plugin containing VTK native readers can be loaded by calling `f3d::engine::loadPlugin("native");`. Other plugins maintained by F3D team are available if their build is enabled: `alembic`, `assimp`, `draco`, `hdf`, `occt` and `usd`.
+If CMake option `F3D_PLUGINS_STATIC_BUILD` is enabled, the plugins listed above are also static just like `native` plugin.
+All static plugins can be loaded using `f3d::engine::autoloadPlugins()`.
+
+## Scene class
+
+The scene class is responsible to `add` file from the disk into the scene. It supports reading multiple files at the same time and even mesh from memory.
+It is possible to `clear` the scene and to check if the scene `supports` a file.
+
+## Context class
+
+Convenience class providing generic context API when using a external rendering backend (using `f3d::engine::createExternal()` factory).
+
+## Window class
+
+The window class is responsible for rendering the data.
+Window lets you `render`, `renderToImage` and control other parameters of the window, like icon or windowName.
+
+## Interactor class
+
+When provided by the engine, the interactor class lets you choose how to interact with the data.
+
+It contains the animation API to start and stop animation as well as recover the number of available animations and their names.
+
+Interactor lets you add, remove and trigger your [commands](../user/07-COMMANDS.md).
+The commands let you interact with the libf3d in a very flexible manner.
+
+Interactor also lets you add and remove bindings in order to modify how
+the libf3d react to different interactions, eg. when a key is pressed or when a file is dropped.
+The libf3d has [default bindings](../user/04-INTERACTIONS.md) but this API lets you defined your own
+by specifying [binds](../user/06-CONFIGURATION_FILE.md#bindings) and commands.
+This API also lets you control the content of the cheatsheet.
+
+Use `log::setVerboseLevel(log::VerboseLevel::DEBUG)` to print debug information on interaction and command use.
+
+Of course, you can use `start` and `stop` to control the interactor behavior.
+`start` lets you specify time for the event loop and a `std::function` to execute at each loop.
+
+## Camera class
+
+Provided by the window, this class lets you control the camera. You can either specify the camera position, target, and up direction directly, or specify movements relative to the current camera state.
+
+## Image class
+
+A generic image class that can either be created from a window, from an image filepath or even from a data buffer. It supports comparison making it very practical in testing context.
+
+## Log class
+
+A class to control logging in the libf3d. Simple using the different dedicated methods (`print`, `debug`, `info`, `warn`, `error`) and `setVerboseLevel`, you can easily control what to display. Please note that, on windows, a dedicated output window may be created.
+
+## Options class
+
+This class lets you control the behavior of the libf3d. An option is basically a value that can be a optional or not. There is different API to access it, see the exhaustive [doc](03-OPTIONS.md).
+
+## Reader class
+
+This class is used internally by the [plugin SDK](05-PLUGINS.md) and is not intended to be used directly.
+
+## Plugin class
+
+This class is used internally by the [plugin SDK](05-PLUGINS.md) and is not intended to be used directly.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/03-OPTIONS.md f3d-3.4.1/doc/libf3d/03-OPTIONS.md
--- f3d-3.2.0+dfsg/doc/libf3d/03-OPTIONS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/libf3d/03-OPTIONS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,623 @@
+# Options
+
+An option is a specific value stored in different struct in an `options` instance.
+They can be accessed directly through the structs, through a string API or through a std::variant API.
+
+The possible option are listed below and are organized by categories and subcategories, here is a non-exhaustive explanation of the categories.
+
+- `scene` options are related to how the scene is being displayed
+- `render` options are related to the way the render is done
+- `render.effect` options are related to specific techniques used that modify the render
+- `ui` options are related to the screenspace UI element displayed
+- `model` options are related to modifications on the model
+- `interactor` options requires an interactor to be present to have any effect
+
+Please note certain options are taken into account when rendering, others when loading a file.
+See the exhaustive list below.
+
+Please note certain options are `optional` by default, which means they are not set initially,
+See the [APIs](#struct-api) details below for more info.
+
+## Scene Options
+
+### `scene.animation.autoplay` (_bool_, default: `false`, **on load**)
+
+Automatically start animation.
+
+CLI: `--animation-autoplay`.
+
+### `scene.animation.indices` (_vector\<int\>_, default: `0`, **on load**)
+
+Select the animations to load.
+Any negative value means all animations.
+The default scene always has at most one animation.
+
+CLI: `--animation-indices`.
+
+### `scene.animation.speed_factor` (_ratio_, default: `1`)
+
+Set the animation speed factor to slow, speed up or even invert animation.
+
+CLI: `--animation-speed-factor`.
+
+### `scene.animation.time` (_double_, optional, **on load**)
+
+Set the animation time to load.
+
+CLI: `--animation-time`.
+
+### `scene.camera.index` (_int_, optional, **on load**)
+
+Select the scene camera to use when available in the file.
+The default scene always uses automatic camera.
+
+CLI: `--camera-index`.
+
+### `scene.up_direction` (_direction_, default: `+Y`)
+
+Define the Up direction. It impacts the grid, the axis, the HDRI and the camera.
+
+CLI: `--up`.
+
+### `scene.force_reader` (_string_, optional, **on load**)
+
+Force a specific reader to be used, disregarding the file extension. See [user documentation](../user/02-SUPPORTED_FORMATS.md).
+
+CLI: `--force-reader`.
+
+### `scene.camera.orthographic` (_bool_, optional)
+
+Set to true to force orthographic projection. Model-specified by default, which is false if not specified.
+
+CLI: `--camera-orthographic`.
+
+## Interactor Options
+
+### `interactor.trackball` (_bool_, default: `false`)
+
+Enable trackball interaction.
+
+CLI: `--trackball`.
+
+### `interactor.invert_zoom` (_bool_, default: `false`)
+
+Invert the zoom direction.
+
+CLI: `--invert-zoom`.
+
+## Model Options
+
+### `model.matcap.texture` (_path_, optional)
+
+Path to a texture file containing a material capture. All other model options for surfaces are ignored if this is set. Model-specified by default.
+
+CLI: `--texture-matcap`.
+
+### `model.color.opacity` (_double_, optional)
+
+Set _opacity_ on the geometry. Usually used with Depth Peeling option. Multiplied with the `model.color.texture` when present. Model-specified by default.
+
+CLI: `--opacity`.
+
+### `model.color.rgb` (_color_, optional)
+
+Set a _color_ on the geometry. Multiplied with the `model.color.texture` when present. Model-specified by default.
+
+CLI: `--color`.
+
+### `model.color.texture` (_path_, optional)
+
+Path to a texture file that sets the color of the object. Will be multiplied with rgb and opacity. Model-specified by default.
+
+CLI: `--texture-base-color`.
+
+### `model.emissive.factor` (_color_, optional)
+
+Multiply the emissive color when an emissive texture is present. Model-specified by default.
+
+CLI: `--emissive-factor`.
+
+### `model.emissive.texture` (_path_, optional)
+
+Path to a texture file that sets the emitted light of the object. Multiplied with the `model.emissive.factor`. Model-specified by default.
+
+CLI: `--texture-emissive`.
+
+### `model.material.metallic` (_double_, optional)
+
+Set the _metallic coefficient_ on the geometry (between `0.0` and `1.0`). Multiplied with the `model.material.texture` when present. Model-specified by default.
+
+CLI: `--metallic`.
+
+### `model.material.roughness` (_double_, optional)
+
+Set the _roughness coefficient_ on the geometry (between `0.0` and `1.0`). Multiplied with the `model.material.texture` when present. Model-specified by default.
+
+CLI: `--roughness`.
+
+### `model.material.base_ior` (_double_, optional)
+
+Set the _index of refraction of the base layer_ (between `1.0` and `2.5`). Model-specified by default.
+
+CLI: `--base-ior`.
+
+### `model.material.texture` (_path_, optional)
+
+Path to a texture file that sets the Occlusion, Roughness and Metallic values of the object. Multiplied with the `model.material.roughness` and `model.material.metallic`, set both of them to `1.0` to get a true result. Model-specified by default.
+
+CLI: `--texture-material`.
+
+### `model.normal.scale` (_double_, optional)
+
+Normal scale affects the strength of the normal deviation from the normal texture. Model-specified by default.
+
+CLI: `--normal-scale`.
+
+### `model.normal.texture` (_path_, optional)
+
+Path to a texture file that sets the normal map of the object. Model-specified by default.
+
+CLI: `--texture-normal`.
+
+### `model.scivis.enable` (_bool_, default: `false`)
+
+_Color by an array_ present in on the data. If `model.scivis.array_name` is not set, the first available array will be used.
+
+CLI: `--scalar-coloring`.
+
+### `model.scivis.cells` (_bool_, default: `false`)
+
+Color the data with value found _on the cells_ instead of points
+
+CLI: `--cells`.
+
+### `model.scivis.discretization` (_int_, optional)
+
+Set how many distinct colors will be used from the colormap. Should be `>=1`.
+
+CLI: `--colormap-discretization`.
+
+### `model.scivis.colormap` (_colormap_, default: `<inferno>`)
+
+Set a _custom colormap for the coloring_.See [colormap parsing](../user/08-PARSING.md#colormap) for details.
+
+CLI: `--colormap`.
+
+### `model.scivis.component` (_int_, default: `-1`)
+
+Specify the component to color with. `-1` means _magnitude_. `-2` means _direct values_.
+
+CLI: `--comp`.
+
+### `model.scivis.array_name` (_string_, optional)
+
+Select the name of the array to color with.
+
+CLI: `--coloring-array`.
+
+### `model.scivis.range` (_vector\<double\>_, optional)
+
+Set the _coloring range_. Automatically computed by default.
+
+CLI: `--range`.
+
+### `model.point_sprites.enable` (_bool_, default: `false`)
+
+Show sphere _points sprites_ instead of the geometry.
+
+CLI: `--point-sprites`.
+
+### `model.point_sprites.type` (_string_, default: `sphere`)
+
+Set the sprites type when showing point sprites (can be `sphere` or `gaussian`).
+
+CLI: `--point-sprites`.
+
+### `model.point_sprites.size` (_double_, default: `10.0`)
+
+Set the _size_ of point sprites.
+
+CLI: `--point-sprites-size`.
+
+### `model.point_sprites.absolute_size` (_bool_, default: `false`)
+
+Do not scale the point sprites size by the scene bounding box.
+
+CLI: `--point-sprites-absolute-size`.
+
+### `model.volume.enable` (_bool_, default: `false`)
+
+Enable _volume rendering_. It is only available for 3D image data and will display nothing with incompatible data. It forces coloring.
+
+CLI: `--volume`.
+
+### `model.volume.inverse` (_bool_, default: `false`)
+
+Inverse the linear opacity function.
+
+CLI: `--inverse`.
+
+### `model.textures_transform` (_transform2d_, optional)
+
+Transform applied to textures on the model. If a default transform is set by the importer, the default value will be multiplied by this transform.
+
+CLI: `--textures-transform`.
+
+## Render Options
+
+### `render.effect.blending.enable` (_bool_, default: `false`)
+
+Enable _blending support_. This is a technique used to correctly render translucent objects.
+
+CLI: `--blending`.
+
+### `render.effect.blending.mode` (_string_, default: `ddp`)
+
+Set the blending technique. Valid options are: `ddp` (dual depth peeling, quality), `sort` (only for gaussians), `stochastic` (fast)
+
+CLI: `--blending`.
+
+### `render.effect.antialiasing.enable` (_string_, default: `false`)
+
+Enable _anti-aliasing_. This technique is used to reduce aliasing.
+
+CLI: `--anti-aliasing`.
+
+### `render.effect.antialiasing.mode` (_string_, default: `fxaa`)
+
+Set the anti-aliasing technique. Valid options are: `fxaa` (fast), `ssaa` (quality), `taa` (balanced)
+
+CLI: `--anti-aliasing`.
+
+### `render.effect.ambient_occlusion` (_bool_, default: `false`)
+
+Enable _ambient occlusion_. This is a technique providing approximate shadows, used to improve the depth perception of the object. Implemented using SSAO
+
+CLI: `--ambient-occlusion`.
+
+### `render.effect.tone_mapping` (_bool_, default: `false`)
+
+Enable generic filmic _Tone Mapping Pass_. This technique is used to map colors properly to the monitor colors.
+
+CLI: `--tone-mapping`.
+
+### `render.effect.final_shader` (_string_, optional)
+
+Add a final shader to the output image. See [user documentation](../user/10-FINAL_SHADER.md).
+
+CLI: `--final-shader`
+
+### `render.line_width` (_double_, optional)
+
+Set the _width_ of lines when showing edges. Model-specified by default.
+
+CLI: `--line-width`.
+
+### `render.show_edges` (_bool_, optional)
+
+Set to true to show the _cell edges_. Model-specified by default.
+
+CLI: `--edges`.
+
+### `render.point_size` (_double_, optional)
+
+Set the _size_ of points when showing vertices. Model-specified by default.
+
+CLI: `--point-size`.
+
+### `render.backface_type` (_string_, optional, **on load**)
+
+Set the Backface type, can be `visible` or `hidden`, Model-specified by default.
+
+CLI: `--backface-type`.
+
+### `render.grid.enable` (_bool_, default: `false`)
+
+Show _a grid_ aligned with the horizontal (orthogonal to the Up direction) plane.
+
+CLI: `--grid`.
+
+### `render.grid.absolute` (_bool_, default: `false`)
+
+Position the grid at the _absolute origin_ of the model's coordinate system instead of below the model.
+
+CLI: `--grid-absolute`.
+
+### `render.grid.unit` (_double_, optional)
+
+Set the size of the _unit square_ for the grid. Automatically computed by default.
+
+CLI: `--grid-unit`.
+
+### `render.grid.subdivisions` (_int_, default: `10`)
+
+Set the number of subdivisions for the grid.
+
+CLI: `--grid-subdivisions`.
+
+### `render.grid.color` (_color_, default: `0, 0, 0`)
+
+Set the color of grid lines.
+
+CLI: `--grid-color`.
+
+### `render.axes_grid` (_bool_, default: `false`)
+
+Show _axes grid_ in the scene.
+
+CLI: `--axes-grid`.
+
+### `render.raytracing.enable` (_bool_, default: `false`)
+
+Enable _raytracing_. Requires the raytracing module to be enabled.
+
+CLI: `--raytracing`.
+
+### `render.raytracing.samples` (_int_, default: `5`)
+
+The number of _samples per pixel_.
+
+CLI: `--samples`.
+
+### `render.raytracing.denoise` (_bool_, default: `false`)
+
+_Denoise_ the raytracing rendering.
+
+CLI: `--denoise`.
+
+### `render.hdri.file` (_path_, optional)
+
+Set the _HDRI_ image that can be used for ambient lighting and skybox.
+Valid file format are `.hdr`, `.exr`, `.png`, `.jpg`, `.pnm`, `.tiff`, `.bmp`.
+If not set, a default is provided.
+
+CLI: `--hdri-file`.
+
+### `render.hdri.ambient` (_bool_, default: `false`)
+
+Light the scene using the _HDRI_ image as ambient lighting
+The environment act as a light source and is reflected on the material.
+
+CLI: `--hdri-ambient`.
+
+### `render.background.color` (_color_, default: `0.2,0.2,0.2`)
+
+Set the window _background color_.
+Ignored if a _hdri_ skybox is used.
+
+CLI: `--background-color`.
+
+### `render.background.skybox` (_bool_, default: `false`)
+
+Show the _HDRI_ image as a skybox
+Overrides the the background color if any
+
+CLI: `--hdri-skybox`.
+
+### `render.background.blur.enable` (_bool_, default: `false`)
+
+Blur background, useful with a skybox.
+
+CLI: `--blur-background`.
+
+### `render.background.blur.coc` (_double_, default: `20.0`)
+
+Blur background circle of confusion radius.
+
+CLI: `--blur-coc`.
+
+### `render.light.intensity` (_double_, default: `1.0`)
+
+Adjust the intensity of every light in the scene.
+
+CLI: `--light-intensity`.
+
+### `render.armature.enable` (_bool_, default: `false`)
+
+Display armatures if present in the scene (glTF only).
+
+CLI: `--armature`.
+
+## UI Options
+
+### `ui.axis` (_bool_, default: `false`)
+
+Show _axes_ as a trihedron in the scene. Requires an interactor.
+
+CLI: `--axis`.
+
+### `ui.x_color` (_color_, default: `#f94306`)
+
+Set the X axis color in both widget and grid.
+
+CLI: `--x-color`.
+
+### `ui.y_color` (_color_, default: `#7aff7a`)
+
+Set the Y axis color in both widget and grid.
+
+CLI: `--y-color`.
+
+### `ui.z_color` (_color_, default: `#788bff`)
+
+Set the Z axis color in both widget and grid.
+
+CLI: `--z-color`.
+
+### `ui.scalar_bar` (_bool_, default: `false`)
+
+Show _scalar bar_ of the coloring by data array.
+
+CLI: `--bar`.
+
+### `ui.cheatsheet` (_bool_, default: `false`)
+
+Show a interaction cheatsheet
+
+### `ui.console` (_bool_, default: `false`)
+
+Show the console
+
+### `ui.minimal_console` (_bool_, default: `false`)
+
+Show the minimal console. Useful to execute a quick command and exit.
+
+### `ui.filename` (_bool_, default: `false`)
+
+Display the _filename info content_ on top of the window.
+
+### `ui.filename_info` (_string_, default: `""`)
+
+Content of _filename info_ to display.
+
+### `ui.hdri_filename` (_bool_, default: `false`)
+
+Display the _HDRI filename info content_ on top of the window.
+
+CLI: `--hdri-filename`.
+
+### `ui.font_file` (_path_, optional)
+
+Use the provided FreeType compatible font file to display text.
+Can be useful to display non-ASCII filenames.
+
+CLI: `--font-file`.
+
+### `ui.scale` (_ratio_, default: `1.0`)
+
+Scale fonts. Useful for HiDPI displays.
+
+CLI: `--font-scale`.
+
+### `ui.fps` (_bool_, default: `false`)
+
+Display a _frame per second counter_.
+
+CLI: `--fps`.
+
+### `ui.loader_progress` (_bool_, default: `false`, **on load**)
+
+Show a _progress bar_ when loading the file.
+
+### `ui.loader_progress_color` (_color_, optional, **on load**)
+
+Set loader progress bar color or use default one #FFFFFF if not set.
+
+CLI: `--progress`.
+
+### `ui.animation_progress` (_bool_, default: `false`, **on load**)
+
+Show a _progress bar_ when playing the animation.
+
+CLI: `--animation-progress`.
+
+### `ui.animation_progress_color` (_color_, optional, **on load**)
+
+Set animation progress bar color or use default one #788BFF if not set.
+
+### `ui.metadata` (_bool_, default: `false`)
+
+Display the _metadata_.
+
+CLI: `--metadata`.
+
+### `ui.drop_zone.enable` (_bool_, default: `false`)
+
+Show a drop zone. Rendering is disabled when the viewport is smaller than 10×10 pixels.
+
+### `ui.drop_zone.custom_binds` (_string_, default: `""`)
+
+Space separated custom key bindings to display in the drop zone.
+
+### `ui.drop_zone.show_logo` (_bool_, default: `false`)
+
+Display the logo. Needs ui.drop_zone.enable to be true.
+
+### `ui.backdrop.opacity` (_double_, default: `0.9`)
+
+Set the opacity of the backdrop behind the UI elements. Value is between 0.0 (fully transparent) and 1.0 (fully opaque).
+
+CLI: `--backdrop-opacity`.
+
+# APIs
+
+There are three APIs to access the options
+
+## Struct API
+
+The most straightforward and easy to use API, just access it through the structs available in the options instance, eg:
+
+```cpp
+  f3d::engine eng = f3d::engine::create();
+  f3d::options& opt = eng.getOptions();
+  opt.render.show_edges = true;
+  opt.render.grid.enable = true;
+  opt.ui.metadata = true;
+  opt.model.material.roughness = 0.6;
+```
+
+Please note that when accessing optional options, special care must be used, eg:
+
+```cpp
+  f3d::engine eng = f3d::engine::create();
+  f3d::options& opt = eng.getOptions();
+  if (opt.render.line_width.has_value())
+  {
+    std::cout << "Line Width: " << opt.render.line_width.value() << "\n";
+  }
+  else
+  {
+    std::cout << "Line Width: unset\n";
+  }
+```
+
+It's even more true with the few optional boolean options as std::optional has an implicit boolean cast operator.
+
+## String API
+
+The most generic and flexible API, as it rely on parsing and string generation.
+See the [parsing documentation](../user/08-PARSING.md) for more details.
+
+```cpp
+  f3d::engine eng = f3d::engine::create();
+  f3d::options& opt = eng.getOptions();
+  opt.setAsString("render.show_edges", "true");
+  opt.setAsString("render.grid.enable", "true");
+  opt.setAsString("ui.metadata", "true");
+  opt.setAsString("model.material.roughness", "0.6");
+```
+
+When using this API make sure to catch exceptions has needed, eg:
+
+```cpp
+  f3d::engine eng = f3d::engine::create();
+  f3d::options& opt = eng.getOptions();
+
+  try
+  {
+    std::cout << userProvidedName << ": " << opt.getAsString(userProvidedName) << "\n";
+  }
+  catch (const f3d::options::inexistent_exception&)
+  {
+    std::cout << userProvidedName << " does not exist.\n";
+  }
+  catch (const f3d::options::no_value_exception&)
+  {
+    std::cout << userProvidedName << " is not set.\n";
+  }
+```
+
+## Variant API
+
+An API that is similar to the F3D 2.0 options API thanks to std::variant.
+
+```cpp
+  f3d::engine eng = f3d::engine::create();
+  f3d::options& opt = eng.getOptions();
+  opt.set("render.show_edges", true);
+  opt.set("render.grid.enable", true);
+  opt.set("ui.metadata", true);
+  opt.set("model.material.roughness", 0.6);
+```
+
+When using this API make sure to catch exception shown above with the string API.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/04-LANGUAGE_BINDINGS.md f3d-3.4.1/doc/libf3d/04-LANGUAGE_BINDINGS.md
--- f3d-3.2.0+dfsg/doc/libf3d/04-LANGUAGE_BINDINGS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/libf3d/04-LANGUAGE_BINDINGS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,177 @@
+# Language Bindings
+
+## C
+
+If the C bindings have been generated using the `F3D_BINDINGS_C` CMake option, the libf3d can be used directly from C.
+If you want to use CMake, make sure to install not only f3d but also the `sdk` component using `cmake --install <build_dir> --component sdk`
+
+Here is an example showing how to use libf3d c bindings:
+
+```c
+  f3d_engine_autoload_plugins();
+  f3d_engine_t* engine = f3d_engine_create(0);
+  f3d_options_t* options = f3d_engine_get_options(engine);
+
+  f3d_options_set_as_bool(options, "render.grid.enable", 1);
+  f3d_options_set_as_bool(options, "render.show_edges", 1);
+  f3d_options_set_as_bool(options, "ui.axis", 1);
+  f3d_options_set_as_bool(options, "ui.fps", 1);
+  f3d_options_set_as_bool(options, "ui.animation_progress", 1);
+  f3d_options_set_as_bool(options, "ui.filename", 1);
+
+  f3d_scene_t* scene = f3d_engine_get_scene(engine);
+  f3d_scene_add(scene, "f3d/testing/data/dragon.vtu");
+
+  f3d_interactor_t* interactor = f3d_engine_get_interactor(engine);
+  f3d_interactor_start(interactor, 1.0 / 30.0);
+
+  // ...
+
+  f3d_engine_delete(engine);
+```
+
+```cmake
+cmake_minimum_required(VERSION 3.10)
+project(f3d-example-c)
+find_package(f3d REQUIRED COMPONENTS c_api)
+add_executable(f3d-example-c main.c)
+target_link_libraries(f3d-example-c
+  PRIVATE
+  f3d::c_api
+)
+```
+
+You can see more examples using c bindings in the dedicated example directory [here](https://github.com/f3d-app/f3d/tree/master/examples/libf3d/c).
+
+## Python
+
+If the python bindings have been generated using the `F3D_BINDINGS_PYTHON` CMake option, the libf3d can be used directly from python.
+Make sure to set `PYTHONPATH` to path where the python module is built.
+Here is an example showing how to use libf3d python bindings:
+
+```python
+import f3d
+
+eng = f3d.Engine.create(False)
+eng.options.update({
+  "model.scivis.array-name": "Normals",
+  "model.scivis.component": 0,
+  "ui.bar": True,
+  "render.grid.enable": True,
+  })
+
+eng.scene.add("f3d/testing/data/dragon.vtu")
+eng.interactor.start()
+```
+
+You can see more examples using python bindings in the dedicated example directory [here](https://github.com/f3d-app/f3d/tree/master/examples/libf3d/python).
+
+### Stubs
+
+It's also possible to generate Python stubs automatically by enabling the CMake option `F3D_BINDINGS_PYTHON_GENERATE_STUBS`.
+Python stubs are `.pyi` files defining the public interface, allowing IDEs to auto-complete and do static analysis.
+
+## Javascript
+
+If the Javascript bindings have been generated by building F3D with webassembly and emscriptem, the libf3d can be used directly from a browser.
+
+You can see a simple but complete example web application [here](https://github.com/f3d-app/f3d/blob/master/examples/libf3d/web).
+
+The package can be installed using npm, by running the following command:
+
+```bash
+npm install f3d
+```
+
+Create the target DOM element `<canvas id="canvas"></canvas>` in your app, then use the following javascript code:
+
+```javascript
+// import the module
+import f3d from "f3d";
+
+// setup a setting object
+const settings = {
+  canvas: document.getElementById("canvas"),
+  setupOptions: (options) => {
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // make it look nice
+    options.toggle("render.effect.antialiasing.enable");
+    options.toggle("render.effect.tone_mapping");
+    options.toggle("render.effect.ambient_occlusion");
+    options.toggle("render.hdri.ambient");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+  },
+};
+
+f3d(settings)
+  .then(async (Module) => {
+    // write a 3D file located on the server to the internal filesystem
+    const modelFile = await fetch("/assets/example.obj").then((b) =>
+      b.arrayBuffer(),
+    );
+    Module.FS.writeFile("example.obj", new Uint8Array(modelFile));
+
+    // automatically load all supported file format readers
+    Module.Engine.autoloadPlugins();
+
+    // create an engine
+    Module.engineInstance = Module.Engine.create();
+
+    Module.setupOptions(Module.engineInstance.getOptions());
+
+    // setup the window size based on the canvas size
+    const scale = window.devicePixelRatio;
+    Module.engineInstance
+      .getWindow()
+      .setSize(Module.canvas.clientWidth, scale * Module.canvas.clientHeight);
+
+    const scene = Module.engineInstance.getScene();
+    scene.add("/example.obj");
+
+    // do a first render and start the interactor
+    Module.engineInstance.getWindow().render();
+    Module.engineInstance.getInteractor().start();
+  })
+  .catch((error) => {
+    console.error("Internal exception: " + error);
+  });
+```
+
+Some advanced libf3d API are not bound yet, here's the exhaustive list:
+
+- `f3d::scene`: light related functions
+- `f3d::scene`: `scene& add(const mesh_t& mesh)`
+- `f3d::camera`: state management
+- `f3d::interactor`: bindings related functions
+- `f3d::interactor`: `playInteraction` and `recordInteraction`
+
+## Java (experimental)
+
+If the Java bindings have been generated using the `F3D_BINDINGS_JAVA` CMake option, the libf3d can be used directly from Java.
+You can import the `f3d.jar` package and use the provided Java classes directly.
+Make sure to set `java.library.path` to the path where the JNI library is built.
+Here is an example showing how to use libf3d Java bindings:
+
+```java
+import app.f3d.F3D.*;
+
+public class F3DExample {
+  public static void main(String[] args) {
+
+    Engine.autoloadPlugins();
+
+    // Always use try-with-resources idiom to ensure the native engine is released
+    try (Engine engine = new Engine(Window.Type.NATIVE)) {
+      Scene scene = engine.getScene();
+      scene.add("f3d/testing/data/dragon.vtu");
+
+      engine.getWindow().render();
+    }
+  }
+}
+```
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/05-PLUGINS.md f3d-3.4.1/doc/libf3d/05-PLUGINS.md
--- f3d-3.2.0+dfsg/doc/libf3d/05-PLUGINS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/libf3d/05-PLUGINS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,88 @@
+# Plugin SDK
+
+> [!NOTE]
+> `libf3d` works best when built as a shared library, make sure it's the case. The `plugin_sdk` component must also be installed in order to be able to create a new plugin.
+
+You will then be able to call `find_package(f3d REQUIRED COMPONENTS pluginsdk)` in your plugin CMakeLists.txt, a few macros are made available to you to generate a plugin which allow you to extend libf3d to support your own file format. Access to a f3d::vtkext VTK module is also provided if needed.
+
+> [!NOTE]
+> Please consider contributing your plugin in [F3D directly](https://github.com/f3d-app/f3d/tree/master/plugins) if you think it can be useful to the community.
+> You can also consider contributing directly [in VTK](https://gitlab.kitware.com/vtk/vtk/blob/master/Documentation/dev/git/develop.md).
+
+## Creating a plugin
+
+You can take a look at the example in the [examples/plugin](https://github.com/f3d-app/f3d/tree/master/examples/plugins) directory or at the official [plugins](https://github.com/f3d-app/f3d/tree/master/plugins).
+
+The first thing (and most difficult part) you have to do is creating a VTK reader (or a VTK importer if you want to support a full scene with materials, lights and cameras), and wrap it into a VTK module. You can create several readers/importers in the same VTK module if you need to support several file formats in a single plugin.
+
+Then, declare the reader(s) and the plugin using the CMake macros:
+
+```cmake
+f3d_plugin_init()
+
+f3d_plugin_declare_reader(
+  NAME "ReaderName"
+  EXTENSIONS "myext"                # set the extensions the reader can support
+  MIMETYPES "application/vnd.myext" # set the mimetypes the reader can support
+  VTK_READER ${vtk_classname}       # set the name of the VTK reader class you have created
+  FORMAT_DESCRIPTION "description"  # set the proper name of the file format
+  EXCLUDE_FROM_THUMBNAILER          # add this flag if you don't want thumbnail generation for this reader
+  OPTIONS "option1" "option2"       # use this to define reader specific option that can be defined by the user
+)
+
+f3d_plugin_declare_reader(
+  NAME "ReaderNameWithScene"
+  EXTENSIONS "myext2"                # set the extensions the reader can support
+  MIMETYPES "application/vnd.myext2" # set the mimetypes the reader can support
+  VTK_IMPORTER ${vtk_classname}      # set the name of the VTK importer class you have created
+  FORMAT_DESCRIPTION "description"   # set the proper name of the file format
+  CUSTOM_CODE "file.inl"             # set this to add a custom code when instancing your class, this is where reader options should be processed
+)
+
+# More f3d_plugin_declare_reader calls are possible
+
+f3d_plugin_build(
+  NAME "PluginName"
+  VERSION 1.0
+  DESCRIPTION "Plugin description"
+  VTK_MODULES CommonCore CommonExecutionModel # Add more VTK modules if necessary
+)
+```
+
+If the build succeeds, a library called `libf3d-plugin-<name>.so` will be created (`f3d-plugin-<name>.dll` on Windows)
+A JSON file of the following form will also be generated. It's used by F3D internally to get information about supported file formats.
+
+```json
+{
+  "description": "Plugin description",
+  "name": "PluginName",
+  "readers": [
+    {
+      "description": "Reader description",
+      "extensions": ["myext"],
+      "mimetypes": ["application/vnd.myext"],
+      "name": "ReaderName"
+    }
+  ],
+  "type": "MODULE",
+  "version": "1.0"
+}
+```
+
+The list of existing mimetypes can be find [here](https://www.iana.org/assignments/media-types/media-types.xhtml). If your file format is not listed, the mimetype should be `application/vnd.${extension}`
+
+## Loading your plugin
+
+The plugin can be loaded using `f3d::engine::loadPlugin("path or name")` API if you are using libf3d, or `--load-plugins="path or name"` option if you are using F3D application.
+The option can also be set in a configuration file that you could distribute with your plugin.
+
+## f3d::vtkext
+
+F3D provides access to a VTK modules containing utilities that may be useful for plugin developers:
+
+- `vtkF3DFaceVaryingPointDispatcher`: A VTK filter that manipulates point data so that F3D can display them as face-varying data (used by `usd` plugin)
+- `vtkF3DBitonicSort`: A VTK class that perform Bitonic Sort algorithm on the GPU (used by the translucent point sprites rendering algorithm)
+- `vtkF3DImporter`: An Importer class that abstract away support for different version of VTK after some API changes.
+- `vtkF3DGLTFImporter`: An custom glTF importer class that support armatures, useful when creating other plugin supporting glTF extensions.
+
+For the complete documentation, please consult the [vtkext doxygen documentation.](https://f3d.app/docs/next/category/vtkext-api-reference).
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/CLASSES.md f3d-3.4.1/doc/libf3d/CLASSES.md
--- f3d-3.2.0+dfsg/doc/libf3d/CLASSES.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/CLASSES.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,82 +0,0 @@
-# Classes Introduction
-
-This is only an introduction to libf3d classes.
-For the complete documentation, please consult the [libf3d doxygen documentation.](https://f3d.app/doc/libf3d/doxygen/).
-
-## Engine class
-
-The engine class is the main class that needs to be instantiated. All other classes instance are provided by the engine using getters, `getScene`, `getWindow`, `getInteractor`, `getOptions`.
-
-The engine factory lets you choose between the different types of OpenGL rendering backend.
-The generic `create()` is recommended in most cases and will use the best context possible available on your system.
-However, it's possible to force the rendering backend in some specific use cases:
-
-- `createGLX()`: force usage of GLX backend, works on Linux only and requires a X11 server to run.
-- `createWGL()`: force usage of WGL native backend on Windows.
-- `createEGL()`: force usage of EGL backend, recommended when doing offscreen rendering with a GPU available. Requires EGL library available. No interactor provided.
-- `createOSMesa()`: force usage of OSMesa backend (software rendering), recommended when doing offscreen rendering without any GPU. Requires OSMesa library available. No interactor provided.
-- `createNone()`: do not use any rendering. Useful to retrieve metadata only.
-- `createExternal()`: the user is responsible of the rendering stack. It lets the user integrate libf3d in other frameworks like Qt or GLFW. No interactor provided. See [Context](#context-class) documentation for more info.
-  An additional boolean argument is available to specify if offscreen rendering is requested when relevant on the selected rendering backend.
-
-A static function `loadPlugin` can also be called to load reader plugins. It must be called before loading any file. An internal plugin containing VTK native readers can be loaded by calling `f3d::engine::loadPlugin("native");`. Other plugins maintained by F3D team are available if their build is enabled: `alembic`, `assimp`, `draco`, `hdf`, `occt` and `usd`.
-If CMake option `F3D_PLUGINS_STATIC_BUILD` is enabled, the plugins listed above are also static just like `native` plugin.
-All static plugins can be loaded using `f3d::engine::autoloadPlugins()`.
-
-## Scene class
-
-The scene class is responsible to `add` file from the disk into the scene. It supports reading multiple files at the same time and even mesh from memory.
-It is possible to `clear` the scene and to check if the scene `supports` a file.
-
-## Context class
-
-Convenience class providing generic context API when using a external rendering backend (using `f3d::engine::createExternal()` factory).
-
-## Window class
-
-The window class is responsible for rendering the data.
-Window lets you `render`, `renderToImage` and control other parameters of the window, like icon or windowName.
-
-## Interactor class
-
-When provided by the engine, the interactor class lets you choose how to interact with the data.
-
-It contains the animation API to start and stop animation as well as recover the number of available animations and their names.
-
-Interactor lets you add, remove and trigger your [commands](../user/COMMANDS.md).
-The commands let you interact with the libf3d in a very flexible manner.
-
-Interactor also lets you add and remove bindings in order to modify how
-the libf3d react to different interactions, eg. when a key is pressed or when a file is dropped.
-The libf3d has [default bindings](../user/INTERACTIONS.md) but this API lets you defined your own
-by specifying [binds](../user/CONFIGURATION_FILE.md#Bind) and commands.
-This API also lets you control the content of the cheatsheet.
-
-Use `log::setVerboseLevel(log::VerboseLevel::DEBUG)` to print debug information on interaction and command use.
-
-Of course, you can use `start` and `stop` to control the interactor behavior.
-`start` lets you specify time for the event loop and a `std::function` to execute at each loop.
-
-## Camera class
-
-Provided by the window, this class lets you control the camera. You can either specify the camera position, target, and up direction directly, or specify movements relative to the current camera state.
-
-## Image class
-
-A generic image class that can either be created from a window, from an image filepath or even from a data buffer. It supports comparison making it very practical in testing context.
-
-## Log class
-
-A class to control logging in the libf3d. Simple using the different dedicated methods (`print`, `debug`, `info`, `warn`, `error`) and `setVerboseLevel`, you can easily control what to display. Please note that, on windows, a dedicated output window may be created.
-
-## Options class
-
-This class lets you control the behavior of the libf3d. An option is basically a value that can be a optional or not. There is different API to access it, see the exhaustive [doc](OPTIONS.md).
-
-## Reader class
-
-This class is used internally by the [plugin SDK](PLUGINS.md) and is not intended to be used directly.
-
-## Plugin class
-
-This class is used internally by the [plugin SDK](PLUGINS.md) and is not intended to be used directly.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/LANGUAGE_BINDINGS.md f3d-3.4.1/doc/libf3d/LANGUAGE_BINDINGS.md
--- f3d-3.2.0+dfsg/doc/libf3d/LANGUAGE_BINDINGS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/LANGUAGE_BINDINGS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,61 +0,0 @@
-# Language Bindings
-
-## Python
-
-If the python bindings have been generated using the `F3D_BINDINGS_PYTHON` CMake option, the libf3d can be used directly from python.
-Make sure to set `PYTHONPATH` to path where the python module is built.
-Here is an example showing how to use libf3d python bindings:
-
-```python
-import f3d
-
-eng = f3d.Engine.create(False)
-eng.options.update({
-  "model.scivis.array-name": "Normals",
-  "model.scivis.component": 0,
-  "ui.bar": True,
-  "render.grid.enable": True,
-  })
-
-eng.scene.add("f3d/testing/data/dragon.vtu")
-eng.interactor.start()
-```
-
-You can see more examples using python bindings in the dedicated example folder [here](https://github.com/f3d-app/f3d/tree/master/examples/libf3d/python).
-
-### Stubs
-
-It's also possible to generate Python stubs automatically by enabling the CMake option `F3D_BINDINGS_PYTHON_GENERATE_STUBS`.
-Python stubs are `.pyi` files defining the public interface, allowing IDEs to auto-complete and do static analysis.
-
-## Java (experimental)
-
-If the Java bindings have been generated using the `F3D_BINDINGS_JAVA` CMake option, the libf3d can be used directly from Java.
-You can import the `f3d.jar` package and use the provided Java classes directly.
-Make sure to set `java.library.path` to the path where the JNI library is built.
-Here is an example showing how to use libf3d Java bindings:
-
-```java
-import app.f3d.F3D.*;
-
-public class F3DExample {
-  public static void main(String[] args) {
-
-    Engine.autoloadPlugins();
-
-    // Always use try-with-resources idiom to ensure the native engine is released
-    try (Engine engine = new Engine(Window.Type.NATIVE)) {
-      Scene scene = engine.getScene();
-      scene.add("f3d/testing/data/dragon.vtu");
-
-      engine.getWindow().render();
-    }
-  }
-}
-```
-
-## Javascript (experimental)
-
-If the Javascript bindings have been generated by building F3D with webassembly and emscriptem, the libf3d can be used directly from a browser.
-
-See the dedicated [build](../dev/BUILD_WASM.md) guide, this [example app](https://github.com/f3d-app/f3d/blob/master/webassembly/example.html) which you can test live [here](https://f3d.app/web/)!
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/OPTIONS.md f3d-3.4.1/doc/libf3d/OPTIONS.md
--- f3d-3.2.0+dfsg/doc/libf3d/OPTIONS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/OPTIONS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,203 +0,0 @@
-# Options exhaustive list
-
-An option is a specific value stored in different struct in an `options` instance.
-They can be accessed directly through the structs, through a string API or through a std::variant API.
-
-The possible option are listed below and are organized by categories and subcategories, here is a non-exhaustive explanation of the categories.
-
-- `scene` options are related to how the scene is being displayed
-- `render` options are related to the way the render is done
-- `render.effect` options are related to specific techniques used that modify the render
-- `ui` options are related to the screenspace UI element displayed
-- `model` options are related to modifications on the model
-- `interactor` options requires an interactor to be present to have any effect
-
-Please note certain options are taken into account when rendering, others when loading a file.
-See the exhaustive list below, but note that this may change in the future.
-
-Please note certain options are `optional` by default, which means they are not set initially,
-See the [APIs](#APIs) details below for more info.
-
-## Scene Options
-
-|            Option            | Type<br>Default<br>Trigger | Description                                                                                                                       |         F3D option         |
-| :--------------------------: | :------------------------: | :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------: |
-|   scene.animation.autoplay   |   bool<br>false<br>load    | Automatically start animation.                                                                                                    |   \-\-animation-autoplay   |
-|   scene.animation.indices    | vector\<int\><br>0<br>load | Select the animations to load.<br>Any negative value means all animations.<br>The default scene always has at most one animation. |   \-\-animation-indices    |
-| scene.animation.speed_factor |    ratio<br>1<br>render    | Set the animation speed factor to slow, speed up or even invert animation.                                                        | \-\-animation-speed-factor |
-|     scene.animation.time     | double<br>optional<br>load | Set the animation time to load.                                                                                                   |     \-\-animation-time     |
-|      scene.camera.index      |  int<br>optional<br>load   | Select the scene camera to use when available in the file.<br>The default scene always uses automatic camera.                     |      \-\-camera-index      |
-|      scene.up_direction      |  direction<br>+Y<br>load   | Define the Up direction. It impacts the grid, the axis, the HDRI and the camera.                                                  |           \-\-up           |
-|      scene.force_reader      | string<br>optional<br>load | Force a specific reader to be used, disregarding the file extension. See [user documentation](../user/SUPPORTED_FORMATS.md)       |      \-\-force-reader      |
-|  scene.camera.orthographic   |  bool<br>optional<br>load  | Set to true to force orthographic projection. Model specified by default, which is false if not specified.                        |  \-\-camera\-orthographic  |
-
-## Interactor Options
-
-|         Option         | Type<br>Default<br>Trigger | Description                   |   F3D option    |
-| :--------------------: | :------------------------: | :---------------------------- | :-------------: |
-|  interactor.trackball  |  bool<br>false<br>render   | Enable trackball interaction. |  \-\-trackball  |
-| interactor.invert_zoom |  bool<br>false<br>render   | Invert the zoom direction.    | \-\-invert-zoom |
-
-## Model Options
-
-|           Option            |       Type<br>Default<br>Trigger       | Description                                                                                                                                                                                                                                          |         F3D option          |
-| :-------------------------: | :------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------: |
-|    model.matcap.texture     |       path<br>optional<br>render       | Path to a texture file containing a material capture. All other model options for surfaces are ignored if this is set. Model specified by default.                                                                                                   |     \-\-texture-matcap      |
-|     model.color.opacity     |      double<br>optional<br>render      | Set _opacity_ on the geometry. Usually used with Depth Peeling option. Multiplied with the `model.color.texture` when present. Model specified by default.                                                                                           |         \-\-opacity         |
-|       model.color.rgb       |      color<br>optional<br>render       | Set a _color_ on the geometry. Multiplied with the `model.color.texture` when present. Model specified by default.                                                                                                                                   |          \-\-color          |
-|     model.color.texture     |       path<br>optional<br>render       | Path to a texture file that sets the color of the object. Will be multiplied with rgb and opacity. Model specified by default.                                                                                                                       |   \-\-texture-base-color    |
-|    model.emissive.factor    |      color<br>optional<br>render       | Multiply the emissive color when an emissive texture is present. Model specified by default.                                                                                                                                                         |     \-\-emissive-factor     |
-|   model.emissive.texture    |           path<br><br>render           | Path to a texture file that sets the emitted light of the object. Multiplied with the `model.emissive.factor`. Model specified by default.                                                                                                           |    \-\-texture-emissive     |
-|   model.material.metallic   |      double<br>optional<br>render      | Set the _metallic coefficient_ on the geometry (0.0-1.0). Multiplied with the `model.material.texture` when present. Model specified by default.                                                                                                     |        \-\-metallic         |
-|  model.material.roughness   |      double<br>optional<br>render      | Set the _roughness coefficient_ on the geometry (0.0-1.0). Multiplied with the `model.material.texture` when present. Model specified by default.                                                                                                    |        \-\-roughness        |
-|   model.material.base_ior   |      double<br>optional<br>render      | Set the _index of refraction of the base layer_ (1.0-2.5). Model specified by default.                                                                                                                                                               |        \-\-base\-ior        |
-|   model.material.texture    |       path<br>optional<br>render       | Path to a texture file that sets the Occlusion, Roughness and Metallic values of the object. Multiplied with the `model.material.roughness` and `model.material.metallic`, set both of them to 1.0 to get a true result. Model specified by default. |    \-\-texture-material     |
-|     model.normal.scale      |      double<br>optional<br>render      | Normal scale affects the strength of the normal deviation from the normal texture. Model specified by default.                                                                                                                                       |      \-\-normal-scale       |
-|    model.normal.texture     |       path<br>optional<br>render       | Path to a texture file that sets the normal map of the object. Model specified by default.                                                                                                                                                           |     \-\-texture-normal      |
-|     model.scivis.enable     |        bool<br>false<br>render         | _Color by an array_ present in on the data. If `model.scivis.array_name` is not set, the first available array will be used.                                                                                                                         |     \-\-scalar-coloring     |
-|     model.scivis.cells      |        bool<br>false<br>render         | Color the data with value found _on the cells_ instead of points                                                                                                                                                                                     |          \-\-cells          |
-| model.scivis.discretization |       int<br>optional<br>render        | Set how many distinct colors will be used from the colormap. \[1, N\] will discretize, values outside that range will result in smooth shading.                                                                                                      | \-\-colormap-discretization |
-|    model.scivis.colormap    |   colormap<br>\<inferno\><br>render    | Set a _custom colormap for the coloring_.See [colormap parsing](PARSING.md#colormap) for details.                                                                                                                                                    |        \-\-colormap         |
-|   model.scivis.component    |          int<br>-1<br>render           | Specify the component to color with. -1 means _magnitude_. -2 means _direct values_.                                                                                                                                                                 |          \-\-comp           |
-|   model.scivis.array_name   |          string<br><br>render          | Select the name of the array to color with.                                                                                                                                                                                                          |     \-\-coloring-array      |
-|     model.scivis.range      | vector\<double\><br>optional<br>render | Set the _coloring range_. Automatically computed by default.                                                                                                                                                                                         |          \-\-range          |
-| model.point_sprites.enable  |        bool<br>false<br>render         | Show sphere _points sprites_ instead of the geometry.                                                                                                                                                                                                |      \-\-point-sprites      |
-|  model.point_sprites.type   |       string<br>sphere<br>render       | Set the sprites type when showing point sprites (can be `sphere` or `gaussian`).                                                                                                                                                                     |   \-\-point-stripes-type    |
-|  model.point_sprites.size   |        double<br>10.0<br>render        | Set the _size_ of point sprites.                                                                                                                                                                                                                     |   \-\-point-stripes-size    |
-|     model.volume.enable     |        bool<br>false<br>render         | Enable _volume rendering_. It is only available for 3D image data (vti, dcm, nrrd, mhd files) and will display nothing with other formats. It forces coloring.                                                                                       |         \-\-volume          |
-|    model.volume.inverse     |        bool<br>false<br>render         | Inverse the linear opacity function.                                                                                                                                                                                                                 |         \-\-inverse         |
-|  model.textures_transform   |   transform2d<br>optional<br>render    | Transform applied to textures on the model. If a default transform is set by the importer, the default value will be multiplied by this transform.                                                                                                   |   \-\-textures-transform    |
-
-## Render Options
-
-|               Option               |   Type<br>Default<br>Trigger   | Description                                                                                                                                                               |                              F3D option                              |
-| :--------------------------------: | :----------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------: |
-| render.effect.translucency_support |    bool<br>false<br>render     | Enable _translucency support_. This is a technique used to correctly render translucent objects, implemented using depth peeling                                          |                       \-\-translucency-support                       |
-| render.effect.antialiasing.enable  |   string<br>false<br>render    | Enable _anti-aliasing_. This technique is used to reduce aliasing.                                                                                                        |                          \-\-anti-aliasing                           |
-|  render.effect.antialiasing.mode   |    string<br>fxaa<br>render    | Set the anti-aliasing technique. Valid options are: FXAA (fast), SSAA (quality)                                                                                           |                        \-\-anti-aliasing-mode                        |
-|  render.effect.ambient_occlusion   |    bool<br>false<br>render     | Enable _ambient occlusion_. This is a technique providing approximate shadows, used to improve the depth perception of the object. Implemented using SSAO                 |                        \-\-ambient_occlusion                         |
-|     render.effect.tone_mapping     |    bool<br>false<br>render     | Enable generic filmic _Tone Mapping Pass_. This technique is used to map colors properly to the monitor colors.                                                           |                           \-\-tone-mapping                           |
-|     render.effect.final_shader     |  string<br>optional<br>render  | Add a final shader to the output image                                                                                                                                    | \-\-final-shader. See [user documentation](../user/FINAL_SHADER.md). |
-|         render.line_width          |  double<br>optional<br>render  | Set the _width_ of lines when showing edges. Model specified by default.                                                                                                  |                            \-\-line-width                            |
-|         render.show_edges          |   bool<br>optional<br>render   | Set to true to show the _cell edges_. Model specified by default.                                                                                                         |                              \-\-edges                               |
-|         render.point_size          |  double<br>optional<br>render  | Set the _size_ of points when showing vertices. Model specified by default.                                                                                               |                            \-\-point-size                            |
-|        render.backface_type        |  string<br>optional<br>render  | Set the Backface type, can be `visible` or `hidden`, model specified by default.                                                                                          |                          \-\-backface-type                           |
-|         render.grid.enable         |    bool<br>false<br>render     | Show _a grid_ aligned with the horizontal (orthogonal to the Up direction) plane.                                                                                         |                               \-\-grid                               |
-|        render.grid.absolute        |    bool<br>false<br>render     | Position the grid at the _absolute origin_ of the model's coordinate system instead of below the model.                                                                   |                               \-\-grid                               |
-|          render.grid.unit          |  double<br>optional<br>render  | Set the size of the _unit square_ for the grid. Automatically computed by default.                                                                                        |                            \-\-grid-unit                             |
-|      render.grid.subdivisions      |      int<br>10<br>render       | Set the number of subdivisions for the grid.                                                                                                                              |                        \-\-grid-subdivisions                         |
-|         render.grid.color          |   color<br>0, 0, 0<br>render   | Set the color of grid lines.                                                                                                                                              |                            \-\-grid-color                            |
-|      render.raytracing.enable      |    bool<br>false<br>render     | Enable _raytracing_. Requires the raytracing module to be enabled.                                                                                                        |                            \-\-raytracing                            |
-|     render.raytracing.samples      |       int<br>5<br>render       | The number of _samples per pixel_.                                                                                                                                        |                             \-\-samples                              |
-|     render.raytracing.denoise      |    bool<br>false<br>render     | _Denoise_ the raytracing rendering.                                                                                                                                       |                             \-\-denoise                              |
-|          render.hdri.file          |   path<br>optional<br>render   | Set the _HDRI_ image that can be used for ambient lighting and skybox.<br>Valid file format are hdr, exr, png, jpg, pnm, tiff, bmp.<br>If not set, a default is provided. |                            \-\-hdri-file                             |
-|        render.hdri.ambient         |    bool<br>false<br>render     | Light the scene using the _HDRI_ image as ambient lighting<br>The environment act as a light source and is reflected on the material.                                     |                           \-\-hdri-ambient                           |
-|      render.background.color       | color<br>0.2,0.2,0.2<br>render | Set the window _background color_.<br>Ignored if a _hdri_ skybox is used.                                                                                                 |                         \-\-background-color                         |
-|      render.background.skybox      |    bool<br>false<br>render     | Show the _HDRI_ image as a skybox<br>Overrides the the background color if any                                                                                            |                           \-\-hdri-skybox                            |
-|   render.background.blur.enable    |    bool<br>false<br>render     | Blur background, useful with a skybox.                                                                                                                                    |                         \-\-blur-background                          |
-|     render.background.blur.coc     |    double<br>20.0<br>render    | Blur background circle of confusion radius.                                                                                                                               |                             \-\-blur-coc                             |
-|       render.light.intensity       |    double<br>1.0<br>render     | Adjust the intensity of every light in the scene.                                                                                                                         |                         \-\-light-intensity                          |
-|       render.armature.enable       |    bool<br>false<br>render     | Display armatures if present in the scene (glTF only).                                                                                                                    |                             \-\-armature                             |
-
-## UI Options
-
-|        Option         | Type<br>Default<br>Trigger | Description                                                                                                      |       F3D option       |
-| :-------------------: | :------------------------: | :--------------------------------------------------------------------------------------------------------------- | :--------------------: |
-|        ui.axis        |  bool<br>false<br>render   | Show _axes_ as a trihedron in the scene. Requires an interactor.                                                 |        \-\-axis        |
-|     ui.scalar_bar     |  bool<br>false<br>render   | Show _scalar bar_ of the coloring by data array.                                                                 |        \-\-bar         |
-|     ui.cheatsheet     |  bool<br>false<br>render   | Show a interaction cheatsheet                                                                                    |           -            |
-|      ui.console       |  bool<br>false<br>render   | Show the console                                                                                                 |           -            |
-|  ui.minimal_console   |  bool<br>false<br>render   | Show the minimal console. Useful to execute a quick command and exit.                                            |           -            |
-|      ui.filename      |  bool<br>false<br>render   | Display the _filename info content_ on top of the window.                                                        |      \-\-filename      |
-|   ui.filename_info    |   string<br>-<br>render    | Content of _filename info_ to display.                                                                           |           -            |
-|     ui.font_file      | path<br>optional<br>render | Use the provided FreeType compatible font file to display text.<br>Can be useful to display non-ASCII filenames. |     \-\-font-file      |
-|       ui.scale        |   ratio<br>1.0<br>render   | Scale fonts.                                                                                                     |     \-\-font-scale     |
-|        ui.fps         |  bool<br>false<br>render   | Display a _frame per second counter_.                                                                            |        \-\-fps         |
-|  ui.loader_progress   |   bool<br>false<br>load    | Show a _progress bar_ when loading the file.                                                                     |      \-\-progress      |
-| ui.animation_progress |   bool<br>false<br>load    | Show a _progress bar_ when playing the animation.                                                                | \-\-animation-progress |
-|      ui.metadata      |  bool<br>false<br>render   | Display the _metadata_.                                                                                          |      \-\-metadata      |
-|      ui.dropzone      |  bool<br>false<br>render   | Show a drop zone. Rendering is disabled when the viewport is smaller than 10×10 pixels.                          |           -            |
-|   ui.dropzone_info    |   string<br>-<br>render    | Content of the drop zone text to display.                                                                        |           -            |
-
-# APIs
-
-There are three APIs to access the options
-
-## Struct API
-
-The most straightforward and easy to use API, just access it through the structs available in the options instance, eg:
-
-```cpp
-  f3d::engine eng = f3d::engine::create();
-  f3d::options& opt = eng.getOptions();
-  opt.render.show_edges = true;
-  opt.render.grid.enable = true;
-  opt.ui.metadata = true;
-  opt.model.material.roughness = 0.6;
-```
-
-Please note that when accessing optional options, special care must be used, eg:
-
-```cpp
-  f3d::engine eng = f3d::engine::create();
-  f3d::options& opt = eng.getOptions();
-  if (opt.render.line_width.has_value())
-  {
-    std::cout << "Line Width: " << opt.render.line_width.value() << std::endl;
-  }
-  else
-  {
-    std::cout << "Line Width: unset" << std::endl;
-  }
-```
-
-It's even more true with the few optional boolean options as std::optional has an implicit boolean cast operator.
-
-## String API
-
-The most generic and flexible API, as it rely on parsing and string generation.
-See the [parsing documentation](../user/PARSING.md) for more details.
-
-```cpp
-  f3d::engine eng = f3d::engine::create();
-  f3d::options& opt = eng.getOptions();
-  opt.setAsString("render.show_edges", "true");
-  opt.setAsString("render.grid.enable", "true");
-  opt.setAsString("ui.metadata", "true");
-  opt.setAsString("model.material.roughness", "0.6");
-```
-
-When using this API make sure to catch exceptions has needed, eg:
-
-```cpp
-  f3d::engine eng = f3d::engine::create();
-  f3d::options& opt = eng.getOptions();
-
-  try
-  {
-    std::cout << userProvidedName << ": " << opt.getAsString(userProvidedName) << std::endl;
-  }
-  catch (const f3d::options::inexistent_exception&)
-  {
-    std::cout << userProvidedName << " does not exist." << std::endl;
-  }
-  catch (const f3d::options::no_value_exception&)
-  {
-    std::cout << userProvidedName << " is not set." << std::endl;
-  }
-```
-
-## Variant API
-
-An API that is similar to the F3D 2.0 options API thanks to std::variant.
-
-```cpp
-  f3d::engine eng = f3d::engine::create();
-  f3d::options& opt = eng.getOptions();
-  opt.set("render.show_edges", true);
-  opt.set("render.grid.enable", true);
-  opt.set("ui.metadata", true);
-  opt.set("model.material.roughness", 0.6);
-```
-
-When using this API make sure to catch exception shown above with the string API.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/OVERVIEW.md f3d-3.4.1/doc/libf3d/OVERVIEW.md
--- f3d-3.2.0+dfsg/doc/libf3d/OVERVIEW.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/OVERVIEW.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,151 +0,0 @@
-# libf3d - A library to render 3D meshes
-
-By Michael Migliore and Mathieu Westphal.
-
-libf3d is a BSD-licensed C++17 library to open and render 3D meshes. It is of course used by F3D.
-libf3d API is simple and easy to learn. Python bindings are provided through pybind11. Java bindings are also available.
-libf3d API is still in alpha version and may change drastically in the future.
-
-## Getting Started
-
-Rendering a file and starting the interaction is very easy:
-
-```cpp
-#include <f3d/engine.h>
-#include <f3d/interactor.h>
-#include <f3d/scene.h>
-
-// Load VTK native readers
-f3d::engine::autoloadPlugins();
-
-// Create a f3d::engine
-f3d::engine eng = f3d::engine::create();
-
-// Add a file into a scene
-eng.getScene().add("path/to/file.ext");
-
-// Start rendering and interacting
-eng.getInteractor().start();
-```
-
-As well as loading multiple files:
-
-```cpp
-#include <f3d/engine.h>
-#include <f3d/interactor.h>
-#include <f3d/scene.h>
-
-// Load VTK native readers
-f3d::engine::autoloadPlugins();
-
-// Create a f3d::engine
-f3d::engine eng = f3d::engine::create();
-
-// Load multiples geometries
-eng.getScene().add({"path/to/file.ext", "path/to/file2.ext"});
-
-// Start rendering and interacting
-eng.getInteractor().start();
-```
-
-It's also possible to load a geometry from memory buffers:
-
-```cpp
-#include <f3d/engine.h>
-#include <f3d/interactor.h>
-#include <f3d/scene.h>
-
-// Create a f3d::engine
-f3d::engine eng = f3d::engine::create();
-
-// Create a single triangle
-f3d::mesh_t mesh = {};
-mesh.points = { 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f };
-mesh.face_sides = { 3 };
-mesh.face_indices = { 0, 1, 2 };
-eng.getScene().add(mesh);
-
-// Start rendering and interacting
-eng.getInteractor().start();
-```
-
-Manipulating the window directly can be done this way:
-
-```cpp
-#include <f3d/engine.h>
-#include <f3d/image.h>
-#include <f3d/scene.h>
-#include <f3d/window.h>
-
-// Load VTK native readers
-f3d::engine::autoloadPlugins();
-
-// Create a f3d::engine with a offscreen window
-f3d::engine eng = f3d::engine::create(true);
-
-// Load a geometry
-eng.getScene().add("path/to/file.ext");
-
-// Set the window size and render to an image
-f3d::image img = eng.getWindow().setSize(300, 300).renderToImage();
-
-// Save the image to a file
-img.save("/path/to/img.png");
-```
-
-Changing some options can be done this way:
-
-```cpp
-#include <f3d/engine.h>
-#include <f3d/interactor.h>
-#include <f3d/options.h>
-#include <f3d/scene.h>
-
-// Load VTK native readers
-f3d::engine::autoloadPlugins();
-
-// Create a f3d::engine
-f3d::engine eng = f3d::engine::create();
-
-// Recover the options and set the wanted value
-f3d::options& opt = eng.getOptions();
-opt.render.effect.ambient_occlusion = true;
-opt.render.effect.antialiasing.enable = true;
-
-// Standard libf3d usage
-eng.getScene().add("path/to/file.ext");
-eng.getInteractor().start();
-```
-
-Most options are dynamic, some are only taken into account when loading a file. See the [options](OPTIONS.md) documentation.
-
-Find more examples in the [examples directory](https://github.com/f3d-app/f3d/tree/master/examples),
-you can also find other usages in the [testing directory](https://github.com/f3d-app/f3d/tree/master/library/testing).
-
-For the complete documentation, please consult the [libf3d doxygen documentation.](https://f3d.app/doc/libf3d/doxygen/).
-
-## Building against the libf3d
-
-Please follow instructions in the [F3D build guide](../dev/BUILD.md), make sure to build with `BUILD_SHARED_LIBS=ON` and to install the `sdk` component then use CMake to find the libf3d
-and link against it like this in your CMakeLists.txt:
-
-```cmake
-find_package(f3d REQUIRED COMPONENTS library)
-[...]
-target_link_libraries(target f3d::libf3d)
-```
-
-## find_package COMPONENTS
-
-When using `find_package(f3d)` and `sdk` CMake component have been installed, you can require specific components:
-
-- `application`: access to f3d::f3d target
-- `library`: access to f3d::libf3d target and include dir
-- `plugin_sdk`: access to CMake macros to create plugins, the f3d::vtkext target and include dir
-
-see the installed `f3dConfig.cmake` file for more info.
-
-## Extending libf3d with plugins
-
-In order to add new supported file format to libf3d, you can create a plugin using CMake macros. Please follow instructions in the [F3D plugin SDK guide](PLUGINS.md).
-Also make sure the `plugin_sdk` component have been installed as well as the `sdk` component.
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/PLUGINS.md f3d-3.4.1/doc/libf3d/PLUGINS.md
--- f3d-3.2.0+dfsg/doc/libf3d/PLUGINS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/PLUGINS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,85 +0,0 @@
-# Plugin SDK
-
-Please follow instructions in the [F3D build guide](../dev/BUILD.md), make sure to build with `BUILD_SHARED_LIBS=ON` and to install the `plugin_sdk` component.
-You will then be able to call `find_package(f3d REQUIRED COMPONENTS pluginsdk)` in your plugin CMakeLists.txt, a few macros are made available to you to generate a plugin which allow you to extend libf3d to support your own file format. Access to a f3d::vtkext VTK module is also provided if needed.
-
-> Please consider [contributing](../../CONTRIBUTING.md) your plugin in [F3D directly](https://github.com/f3d-app/f3d/tree/master/plugins) if you think it can be useful to the community.
-> You can also consider contributing directly [in VTK](https://gitlab.kitware.com/vtk/vtk/blob/master/Documentation/dev/git/develop.md).
-
-## Creating a plugin
-
-You can take a look at the example in the [examples/plugin](https://github.com/f3d-app/f3d/tree/master/examples/plugins) directory or at the official [plugins](https://github.com/f3d-app/f3d/tree/master/plugins).
-
-The first thing (and most difficult part) you have to do is creating a VTK reader (or a VTK importer if you want to support a full scene with materials, lights and cameras), and wrap it into a VTK module. You can create several readers/importers in the same VTK module if you need to support several file formats in a single plugin.
-
-Then, declare the reader(s) and the plugin using the CMake macros:
-
-```cmake
-f3d_plugin_init()
-
-f3d_plugin_declare_reader(
-  NAME "ReaderName"
-  EXTENSIONS "myext"                # set the extensions the reader can support
-  MIMETYPES "application/vnd.myext" # set the mimetypes the reader can support
-  VTK_READER ${vtk_classname}       # set the name of the VTK reader class you have created
-  FORMAT_DESCRIPTION "description"  # set the proper name of the file format
-  EXCLUDE_FROM_THUMBNAILER          # add this flag if you don't want thumbnail generation for this reader
-  OPTIONS "option1" "option2"       # use this to define reader specific option that can be defined by the user
-)
-
-f3d_plugin_declare_reader(
-  NAME "ReaderNameWithScene"
-  EXTENSIONS "myext2"                # set the extensions the reader can support
-  MIMETYPES "application/vnd.myext2" # set the mimetypes the reader can support
-  VTK_IMPORTER ${vtk_classname}      # set the name of the VTK importer class you have created
-  FORMAT_DESCRIPTION "description"   # set the proper name of the file format
-  CUSTOM_CODE "file.inl"             # set this to add a custom code when instancing your class, this is where reader options should be processed
-)
-
-# More f3d_plugin_declare_reader calls are possible
-
-f3d_plugin_build(
-  NAME "PluginName"
-  VERSION 1.0
-  DESCRIPTION "Plugin description"
-  VTK_MODULES CommonCore CommonExecutionModel # Add more VTK modules if necessary
-)
-```
-
-If the build succeeds, a library called `libf3d-plugin-<name>.so` will be created (`f3d-plugin-<name>.dll` on Windows)
-A JSON file of the following form will also be generated. It's used by F3D internally to get information about supported file formats.
-
-```json
-{
-  "description": "Plugin description",
-  "name": "PluginName",
-  "readers": [
-    {
-      "description": "Reader description",
-      "extensions": ["myext"],
-      "mimetypes": ["application/vnd.myext"],
-      "name": "ReaderName"
-    }
-  ],
-  "type": "MODULE",
-  "version": "1.0"
-}
-```
-
-The list of existing mimetypes can be find [here](https://www.iana.org/assignments/media-types/media-types.xhtml). If your file format is not listed, the mimetype should be `application/vnd.${extension}`
-
-## Loading your plugin
-
-The plugin can be loaded using `f3d::engine::loadPlugin("path or name")` API if you are using libf3d, or `--load-plugins="path or name"` option if you are using F3D application.
-The option can also be set in a configuration file that you could distribute with your plugin.
-
-## f3d::vtkext
-
-F3D provides access to a VTK modules containing utilities that may be useful for plugin developers:
-
-- `vtkF3DFaceVaryingPointDispatcher`: A VTK filter that manipulates point data so that F3D can display them as face-varying data (used by `usd` plugin)
-- `vtkF3DBitonicSort`: A VTK class that perform Bitonic Sort algorithm on the GPU (used by the translucent point sprites rendering algorithm)
-- `vtkF3DImporter`: An Importer class that abstract away support for different version of VTK after some API changes.
-- `vtkF3DGLTFImporter`: An custom glTF importer class that support armatures, useful when creating other plugin supporting glTF extensions.
-
-For the complete documentation, please consult the [vtkext doxygen documentation.](https://f3d.app/doc/libf3d/vtkext_doxygen/).
diff -Nru f3d-3.2.0+dfsg/doc/libf3d/README_LIBF3D.md f3d-3.4.1/doc/libf3d/README_LIBF3D.md
--- f3d-3.2.0+dfsg/doc/libf3d/README_LIBF3D.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/libf3d/README_LIBF3D.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,7 +0,0 @@
-# libf3d Documentation
-
-- [Overview of the libf3d.](OVERVIEW.md)
-- [libf3d classes introduction.](CLASSES.md)
-- [Exhaustive list of libf3d options.](OPTIONS.md)
-- [How to use libf3d language bindings.](LANGUAGE_BINDINGS.md)
-- [How to create a plugin.](PLUGINS.md)
diff -Nru f3d-3.2.0+dfsg/doc/THIRD_PARTY_LICENSES.md f3d-3.4.1/doc/THIRD_PARTY_LICENSES.md
--- f3d-3.2.0+dfsg/doc/THIRD_PARTY_LICENSES.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/THIRD_PARTY_LICENSES.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,268 +0,0 @@
-# Third Party Copyrights and License within F3D application source
-
-## cxxopts.hpp:
-
-```
-Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck
-
-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.
-```
-
-## json.hpp:
-
-```
-    __ _____ _____ _____
- __|  |   __|     |   | |  JSON for Modern C++
-|  |  |__   |  |  | | | |  version 3.10.5
-|_____|_____|_____|_|___|  https://github.com/nlohmann/json
-
-Licensed under the MIT License <http://opensource.org/licenses/MIT>.
-SPDX-License-Identifier: MIT
-Copyright (c) 2013-2022 Niels Lohmann <http://nlohmann.me>.
-
-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.
-```
-
-## dmon.h:
-
-```
-BSD 2-Clause License
-
-Copyright (c) 2019, Sepehr Taghdisian (septag@protonmail.com)
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-```
-
-## ImGui
-
-```
-The MIT License (MIT)
-
-Copyright (c) 2014-2024 Omar Cornut
-
-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.
-```
-
-## MonaspaceNeon-Regular.0tf
-
-```
-Copyright (c) 2023, GitHub https://github.com/githubnext/monaspace
-with Reserved Font Name "Monaspace", including subfamilies: "Argon", "Neon", "Xenon", "Radon", and "Krypton"
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting — in part or in whole — any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
-```
-
-### tinyfiledialogs
-
-```
-SPDX-License-Identifier: Zlib
-Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
-	 ________________________________________________________________
-	|                                                                |
-	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
-	|________________________________________________________________|
-
-********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
-  _________
- /         \ tinyfiledialogs.c v3.18.2 [Jun 8, 2024] zlib licence
- |tiny file| Unique code file created [November 9, 2014]
- | dialogs |
- \____  ___/ http://tinyfiledialogs.sourceforge.net
-	  \|     git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
-			  ____________________________________________
-			 |                                            |
-			 |   email: tinyfiledialogs at ysengrin.com   |
-			 |____________________________________________|
-  _________________________________________________________________________________
- |                                                                                 |
- | the windows only wchar_t UTF-16 prototypes are at the bottom of the header file |
- |_________________________________________________________________________________|
-  _________________________________________________________
- |                                                         |
- | on windows: - since v3.6 char is UTF-8 by default       |
- |             - if you want MBCS set tinyfd_winUtf8 to 0  |
- |             - functions like fopen expect MBCS          |
- |_________________________________________________________|
-
-If you like tinyfiledialogs, please upvote my stackoverflow answer
-https://stackoverflow.com/a/47651444
-
-- License -
-This software is provided 'as-is', without any express or implied
-warranty.  In no event will the authors be held liable for any damages
-arising from the use of this software.
-Permission is granted to anyone to use this software for any purpose,
-including commercial applications, and to alter it and redistribute it
-freely, subject to the following restrictions:
-1. The origin of this software must not be misrepresented; you must not
-claim that you wrote the original software.  If you use this software
-in a product, an acknowledgment in the product documentation would be
-appreciated but is not required.
-2. Altered source versions must be plainly marked as such, and must not be
-misrepresented as being the original software.
-3. This notice may not be removed or altered from any source distribution.
-
-     __________________________________________
-    |  ______________________________________  |
-    | |                                      | |
-    | | DO NOT USE USER INPUT IN THE DIALOGS | |
-    | |______________________________________| |
-    |__________________________________________|
-```
diff -Nru f3d-3.2.0+dfsg/doc/user/01-QUICKSTART.md f3d-3.4.1/doc/user/01-QUICKSTART.md
--- f3d-3.2.0+dfsg/doc/user/01-QUICKSTART.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/01-QUICKSTART.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,102 @@
+# Quickstart Guide
+
+This guide will help you get started using F3D.
+
+As an overview, you'll learn how to run F3D and open files, configure basic scene constructions, interact with them, and play animations.
+
+## Prerequisites
+
+To use F3D, you'll need to install F3D. See the download page for the latest release package supported by your system.
+
+## Running F3D
+
+Once F3D has been installed, you should be able to open a file in any of the following ways:
+
+- Open a file directly from your file manager.
+- Open the F3D application directly, then drag and drop a file into the application window.
+- Run F3D from the terminal with a set of command-line [options](03-OPTIONS.md).
+- Configure F3D as a [thumbnailer](11-DESKTOP_INTEGRATION.md) for supported file formats with certain file managers.
+
+If you choose to run F3D from the terminal, you can open your file by running the following:
+
+```
+f3d /path/to/file.ext
+```
+
+Optionally, you can also save the rendering into an image file:
+
+```
+f3d /path/to/file.ext --output=/path/to/img.png
+```
+
+If you need help, specify the `--help` [option](03-OPTIONS.md):
+
+```
+f3d --help
+man f3d # Linux only
+```
+
+Once you've opened your file in F3D, you're all set to start visualizing! Press <kbd>H</kbd> to open a list of shortcuts to help you interact with your scene.
+
+See the [supported file formats](02-SUPPORTED_FORMATS.md) page for the full list of file types that F3D can open.
+
+## Constructing scenes
+
+Certain [formats](02-SUPPORTED_FORMATS.md) are listed as **full scene** formats (.gltf/.glb, .3ds, .wrl, .obj, .fbx, .dae, .off, .x, .3mf, .usd)
+which means these files contain not only _geometry_ but also some scene information like _lights_, _cameras_, _actors_ in the scene,
+as well as _texture_ properties. By default, all this information will be loaded from the file and displayed.
+For file formats that are not **full scene**, **a default scene** is created.
+
+For **default scene** formats, certain default values are set automatically:
+
+- texture-\*: ""
+- line-width: 1.0
+- point-size: 10.0
+- opacity: 1.0
+- color: 1.0, 1.0, 1.0
+- emissive-factor: 1.0, 1.0, 1.0
+- normal-scale: 1.0
+- metallic: 0.0
+- roughness: 0.3
+- base-ior: 1.5
+- camera-orthographic: false
+
+They will be overridden when using corresponding [options](03-OPTIONS.md), either from command line or [configuration file](06-CONFIGURATION_FILE.md).
+
+## Interacting with your scene
+
+Once you've loaded your scene in F3D, you can interact with it by using your mouse and certain hotkeys.
+
+### Moving the camera
+
+- _Click and drag_ with the _left_ mouse button to **rotate** around the focal point of the camera.
+- _Click and drag_ vertically with the _right_ mouse button <u>OR</u> _move the mouse wheel_ to **zoom in/out**.
+- _Click and drag_ with the _middle_ mouse button to **translate** the camera.
+
+### Other shortcuts
+
+- Press <kbd>Enter</kbd> to reset the camera.
+- Press <kbd>Space</kbd> to play animation, if any.
+- Press <kbd>G</kbd> to toggle the horizontal grid.
+- Press <kbd>H</kbd> to display a cheatsheet of hotkey interactions.
+
+For more information, see the [Interactions](04-INTERACTIONS.md) page.
+
+## Playing animations
+
+F3D can play [animations](05-ANIMATIONS.md) for any [supported files](02-SUPPORTED_FORMATS.md) that contain them.
+
+With your file loaded into F3D, press the <kbd>W</kbd> hotkey to cycle through available animations. Then, press <kbd>Space</kbd> to play your selected animation.
+
+Alternatively, you can use the command line to play animations. Use the `--animation-indices` [option](03-OPTIONS.md) to specify which animation you want to play. To play all animations at once, use `--animation-indices=-1`.
+
+For more information, see the [Animations](05-ANIMATIONS.md) page.
+
+## Further Reading
+
+- [A list of all F3D command-line options.](03-OPTIONS.md)
+- [How to interact with scenes in F3D.](04-INTERACTIONS.md)
+- [How to play animations in F3D.](05-ANIMATIONS.md)
+- [How to specify colormaps in F3D.](09-COLOR_MAPS.md)
+- [How to configure plugins in F3D.](12-PLUGINS.md)
+- [Limitations and how to troubleshoot F3D.](13-LIMITATIONS_AND_TROUBLESHOOTING.md)
diff -Nru f3d-3.2.0+dfsg/doc/user/02-SUPPORTED_FORMATS.md f3d-3.4.1/doc/user/02-SUPPORTED_FORMATS.md
--- f3d-3.2.0+dfsg/doc/user/02-SUPPORTED_FORMATS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/02-SUPPORTED_FORMATS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,88 @@
+# Supported File Formats
+
+F3D supports the following file formats:
+
+| Name                                      | File Extension(s)                              | Full scene | Animation Support | Plugin    | Reader Name             |
+| ----------------------------------------- | ---------------------------------------------- | ---------- | ----------------- | --------- | ----------------------- |
+| Legacy VTK                                | `.vtk`                                         | No         | NONE              | `native`  | `VTKLegacy`             |
+| VTK XML                                   | `.vtp`, `.vtu`, `.vtr`, `.vti`, `.vts`, `.vtm` | No         | NONE              | `native`  | `VTKXMLVT<P,U,R,I,S,M>` |
+| VTKHDF                                    | `.vtkhdf`                                      | No         | UNIQUE            | `hdf`     | `VTKHDF`                |
+| EXODUS II                                 | `.e`, `.ex2`, `.exo`, `.g`                     | No         | UNIQUE            | `hdf`     | `ExodusII`              |
+| NetCDF                                    | `.nc`, `.cdf`, `.ncdf`                         | No         | UNIQUE            | `hdf`     | `NetCDF`                |
+| Polygon File Format                       | `.ply`                                         | No         | NONE              | `native`  | `PLYReader`             |
+| Standard Triangle Language                | `.stl`                                         | No         | NONE              | `native`  | `STL`                   |
+| DICOM                                     | `.dcm`                                         | No         | NONE              | `native`  | `DICOM`                 |
+| NRRD ("nearly raw raster data")           | `.nrrd`, `.nhdr`                               | No         | NONE              | `native`  | `Nrrd`                  |
+| MetaHeader MetaIO                         | `.mhd`, `.mha`                                 | No         | NONE              | `native`  | `MetaImage`             |
+| Tag Image File Format 2D/3D               | `.tif`, `.tiff`                                | No         | NONE              | `native`  | `TIFF`                  |
+| QuakeMDL                                  | `.mdl`                                         | Yes        | SINGLE            | `native`  | `QuakeMDL`              |
+| CityGML                                   | `.gml`                                         | No         | NONE              | `native`  | `CityGML`               |
+| Point Cloud                               | `.pts`                                         | No         | NONE              | `native`  | `PTS`                   |
+| Standard for the Exchange of Product Data | `.step`, `.stp`, `.stpnc`, `.p21`, `.210`      | No         | NONE              | `occt`    | `STEP`                  |
+| Initial Graphics Exchange Specification   | `.iges`, `.igs`                                | No         | NONE              | `occt`    | `IGES`                  |
+| Open CASCADE Technology BRep format       | `.brep`                                        | No         | NONE              | `occt`    | `BREP`                  |
+| Open CASCADE Technology XBF format        | `.xbf`                                         | No         | NONE              | `occt`    | `XBF`                   |
+| Alembic                                   | `.abc`                                         | No         | UNIQUE            | `alembic` | `Alembic`               |
+| Wavefront OBJ                             | `.obj`                                         | Yes        | NONE              | `native`  | `OBJ`                   |
+| GL Transmission Format                    | `.gltf`, `.glb`                                | Yes        | MULTI             | `native`  | `GLTF`, `GLTFDraco`     |
+| Draco                                     | `.drc`                                         | No         | NONE              | `draco`   | `Draco`                 |
+| Autodesk 3D Studio                        | `.3ds`                                         | Yes        | NONE              | `native`  | `3DS`                   |
+| Virtual Reality Modeling Language         | `.wrl`, `.vrml`                                | Yes        | NONE              | `native`  | `VRMLReader`            |
+| Autodesk Filmbox                          | `.fbx`                                         | Yes        | SINGLE            | `assimp`  | `FBX`                   |
+| COLLADA                                   | `.dae`                                         | Yes        | SINGLE            | `assimp`  | `COLLADA`               |
+| Object File Format                        | `.off`                                         | Yes        | NONE              | `assimp`  | `OFF`                   |
+| Drawing Exchange Format                   | `.dxf`                                         | Yes        | NONE              | `assimp`  | `DXF`                   |
+| DirectX                                   | `.x`                                           | Yes        | SINGLE            | `assimp`  | `DirectX`               |
+| 3D Manufacturing Format                   | `.3mf`                                         | Yes        | NONE              | `assimp`  | `3MF`                   |
+| Universal Scene Description               | `.usd`, `.usda`, `.usdc`, `.usdz`              | Yes        | SINGLE            | `usd`     | `USD`                   |
+| VDB                                       | `.vdb`                                         | No         | NONE              | `vdb`     | `VDB`                   |
+| 3D Gaussian splatting                     | `.splat`                                       | No         | NONE              | `native`  | `Splat`                 |
+| Compressed 3D Gaussian splatting          | `.spz`                                         | No         | NONE              | `native`  | `SPZ`                   |
+
+## Reader options
+
+Readers can provide option that can be set using the `-D/--define` [command line option](03-OPTIONS.md).
+eg: `-DVDB.downsampling_factor=0.5` or using the `set_reader_option` [command](07-COMMANDS.md).
+
+For booleans, 0 means false, not 0 means true. Unsigned int will interpret anything that is not a non-negative integer as the default value.
+
+| File extension | Option Name                | Argument Type  | Description                                                                          |
+| -------------- | -------------------------- | -------------- | ------------------------------------------------------------------------------------ |
+| `vdb`          | `VDB.downsampling_factor`  | `double`       | Control the level of downsampling when reading a volume, default is 0.1.             |
+| `occt`         | `STEP.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
+| `occt`         | `STEP.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
+| `occt`         | `STEP.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
+| `occt`         | `STEP.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
+| `occt`         | `IGES.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
+| `occt`         | `IGES.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
+| `occt`         | `IGES.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
+| `occt`         | `IGES.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
+| `occt`         | `BREP.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
+| `occt`         | `BREP.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
+| `occt`         | `BREP.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
+| `occt`         | `BREP.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
+| `occt`         | `XBF.linear_deflection`    | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
+| `occt`         | `XBF.angular_deflection`   | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
+| `occt`         | `XBF.relative_deflection`  | `bool`         | Control if the deflection values are relative to object size, default is false.      |
+| `occt`         | `XBF.read_wire`            | `bool`         | Control if lines should be read, default is true.                                    |
+| `mdl`          | `QuakeMDL.skin_index`      | `unsigned int` | Select a particular skin from a `mdl` file. Uses 0-indexing, default is 0.           |
+
+## Format details
+
+### QuakeMDL
+
+- Models texture are loaded with a simple PBR lighting (diffuse color only, no specular, index of refraction set to 1.0).
+- Selecting `skin` is not supported.
+- Animation frames are split based on their names, eg: `stand1`, `stand2`, `stand3`, `run1`, `run2`, `run3`.
+
+### 3D Gaussian splatting
+
+Currently, 3 different formats are supported by F3D:
+
+- `.ply`: Original 3DGS format
+- `.splat`: Format specified by https://github.com/antimatter15/splat. Does not support spherical harmonics.
+- `.spz`: Niantic's format specified by https://github.com/nianticlabs/spz (v2 and v3)
+
+Note that no config files come with the `.ply` format because this format isn't dedicated to 3DGS only so we cannot generalize.
+If you are using `.ply` for 3DGS only, you can set up a config file similar to what is done for `.splat` or `.spz`.
+See configuration file [documentation](./06-CONFIGURATION_FILE.md)
diff -Nru f3d-3.2.0+dfsg/doc/user/03-OPTIONS.md f3d-3.4.1/doc/user/03-OPTIONS.md
--- f3d-3.2.0+dfsg/doc/user/03-OPTIONS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/03-OPTIONS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,578 @@
+# Command line options
+
+F3D behavior can be fully controlled from the command line using the following options.
+
+## Application Options
+
+### `--input=<input file>` (_string_)
+
+The input file or files to read, can also be provided as a positional argument. Support directories as well.
+
+### `--output=<png file>` (_string_)
+
+Instead of showing a render view and render into it, _render directly into a png file_. When used with --ref option, only outputs on failure. If `-` is specified instead of a filename, the PNG file is streamed to the stdout. Can use [template variables](#filename-templating).
+
+### `--no-background` (_bool_, default: `false`)
+
+Use with --output to output a png file with a transparent background.
+
+### `-h`, `--help`
+
+Print _help_ and exit. Ignore `--verbose`.
+
+### `--version`
+
+Show _version_ information and exit. Ignore `--verbose`.
+
+### `--list-readers`
+
+List available _readers_ and exit. Ignore `--verbose`.
+
+### `--force-reader=<reader>` (_string_)
+
+Force a specific [reader](02-SUPPORTED_FORMATS.md) to be used, disregarding the file extension.
+
+### `--list-bindings`
+
+List available _bindings_ and exit. Ignore `--verbose`.
+
+### `--list-rendering-backends`
+
+List available _rendering backends_ and exit. Ignore `--verbose`.
+
+### `--config=<config file path/name/stem>` (_string_, default: `config`)
+
+Specify the [configuration file](06-CONFIGURATION_FILE.md) to use. Supports absolute/relative path but also filename/filestem to search for in standard configuration file locations.
+
+### `--no-config` (_bool_, default: `false`)
+
+Do not read any configuration file and consider only the command line options.
+
+### `--no-render` (_bool_, default: `false`)
+
+Do not render anything and quit just after loading the first file, use with --verbose to recover information about a file.
+
+### `--max-size=<size in MiB>` (_int_, default: `-1`)
+
+Prevent F3D to load a file bigger than the provided size in Mib, leave empty for unlimited, useful for thumbnails.
+
+### `--watch` (_bool_, default: `false`)
+
+Watch current file and automatically reload it whenever it is modified on disk. Consider ensuring `--remove-empty-file-groups` is not enabled when using this option.
+
+### `--frame-rate=<fps>` (_double_, default: `30.0`)
+
+Frame rate used to refresh animation and other repeated tasks (watch, UI). Does not impact rendering frame rate.
+
+### `--load-plugins=<paths or names>` (_string_)
+
+List of plugins to load separated with a comma. Official plugins are `alembic`, `assimp`, `draco`, `hdf`, `occt`, `usd`, `vdb`. See [plugins](12-PLUGINS.md) for more info.
+
+### `--scan-plugins`
+
+Scan standard directories for plugins and display their names, results may be incomplete. See [plugins](12-PLUGINS.md) for more info.
+
+### `--screenshot-filename=<png file>` (_string_, default: `{app}/{model}_{n}.png`)
+
+Filename to save [screenshots](04-INTERACTIONS.md#taking-screenshots) to. Can use [template variables](#filename-templating). Supports relative paths [as described](04-INTERACTIONS.md#taking-screenshots).
+
+### `--rendering-backend=<auto|egl|osmesa|glx|wgl>` (_string_, default: `auto`)
+
+Rendering backend to load, `auto` means to let F3D pick the correct one for you depending on your system capabilities. Use `egl` or `osmesa` on linux to force headless rendering.
+
+### `-D`, `--define=<libf3d.option=value>` (_special_)
+
+A repeatable option to set [libf3d](../libf3d/03-OPTIONS.md) and [reader](02-SUPPORTED_FORMATS.md#reader-options) option manually. May trigger unexpected behavior.
+
+### `-R`, `--reset=<libf3d.option>` (_special_)
+
+A repeatable option to reset [libf3d options](../libf3d/03-OPTIONS.md) manually. Useful when overidding option set in [configuration files](06-CONFIGURATION_FILE.md).
+
+## General Options
+
+### `--verbose=<[debug|info|warning|error|quiet]>` (_string_, default: `info`)
+
+Set _verbose_ level, in order to provide more information about the loaded data in the output. If no level is provided, assume `debug`. Option parsing may ignore this flag.
+
+### `--progress` (_bool_, default: `false`)
+
+Show a _progress bar_ when loading the file.
+
+### `--animation-progress` (_bool_, default: `false`)
+
+Show a _progress bar_ when playing the animation.
+
+### `--multi-file-mode=<single|all| dir>` (_string_, default: `single`)
+
+When opening multiple files, select if they should be shown all at once (`all`), one by one (`single`), or by directory (`dir`). Configuration files for all loaded files will be used in the order they are provided.
+
+### `--multi-file-regex=<regex>` (_string_)
+
+Regular expression pattern to group files. Captured groups are replaced with `*` so that, for example, the pattern `part(\d+)` would group files `foo-part1.xyz` and `foo-part2.xyz` together as `foo-part*.xyz`.
+
+### `--recursive-dir-add` (_bool_, default: `false`)
+
+When opening a directory, choose if they should be recursively added or not. If not, only the files in the provided directory will be added.
+
+### `--remove-empty-file-groups` (_bool_, default: `false`)
+
+When loading a file group, if they results in an empty scene, remove the file group and load the next file group.
+
+### `--up=<direction>` (_direction_, default: `+Y`)
+
+Define the Up direction.
+
+### `-x`, `--axis` (_bool_, default: `false`)
+
+Show _axes_ as a trihedron in the scene.
+
+### `--x-color` (_color_, default: `#f94306`)
+
+Set the X axis color in both widget and grid.
+
+### `--y-color` (_color_, default: `#7aff7a`)
+
+Set the Y axis color in both widget and grid.
+
+### `--z-color` (_color_, default: `#788bff`)
+
+Set the Z axis color in both widget and grid.
+
+### `-g`, `--grid` (_bool_, default: `false`)
+
+Show _a grid_ aligned with the horizontal (orthogonal to the Up direction) plane.
+
+### `--grid-unit=<length>` (_double_)
+
+Set the size of the _unit square_ for the grid. If not set (the default) a suitable value will be automatically computed.
+
+### `--grid-subdivisions=<count>` (_int_, default: `10`)
+
+Set the number of subdivisions for the grid.
+
+### `--grid-color=<color>` (_color_, default: `0,0,0`)
+
+Set the color grid lines.
+
+### `--grid-absolute` (_bool_, default: `false`)
+
+Position the grid at absolute origin instead of below the model.
+
+### `--axes-grid` (_bool_, default: `false`)
+
+Show _axes grid_ in the scene.
+
+### `-e`, `--edges` (_bool_, default: `false`)
+
+Show the _cell edges_.
+
+### `--armature` (_bool_, default: `false`)
+
+Show armature if present (glTF only).
+
+### `--camera-index=<idx>` (_int_)
+
+Select the scene camera to use when available in the file. Automatically computed by default.
+
+### `-k`, `--trackball` (_bool_, default: `false`)
+
+Enable trackball interaction.
+
+### `--invert-zoom` (_bool_, default: `false`)
+
+Invert zoom direction with right mouse click.
+
+### `--animation-autoplay` (_bool_, default: `false`)
+
+Automatically start animation.
+
+### `--animation-indices=<idx1,idx2>` (_vector\<int\>_, default: `0`)
+
+Select the animations to show.
+Any negative value all animations.
+The default scene always has at most one animation.
+
+### `--animation-speed-factor=<ratio>` (_ratio_, default: `1`)
+
+Set the animation speed factor to slow, speed up or even invert animation time.
+
+### `--animation-time=<time>` (_double_)
+
+Set the animation time to load.
+
+### `--font-file=<font file>` (_path_)
+
+Use the provided FreeType compatible font file to display text.
+Can be useful to display non-ASCII filenames.
+
+### `--font-scale=<ratio>` (_ratio_, default: `1.0`)
+
+Scale fonts. Useful for HiDPI displays.
+
+### `--command-script=<command script>` (_script_)
+
+Provide a script file containing a list of [commands](07-COMMANDS.md) to be executed sequentially.
+Allows automation of multiple commands or pre-defined tasks.
+
+### `--backdrop-opacity=<opacity>` (_double_, default: `0.9`)
+
+Set the opacity of the backdrop behind text information such as FPS, filename, metadata or cheatsheet.
+
+## Material options
+
+### `-o`, `--point-sprites=<none|sphere|gaussian>` (_string_, default: `none`)
+
+Select _points sprites_ types to show instead of the geometry.
+
+### `--point-sprites-size=<size>` (_double_, default: `10.0`)
+
+Set the _size_ of point sprites.
+
+### `--point-sprites-absolute-size` (_bool_, default: `false`)
+
+Do not scale the point sprites size by the scene bounding box.
+
+### `--point-size=<size>` (_double_)
+
+Set the _size_ of points when showing vertices. Model-specified by default.
+
+### `--line-width=<size>` (_double_)
+
+Set the _width_ of lines when showing edges. Model-specified by default.
+
+### `--backface-type=<visible|hidden>` (_string_)
+
+Set the Backface type. Model-specified by default.
+
+### `--color=<color>` (_color_)
+
+Set a _color_ on the geometry. Multiplied with the base color texture when present.
+Model-specified by default.
+
+### `--opacity=<opacity>` (_double_)
+
+Set _opacity_ on the geometry. Multiplied with the base color texture when present.
+Model-specified by default. Usually used with `--blending`.
+
+### `--roughness=<roughness>` (_double_)
+
+Set the _roughness coefficient_ on the geometry (0.0-1.0). Multiplied with the material texture when present.
+Model-specified by default.
+
+### `--metallic=<metallic>` (_double_)
+
+Set the _metallic coefficient_ on the geometry (0.0-1.0). Multiplied with the material texture when present.
+Model-specified by default.
+
+### `--base-ior=<base-ior>` (_double_)
+
+Set the _index of refraction of the base layer_ (1.0-2.5). Model-specified by default.
+
+### `--hdri-file=<HDRI file>` (_path_)
+
+Set the _HDRI_ image that can be used as ambient lighting and skybox.
+Valid file format are `.hdr`, `.exr`, `.png`, `.jpg`, `.pnm`, `.tiff`, `.bmp`.
+If not set, a default is provided.
+
+### `--hdri-ambient` (_string_)
+
+Light the scene using the _HDRI_ image as ambient lighting.
+The environment act as a light source and is reflected on the material.
+
+### `--texture-matcap=<texture file>` (_path_)
+
+Set the texture file to control the material capture of the object. All other model options for surfaces are ignored if this is set. Must be in linear color space.
+Model-specified by default.
+
+### `--texture-base-color=<texture file>` (_path_)
+
+Set the texture file to control the color of the object. Please note this will be multiplied with the color and opacity options. Must be in sRGB color space.
+Model-specified by default.
+
+### `--texture-material=<texture file>` (_path_)
+
+Set the texture file to control the occlusion, roughness and metallic values of the object. Please note this will be multiplied with the roughness and metallic options, which have impactful default values. To obtain true results, use `--roughness=1` and `--metallic=1`. Must be in linear color space.
+Model-specified by default.
+
+### `--texture-emissive=<texture file>` (_path_)
+
+Set the texture file to control the emitted light of the object. Please note this will be multiplied with the emissive factor. Must be in sRGB color space.
+Model-specified by default.
+
+### `--emissive-factor=<color>` (_color_)
+
+Set the emissive factor. This value is multiplied with the emissive color when an emissive texture is present.
+Model-specified by default.
+
+### `--texture-normal=<texture file>` (_path_)
+
+Set the texture file to control the normal map of the object. Must be in sRGB color space.
+Model-specified by default.
+
+### `--normal-scale=<color>` (_double_)
+
+Set the normal scale. This value affects the strength of the normal deviation from the normal texture.
+Model-specified by default.
+
+### `--textures-transform=<transform2d>` (_transform2d_)
+
+Set the 2d transform to use for all textures applied to the model.
+Importer may set a default value depending on file type. If a default value exists, the default value is multiplied by the provided transform.
+
+## Window options
+
+### `--background-color=<color>` (_color_, default: `0.2, 0.2, 0.2`)
+
+Set the window _background color_.
+Ignored if `--hdri-skybox` is enabled.
+
+### `--resolution=<width,height>` (_vector\<double\>_, default: `1000, 600`)
+
+Set the _window resolution_.
+
+### `--position=<x,y>` (_vector\<double\>_)
+
+Set the _window position_ (top left corner) , in pixels, starting from the top left of your screens.
+
+### `-z`, `--fps` (_bool_, default: `false`)
+
+Display a rendering _frame per second counter_.
+
+### `-n`, `--filename` (_bool_, default: `false`)
+
+Display the _name of the file_ on top of the window.
+
+### `-m`, `--metadata` (_bool_, default: `false`)
+
+Display the _metadata_.
+
+### `--hdri-filename` (_bool_, default: `false`)
+
+Display the _hdri-filename_.
+
+### `--hdri-skybox` (_bool_, default: `false`)
+
+Show the HDRI as a skybox. Overrides `--background-color` and `--no-background`.
+
+### `-u`, `--blur-background` (_bool_, default: `false`)
+
+Blur background.
+Useful with a HDRI skybox.
+
+### `--blur-coc` (_double_, default: `20`)
+
+Blur circle of confusion radius.
+
+### `--light-intensity` (_double_, default: `1.0`)
+
+_Adjust the intensity_ of every light in the scene.
+
+## Scientific visualization options
+
+### `-s`, `--scalar-coloring` (_bool_, default: `false`)
+
+Enable scalar coloring if present in the file. If `--coloring-array` is not set, the first in alphabetical order will be picked if any are available.
+
+### `--coloring-array=<array_name>` (_string_)
+
+The coloring array name to use when coloring.
+Use `--verbose` to recover the usable array names.
+
+### `-y`, `--comp=<comp_index>` (_int_, default: `-1`)
+
+Specify the _component from the scalar_ array to color with.
+Use with the scalar option. -1 means _magnitude_. -2 or the short option, -y, means _direct values_.
+When using _direct values_, components are used as L, LA, RGB, RGBA values depending on the number of components.
+
+### `-c`, `--cells` (_bool_, default: `false`)
+
+Specify that the scalar array is to be found _on the cells_ instead of on the points.
+Use with the scalar option.
+
+### `--range=<min,max>` (_vector\<double\>_)
+
+Set the _coloring range_. Automatically computed by default.
+Use with the scalar option.
+
+### `-b`, `--bar` (_bool_, default: `false`)
+
+Show _scalar bar_ of the coloring by array.
+Use with the scalar option.
+
+### `--colormap-file=<name>` (_string_)
+
+Set a _colormap file for the coloring_.
+See [color maps](09-COLOR_MAPS.md).
+Use with the scalar option.
+
+### `--colormap=<colormap>` (_colormap_)
+
+Set a _custom colormap for the coloring_.See [colormap parsing](08-PARSING.md#colormap) for details.
+Ignored if `--colormap-file` option is specified.
+Use with the scalar option.
+
+### `--colormap-discretization=<colors>` (_int_)
+
+Set the number of distinct colors from [1, N] will be used in the colormap. Any values outside the valid range will result in smooth shading.
+
+### `-v`, `--volume` (_bool_, default: `false`)
+
+Enable _volume rendering_. It is only functional for 3D image data (VTKXMLVTI, DICOM, NRRD, MetaImage files) and will display nothing with other formats. It forces coloring.
+
+### `-i`, `--inverse` (_bool_, default: `false`)
+
+Inverse the linear opacity function used for volume rendering.
+
+## Camera configuration options
+
+### `--camera-position=<X,Y,Z>` (_vector\<double\>_)
+
+Set the camera position, overrides `--camera-direction` and `--camera-zoom-factor`.
+
+### `--camera-focal-point=<X,Y,Z>` (_vector\<double\>_)
+
+Set the camera focal point.
+
+### `--camera-view-up=<direction>` (_direction_)
+
+Set the camera view up vector. Will be orthogonalized.
+
+### `--camera-view-angle=<angle>` (_double_)
+
+Set the camera view angle, a strictly positive value in degrees.
+
+### `--camera-direction=<direction>` (_direction_)
+
+Set the camera direction, looking at the focal point.
+
+### `--camera-zoom-factor=<factor>` (_double_)
+
+Set the camera zoom factor relative to the autozoom on data, a strictly positive value.
+
+### `--camera-azimuth-angle=<angle>` (_double_, default: `0.0`)
+
+Apply an azimuth transformation to the camera, in degrees, added after other camera options.
+
+### `--camera-elevation-angle=<angle>` (_double_, default: `0.0`)
+
+Apply an elevation transformation to the camera, in degrees, added after other camera options.
+
+### `--camera-orthographic` (_bool_)
+
+Set the camera to use the orthographic projection. Model-specified by default.
+
+## Raytracing options
+
+### `-r`, `--raytracing` (_bool_, default: `false`)
+
+Enable _OSPRay raytracing_. Requires OSPRay raytracing to be enabled in the linked VTK dependency.
+
+### `--samples=<samples>` (_int_, default: `5`)
+
+Set the number of _samples per pixel_ when using raytracing.
+
+### `-d`, `--denoise` (_bool_, default: `false`)
+
+_Denoise_ the image when using raytracing.
+
+## PostFX (OpenGL) options
+
+### `-p`, `--blending` (_string_, default: `ddp`)
+
+Enable _translucency blending support_.
+This is a technique used to correctly render translucent objects (`ddp`: dual depth peeling for quality, `sort`: for gaussians, `stochastic`: fast).
+
+> [!WARNING]
+> `stochastic` is introducing a lot of noise with strong translucency.
+> It works better when combined with temporal anti-aliasing (when using `--anti-aliasing=taa` option)
+> `sort` is only working for 3D gaussians and requires compute shaders support.
+
+### `-q`, `--ambient-occlusion` (_bool_, default: `false`)
+
+Enable _ambient occlusion_. This is a technique used to improve the depth perception of the object.
+
+### `-a`, `--anti-aliasing` (_string_, default: `fxaa`)
+
+Anti-aliasing method (`fxaa`: fast, `ssaa`: quality, `taa`: balanced)
+
+> [!WARNING]
+> `taa` forces rendering of the scene at regular interval and will introduce ghosting artifacts on animated scenes.
+> It also doesn't work with offscreen rendering (when using `--output` option)
+
+### `-t`, `--tone-mapping` (_bool_, default: `false`)
+
+Enable generic filmic _Tone Mapping Pass_. This technique is used to map colors properly to the monitor colors.
+
+### `--final-shader` (_string_)
+
+Add a final shader to the output image. See the [dedicated documentation](10-FINAL_SHADER.md) for more details.
+
+## Testing options
+
+### `--ref=<png file>` (_string_)
+
+Render and compare with the provided _reference image_, for testing purposes. Use with output option to generate new baselines and diff images.
+
+### `--ref-threshold=<threshold>` (_double_, default: `0.04`)
+
+Set the _comparison threshold_ to trigger a test failure or success. The default (0.04) correspond to almost visually identical images.
+
+### `--interaction-test-record=<log file>` (_string_)
+
+Path to an interaction log file to _record interaction events_ to.
+
+### `--interaction-test-play=<log file>` (_string_)
+
+Path to an interaction log file to _play interactions events_ from when loading a file.
+
+## Rendering options precedence
+
+Some rendering options are not compatible between them, here is the precedence order if several are provided:
+
+- Raytracing (`-r`)
+- Volume (`-v`)
+- Point Sprites (`-o`)
+
+## Options syntax
+
+To turn on/off boolean options, it is possible to write `--option=true` and `--option=false`, eg `--points-sprites=false`.
+
+As documented, the `--option=value` syntax should be preferred. The syntax `--option value` can have unintended effect with positional arguments.
+
+The `-R` short option has a special syntax: `-Rlibf3d.option` but can also be used with `--reset=libf3d.option`
+
+The `-D/--define` option has a special syntax: `-Dlibf3d.option=value` or `--define=libf3d.option=value`.
+
+All options are parsed according to their type, see the [parsing documentation](08-PARSING.md) for more details.
+
+## Filename templating
+
+The destination filename used by `--output` or to save screenshots using `--screenshot-filename` can use the following template variables:
+
+- `{app}`: application name (ie. `F3D`)
+- `{version}`: application version (eg. `2.4.0`)
+- `{version_full}`: full application version (eg. `2.4.0-abcdefgh`)
+- `{model}`: current model filename without extension (eg. `foo` for `/home/user/foo.glb`)
+- `{model.ext}`: current model filename with extension (eg. `foo.glb` for `/home/user/foo.glb`)
+- `{model_ext}`: current model filename extension (eg. `glb` for `/home/user/foo.glb`)
+- `{date}`: current date in YYYYMMDD format
+- `{date:format}`: current date as per C++'s `std::put_time` format
+- `{n}`: auto-incremented number to make filename unique (up to 1000000)
+- `{n:2}`, `{n:3}`, ...: zero-padded auto-incremented number to make filename unique (up to 1000000)
+- variable names can be escaped by doubling the braces (eg. use `{{model}}.png` to output `{model}.png` without the model name being substituted)
+
+For example the screenshot filename is configured as `{app}/{model}_{n}.png` by default, meaning that, assuming the model `hello.glb` is being viewed,
+consecutive screenshots are going to be saved as `F3D/hello_1.png`, `F3D/hello_2.png`, `F3D/hello_3.png`, ...
+
+Model related variables will be replaced by `no_file` if no file is loaded and `multi_file` if multiple files are loaded using the `multi-file-mode` option.
+
+## HDRI Caches
+
+When using HDRI related options, F3D will create and use a cache directory to store related data in order to speed up rendering.
+These cache files can be safely removed at the cost of recomputing them on next use.
+
+The cache directory location is as follows, in order, using the first defined environment variables:
+
+- Windows: `%LOCALAPPDATA%\f3d`
+- Linux: `${XDG_CACHE_HOME}/f3d`,`${HOME}/.cache/f3d`
+- macOS: `${HOME}/Library/Caches/f3d`
diff -Nru f3d-3.2.0+dfsg/doc/user/04-INTERACTIONS.md f3d-3.4.1/doc/user/04-INTERACTIONS.md
--- f3d-3.2.0+dfsg/doc/user/04-INTERACTIONS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/04-INTERACTIONS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,138 @@
+# Interactions
+
+## 3D Mouse Interactions
+
+Simple interaction with the displayed data is possible directly within the window. It is as follows:
+
+- _Click and drag_ with the _left_ mouse button to rotate around the focal point of the camera.
+- Hold <kbd>Shift</kbd> then _Click and drag_ with the _left_ mouse button to translate the camera.
+- Hold <kbd>Ctrl</kbd> then _Click and drag_ with the _left_ mouse button to rotate around the axis of the camera.
+- Hold <kbd>Shift</kbd> then _Click and drag_ horizontally with the _right_ mouse button to rotate the HDRI.
+- _Click and drag_ vertically with the _right_ mouse button to zoom in/out.
+- _Move the mouse wheel_ to zoom in/out.
+- Hold <kbd>Ctrl</kbd>, then _Click and drag_ vertically with the _right_ mouse button to zoom in/out to mouse position.
+- Hold <kbd>Ctrl</kbd>, then _Move the mouse wheel_ to zoom in/out to mouse position.
+- _Click and drag_ with the _middle_ mouse button to translate the camera.
+- _Click_ with the _middle_ mouse button to center the camera on the point under the cursor (hold <kbd>Shift</kbd> to allow forward or backward movement).
+
+> [!NOTE]
+> When playing an animation with a scene camera, camera interactions are locked.
+
+## Bindings
+
+The coloring can be controlled directly by pressing the following hotkeys:
+
+- <kbd>A</kbd>: cycle between anti-aliasing modes.
+- <kbd>C</kbd>: cycle between coloring with array from point data and from cell data.
+- <kbd>S</kbd>: cycle the array to color with.
+- <kbd>Y</kbd>: cycle the component of the array to color with.
+
+See the [coloring cycle](#cycling-coloring) section for more info.
+
+Other options can be toggled or cycled directly by pressing the following hotkeys:
+
+- <kbd>W</kbd>: [cycle animations](05-ANIMATIONS.md#cycling-animations).
+- <kbd>B</kbd>: display of the scalar bar, only when coloring and not using direct scalars.
+- <kbd>V</kbd>: volume rendering, forces coloring.
+- <kbd>I</kbd>: opacity function inversion during volume rendering.
+- <kbd>O</kbd>: cycle point sprites type.
+- <kbd>P</kbd>: cycle blending mode for translucency support.
+- <kbd>Q</kbd>: ambient occlusion.
+- <kbd>T</kbd>: tone mapping.
+- <kbd>E</kbd>: the display of cell edges.
+- <kbd>X</kbd>: the trihedral axes display.
+- <kbd>G</kbd>: the horizontal grid display.
+- <kbd>Shift</kbd>+<kbd>x</kbd>: show/hide axes grid
+- <kbd>N</kbd>: the display of the file name.
+- <kbd>Shift</kbd>+<kbd>N</kbd>: the display of the HDRI file name.
+- <kbd>M</kbd>: the display of the metadata if exists.
+- <kbd>Z</kbd>: the display of the FPS counter.
+- <kbd>R</kbd>: raytracing.
+- <kbd>D</kbd>: the denoiser when raytracing.
+- <kbd>U</kbd>: background blur, useful with an HDRI skybox.
+- <kbd>K</kbd>: trackball interaction mode.
+- <kbd>F</kbd>: HDRI ambient lighting.
+- <kbd>J</kbd>: the display of the HDRI skybox.
+- <kbd>L</kbd>: increase lights intensity.
+- <kbd>Shift</kbd>+<kbd>L</kbd>: decrease lights intensity.
+- <kbd>Ctrl</kbd>+<kbd>P</kbd>: increase opacity.
+- <kbd>Shift</kbd>+<kbd>P</kbd>: decrease opacity.
+- <kbd>Shift</kbd>+<kbd>A</kbd>: toggle armature.
+- <kbd>Ctrl</kbd>+<kbd>Y</kbd>: Scene up direction to +Y
+- <kbd>Ctrl</kbd>+<kbd>Z</kbd>: Scene up direction to +Z
+
+Note that the raytracing hotkeys are only available if F3D is build with raytracing enabled.
+
+Camera Hotkeys:
+
+- <kbd>1</kbd>: front view camera.
+- <kbd>2</kbd>: roll the camera up (elevation) by 90 degrees.
+- <kbd>3</kbd>: right view camera.
+- <kbd>4</kbd>: roll the camera left by 90 degrees.
+- <kbd>5</kbd>: toggle orthographic projection.
+- <kbd>6</kbd>: roll the camera right by 90 degrees.
+- <kbd>7</kbd>: top view camera.
+- <kbd>8</kbd>: roll the camera down (elevation) by 90 degrees.
+- <kbd>9</kbd>: isometric view camera.
+- <kbd>Enter</kbd>: reset the camera to its initial parameters.
+
+Scene Hotkeys:
+
+- <kbd>Ctrl</kbd>+<kbd>Y</kbd>: set scene up direction to +Y.
+- <kbd>Ctrl</kbd>+<kbd>Z</kbd>: set scene up direction to +Z.
+
+Other hotkeys and interactions are available:
+
+- <kbd>H</kbd>: key to toggle the display of a cheat sheet showing all these hotkeys and their statuses.
+- <kbd>CTRL</kbd>+<kbd>Q</kbd>: close the window and quit F3D.
+- <kbd>Esc</kbd>: display the console or hide console/minimal console.
+- <kbd>:</kbd>: display the minimal console
+- <kbd>Space</kbd>: play the animation if any.
+- <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Space</kbd>: play the animation backward if any.
+- <kbd>Shift</kbd>+<kbd>V</kbd>: cycle between verbosity levels.
+- <kbd>&larr;</kbd>: load the previous file if any and reset the camera.
+- <kbd>&rarr;</kbd>: load the next file if any and reset the camera.
+- <kbd>&uarr;</kbd>: reload the currently loaded files.
+- <kbd>&darr;</kbd>: add all current files parent directories to the list of files, reload the currently loaded files and reset the camera, respect the `--recursive-dir-add` CLI option.
+- <kbd>F12</kbd>: take a screenshot, ie. render the current view to an image file.
+- <kbd>Ctrl</kbd>+<kbd>F12</kbd>: take a "minimal" screenshot, ie. render the current view with no grid and no overlays to an image file with a transparent background.
+- _Drop_ a file, directory or HDRI into the F3D window to load it. F3D relies on the extension (`.hdr` or `.exr`) to detect if the dropped file is an HDRI.
+- <kbd>Ctrl</kbd> + _Drop_ a file, directory and load it.
+- <kbd>Shift</kbd> + _Drop_ a HDRI and use it, no extensions checks performed.
+
+When loading another file or reloading, options that have been changed interactively are kept as is, except when using [imperative options](06-CONFIGURATION_FILE.md#imperative-options) in configuration files.
+
+## Cycling Coloring
+
+The following hotkeys let you cycle the coloring of the data:
+
+- <kbd>C</kbd>: cycle between point data and cell data - field data is not supported.
+- <kbd>S</kbd>: cycle the array available on the currently selected data in alphabetical order,
+  skipping array not containing numeric data. It will loop back to not coloring unless using volume rendering.
+- <kbd>Y</kbd>: cycle the component available on the currently selected array, looping to -2 for direct scalars rendering.
+
+When changing the array, the component in use will be kept if valid with the new array, if not it will be cycled until a valid
+component is found.
+When changing the type of data to color with, the index of the array within the data will be kept if valid
+with the new data. If not, it will cycle until a valid array is found. After that, the component will be checked
+as specified above.
+
+## Taking Screenshots
+
+The destination filename used to save the screenshots (created by pressing <kbd>F12</kbd> or <kbd>Ctrl</kbd>+<kbd>F12</kbd>) is configurable (using the `screenshot-filename` option) and can use template variables as described [on the options page](03-OPTIONS.md#filename-templating).
+
+Unless the configured filename template is an absolute path, images will be saved in dedicated directories:
+
+- Windows: `%USERPROFILE%\Pictures`
+- Linux: `${XDG_PICTURES_DIR}`,`${HOME}`
+- macOS: `${HOME}`
+
+So with default options, which are using [templates](03-OPTIONS.md#filename-templating), the screenshots will be saved as:
+
+- Windows: `%USERPROFILE%\Pictures\F3D\{model}_{n}.png`
+- Linux: `${XDG_PICTURES_DIR}/F3D/{model}_{n}.png`,`${HOME}/F3D/{model}_{n}.png`
+- macOS: `${HOME}/F3D/{model}_{n}.png`
+
+## Configuring bindings
+
+It is supported to change all bindings above using a specific syntax in the [configuration file](06-CONFIGURATION_FILE.md) using [commands](07-COMMANDS.md).
diff -Nru f3d-3.2.0+dfsg/doc/user/05-ANIMATIONS.md f3d-3.4.1/doc/user/05-ANIMATIONS.md
--- f3d-3.2.0+dfsg/doc/user/05-ANIMATIONS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/05-ANIMATIONS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,84 @@
+# Animations
+
+F3D is able to play animations for any files which contain them.
+Play them either interactively or by selecting a specific time to display.
+For files containing multiple animations, F3D allows the user to either play each animation separately or to select multiple animations to play at the same.
+
+## Demonstration
+
+This specific example uses an animation file which can be downloaded [here](https://github.com/f3d-app/f3d/blob/606089959c9520085a9cbf70660fb0ffc68fb934/testing/data/InterpolationTest.glb).
+
+<img width="1024" alt="1" src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/docs/user/animation_0.png" />
+
+Load the example animation file shown above by executing within command line: `f3d InterpolationTest.glb`
+
+<img width="1024" alt="2" src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/docs/user/animation_1.png" />
+To view current animation name, press <kbd>H</kbd> to open up cheatsheet menu
+
+<img width="1024" alt="3" src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/docs/user/animation_2.png" />
+Press <kbd>W</kbd> to cycle through available animations
+
+<img width="1024" alt="4" src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/docs/user/animation_3.png" />
+Press <kbd>space</kbd> to play/pause current animation.
+Note: A blue bar runs along the bottom of screen to indicate the current time interval of the animation sequence if animation-progress was turned on.
+
+<img width="1024" alt="5" src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/docs/user/animation_4.png" />
+"All Animations" will play all animations at the same time if supported by the file format.
+
+## Command line options
+
+F3D animation behavior can be fully controlled from the command line using the following options.
+
+| Options                      | Default             | Description                                     |
+| ---------------------------- | ------------------- | ----------------------------------------------- |
+| \-\-animation\-indices       |                     | Select the animations to play.                  |
+| \-\-animation\-indices=-1    |                     | Play all animations at once (only if supported) |
+| \-\-animation\-speed\-factor | Time Unit = Seconds | Adjust time unit.                               |
+| \-\-animation\-frame\-rate   | 60 FPS              | Adjust animation frame rate.                    |
+| \-\-animation\-time          |                     | Load a specific time value on start.            |
+
+## Animation Interactions
+
+- Press <kbd>W</kbd> to cycle through animations
+- Press <kbd>Space</kbd> to play/pause animation
+- Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Space</kbd> to play/pause animation backward
+
+See [COMMANDS](07-COMMANDS.md) for commands like `jump_to_frame`
+
+## Cycling Animations
+
+Pressing <kbd>W</kbd> let you cycle the animation to show.
+
+When cycling, F3D will cycle like this:
+
+- Multiple animations or all animations
+- Animation index 0
+- Animation index 1
+- ...
+- Animation index N
+- All animations (if multiple animations supported and more than one animation is available)
+- No animations
+
+Please note that if you selected multiple animation indices, you will never cycle back to it.
+All animations will only be cycled if supported by the currently loaded files.
+
+## Time Units
+
+When F3D plays an animation, it assumes the time unit is in seconds to show accurate speed of animation.
+
+## Coloring range
+
+When coloring while loading a time value or playing the animation, the coloring range will be automatically expanded from previously loaded time value,
+this include `--animation-time` that first load the initial time before loading the provided time value.
+
+## Animation Support Level
+
+The animation support level is listed for each reader [here](02-SUPPORTED_FORMATS.md).
+
+- NONE: Animation is not supported, either by the file format or the implementation
+- UNIQUE: There will never be more then one animation
+- SINGLE: There can be multiple animations but only one can be selected at a time
+- MULTI: Multiple animations can be selected at a time
+
+When opening multiple files at the same time using the `--multi-file-mode` option and selecting multiple animations to show,
+F3D may warn when combining SINGLE and non SINGLE files, but will still try to respect the chosen animation indices.
diff -Nru f3d-3.2.0+dfsg/doc/user/06-CONFIGURATION_FILE.md f3d-3.4.1/doc/user/06-CONFIGURATION_FILE.md
--- f3d-3.2.0+dfsg/doc/user/06-CONFIGURATION_FILE.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/06-CONFIGURATION_FILE.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,254 @@
+# Configuration File
+
+## Options
+
+Almost all the command-line [options](03-OPTIONS.md) can be controlled using configuration files.
+Configuration files uses the "long" version of the command-line options in a JSON
+formatted file to provide values for these options. It is also possible to use
+the [libf3d options](../libf3d/03-OPTIONS.md) syntax.
+
+These options can be organized by block using a regular expression, glob, or exact match
+for each block in order to provide different default values for the different filetypes. For
+more on glob and exact matching see the [Glob and Exact Matching section](#glob-and-exact-matching).
+
+Using a command-line option will override similar option set in any config files.
+
+Some options are only taken into account on the first load and not on subsequent loads,
+when switching between files.
+
+A typical config file with options may look like this:
+
+```json
+[
+  {
+    "options": {
+      "background-color": "0.7,0.7,0.7",
+      "color": "0.5,0.1,0.1",
+      "anti-aliasing": true,
+      "timer": true,
+      "progress": true,
+      "axis": true,
+      "bar": true,
+      "roughness": 0.2,
+      "grid": true,
+      "scalar-coloring": true
+    }
+  },
+  {
+    "match": ".*vt.",
+    "options": {
+      "edges": true
+    }
+  },
+  {
+    "match": ".*gl(tf|b)",
+    "options": {
+      "raytracing": true,
+      "denoise": true,
+      "samples": 3
+    }
+  },
+  {
+    "match": ".*mhd",
+    "options": {
+      "volume": true
+    }
+  }
+]
+```
+
+Here, the first block defines a basic configuration with many desired options for all files.
+The second block specifies that all files ending with vt., eg: vtk, vtp, vtu, ... will be shown with edges visibility turned on.
+The third block specifies raytracing usage for .gltf and .glb files.
+The last block specifies that volume rendering should be used with .mhd files.
+
+The following options <b> cannot </b> be set via config file:
+`help`, `version`, `list-readers`, `list-rendering-backends`, `scan-plugins`, `config`, `no-config`, `define`, `reset` and `input`.
+
+The following options <b>are only taken on the first load</b>:
+`no-render`, `output`, `position`, `resolution`, `frame-rate` and all testing options.
+
+Boolean options that have been turned on in the configuration file can be turned
+off on the command line if needed, eg: `--point-sprites=false`.
+
+### Imperative Options
+
+Command line options and options that are changed interactively overrides options that are set in configuration files.
+This is not always a desired behavior, so in order to force an option to always be taken into account even if set in command line or changed interactively, it is possible to use imperative options, by adding a `!` in front of the option name, eg:
+
+```json
+[
+  {
+    "options": {
+      "!axis": true
+    }
+  },
+  {
+    "match": ".*(stl)",
+    "options": {
+      "!edges": true
+    }
+  }
+]
+```
+
+In the above example, when loading or reloading a file, the axis is always turned on and when loading a .stl file, the edges are always turned on.
+
+## Bindings
+
+All interaction bindings can be configured using configuration files.
+The logic is the same as with options, where configuration blocks are used depending on the filename being loaded using regular expression.
+A `bindings` block can be added to specify associations between binds (eg : `Ctrl+O`) and one or multiple commands.
+
+A typical config file with bindings may look like this:
+
+```json
+[
+  {
+    "bindings": {
+      "Ctrl+Shift+O": "toggle ui.filename",
+      "Any+3": "roll_camera -90",
+      "O": "set_camera isometric"
+    }
+  },
+  {
+    "match": ".*vtu",
+    "bindings": {
+      "Any+3": "roll_camera 90",
+      "Shift+O": "toggle model.point_sprites.enable",
+      "Ctrl+O": [
+        "toggle render.grid.enable",
+        "toggle scene.camera.orthographic"
+      ],
+      "None+I": "toggle ui.axis"
+    }
+  }
+]
+```
+
+Here, the first block define new bindings for all and any files.
+It even replace an existing default [interaction](04-INTERACTIONS.md) on the `O` key with its own.
+Each bind is associated to the [command](07-COMMANDS.md) to execute when it is pressed.
+
+In the second block, new bindings are defined for files ending in `.vtu`, and there bindings
+will only be available when loading such a file.
+In the first config block, we define bindings for all and any files. It also replace an existing default
+interaction on the `Any+3` bind and even define a bindings that have multiple commands using a json array
+on the `Ctrl+O` bind.
+
+Please note this configuration feature is only available through config file and not through the command line.
+However, it is possible to check your current binding configuration by using the `--list-bindings` CLI options.
+
+### Bind
+
+A bind is the association of a modifier and an interaction.
+
+Supported modifiers are:
+
+`None` : No modifiers is pressed, can be omitted completely.
+`Ctrl` : Control key is pressed, no distinction between left and right.
+`Shift` : Shift key is pressed, no distinction between left and right.
+`Ctrl+Shift` : Control and Shift keys are pressed, no distinction between left and right.
+`Any` : A special modifier that does not consider modifiers keys but can only be reached
+if there is no bind with the same interaction.
+
+Supported interactions are legion, eg:
+
+- Keyboard letter keys, eg:
+  - `O`
+  - `P`
+- Keyboard number keys, eg:
+  - `2`
+  - `5`
+- Keyboard special keys, eg:
+  - `Enter`
+  - `Delete`
+- Other interactions, eg:
+  - `Drop` when files or directories are dropped on F3D window
+
+To identify the interaction to use, just use `f3d --verbose` and perform the interaction, F3D will log it like this:
+
+```
+Interaction: KeyPress Ctrl+Shift+Insert
+```
+
+Please note that the interaction itself can be modified by the modifiers, hence some binds may be unreachable depending on your
+keyboard layout, eg. on a `QWERTY` layout, the bind `Exclam` is unreachable while `Shift+Exclam` is reachable.
+We tried to make the default bindings of F3D to be a keyboard layout independent as possible, but feel free to redefined them
+for your needs.
+
+## Glob and Exact Matching
+
+While regex is the default match type, glob and exact match types are also supported. Below is an example using glob and exact matching.
+
+```json
+[
+  {
+    "options": {
+      "background-color": "0.7,0.7,0.7",
+      "color": "0.5,0.1,0.1",
+      "anti-aliasing": true,
+      "timer": true,
+      "progress": true,
+      "axis": true,
+      "bar": true,
+      "roughness": 0.2,
+      "grid": true,
+      "scalar-coloring": true
+    }
+  },
+  {
+    "match-type": "glob",
+    "match": "*vt?",
+    "options": {
+      "edges": true
+    }
+  },
+  {
+    "match-type": "glob",
+    "match": "**/*gl{tf,b}",
+    "options": {
+      "raytracing": true,
+      "denoise": true,
+      "samples": 3
+    }
+  },
+  {
+    "match-type": "exact",
+    "match": "/path/to/file.mhd",
+    "options": {
+      "volume": true
+    }
+  }
+]
+```
+
+For glob patterns, globstar (`**`) is supported.
+
+Glob and exact matching can be used for bindings as well.
+
+## Locations
+
+Single .json file for configuration is supported, as well a config directory containing multiple .json config files.
+In the case of config directory, .json file are read in alphabetical order and an option or binding in a block can be overridden
+by the same option or bind being set in another block in another .json file read after.
+
+F3D provides a default config directory for generic usage (`config.d`) and a thumbnail specific config directory (`thumbnail.d`).
+You can edit the file contained in these directory or add your own in specific directories (see below).
+
+F3D looks for configuration files in different locations depending on your operating system.
+Existing configuration files are read in order and combined with later entries, potentially overriding previously read entry with the same names.
+
+- Linux: `/etc/f3d/config(.json,.d)`, `/usr/share/f3d/configs/config(.json,.d)`, `[install_dir]/share/f3d/configs/config(.json,.d)`, `${XDG_CONFIG_HOME}/f3d/config(.json,.d)`
+- Windows: `[install_dir]\share\f3d\configs\(config.json,.d)`, `%APPDATA%\f3d\(config.json,.d)`
+- macOS: `/usr/local/etc/f3d/config(.json,.d)`, `f3d.app/Contents/Resources/configs/config(.json,.d)`, `${HOME}/Library/Application Support/f3d/config(.json,.d)`
+
+Please note that, on Linux, `XDG_CONFIG_HOME` implementation can fallback on `HOME` environment variables as specified [here](https://specifications.freedesktop.org/basedir/latest/).
+
+The binary release will install the default config directory.
+On Linux, they will be installed in `[install_dir]/share/f3d/configs/`, on Windows, they will be installed in `[install_dir]\share\f3d\configs\`, on macOS, it will be installed in the bundle.
+
+Please note there is a command line option to control the configuration file to read. Using it, one can specify an absolute/relative path for the configuration path, but also
+only the filename or filestem (`.json` and `.d` will be added) to look for in the locations listed above, , eg: `f3d --config=custom_config` will look
+for `custom_config.json` and `custom_config.d` in locations listed above.
+When specifying an absolute/relative path for the configuration file, a single file is read. If not, all files from locations listed above, with the overriding logic specified above.
diff -Nru f3d-3.2.0+dfsg/doc/user/07-COMMANDS.md f3d-3.4.1/doc/user/07-COMMANDS.md
--- f3d-3.2.0+dfsg/doc/user/07-COMMANDS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/07-COMMANDS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,173 @@
+# Commands
+
+F3D provides access to commands through [interactive console](#interactive-console), [command script](#command-script---command-script) and [bindings configuration](06-CONFIGURATION_FILE.md#bindings).
+Commands let you trigger specific behavior that may not be available otherwise.
+Please note commands are currently experimental and the behaviors, actions may be added or removed without deprecation.
+Action names and arguments may also change without deprecation.
+
+Commands have the following syntax:
+
+`action [args]`
+
+## libf3d provided commands
+
+The libf3d provides a few commands, many related to manipulating libf3d (options)[../libf3d/03-OPTIONS.md].
+
+`set option.name values`: A command to set a libf3d option, eg: `set scene.up.direction +Z` or `set render.hdri.file "/path/to/file with spaces.png"`
+
+`toggle option.name`: A command to toggle a boolean libf3d option, eg: `toggle ui.scalar_bar`.
+
+`reset option.name`: A command to reset a libf3d option to its default values, eg: `reset render.background.blur.coc`.
+
+`clear`: A command to clear console. No argument.
+
+`print option.name`: A command to print the value of an libf3d option, eg: `print scene.up.direction`.
+
+`set_reader_option Reader.option_name value`: A specific command to set a [reader option](02-SUPPORTED_FORMATS.md#reader-options), eg: `set_reader_option QuakeMDL.skin_index 1`
+
+`cycle_anti_aliasing`: A specific command to cycle between the anti-aliasing method (`none`,`fxaa`,`ssaa`,`taa`).
+
+`cycle_animation`: A specific command to cycle `scene.animation.index` option using model information, No argument.
+
+`cycle_coloring field/array/component`: A specific command to manipulate scivis options using model information.
+Supports `field`, `array` or `component` arguments, see [documentation](04-INTERACTIONS.md#cycling-coloring).
+eg: `cycle_coloring array`.
+
+`roll_camera value`: A specific command to roll the camera on its side, takes an angle in degrees as an argument.
+eg: `roll_camera 120`.
+
+`elevation_camera value`: A specific command to tilt the camera up or down, takes an angle in degrees as an argument.
+eg: `elevation_camera 120`.
+
+`azimuth_camera value`: A specific command to tilt the camera right or left, takes an angle in degrees as an argument.
+eg: `azimuth_camera 120`.
+
+`increase_light_intensity`: A specific command to increase light intensity. No argument.
+
+`decrease_light_intensity`: A specific command to decrease light intensity. No argument.
+
+`increase_opacity`: A specific command to increase opacity. Unset opacity will be treated as if it has a value of 1. No argument.
+
+`decrease_opacity`: A specific command to decrease opacity. Unset opacity will be treated as if it has a value of 1. No argument.
+
+`print_scene_info`: A specific command to print information about the scene, No argument.
+
+`print_coloring_info`: A specific command to print information about coloring settings, No argument.
+
+`print_mesh_info`: A specific command to print information from the mesh importer, No argument.
+
+`print_options_info`: A specific command to print libf3d options that have a value, No argument.
+
+`set_camera front/top/right/isometric`: A specific command to position the camera in the specified location relative to the model.
+Supports `front`, `top`, `right`, `isometric` arguments. eg: `set_camera top`.
+
+`toggle_volume_rendering`: A specific command to toggle `model.volume.enable` and print coloring information. No argument.
+
+`stop_interactor`: A specific command to stop the interactor hence quitting the application. No argument.
+
+`reset_camera`: A specific command to reset the camera to its original location. No argument.
+
+`toggle_animation`: A specific command to start/stop the animation. No argument.
+
+`toggle_animation_backward`: A specific command to start/stop the animation backward. No argument.
+
+`jump_to_frame`: A specific command to load an animation at a specific frame, takes a number and a boolean as arguments.
+eg:
+
+- `jump_to_frame 1 true` jump to next frame.
+- `jump_to_frame -1 true` jump to previous frame.
+- `jump_to_frame 0 false` jump to frame 0.
+- `jump_to_frame 1 false` jump to frame 1.
+- `jump_to_frame 10 false` jump to frame 10.
+- `jump_to_frame -1 false` jump to last frame.
+- `jump_to_frame -2 false` jump to second last frame.
+
+`cycle_verbose_level` : A specific command to cycle between the verbose levels (`Debug`, `Info`, `Warning`, `Error`, `Quiet`).
+
+`add_files [path/to/file1] [path/to/file2]`: A specific command to add files to the scene (overridden by a F3D specific command, see below). Take one or more files as arguments.
+eg: `add_files /path/to/dragon.vtu`.
+
+`alias [alias_name] [command]`: A specific command to create an alias for a command. Take an alias name and a command as arguments.
+eg: `alias myrotate roll_camera 90`.
+
+`help [command]: A specific command to print help about a specific command. eg: `help set_camera`.
+
+## F3D provided specific commands
+
+The F3D application provides a few more commands.
+
+`exit`: A specific command to quit the application. No argument.
+
+`load_previous_file_group [keep_camera]`: A specific command to load the previous file or file group. Camera state is kept if `keep_camera` is true (default: false).
+
+`load_next_file_group [keep_camera]`: A specific command to load the next file or file group. No argument. Camera state is kept if `keep_camera` is true (default: false).
+
+`reload_current_file_group`: A specific command to reload the current file or file group. No argument.
+
+`add_current_directories`: A specific command to add all files from the current file or file group directories. No argument.
+
+`take_screenshot [filename]`: A specific command to [take a screenshot](04-INTERACTIONS.md#taking-screenshots). If filename is not specified,
+rely on the `--screenshot-filename` CLI option. eg: `take_screenshot path/to/file.png`.
+
+`take_minimal_screenshot [filename]`: A specific command to [take a minimal screenshot](04-INTERACTIONS.md#taking-screenshots). If filename is not specified,
+rely on the `--screenshot-filename` CLI option. eg: `take_screenshot path/to/file.png`.
+
+`add_files [path/to/file1] [path/to/file2]`: A specific command to add files to the scene according to the current grouping logic. Take one or more files as arguments.
+eg: `add_files /path/to/dragon.vtu`.
+
+`set_hdri [path/to/hdri]`: A specific command to set and use an HDRI image. Take a HDRI file as an argument.
+eg: `set_hdri /path/to/file.hdr`.
+
+`add_files_or_set_hdri [path/to/file1] [path/to/file2]`: A specific command that will process each files and either, `set_hdri` if the provided file uses a recognised HDR extension or `add_files` otherwise. Take one or more files as arguments.
+eg: `add_files_or_set_hdri /path/to/dragon.vtu /path/to/file.hdr`.
+
+`remove_current_file_group`: A specific command to remove current file group and load the next file group if any. No argument.
+
+`remove_file_groups`: A specific command to remove all files. No argument.
+
+`open_file_dialog`: A specific command to open a file dialog to selected a file to load. No argument.
+
+## Command Script (`--command-script`)
+
+F3D provides a feature to execute commands from a script file using the `--command-script` [CLI option](03-OPTIONS.md). This allows users to automate a sequence of commands by listing them in a plain text file, eg: `f3d --command-script path/to/command_script.txt`.
+
+Example Command Script, commands are separated by new lines, comments are supported:
+
+```shell
+# A comment
+roll_camera 90
+toggle ui.scalar_bar
+print_scene_info # Another comment
+increase_light_intensity
+```
+
+## Interactive Console
+
+If F3D is built with `F3D_MODULE_UI` support, pressing <kbd>Esc</kbd> will open the console. It's possible to type any command in the input field and pressing <kbd>Enter</kbd> will trigger the command instantly.
+
+Press <kbd>Tab</kbd> to autocomplete the command and display suggestions.
+
+Press <kbd>&uarr;</kbd> and <kbd>&darr;</kbd> to navigate through the command history.
+
+Press <kbd>Esc</kbd> to close the console.
+
+## Command syntax
+
+Command syntax is similar to bash, as in they will be split by "token" to be processed.
+
+- Tokens are spaces separated, eg: `set scene.up.direction +Z`.
+- Tokens can also be quoted to support spaces inside, eg: `set render.hdri.file "/path/to/file with spaces.png"`.
+- Supported quotes are `` `'" ``, eg: `set render.hdri.file '/path/to/file with spaces.png'`.
+- Quotes inside quotes are supported as well, eg: `set render.hdri.file "/path/to/file'with'quotes.png"`.
+- Quotes and spaces can be escaped, eg: `set render.hdri.file /path/to/file\ with\ spaces\ and\ \'quotes\".png`.
+- Comment are supported using `#`, Any character after will be ignored. Use `\#` to add it verbatim.
+
+> [!NOTE]
+> Comments are only supported in command script, in interactive console `#` and all characters after will be handled
+> as standard character.
+
+- Escapes can be escaped too: eg: `set render.hdri.file C:\\path\\to\\windows\\file.png`.
+- Other escaped character will be processed as if the escape was not present, eg: `set scene.up.direction +\Z`
+- Unfinished quoted section is invalid, eg: `set scene.up.direction "+Z`
+- A escape at the end is also invalid, eg: `set scene.up.direction +Z\`
+- Options values are [parsed](08-PARSING.md) according to their types.
diff -Nru f3d-3.2.0+dfsg/doc/user/08-PARSING.md f3d-3.4.1/doc/user/08-PARSING.md
--- f3d-3.2.0+dfsg/doc/user/08-PARSING.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/08-PARSING.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,117 @@
+# Parsing Options
+
+When setting options from the [CLI Options](03-OPTIONS.md), the [commands](07-COMMANDS.md) or using the [libf3d options string API](../libf3d/03-OPTIONS.md#string-api), the values are parsed according to their type. If parsing fails, the value is not changed.
+
+The following types are supported:
+
+- bool: A boolean, true or false.
+- int: A signed integer.
+- double: A floating point number.
+- ratio: A double dividend over a double divisor, stored in a double.
+- string: A string of characters.
+- path: A path to a local file.
+- color: A RGB color.
+- direction: A 3D vector representing a direction.
+- transform2d: A 9D vector representing a 3x3 matrix.
+
+As well as a list for bool, int, double, ratio, string, noted as
+
+- vector\<type\>
+
+## Bool
+
+The following formats are supported when parsing a bool, case insensitive:
+
+- true/false
+- yes/no
+- on/off
+- 1/0
+
+When formatting a bool into a string, true/false is used.
+
+## Int
+
+Int parsing is supported using [std::stoi](https://en.cppreference.com/w/cpp/string/basic_string/stol) and check
+that the whole string is parsed.
+
+When formatting an int into a string, [std::to_string](https://en.cppreference.com/w/cpp/string/basic_string/to_string) is used.
+
+## Double
+
+Double parsing is supported using [std::stod](https://en.cppreference.com/w/cpp/string/basic_string/stol) and check
+that the whole string is parsed.
+
+When formatting a double into a string, [std::ostringstream](https://en.cppreference.com/w/cpp/io/basic_ostringstream) is used
+with removing the point and precision when the value is exactly an integer.
+
+## Ratio
+
+The following formats are supported when parsing a string into a ratio:
+
+- percent% where percent is a double
+- dividend:divisor where both are doubles
+- dividend/divisor where both are doubles
+- double
+
+Percent, dividend, divisor are then parsed as double.
+
+When formatting a ratio into a string, it is formatted as a double.
+
+## String
+
+String are trimmed of leading and trailing space when parsed.
+String are formatted as is.
+
+## Path
+
+Path are collapsed and tilda (`~`) is supported.
+
+## Vectors
+
+Vector tokens are separated by `,`, tokens are then parsed using their respective types.
+
+When formatting a vector into a string, individual token are formatted according to their type and separated using `,`.
+
+## Color
+
+The following formats are supported when parsing a color, case insensitive:
+
+- R,G,B where R, G, B are doubles >= 0
+- #RRGGBB where RR, GG, BB are hexadecimal values. Shortened format #RGB is also valid.
+- rgb(R, G, B) where R, G and B are integer [0, 255]
+- hsl(H, S%, L%) where H is integer [0, 360], S and L are integer [0, 100]
+- hsv(H, S%, V%) where H is integer [0, 360], S and V are integer [0, 100]
+- hwb(H, W%, B%) where H is integer [0, 360], W and B are integer [0, 100]
+- cmyk(C%, M%, Y%, K%) where C, M, Y, K are integer [0, 100]
+- [color name](https://htmlpreview.github.io/?https://github.com/Kitware/vtk-examples/blob/gh-pages/VTKNamedColorPatches.html)
+
+See [W3C](https://www.w3.org/TR/css-color-3/#rgb-color) doc for more details on these formats.
+
+When formatting a color into a string, it is formatted as `#RRGGBB` if values are multiple of 255. Otherwise, it is formatted as vector of doubles.
+
+## Direction
+
+The following formats are supported when parsing a string into a direction:
+
+- `[[+|-]X][[+|-]Y][[+|-]Z]` (case insensitive), for example `+X` or `X` for `1,0,0`, `-y+z` for `0,-1,1`
+- vector of three doubles, for example `1,2,3.4`
+
+When formatting a direction into a string, it is formatted in the `±XYZ` form if possible or as a vector of doubles otherwise.
+
+## Colormap
+
+The following formats are supported when parsing a string into a colormap:
+
+- `val, red, green, blue, ...`
+- `val, color, ...`
+
+When formatting a colormap into a string, it is formatted as `val, color, ...`.
+
+## Transform2D
+
+The following formats are supported when parsing a string into a transform2D:
+
+- A 9D double vector
+- At least one of the following: `scale: val, val`, `translation: val, val`, `angle: val` in any order, semicolon-separated. `scale` may be given one value, which will apply to both the x and y axes of the matrix. `angle` is expressed in degrees.
+
+When formatting a transform2d into a string, it is formatted as a 9D double vector
diff -Nru f3d-3.2.0+dfsg/doc/user/09-COLOR_MAPS.md f3d-3.4.1/doc/user/09-COLOR_MAPS.md
--- f3d-3.2.0+dfsg/doc/user/09-COLOR_MAPS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/09-COLOR_MAPS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,57 @@
+# Color Maps
+
+## Image files
+
+It is possible to specify a color map using the `--colormap-file` option.
+The value of the option can be an absolute path to an image file, a relative path, a filename or a filestem.
+If it is not an absolute path, these directories are used to find the file (in this order):
+
+- Linux: `${XDG_CONFIG_HOME}/f3d/colormaps`, `~/.config/f3d/colormaps`, `/etc/f3d/colormaps`, `/usr/share/f3d/colormaps`, `[install_dir]/share/f3d/colormaps`
+- Windows: `%APPDATA%\f3d\colormaps`, `[install_dir]\share\f3d\configs\colormaps`
+- macOS: `${XDG_CONFIG_HOME}/f3d/colormaps`, `~/.config/f3d/colormaps`, `/usr/local/etc/f3d/colormaps`, `f3d.app/Contents/Resources/configs/colormaps`
+  The first existing file found is used.
+  If it is a filestem, all [supported image](#supported-formats) extensions are tried.
+
+We provide some ready to use color maps files, listed in the table below:
+
+| Name       | Image                                                                                                                                   |
+| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| cividis    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/cividis.png?raw=true" class="cm" width="256" height="10" />    |
+| cubehelix  | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/cubehelix.png?raw=true" class="cm" width="256" height="10" />  |
+| gist_earth | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/gist_earth.png?raw=true" class="cm" width="256" height="10" /> |
+| hot        | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/hot.png?raw=true" class="cm" width="256" height="10" />        |
+| inferno    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/inferno.png?raw=true" class="cm" width="256" height="10" />    |
+| magma      | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/magma.png?raw=true" class="cm" width="256" height="10" />      |
+| plasma     | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/plasma.png?raw=true" class="cm" width="256" height="10" />     |
+| seismic    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/seismic.png?raw=true" class="cm" width="256" height="10" />    |
+| viridis    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/viridis.png?raw=true" class="cm" width="256" height="10" />    |
+
+It is possible to create a custom color map by creating a simple RGB image in any [supported formats](#supported-formats), and any resolution (if the height is more than 1 row, only the first one is taken into account). The image must be copied in the user config directory:
+
+- Linux/macOS: `~/.config/f3d/colormaps`
+- Windows: `%APPDATA%\f3d\colormaps`
+
+### Supported formats
+
+Here's the list of all supported image formats that can be used as color maps:
+
+- `.png`
+- `.pnm`, `.pgm`, `.ppm`
+- `.tif`, `.tiff`
+- `.bmp`
+- `.slc`
+- `.hdr`
+- `.pic`
+- `.jpeg`, `.jpg`
+- `.MR`
+- `.CT`
+- `.mhd`, `.mha`
+- `.tga`
+- `.exr` (if `F3D_MODULE_EXR` is enabled)
+- `.webp` (if `F3D_MODULE_WEBP` is enabled)
+
+## Custom values
+
+If no colormap file is specified, it is also possible to set values manually using the `--colormap` option with a dedicated [parsing](08-PARSING.md#colormap).
+For example, the default value corresponds to the `hot` preset which can be defined manually with `--colormap=0.0,rgb(0,0,0),0.4,rgb(230,0,0),0.8,rgb(230,230,0),1.0,rgb(255,255,255)`.
+It consists of 4 pairs of increasing scalar values ranging from 0 to 1, and corresponding colors. Values in between are interpolated.
diff -Nru f3d-3.2.0+dfsg/doc/user/10-FINAL_SHADER.md f3d-3.4.1/doc/user/10-FINAL_SHADER.md
--- f3d-3.2.0+dfsg/doc/user/10-FINAL_SHADER.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/10-FINAL_SHADER.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,76 @@
+# Final Shaders
+
+It is possible to specify a final full screen shader with the option `--final-shader`.
+It is executed as the last shader in the rendering pipeline.
+
+The shader code can be passed directly as a single code line.
+It's possible to pass the content of a shader file using the following command:
+
+```sh
+f3d model.obj --final-shader "$(cat final.glsl)"
+```
+
+Note that the command above works on Linux and bash, and should be adapted depending on the operating system and the shell/terminal used.
+
+## Requirements
+
+It is required to implement the function `vec4 pixel(vec2 uv)`.
+The value `uv` is ranging from 0 to 1 in both directions. `(0,0)` is the bottom left corner, and `(1,1)` is the upper right corner.
+The first component is the horizontal direction.
+
+## Uniforms
+
+It is possible to access these uniforms:
+
+- `source` (type: `sampler2d`): texture of the image generated by rendering pipeline.
+- `resolution` (type: `ivec2`): resolution of the texture `source`.
+
+## Examples
+
+Here are three shader examples to illustrate how an implementation looks like.
+
+### Negative
+
+```glsl
+vec4 pixel(vec2 uv)
+{
+    vec3 value = texture(source, uv).rgb;
+    return vec4(vec3(1.0) - value, 1.0);
+}
+```
+
+### Vignette
+
+```glsl
+vec4 pixel(vec2 uv)
+{
+    float l = clamp(1.0 - 2.0 * length(uv - 0.5), 0.0, 1.0);
+    return vec4(l*texture(source, uv).rgb, 1.0);
+}
+```
+
+### Box blur
+
+```glsl
+vec4 pixel(vec2 uv)
+{
+    const float radius = 0.03;
+    const int samples = 20;
+    const float step = radius / float(samples);
+
+    vec3 sum = vec3(0);
+
+    for (int i = -samples; i <= samples; i++)
+    {
+        for (int j = -samples; j <= samples; j++)
+        {
+            sum += texture(source, uv + vec2(i, j) * step).rgb;
+        }
+    }
+
+    float d = 1.0 + 2.0 * float(samples);
+    sum /= d * d;
+
+    return vec4(sum, 1.0);
+}
+```
diff -Nru f3d-3.2.0+dfsg/doc/user/11-DESKTOP_INTEGRATION.md f3d-3.4.1/doc/user/11-DESKTOP_INTEGRATION.md
--- f3d-3.2.0+dfsg/doc/user/11-DESKTOP_INTEGRATION.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/11-DESKTOP_INTEGRATION.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,74 @@
+# Desktop Integration
+
+F3D can be integrated in the desktop experience.
+
+## Linux
+
+For Linux desktop integration, F3D rely on mime types files as defined by the [XDG standard](https://specifications.freedesktop.org/mime-apps-spec/latest-single/), .thumbnailer file as specified [here](https://wiki.archlinux.org/title/File_manager_functionality#Thumbnail_previews) and .desktop file as specified [here](https://wiki.archlinux.org/title/desktop_entries). Many file managers use this mechanism, including nautilus, thunar, pcmanfm and caja.
+
+The simplest way to obtain desktop integration on Linux is to use a package for your distribution, or the .deb binary release package we provide if compatible with your distribution.
+In other cases, the binary release archive can be used like this:
+
+0. Make sure `~/.local/bin` is part of your `PATH`
+1. Extract F3D binary release archive in `~/.local/`
+2. Update your [mime database](https://linux.die.net/man/1/update-mime-database) pointing to `~/.local/share/mime`
+3. Update your [desktop database](https://linuxcommandlibrary.com/man/update-desktop-database) pointing to `~/.local/share/application`
+
+```bash
+export PATH=$PATH:~/.local/bin
+tar -xzvf f3d-1.3.0-Linux.tar.gz -C ~/.local/
+sudo update-mime-database ~/.local/share/mime/
+sudo update-desktop-database ~/.local/share/applications
+```
+
+F3D supports rendering in environments with limited graphical contexts, including sandboxes or without Xorg running.
+In most cases, the default behavior will automatically detect which capabilities are available and use the right rendering backend,
+it is possible to specify it using the `--rendering-backend` CLI option.
+
+- If `libEGL.so` is available, you can use: `--rendering-backend=egl`.
+- If `libOSMesa.so` is available, you can use: `--rendering-backend=osmesa`.
+- When using `--rendering-backend=auto`, F3D will try to load the following libraries in order and use the first one it was able to load and use:
+  - `libGLX.so`
+  - `libEGL.so`
+  - `libOSMesa.so`
+
+You can use that feature for thumbnail generation whenever needed (e.g., Nautilus or other sandboxing file browser),
+by modifying the [thumbnail config file](06-CONFIGURATION_FILE.md), eg:`~/.config/f3d/thumbnail.json`:
+
+```javascript
+{
+    ".*": {
+        "rendering-backend": "egl"
+    }
+}
+```
+
+If you use the portable archive, make sure to extract it somewhere the sandboxing system has access to, eg: Nautilus uses `bwrap` and gives it access to `/usr` but not to `/opt`.
+
+## Windows
+
+For Windows desktop integration, F3D rely on a registered shell extension.
+
+Using the F3D NSIS installer (.exe) is the simplest way to enable thumbnails and integrate F3D on windows.
+
+It is also possible to do it manually when using the zipped binary release archive, on installation, just run:
+
+```
+cd C:\path\to\f3d\bin\
+regsvr32 F3DShellExtension.dll
+```
+
+To remove the shell extension, run:
+
+```
+cd C:\path\to\f3d\bin\
+regsvr32 /u F3DShellExtension.dll
+```
+
+## MacOS
+
+There is no support for thumbnails on MacOS, the .dmg binary release provides automatic file openings.
+
+## Limitations
+
+- TIFF files are excluded from the thumbnailer to avoid conflicts with other better 2D thumbnailers.
diff -Nru f3d-3.2.0+dfsg/doc/user/12-PLUGINS.md f3d-3.4.1/doc/user/12-PLUGINS.md
--- f3d-3.2.0+dfsg/doc/user/12-PLUGINS.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/12-PLUGINS.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,52 @@
+# Plugins
+
+F3D officially supports plugins for certain file formats. If you installed F3D using the binary
+release, there's no need to manually load these plugins when opening your file.
+F3D will load them automatically.
+
+However, if you installed F3D using a package manager,
+it's possible that the packager chose to bundle the plugins into different packages or
+to list plugin dependencies as optional in order to reduce the number of dependencies of the main package.
+
+In order to open a file that requires a plugin, make sure you've installed all necessary
+dependencies. You can then specify the `--load-plugins=<path or name>` [option](03-OPTIONS.md)
+in the command line to load your plugin.
+
+Alternatively, you can add your plugin directly in the
+[configuration file](06-CONFIGURATION_FILE.md) if it isn't there already. You can specify one or
+multiple plugins in a single comma-separated list, like in the example below:
+
+```
+[
+  {
+    "match": ".*(file_extension)",
+    "options":
+    {
+      "load-plugins": "plugin1", "plugin2"
+    }
+  }
+]
+```
+
+## Supported plugins
+
+F3D supports loading plugins and contains the following plugins:
+`native`, `alembic`, `assimp`, `draco`, `hdf`, `occt`, `usd`, `vdb`.
+
+see [the format list](02-SUPPORTED_FORMATS.md) for more details.
+
+> [!NOTE]
+> Native plugin is always loaded automatically.
+> If you downloaded the binaries from the release page, it's not necessary to specify manually the plugins above. F3D loads them automatically.
+
+Here is how the plugins are searched (in preceding order):
+
+1. Search the static plugins.
+2. Consider the `load-plugins` option given it is a full path.
+3. Search in the paths specified in `F3D_PLUGINS_PATH` environment variable.
+4. Search in a directory relative to the F3D application: `../lib`.
+5. Rely on OS specific paths (e.g. `LD_LIBRARY_PATH` on Linux or `DYLD_LIBRARY_PATH` on macOS).
+
+You can also try plugins maintained by the community. If you have created a plugin and would like it to be listed here, please submit a pull request.
+
+- **Abaqus**: ODB support by @YangShen398 ([repository](https://github.com/YangShen398/F3D-ODB-Reader-Plugin))
diff -Nru f3d-3.2.0+dfsg/doc/user/13-LIMITATIONS_AND_TROUBLESHOOTING.md f3d-3.4.1/doc/user/13-LIMITATIONS_AND_TROUBLESHOOTING.md
--- f3d-3.2.0+dfsg/doc/user/13-LIMITATIONS_AND_TROUBLESHOOTING.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/doc/user/13-LIMITATIONS_AND_TROUBLESHOOTING.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,162 @@
+# Limitations & Troubleshooting
+
+## Limitations
+
+Here is a non exhaustive list of F3D limitations:
+
+- No support for specifying manual lighting in the default scene apart from using `--light-intensity` option.
+- Multiblock (.vtm, .gml) support is partial, non-surfacic data will be converted into surfaces.
+- Drag and drop interaction cannot be recorded nor played back.
+- Volume rendering and HDRI support requires a decent GPU.
+- The `--camera-zoom-factor` option require VTK >= 9.3.0
+- Information about the failure to load a file is not provided before VTK >= 9.4.0
+
+### Assimp
+
+FBX, DAE, OFF, DXF, X and 3MF file formats rely on [Assimp](https://github.com/assimp/assimp) library. It comes with some known limitations:
+
+- PBR materials are not supported for FBX file format.
+- Some files can be empty, crash, or show artifacts.
+- DXF support is very limited: only files with polylines and 3D faces are displayed.
+- 3MF files may crash at exit (issue in Assimp: https://github.com/assimp/assimp/issues/5328)
+- Only support RBGA 8-bits embedded textures
+
+### Alembic
+
+ABC file formats rely on [Alembic](https://github.com/alembic/alembic) library. It comes with some known limitations:
+
+- Supports only simple polygonal geometry.
+- Does not support ArbGeomParam feature in Alembic.
+- Does not support Subdivision Meshes.
+- Does not support Materials.
+
+### USD
+
+USD file formats rely on [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) library. It comes with some known limitations:
+
+- Skinning is slow and baked on the CPU.
+- Does not support Face-varying attributes.
+- The `usd` plugin is not shipped in the python wheels yet.
+
+### VDB
+
+VDB file formats rely on [OpenVDB](https://github.com/AcademySoftwareFoundation/openvdb) and VTK libraries. It currently comes with some known limitations:
+
+- VDB Grid files are opened with a hard-coded 0.1 sampling rate.
+- The `vdb` plugin is not shipped in the python wheels yet.
+
+### Gaussian splatting
+
+Gaussian splatting (option `--point-sprites=gaussian`) needs depth sorting which is done internally using a compute shader. This requires support for OpenGL 4.3 which is not supported by macOS and old GPUs/drivers.
+
+## Troubleshooting
+
+### General
+
+> I have built F3D with raytracing support but the denoiser is not working.
+
+Make sure that VTK has been built with _OpenImageDenoise_ support (`VTKOSPRAY_ENABLE_DENOISER` option).
+
+> My model shows up all wrong, with inverted surfaces everywhere.
+
+Your data probably contains some translucent data for some reason, turn on translucency support by pressing `P` or using the `-p` command line option.
+
+> My 3D Gaussian Splatting data in .ply format isn't opened properly.
+
+.ply is a generic file format, we cannot assumes it's a 3DGS, thus we do not give good presets for this specific use case.
+It's recommended to use these options: `--point-sprites-size=1 --point-sprites=gaussian -syk`, but you can also add them in your [config file](06-CONFIGURATION_FILE.md).
+
+### Linux
+
+> I have a link error related to `stdc++fs` not found or I'm unable to run F3D due to filesystem errors.
+
+With some C++ STD library version, explicit linking to `stdc++fs` is not supported or required. We provide a CMake option `F3D_LINUX_LINK_FILESYSTEM` that you can set to the desired value to workaround this issue.
+
+> I have a link error related to undefined reference to symbol of `libatomic`.
+
+The GCC flag `-latomic` is not being added automatically with specific architectures, like `armel` and `RISCV64`. We provide a CMake option `F3D_LINUX_LIBRARY_LINK_ATOMIC` that you can set to `ON` to workaround this issue.
+
+> Thumbnails are not working in my file manager.
+
+- Check that your file manager supports the thumbnailer mechanism.
+- Check that you have updated your mime type database.
+- If all fails, remove your `.cache` user dir and check that `pcmanfm` thumbnails are working.
+- If they are working, then it is an issue specific to your file manager (see below for a potential work around).
+- If only a few format have working thumbnails, then it is an issue with the mime types database.
+- If no formats have working thumbnails, then it is can be an issue with sandboxing or with the `f3d-plugin-xxx.thumbnailer` files.
+- If only big file do not have thumbnails, this is intended, you can modify this behavior in the `thumbnail.d/05_all.json` configuration directory using the `max-size` option.
+
+> `--rendering-backend` CLI option is not working as expected
+
+Dynamically switching rendering backend require VTK 9.4.0, you may need to update VTK or to use our binary release.
+
+> I'm unable to link C++ examples against my local F3D install directory, it complains about missing VTK symbols
+
+Make sure to have VTK installed in your system or to add VTK install lib directory to `LD_LIBRARY_PATH`.
+
+### Windows
+
+> Using the portable binary release, I'm unable to run F3D because Windows warns about a security risk
+
+F3D is not signed by Microsoft yet so Windows shows this warning. Just click on the "more" button to be able to run F3D.
+
+> After installing F3D or registering the shell extension, my explorer is broken.
+
+Unregister the shell extension by running:
+
+```
+cd C:\path\to\f3d\bin\
+regsvr32 /u F3DShellExtension.dll
+```
+
+> I use F3D in a VM, the application fails to launch.
+
+OpenGL applications like F3D can have issues when launched from Windows Server or from a guest Windows because the access to the GPU is restricted.
+You can try to use a software implementation of OpenGL, called [Mesa](https://github.com/pal1000/mesa-dist-win/releases).
+
+- Download the latest `release-msvc`.
+- copy `x64/opengl32.dll`, `libgallium_wgl.dll` and `x64/libglapi.dll` in the same directory as `f3d.exe`.
+- set the environment variable `MESA_GL_VERSION_OVERRIDE` to 4.5.
+- run `f3d.exe`.
+
+> I run f3d from the command prompt and my Unicode characters are not displayed properly.
+
+Set the codepage to UTF-8, run `chcp 65001`.
+
+> When I install F3D, I can't add F3D to the path and the installer complains that: `PATH too long installer unable to modify PATH!`.
+
+There are too many entry in your PATH environment variable, remove the ones you do not use.
+
+> When I try to update F3D, the installer fails with: `Error opening file for writing: F3DShellExtension.dll`
+
+Your previous F3D installation is current being used to generate thumbnails, wait for a bit or reboot your computer before updating again.
+
+> When running F3D without interactivity (e.g. `f3d.exe --version`) from CLI, nothing is displayed.
+
+It's a limitation from the Windows "subsystem". Use `f3d-console.exe` instead.
+
+> When playing an animation, the animation appear to move too slowly
+
+This is a limitation of older VTK version before VTK 9.5.0, use our binaries or compile a recent version of VTK.
+
+### MacOS
+
+> I'm unable to use the binary release, macOS complains the file is invalid or corrupted
+
+Our macOS binary package are not signed, to work around this, you can simply run the following command:
+
+```
+xattr -cr /Applications/F3D.app
+```
+
+Then F3D should work as expected.
+
+### Python
+
+> I can't find `usd` and `vdb` plugins after installing f3d with pip
+
+The `usd` and `vdb` plugins are not shipped in the python wheels for now, you can compile F3D yourself with them though.
+
+> Raytracing does not work after installing f3d with pip
+
+F3D raytracing feature is not shipped in the python wheels for now, you can compile F3D yourself with it though.
diff -Nru f3d-3.2.0+dfsg/doc/user/ANIMATIONS.md f3d-3.4.1/doc/user/ANIMATIONS.md
--- f3d-3.2.0+dfsg/doc/user/ANIMATIONS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/ANIMATIONS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,81 +0,0 @@
-# Animations
-
-F3D is able to play animations for any files which contain them.
-Play them either interactively or by selecting a specific time to display.
-For files containing multiple animations, F3D allows the user to either play each animation separately or to select multiple animations to play at the same.
-
-## Demonstration
-
-This specific example uses an animation file which can be downloaded [here](https://github.com/f3d-app/f3d/blob/606089959c9520085a9cbf70660fb0ffc68fb934/testing/data/InterpolationTest.glb).
-
-<img width="1024" alt="1" src="https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/animation_0.png">
-
-Load the example animation file shown above by executing within command line: `f3d InterpolationTest.glb`
-
-<img width="1024" alt="2" src="https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/animation_1.png">
-To view current animation name, press <kbd>H</kbd> to open up cheatsheet menu
-
-<img width="1024" alt="3" src="https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/animation_2.png">
-Press <kbd>W</kbd> to cycle through available animations
-
-<img width="1024" alt="4" src="https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/animation_3.png">
-Press <kbd>space</kbd> to play/pause current animation.
-Note: A red bar runs along the bottom of screen to indicate the current time interval of the animation sequence if animation-progress was turned on.
-
-<img width="1024" alt="5" src="https://media.githubusercontent.com/media/f3d-app/f3d-media/main/media/animation_4.png">
-"All Animations" will play all animations at the same time if supported by the file format.
-
-## Command line options
-
-F3D animation behavior can be fully controlled from the command line using the following options.
-
-| Options                      | Default             | Description                                     |
-| ---------------------------- | ------------------- | ----------------------------------------------- |
-| \-\-animation\-indices       |                     | Select the animations to play.                  |
-| \-\-animation\-indices=-1    |                     | Play all animations at once (only if supported) |
-| \-\-animation\-speed\-factor | Time Unit = Seconds | Adjust time unit.                               |
-| \-\-animation\-frame\-rate   | 60 FPS              | Adjust animation frame rate.                    |
-| \-\-animation\-time          |                     | Load a specific time value on start.            |
-
-## Animation Interactions
-
-- Press <kbd>W</kbd> to cycle through animations
-- Press <kbd>Space</kbd> to play/pause animation
-
-## Cycling Animations
-
-Pressing <kbd>W</kbd> let you cycle the animation to show.
-
-When cycling, F3D will cycle like this:
-
-- Multiple animations or all animations
-- Animation index 0
-- Animation index 1
-- ...
-- Animation index N
-- All animations (if multiple animations supported and more than one animation is available)
-- No animations
-
-Please note that if you selected multiple animation indices, you will never cycle back to it.
-All animations will only be cycled if supported by the currently loaded files.
-
-## Time Units
-
-When F3D plays an animation, it assumes the time unit is in seconds to show accurate speed of animation.
-
-## Coloring range
-
-When coloring while loading a time value or playing the animation, the coloring range will be automatically expanded from previously loaded time value,
-this include `--animation-time` that first load the initial time before loading the provided time value.
-
-## Animation Support Level
-
-The animation support level is listed for each reader [here](SUPPORTED_FORMATS.md).
-
-- NONE: Animation is not supported, either by the file format or the implementation
-- UNIQUE: There will never be more then one animation
-- SINGLE: There can be multiple animations but only one can be selected at a time
-- MULTI: Multiple animations can be selected at a time
-
-When opening multiple files at the same time using the `--multi-file-mode` option and selecting multiple animations to show,
-F3D may warn when combining SINGLE and non SINGLE files, but will still try to respect the chosen animation indices.
diff -Nru f3d-3.2.0+dfsg/doc/user/COLOR_MAPS.md f3d-3.4.1/doc/user/COLOR_MAPS.md
--- f3d-3.2.0+dfsg/doc/user/COLOR_MAPS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/COLOR_MAPS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,65 +0,0 @@
-# Color maps
-
-## Image files
-
-It is possible to specify a color map using the `--colormap-file` option.
-The value of the option can be an absolute path to an image file, a relative path, a filename or a filestem.
-If it is not an absolute path, these directories are used to find the file (in this order):
-
-- Linux: `${XDG_CONFIG_HOME}/f3d/colormaps`, `~/.config/f3d/colormaps`, `/etc/f3d/colormaps`, `/usr/share/f3d/colormaps`, `[install_dir]/share/f3d/colormaps`
-- Windows: `%APPDATA%\f3d\colormaps`, `[install_dir]\share\f3d\configs\colormaps`
-- macOS: `${XDG_CONFIG_HOME}/f3d/colormaps`, `~/.config/f3d/colormaps`, `/usr/local/etc/f3d/colormaps`, `f3d.app/Contents/Resources/configs/colormaps`
-  The first existing file found is used.
-  If it is a filestem, all [supported image](#supported-formats) extensions are tried.
-
-We provide some ready to use color maps files, listed in the table below:
-
-| Name       | Image                                                                                                                                   |
-| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
-| cividis    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/cividis.png?raw=true" class="cm" width="256" height="10" />    |
-| cubehelix  | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/cubehelix.png?raw=true" class="cm" width="256" height="10" />  |
-| gist_earth | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/gist_earth.png?raw=true" class="cm" width="256" height="10" /> |
-| hot        | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/hot.png?raw=true" class="cm" width="256" height="10" />        |
-| inferno    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/inferno.png?raw=true" class="cm" width="256" height="10" />    |
-| magma      | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/magma.png?raw=true" class="cm" width="256" height="10" />      |
-| plasma     | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/plasma.png?raw=true" class="cm" width="256" height="10" />     |
-| seismic    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/seismic.png?raw=true" class="cm" width="256" height="10" />    |
-| viridis    | <img src="https://github.com/f3d-app/f3d/blob/master/resources/colormaps/viridis.png?raw=true" class="cm" width="256" height="10" />    |
-
-It is possible to create a custom color map by creating a simple RGB image in any [supported formats](#supported-formats), and any resolution (if the height is more than 1 row, only the first one is taken into account). The image must be copied in the user config directory:
-
-- Linux/macOS: `~/.config/f3d/colormaps`
-- Windows: `%APPDATA%\f3d\colormaps`
-
-### Supported formats
-
-Here's the list of all supported image formats that can be used as color maps:
-
-- `.png`
-- `.pnm`, `.pgm`, `.ppm`
-- `.tif`, `.tiff`
-- `.bmp`
-- `.slc`
-- `.hdr`
-- `.pic`
-- `.jpeg`, `.jpg`
-- `.MR`
-- `.CT`
-- `.mhd`, `.mha`
-- `.tga`
-- `.exr` (if `F3D_MODULE_EXR` is [enabled](../dev/BUILD.md))
-
-## Custom values
-
-If no colormap file is specified, it is also possible to set values manually using the `--colormap` option with a dedicated [parsing](PARSING.md#colormap).
-For example, the default value corresponds to the `hot` preset which can be defined manually with `--colormap=0.0,rgb(0,0,0),0.4,rgb(230,0,0),0.8,rgb(230,230,0),1.0,rgb(255,255,255)`.
-It consists of 4 tuples:
-
-| Value | RGB                                                               |
-| ----- | ----------------------------------------------------------------- |
-| 0.0   | <span style="color:rgb(0,0,0)">&#9632;</span> 0, 0, 0             |
-| 0.4   | <span style="color:rgb(230,0,0)">&#9632;</span> 230, 0, 0         |
-| 0.8   | <span style="color:rgb(230,230,0)">&#9632;</span> 230, 230, 0     |
-| 1.0   | <span style="color:rgb(255,255,255)">&#9632;</span> 255, 255, 255 |
-
-Values in between are interpolated.
diff -Nru f3d-3.2.0+dfsg/doc/user/COMMANDS.md f3d-3.4.1/doc/user/COMMANDS.md
--- f3d-3.2.0+dfsg/doc/user/COMMANDS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/COMMANDS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,145 +0,0 @@
-# Commands
-
-F3D provides access to commands through [interactive console](#interactive-console), [command script](#command-script---command-script) and [bindings configuration](CONFIGURATION_FILE.md#bindings).
-Commands let you trigger specific behavior that may not be available otherwise.
-Please note commands are currently experimental and the behaviors, actions may be added or removed without deprecation.
-Action names and arguments may also change without deprecation.
-
-Commands have the following syntax:
-
-`action [args]`
-
-## libf3d provided commands
-
-The libf3d provides a few commands, many related to manipulating libf3d (options)[../libf3d/OPTIONS.md].
-
-`set option.name values`: A command to set a libf3d option, eg: `set scene.up.direction +Z` or `set render.hdri.file "/path/to/file with spaces.png"`
-
-`toggle option.name`: A command to toggle a boolean libf3d option, eg: `toggle ui.scalar_bar`.
-
-`reset option.name`: A command to reset a libf3d option to its default values, eg: `reset render.background.blur.coc`.
-
-`clear`: A command to clear console. No argument.
-
-`print option.name`: A command to print the value of an libf3d option, eg: `print scene.up.direction`.
-
-`cycle_anti_aliasing`: A command to cycle between the anti-aliasing method (`none`,`fxaa`,`ssaa`).
-
-`cycle_animation`: A specific command to cycle `scene.animation.index` option using model information, No argument.
-
-`cycle_coloring field/array/component`: A specific command to manipulate scivis options using model information.
-Supports `field`, `array` or `component` arguments, see [documentation](INTERACTIONS.md#cycling-coloring).
-eg: `cycle_coloring array`.
-
-`roll_camera value`: A specific command to roll the camera on its side, takes an angle in degrees as an argument.
-eg: `roll_camera 120`.
-
-`elevation_camera value`: A specific command to tilt the camera up or down, takes an angle in degrees as an argument.
-eg: `elevation_camera 120`.
-
-`azimuth_camera value`: A specific command to tilt the camera right or left, takes an angle in degrees as an argument.
-eg: `azimuth_camera 120`.
-
-`increase_light_intensity`: A specific command to increase light intensity. No argument.
-
-`decrease_light_intensity`: A specific command to decrease light intensity. No argument.
-
-`increase_opacity`: A specific command to increase opacity. Unset opacity will be treated as if it has a value of 1. No argument.
-
-`decrease_opacity`: A specific command to decrease opacity. Unset opacity will be treated as if it has a value of 1. No argument.
-
-`print_scene_info`: A specific command to print information about the scene, No argument.
-
-`print_coloring_info`: A specific command to print information about coloring settings, No argument.
-
-`print_mesh_info`: A specific command to print information from the mesh importer, No argument.
-
-`print_options_info`: A specific command to print configuration options that have a value, No argument.
-
-`set_camera front/top/right/isometric`: A specific command to position the camera in the specified location relative to the model.
-Supports `front`, `top`, `right`, `isometric` arguments. eg: `set_camera top`.
-
-`toggle_volume_rendering`: A specific command to toggle `model.volume.enable` and print coloring information. No argument.
-
-`stop_interactor`: A specific command to stop the interactor hence quitting the application. No argument.
-
-`reset_camera`: A specific command to reset the camera to its original location. No argument.
-
-`toggle_animation`: A specific command to start/stop the animation. No argument.
-
-`cycle_verbose_level` : Cycles between the verbose level (`Debug`, `Info`, `Warning`, `Error`, `Quiet`).
-
-`add_files [path/to/file1] [path/to/file2]`: A specific command to add files to the scene (overridden by a F3D specific command, see below). Take one or more files as arguments.
-eg: `add_files /path/to/dragon.vtu`.
-
-## F3D provided specific commands
-
-The F3D application provides a few more commands.
-
-`exit`: A specific command to quit the application. No argument.
-
-`load_previous_file_group [keep_camera]`: A specific command to load the previous file or file group. Camera state is kept if `keep_camera` is true (default: false).
-
-`load_next_file_group [keep_camera]`: A specific command to load the next file or file group. No argument. Camera state is kept if `keep_camera` is true (default: false).
-
-`reload_current_file_group`: A specific command to reload the current file or file group. No argument.
-
-`add_current_directories`: A specific command to add all files from the current file or file group directories. No argument.
-
-`take_screenshot [filename]`: A specific command to [take a screenshot](INTERACTIONS.md#taking-screenshots). If filename is not specified,
-rely on the `--screenshot-filename` CLI option. eg: `take_screenshot path/to/file.png`.
-
-`take_minimal_screenshot [filename]`: A specific command to [take a minimal screenshot](INTERACTIONS.md#taking-screenshots). If filename is not specified,
-rely on the `--screenshot-filename` CLI option. eg: `take_screenshot path/to/file.png`.
-
-`add_files [path/to/file1] [path/to/file2]`: A specific command to add files to the scene according to the current grouping logic. Take one or more files as arguments.
-eg: `add_files /path/to/dragon.vtu`.
-
-`set_hdri [path/to/hdri]`: A specific command to set and use an HDRI image. Take a HDRI file as an argument.
-eg: `set_hdri /path/to/file.hdr`.
-
-`add_files_or_set_hdri [path/to/file1] [path/to/file2]`: A specific command that will process each files and either, `set_hdri` if the provided file uses a recognised HDR extension or `add_files` otherwise. Take one or more files as arguments.
-eg: `add_files_or_set_hdri /path/to/dragon.vtu /path/to/file.hdr`.
-
-`remove_file_groups`: A specific command to remove all files. No argument.
-
-`alias [alias_name] [command]`: A specific command to create an alias for a command. Take an alias name and a command as arguments.
-eg: `alias myrotate roll_camera 90`.
-
-## Command Script (`--command-script`)
-
-F3D provides a feature to execute commands from a script file using the `--command-script` [CLI option](OPTIONS.md). This allows users to automate a sequence of commands by listing them in a plain text file, eg: `f3d --command-script path/to/command_script.txt`.
-
-Example Command Script, commands are separated by new lines, comments are supported:
-
-```shell
-# A comment
-roll_camera 90
-toggle ui.scalar_bar
-print_scene_info # Another comment
-increase_light_intensity
-```
-
-## Interactive Console
-
-If F3D is built with `F3D_MODULE_UI` support, pressing <kbd>Esc</kbd> will open the console mode. It's possible to type any command in the input field and pressing <kbd>Enter</kbd> will trigger the command instantly.
-
-Press <kbd>Tab</kbd> to autocomplete the command action and display command action suggestions.
-
-Press <kbd>&uarr;</kbd> and <kbd>&darr;</kbd> to navigate through the command history.
-
-## Command syntax
-
-Command syntax is similar to bash, as in they will be split by "token" to be processed.
-
-- Tokens are spaces separated, eg: `set scene.up.direction +Z`.
-- Tokens can also be quoted to support spaces inside, eg: `set render.hdri.file "/path/to/file with spaces.png"`.
-- Supported quotes are `` `'" ``, eg: `set render.hdri.file '/path/to/file with spaces.png'`.
-- Quotes inside quotes are supported as well, eg: `set render.hdri.file "/path/to/file'with'quotes.png"`.
-- Quotes and spaces can be escaped, eg: `set render.hdri.file /path/to/file\ with\ spaces\ and\ \'quotes\".png`.
-- Comment are supported using `#`, Any character after will be ignored. Use `\#` to add it verbatim.
-- Escapes can be escaped too: eg: `set render.hdri.file C:\\path\\to\\windows\\file.png`.
-- Other escaped character will be processed as if the escape was not present, eg: `set scene.up.direction +\Z`
-- Unfinished quoted section is invalid, eg: `set scene.up.direction "+Z`
-- A escape at the end is also invalid, eg: `set scene.up.direction +Z\`
-- Options values are [parsed](PARSING.md) according to their types.
diff -Nru f3d-3.2.0+dfsg/doc/user/CONFIGURATION_FILE.md f3d-3.4.1/doc/user/CONFIGURATION_FILE.md
--- f3d-3.2.0+dfsg/doc/user/CONFIGURATION_FILE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/CONFIGURATION_FILE.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,254 +0,0 @@
-# Configuration File
-
-## Options
-
-Almost all the command-line [options](OPTIONS.md) can be controlled using configuration files.
-Configuration files uses the "long" version of the command-line options in a JSON
-formatted file to provide values for these options. It is also possible to use
-the [libf3d options](../libf3d/OPTIONS.md) syntax.
-
-These options can be organized by block using a regular expression, glob, or exact match
-for each block in order to provide different default values for the different filetypes. For
-more on glob and exact matching see the [Glob and Exact Matching section](#glob-and-exact-matching).
-
-Using a command-line option will override similar option set in any config files.
-
-Some options are only taken into account on the first load and not on subsequent loads,
-when switching between files.
-
-A typical config file with options may look like this:
-
-```json
-[
-  {
-    "options": {
-      "background-color": "0.7,0.7,0.7",
-      "color": "0.5,0.1,0.1",
-      "anti-aliasing": true,
-      "timer": true,
-      "progress": true,
-      "axis": true,
-      "bar": true,
-      "roughness": 0.2,
-      "grid": true,
-      "scalar-coloring": true
-    }
-  },
-  {
-    "match": ".*vt.",
-    "options": {
-      "edges": true
-    }
-  },
-  {
-    "match": ".*gl(tf|b)",
-    "options": {
-      "raytracing": true,
-      "denoise": true,
-      "samples": 3
-    }
-  },
-  {
-    "match": ".*mhd",
-    "options": {
-      "volume": true
-    }
-  }
-]
-```
-
-Here, the first block defines a basic configuration with many desired options for all files.
-The second block specifies that all files ending with vt., eg: vtk, vtp, vtu, ... will be shown with edges visibility turned on.
-The third block specifies raytracing usage for .gltf and .glb files.
-The last block specifies that volume rendering should be used with .mhd files.
-
-The following options <b> cannot </b> be set via config file:
-`help`, `version`, `list-readers`, `list-rendering-backends`, `scan-plugins`, `config`, `no-config`, `define`, `reset` and `input`.
-
-The following options <b>are only taken on the first load</b>:
-`no-render`, `output`, `position`, `resolution`, `frame-rate` and all testing options.
-
-Boolean options that have been turned on in the configuration file can be turned
-off on the command line if needed, eg: `--point-sprites=false`.
-
-### Imperative Options
-
-Command line options and options that are changed interactively overrides options that are set in configuration files.
-This is not always a desired behavior, so in order to force an option to always be taken into account even if set in command line or changed interactively, it is possible to use imperative options, by adding a `!` in front of the option name, eg:
-
-```json
-[
-  {
-    "options": {
-      "!axis": true
-    }
-  },
-  {
-    "match": ".*(stl)",
-    "options": {
-      "!edges": true
-    }
-  }
-]
-```
-
-In the above example, when loading or reloading a file, the axis is always turned on and when loading a .stl file, the edges are always turned on.
-
-## Bindings
-
-All interaction bindings can be configured using configuration files.
-The logic is the same as with options, where configuration blocks are used depending on the filename being loaded using regular expression.
-A `bindings` block can be added to specify associations between binds (eg : `Ctrl+O`) and one or multiple commands.
-
-A typical config file with bindings may look like this:
-
-```json
-[
-  {
-    "bindings": {
-      "Ctrl+Shift+O": "toggle ui.filename",
-      "Any+3": "roll_camera -90",
-      "O": "set_camera isometric"
-    }
-  },
-  {
-    "match": ".*vtu",
-    "bindings": {
-      "Any+3": "roll_camera 90",
-      "Shift+O": "toggle model.point_sprites.enable",
-      "Ctrl+O": [
-        "toggle render.grid.enable",
-        "toggle scene.camera.orthographic"
-      ],
-      "None+I": "toggle ui.axis"
-    }
-  }
-]
-```
-
-Here, the first block define new bindings for all and any files.
-It even replace an existing default [interaction](INTERACTIONS.md) on the `O` key with its own.
-Each bind is associated to the [command](COMMANDS.md) to execute when it is pressed.
-
-In the second block, new bindings are defined for files ending in `.vtu`, and there bindings
-will only be available when loading such a file.
-In the first config block, we define bindings for all and any files. It also replace an existing default
-interaction on the `Any+3` bind and even define a bindings that have multiple commands using a json array
-on the `Ctrl+O` bind.
-
-Please note this configuration feature is only available through config file and not through the command line.
-However, it is possible to check your current binding configuration by using the `--list-bindings` CLI options.
-
-### Bind
-
-A bind is the association of a modifier and an interaction.
-
-Supported modifiers are:
-
-`None` : No modifiers is pressed, can be omitted completely.
-`Ctrl` : Control key is pressed, no distinction between left and right.
-`Shift` : Shift key is pressed, no distinction between left and right.
-`Ctrl+Shift` : Control and Shift keys are pressed, no distinction between left and right.
-`Any` : A special modifier that does not consider modifiers keys but can only be reached
-if there is no bind with the same interaction.
-
-Supported interactions are legion, eg:
-
-- Keyboard letter keys, eg:
-  - `O`
-  - `P`
-- Keyboard number keys, eg:
-  - `2`
-  - `5`
-- Keyboard special keys, eg:
-  - `Enter`
-  - `Delete`
-- Other interactions, eg:
-  - `Drop` when files or folders are dropped on F3D window
-
-To identify the interaction to use, just use `f3d --verbose` and perform the interaction, F3D will log it like this:
-
-```
-Interaction: KeyPress Ctrl+Shift+Insert
-```
-
-Please note that the interaction itself can be modified by the modifiers, hence some binds may be unreachable depending on your
-keyboard layout, eg. on a `QWERTY` layout, the bind `Exclam` is unreachable while `Shift+Exclam` is reachable.
-We tried to make the default bindings of F3D to be a keyboard layout independent as possible, but feel free to redefined them
-for your needs.
-
-## Glob and Exact Matching
-
-While regex is the default match type, glob and exact match types are also supported. Below is an example using glob and exact matching.
-
-```json
-[
-  {
-    "options": {
-      "background-color": "0.7,0.7,0.7",
-      "color": "0.5,0.1,0.1",
-      "anti-aliasing": true,
-      "timer": true,
-      "progress": true,
-      "axis": true,
-      "bar": true,
-      "roughness": 0.2,
-      "grid": true,
-      "scalar-coloring": true
-    }
-  },
-  {
-    "match-type": "glob",
-    "match": "*vt?",
-    "options": {
-      "edges": true
-    }
-  },
-  {
-    "match-type": "glob",
-    "match": "**/*gl{tf,b}",
-    "options": {
-      "raytracing": true,
-      "denoise": true,
-      "samples": 3
-    }
-  },
-  {
-    "match-type": "exact",
-    "match": "/path/to/file.mhd",
-    "options": {
-      "volume": true
-    }
-  }
-]
-```
-
-For glob patterns, globstar (`**`) is supported.
-
-Glob and exact matching can be used for bindings as well.
-
-## Locations
-
-Single .json file for configuration is supported, as well a config directory containing multiple .json config files.
-In the case of config directory, .json file are read in alphabetical order and an option or binding in a block can be overridden
-by the same option or bind being set in another block in another .json file read after.
-
-F3D provides a default config directory for generic usage (`config.d`) and a thumbnail specific config directory (`thumbnail.d`).
-You can edit the file contained in these directory or add your own in specific directories (see below).
-
-F3D looks for configuration files in different locations depending on your operating system.
-Existing configuration files are read in order and combined with later entries, potentially overriding previously read entry with the same names.
-
-- Linux: `/etc/f3d/config(.json,.d)`, `/usr/share/f3d/configs/config(.json,.d)`, `[install_dir]/share/f3d/configs/config(.json,.d)`, `${XDG_CONFIG_HOME}/f3d/config(.json,.d)`
-- Windows: `[install_dir]\share\f3d\configs\(config.json,.d)`, `%APPDATA%\f3d\(config.json,.d)`
-- macOS: `/usr/local/etc/f3d/config(.json,.d)`, `f3d.app/Contents/Resources/configs/config(.json,.d)`, `${HOME}/Library/Application Support/f3d/config(.json,.d)`
-
-Please note that, on Linux, `XDG_CONFIG_HOME` implementation can fallback on `HOME` environment variables as specified [here](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
-
-The binary release will install the default config directory.
-On Linux, they will be installed in `[install_dir]/share/f3d/configs/`, on Windows, they will be installed in `[install_dir]\share\f3d\configs\`, on macOS, it will be installed in the bundle.
-
-Please note there is a command line option to control the configuration file to read. Using it, one can specify an absolute/relative path for the configuration path, but also
-only the filename or filestem (`.json` and `.d` will be added) to look for in the locations listed above, , eg: `f3d --config=custom_config` will look
-for `custom_config.json` and `custom_config.d` in locations listed above.
-When specifying an absolute/relative path for the configuration file, a single file is read. If not, all files from locations listed above, with the overriding logic specified above.
diff -Nru f3d-3.2.0+dfsg/doc/user/DESKTOP_INTEGRATION.md f3d-3.4.1/doc/user/DESKTOP_INTEGRATION.md
--- f3d-3.2.0+dfsg/doc/user/DESKTOP_INTEGRATION.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/DESKTOP_INTEGRATION.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,74 +0,0 @@
-# Desktop Integration
-
-F3D can be integrated in the desktop experience.
-
-## Linux
-
-For Linux desktop integration, F3D rely on mime types files as defined by the [XDG standard](https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html), .thumbnailer file as specified [here](https://wiki.archlinux.org/title/File_manager_functionality#Thumbnail_previews) and .desktop file as specified [here](https://wiki.archlinux.org/title/desktop_entries). Many file managers use this mechanism, including nautilus, thunar, pcmanfm and caja.
-
-The simplest way to obtain desktop integration on Linux is to use a package for your distribution, or the .deb binary release package we provide if compatible with your distribution.
-In other cases, the binary release archive can be used like this:
-
-0. Make sure `~/.local/bin` is part of your `PATH`
-1. Extract F3D binary release archive in `~/.local/`
-2. Update your [mime database](https://linux.die.net/man/1/update-mime-database) pointing to `~/.local/share/mime`
-3. Update your [desktop database](https://linuxcommandlibrary.com/man/update-desktop-database) pointing to `~/.local/share/application`
-
-```bash
-export PATH=$PATH:~/.local/bin
-tar -xzvf f3d-1.3.0-Linux.tar.gz -C ~/.local/
-sudo update-mime-database ~/.local/share/mime/
-sudo update-desktop-database ~/.local/share/applications
-```
-
-F3D supports rendering in environments with limited graphical contexts, including sandboxes or without Xorg running.
-In most cases, the default behavior will automatically detect which capabilities are available and use the right rendering backend,
-it is possible to specify it using the `--rendering-backend` CLI option.
-
-- If `libEGL.so` is available, you can use: `--rendering-backend=egl`.
-- If `libOSMesa.so` is available, you can use: `--rendering-backend=osmesa`.
-- When using `--rendering-backend=auto`, F3D will try to load the following libraries in order and use the first one it was able to load and use:
-  - `libGLX.so`
-  - `libEGL.so`
-  - `libOSMesa.so`
-
-You can use that feature for thumbnail generation whenever needed (e.g., Nautilus or other sandboxing file browser),
-by modifying the [thumbnail config file](CONFIGURATION_FILE.md), eg:`~/.config/f3d/thumbnail.json`:
-
-```javascript
-{
-    ".*": {
-        "rendering-backend": "egl"
-    }
-}
-```
-
-If you use the portable archive, make sure to extract it somewhere the sandboxing system has access to, eg: Nautilus uses `bwrap` and gives it access to `/usr` but not to `/opt`.
-
-## Windows
-
-For Windows desktop integration, F3D rely on a registered shell extension.
-
-Using the F3D NSIS installer (.exe) is the simplest way to enable thumbnails and integrate F3D on windows.
-
-It is also possible to do it manually when using the zipped binary release archive, on installation, just run:
-
-```
-cd C:\path\to\f3d\bin\
-regsvr32 F3DShellExtension.dll
-```
-
-To remove the shell extension, run:
-
-```
-cd C:\path\to\f3d\bin\
-regsvr32 /u F3DShellExtension.dll
-```
-
-## MacOS
-
-There is no support for thumbnails on MacOS, the .dmg binary release provides automatic file openings.
-
-## Limitations
-
-- TIFF files are excluded from the thumbnailer to avoid conflicts with other better 2D thumbnailers.
diff -Nru f3d-3.2.0+dfsg/doc/user/FINAL_SHADER.md f3d-3.4.1/doc/user/FINAL_SHADER.md
--- f3d-3.2.0+dfsg/doc/user/FINAL_SHADER.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/FINAL_SHADER.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,76 +0,0 @@
-# Final shaders
-
-It is possible to specify a final full screen shader with the option `--final-shader`.
-It is executed as the last shader in the rendering pipeline.
-
-The shader code can be passed directly as a single code line.
-It's possible to pass the content of a shader file using the following command:
-
-```sh
-f3d model.obj --final-shader "$(cat final.glsl)"
-```
-
-Note that the command above works on Linux and bash, and should be adapted depending on the operating system and the shell/terminal used.
-
-## Requirements
-
-It is required to implement the function `vec4 pixel(vec2 uv)`.
-The value `uv` is ranging from 0 to 1 in both directions. `(0,0)` is the bottom left corner, and `(1,1)` is the upper right corner.
-The first component is the horizontal direction.
-
-## Uniforms
-
-It is possible to access these uniforms:
-
-- `source` (type: `sampler2d`): texture of the image generated by rendering pipeline.
-- `resolution` (type: `ivec2`): resolution of the texture `source`.
-
-## Examples
-
-Here are three shader examples to illustrate how an implementation looks like.
-
-### Negative
-
-```glsl
-vec4 pixel(vec2 uv)
-{
-    vec3 value = texture(source, uv).rgb;
-    return vec4(vec3(1.0) - value, 1.0);
-}
-```
-
-### Vignette
-
-```glsl
-vec4 pixel(vec2 uv)
-{
-    float l = clamp(1.0 - 2.0 * length(uv - 0.5), 0.0, 1.0);
-    return vec4(l*texture(source, uv).rgb, 1.0);
-}
-```
-
-### Box blur
-
-```glsl
-vec4 pixel(vec2 uv)
-{
-    const float radius = 0.03;
-    const int samples = 20;
-    const float step = radius / float(samples);
-
-    vec3 sum = vec3(0);
-
-    for (int i = -samples; i <= samples; i++)
-    {
-        for (int j = -samples; j <= samples; j++)
-        {
-            sum += texture(source, uv + vec2(i, j) * step).rgb;
-        }
-    }
-
-    float d = 1.0 + 2.0 * float(samples);
-    sum /= d * d;
-
-    return vec4(sum, 1.0);
-}
-```
diff -Nru f3d-3.2.0+dfsg/doc/user/INSTALLATION.md f3d-3.4.1/doc/user/INSTALLATION.md
--- f3d-3.2.0+dfsg/doc/user/INSTALLATION.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/INSTALLATION.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,62 +0,0 @@
-# Installation
-
-## Current release packages (3.1.0)
-
-| Platform               | Files                                                                                                                       |
-| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| Windows (installer)    | [F3D-3.1.0-Windows.exe](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-Windows-x86_64-raytracing.exe)    |
-| Windows (portable)     | [F3D-3.1.0-Windows.zip](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-Windows-x86_64-raytracing.zip)    |
-| MacOS (Silicon)        | [F3D-3.1.0-macOS-arm64.dmg](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-macOS-arm64-raytracing.dmg)   |
-| MacOS (Intel)          | [F3D-3.1.0-macOS-x86_64.dmg](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-macOS-x86_64-raytracing.dmg) |
-| Linux (Debian package) | [F3D-3.1.0-Linux.deb](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-Linux-x86_64-raytracing.deb)        |
-| Linux (portable)       | [F3D-3.1.0-Linux.tar.xz](https://github.com/f3d-app/f3d/releases/download/v3.1.0/F3D-3.1.0-Linux-x86_64-raytracing.tar.xz)  |
-
-### Notes
-
-- MacOS package is not signed, see the [troubleshooting](LIMITATIONS_AND_TROUBLESHOOTING.md#macos) section for a workaround if needed.
-- MacOS packages require macOS >= 12.0
-- Linux packages require GLIBC >= 2.31
-
-## Alternatives
-
-You can find other (older, smaller, checksum, RCs) release related packages and files on the [releases page](https://github.com/f3d-app/f3d/releases).
-You can also find there the [nightly release](https://github.com/f3d-app/f3d/releases/tag/nightly) to test the development version of F3D.
-See the [desktop integration](DESKTOP_INTEGRATION.md) section in order actually integrate the binary release in your desktop.
-Alternatively, you can build F3D yourself by following the [build](../dev/BUILD.md) guide.
-
-You can also find packages for the following repositories:
-
-### Windows
-
-- [Scoop](https://scoop.sh/#/apps?q=f3d&s=0&d=1&o=true)
-- [winget](https://winstall.app/apps/f3d-app.f3d)
-
-### MacOS
-
-- [Homebrew](https://formulae.brew.sh/formula/f3d)
-
-### Linux
-
-- [Ubuntu](https://packages.ubuntu.com/search?keywords=f3d&searchon=names&exact=1&suite=all&section=all)
-- [Debian](https://packages.debian.org/search?keywords=f3d&searchon=names&exact=1&suite=all&section=all)
-- [Arch](https://archlinux.org/packages/extra/x86_64/f3d/)
-- [NixOS](https://search.nixos.org/packages?query=f3d)
-- [openSUSE](https://software.opensuse.org/package/f3d)
-- [OpenMandriva](https://github.com/OpenMandrivaAssociation/f3d)
-- [Mageia](https://madb.mageialinux-online.org/show?distribution=cauldron&architecture=x86_64&rpm=f3d&repo=&group=&page=)
-- [ALT Linux](https://packages.altlinux.org/en/sisyphus/srpms/f3d/)
-- Fedora and others through [openSUSE OBS](https://build.opensuse.org/package/show/home:AndnoVember:F3D/f3d)
-- [Guix](https://hpc.guix.info/package/f3d)
-- [Flathub](https://flathub.org/apps/details/io.github.f3d_app.f3d)
-- [ALT linux](https://packages.altlinux.org/en/sisyphus/srpms/f3d/)
-
-### Other
-
-- [FreeBSD](https://cgit.freebsd.org/ports/tree/graphics/f3d)
-- [Spack](https://packages.spack.io/package.html?name=f3d)
-- [pypi](https://pypi.org/project/f3d/) (library only)
-
-### libf3d based tools
-
-- [Exhibit](https://flathub.org/apps/io.github.nokse22.Exhibit)
-- [f3dviewer](https://github.com/ccseer/f3dviewer)
diff -Nru f3d-3.2.0+dfsg/doc/user/INTERACTIONS.md f3d-3.4.1/doc/user/INTERACTIONS.md
--- f3d-3.2.0+dfsg/doc/user/INTERACTIONS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/INTERACTIONS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,127 +0,0 @@
-# Interactions
-
-## 3D Mouse Interactions
-
-Simple interaction with the displayed data is possible directly within the window. It is as follows:
-
-- _Click and drag_ with the _left_ mouse button to rotate around the focal point of the camera.
-- Hold <kbd>Shift</kbd> then _Click and drag_ with the _left_ mouse button to translate the camera.
-- Hold <kbd>Ctrl</kbd> then _Click and drag_ with the _left_ mouse button to rotate around the axis of the camera.
-- Hold <kbd>Shift</kbd> then _Click and drag_ horizontally with the _right_ mouse button to rotate the HDRI.
-- _Click and drag_ vertically with the _right_ mouse button to zoom in/out.
-- _Move the mouse wheel_ to zoom in/out.
-- Hold <kbd>Ctrl</kbd>, then _Click and drag_ vertically with the _right_ mouse button to zoom in/out to mouse position.
-- Hold <kbd>Ctrl</kbd>, then _Move the mouse wheel_ to zoom in/out to mouse position.
-- _Click and drag_ with the _middle_ mouse button to translate the camera.
-- _Click_ with the _middle_ mouse button to center the camera on the point under the cursor (hold <kbd>Shift</kbd> to allow forward or backward movement).
-
-> Note: When playing an animation with a scene camera, camera interactions are locked.
-
-## Bindings
-
-The coloring can be controlled directly by pressing the following hotkeys:
-
-- <kbd>A</kbd>: cycle between anti-aliasing modes.
-- <kbd>C</kbd>: cycle between coloring with array from point data and from cell data.
-- <kbd>S</kbd>: cycle the array to color with.
-- <kbd>Y</kbd>: cycle the component of the array to color with.
-
-See the [coloring cycle](#cycling-coloring) section for more info.
-
-Other options can be toggled directly by pressing the following hotkeys:
-
-- <kbd>W</kbd>: [cycle animations](ANIMATIONS.md#cycling-animations).
-- <kbd>B</kbd>: display of the scalar bar, only when coloring and not using direct scalars.
-- <kbd>V</kbd>: volume rendering, forces coloring.
-- <kbd>I</kbd>: opacity function inversion during volume rendering.
-- <kbd>O</kbd>: point sprites rendering.
-- <kbd>P</kbd>: translucency support.
-- <kbd>Q</kbd>: ambient occlusion.
-- <kbd>T</kbd>: tone mapping.
-- <kbd>E</kbd>: the display of cell edges.
-- <kbd>X</kbd>: the trihedral axes display.
-- <kbd>G</kbd>: the horizontal grid display.
-- <kbd>N</kbd>: the display of the file name.
-- <kbd>M</kbd>: the display of the metadata if exists.
-- <kbd>Z</kbd>: the display of the FPS counter.
-- <kbd>R</kbd>: raytracing.
-- <kbd>D</kbd>: the denoiser when raytracing.
-- <kbd>U</kbd>: background blur, useful with an HDRI skybox.
-- <kbd>K</kbd>: trackball interaction mode.
-- <kbd>F</kbd>: HDRI ambient lighting.
-- <kbd>J</kbd>: the display of the HDRI skybox.
-- <kbd>L</kbd>: increase lights intensity.
-- <kbd>Shift</kbd>+<kbd>L</kbd>: decrease lights intensity.
-- <kbd>Ctrl</kbd>+<kbd>P</kbd>: increase opacity.
-- <kbd>Shift</kbd>+<kbd>P</kbd>: decrease opacity.
-- <kbd>Shift</kbd>+<kbd>A</kbd>: toggle armature.
-
-Note that the raytracing hotkeys are only available if F3D is build with raytracing enabled.
-
-Camera Hotkeys:
-
-- <kbd>1</kbd>: front view camera.
-- <kbd>2</kbd>: roll the camera up (elevation) by 90 degrees.
-- <kbd>3</kbd>: right view camera.
-- <kbd>4</kbd>: roll the camera left by 90 degrees.
-- <kbd>5</kbd>: toggle orthographic projection.
-- <kbd>6</kbd>: roll the camera right by 90 degrees.
-- <kbd>7</kbd>: top view camera.
-- <kbd>8</kbd>: roll the camera down (elevation) by 90 degrees.
-- <kbd>9</kbd>: isometric view camera.
-- <kbd>Enter</kbd>: reset the camera to its initial parameters.
-
-Other hotkeys and interactions are available:
-
-- <kbd>H</kbd>: key to toggle the display of a cheat sheet showing all these hotkeys and their statuses.
-- <kbd>CTRL</kbd> + <kbd>Q</kbd>: close the window and quit F3D.
-- <kbd>Esc</kbd>: display the console or hide console/minimal console.
-- <kbd>:</kbd>: display the minimal console
-- <kbd>Space</kbd>: play the animation if any.
-- <kbd>Shift</kbd> + <kbd>V</kbd>: cycle between verbosity levels.
-- <kbd>&larr;</kbd>: load the previous file if any and reset the camera.
-- <kbd>&rarr;</kbd>: load the next file if any and reset the camera.
-- <kbd>&uarr;</kbd>: reload the currently loaded files.
-- <kbd>&darr;</kbd>: add all current files parent directories to the list of files, reload the currently loaded files and reset the camera.
-- <kbd>F12</kbd>: take a screenshot, ie. render the current view to an image file.
-- <kbd>Ctrl</kbd> + <kbd>F12</kbd>: take a "minimal" screenshot, ie. render the current view with no grid and no overlays to an image file with a transparent background.
-- _Drop_ a file, directory or HDRI into the F3D window to load it. F3D relies on the extension (`.hdr` or `.exr`) to detect if the dropped file is an HDRI.
-- <kbd>Ctrl</kbd> + _Drop_ a file, directory and load it.
-- <kbd>Shift</kbd> + _Drop_ a HDRI and use it, no extensions checks performed.
-
-When loading another file or reloading, options that have been changed interactively are kept as is, except when using [imperative options](CONFIGURATION_FILE.md#imperative-options) in configuration files.
-
-## Cycling Coloring
-
-The following hotkeys let you cycle the coloring of the data:
-
-- <kbd>C</kbd>: cycle between point data and cell data - field data is not supported.
-- <kbd>S</kbd>: cycle the array available on the currently selected data in alphabetical order,
-  skipping array not containing numeric data. It will loop back to not coloring unless using volume rendering.
-- <kbd>Y</kbd>: cycle the component available on the currently selected array, looping to -2 for direct scalars rendering.
-
-When changing the array, the component in use will be kept if valid with the new array, if not it will be cycled until a valid
-component is found.
-When changing the type of data to color with, the index of the array within the data will be kept if valid
-with the new data. If not, it will cycle until a valid array is found. After that, the component will be checked
-as specified above.
-
-## Taking Screenshots
-
-The destination filename used to save the screenshots (created by pressing <kbd>F12</kbd> or <kbd>Ctrl</kbd> + <kbd>F12</kbd>) is configurable (using the `screenshot-filename` option) and can use template variables as described [on the options page](OPTIONS.md#filename-templating).
-
-Unless the configured filename template is an absolute path, images will be saved in dedicated directories:
-
-- Windows: `%USERPROFILE%\Pictures`
-- Linux: `${XDG_PICTURES_DIR}`,`${HOME}`
-- macOS: `${HOME}`
-
-So with default options, which are using [templates](OPTIONS.md#filename-templating), the screenshots will be saved as:
-
-- Windows: `%USERPROFILE%\Pictures\F3D\{model}_{n}.png`
-- Linux: `${XDG_PICTURES_DIR}/F3D/{model}_{n}.png`,`${HOME}/F3D/{model}_{n}.png`
-- macOS: `${HOME}/F3D/{model}_{n}.png`
-
-## Configuring bindings
-
-It is supported to change all bindings above using a specific syntax in the [configuration file](CONFIGURATION_FILE.md) using [commands](COMMANDS.md).
diff -Nru f3d-3.2.0+dfsg/doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md f3d-3.4.1/doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md
--- f3d-3.2.0+dfsg/doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/LIMITATIONS_AND_TROUBLESHOOTING.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,155 +0,0 @@
-# Limitations
-
-Here is a non exhaustive list of F3D limitations:
-
-- No support for specifying manual lighting in the default scene apart from using `--light-intensity` option.
-- Multiblock (.vtm, .gml) support is partial, non-surfacic data will be converted into surfaces.
-- Drag and drop interaction cannot be recorded nor played back.
-- Volume rendering and HDRI support requires a decent GPU.
-- The `--camera-zoom-factor` option require VTK >= 9.3.0
-- Information about the failure to load a file is not provided before VTK >= 9.4.0
-
-## Assimp
-
-FBX, DAE, OFF, DXF, X and 3MF file formats rely on [Assimp](https://github.com/assimp/assimp) library. It comes with some known limitations:
-
-- PBR materials are not supported for FBX file format.
-- Some files can be empty, crash, or show artifacts.
-- DXF support is very limited: only files with polylines and 3D faces are displayed.
-- 3MF files may crash at exit (issue in Assimp: https://github.com/assimp/assimp/issues/5328)
-- Only support RBGA 8-bits embedded textures
-
-## Alembic
-
-ABC file formats rely on [Alembic](https://github.com/alembic/alembic) library. It comes with some known limitations:
-
-- Supports only simple polygonal geometry.
-- Does not support ArbGeomParam feature in Alembic.
-- Does not support Subdivision Meshes.
-- Does not support Materials.
-
-## USD
-
-USD file formats rely on [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) library. It comes with some known limitations:
-
-- Skinning is slow and baked on the CPU.
-- Does not support Face-varying attributes.
-- The `usd` plugin is not shipped in the python wheels yet.
-
-## VDB
-
-VDB file formats rely on [OpenVDB](https://github.com/AcademySoftwareFoundation/openvdb) and VTK libraries. It currently comes with some known limitations:
-
-- VDB Grid files are opened with a hard-coded 0.1 sampling rate.
-- The `vdb` plugin is not shipped in the python wheels yet.
-
-## Gaussian splatting
-
-Gaussian splatting (option `--point-sprites-type=gaussian`) needs depth sorting which is done internally using a compute shader. This requires support for OpenGL 4.3 which is not supported by macOS and old GPUs/drivers.
-
-# Troubleshooting
-
-## General
-
-> I have built F3D with raytracing support but the denoiser is not working.
-
-Make sure that VTK has been built with _OpenImageDenoise_ support (`VTKOSPRAY_ENABLE_DENOISER` option).
-
-> My model shows up all wrong, with inverted surfaces everywhere.
-
-Your data probably contains some translucent data for some reason, turn on translucency support by pressing `P` or using the `-p` command line option.
-
-## Linux
-
-> I have a link error related to `stdc++fs` not found or I'm unable to run F3D due to filesystem errors.
-
-With some C++ STD library version, explicit linking to `stdc++fs` is not supported or required. We provide a CMake option `F3D_LINUX_LINK_FILESYSTEM` that you can set to the desired value to workaround this issue.
-
-> I have a link error related to undefined reference to symbol of `libatomic`.
-
-The GCC flag `-latomic` is not being added automatically with specific architectures, like `armel` and `RISCV64`. We provide a CMake option `F3D_LINUX_LIBRARY_LINK_ATOMIC` that you can set to `ON` to workaround this issue.
-
-> Thumbnails are not working in my file manager.
-
-- Check that your file manager supports the thumbnailer mechanism.
-- Check that you have updated your mime type database.
-- If all fails, remove your `.cache` user dir and check that `pcmanfm` thumbnails are working.
-- If they are working, then it is an issue specific to your file manager (see below for a potential work around).
-- If only a few format have working thumbnails, then it is an issue with the mime types database.
-- If no formats have working thumbnails, then it is can be an issue with sandboxing or with the `f3d-plugin-xxx.thumbnailer` files.
-- If only big file do not have thumbnails, this is intended, you can modify this behavior in the `thumbnail.d/05_all.json` configuration folder using the `max-size` option.
-
-> `--rendering-backend` CLI option is not working as expected
-
-Dynamically switching rendering backend require VTK 9.4.0, you may need to update VTK or to use our [binary release](INSTALLATION.md).
-
-> I'm unable to link C++ examples against my local F3D install directory, it complains about missing VTK symbols
-
-Make sure to have VTK installed in your system or to add VTK install lib directory to `LD_LIBRARY_PATH`.
-
-## Windows
-
-> Using the portable binary release, I'm unable to run F3D because Windows warns about a security risk
-
-F3D is not signed by Microsoft yet so Windows shows this warning. Just click on the "more" button to be able to run F3D.
-
-> After installing F3D or registering the shell extension, my explorer is broken.
-
-Unregister the shell extension by running:
-
-```
-cd C:\path\to\f3d\bin\
-regsvr32 /u F3DShellExtension.dll
-```
-
-> I use F3D in a VM, the application fails to launch.
-
-OpenGL applications like F3D can have issues when launched from Windows Server or from a guest Windows because the access to the GPU is restricted.
-You can try to use a software implementation of OpenGL, called [Mesa](https://github.com/pal1000/mesa-dist-win/releases).
-
-- Download the latest `release-msvc`.
-- copy `x64/opengl32.dll`, `libgallium_wgl.dll` and `x64/libglapi.dll` in the same folder as `f3d.exe`.
-- set the environment variable `MESA_GL_VERSION_OVERRIDE` to 4.5.
-- run `f3d.exe`.
-
-> I run f3d from the command prompt and my Unicode characters are not displayed properly.
-
-Set the codepage to UTF-8, run `chcp 65001`.
-
-> When I install F3D, I can't add F3D to the path and the installer complains that: `PATH too long installer unable to modify PATH!`.
-
-There are too many entry in your PATH environment variable, remove the ones you do not use.
-
-> When I try to update F3D, the installer fails with: `Error opening file for writing: F3DShellExtension.dll`
-
-Your previous F3D installation is current being used to generate thumbnails, wait for a bit or reboot your computer before updating again.
-
-> When running F3D without interactivity (e.g. `f3d.exe --version`) from CLI, nothing is displayed.
-
-It's a limitation from the Windows "subsystem". Use `f3d-console.exe` instead.
-
-> When playing an animation, the animation appear to move too slowly
-
-This is a limitation of older VTK version before VTK 9.5.0, use our [binaries](INSTALLATION.md) or compile a recent version of VTK.
-
-## macOS
-
-> I'm unable to use the binary release, macOS complains the file is invalid or corrupted
-
-Our macOS binary package are not signed, to work around this, you can simply run the following command:
-
-```
-xattr -cr /Applications/F3D.app
-```
-
-Then F3D should work as expected.
-
-## Python
-
-> I can't find `usd` and `vdb` plugins after installing f3d with pip
-
-The `usd` and `vdb` plugins are not shipped in the python wheels for now, you can compile F3D yourself with them though.
-
-> Raytracing does not work after installing f3d with pip
-
-F3D raytracing feature is not shipped in the python wheels for now, you can compile F3D yourself with it though.
diff -Nru f3d-3.2.0+dfsg/doc/user/OPTIONS.md f3d-3.4.1/doc/user/OPTIONS.md
--- f3d-3.2.0+dfsg/doc/user/OPTIONS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/OPTIONS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,208 +0,0 @@
-<!-- {% assign model = "{{model}}" %} set variable to prevent Jekyll from swallowing `{{model}}` when expanding template variables -->
-
-# Command line options
-
-F3D behavior can be fully controlled from the command line using the following options.
-
-## Application Options
-
-| Options                                               | Type<br>Default                   | Description                                                                                                                                                                                                                                                                            |
-| ----------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| \-\-input=\<input file\>                              | string<br>-                       | The input file or files to read, can also be provided as a positional argument.                                                                                                                                                                                                        |
-| \-\-output=\<png file\>                               | string<br>-                       | Instead of showing a render view and render into it, _render directly into a png file_. When used with \-\-ref option, only outputs on failure. If `-` is specified instead of a filename, the PNG file is streamed to the stdout. Can use [template variables](#filename-templating). |
-| \-\-no-background                                     | bool<br>false                     | Use with \-\-output to output a png file with a transparent background.                                                                                                                                                                                                                |
-| -h, \-\-help                                          |                                   | Print _help_ and exit. Ignore `--verbose`.                                                                                                                                                                                                                                             |
-| \-\-version                                           |                                   | Show _version_ information and exit. Ignore `--verbose`.                                                                                                                                                                                                                               |
-| \-\-list-readers                                      |                                   | List available _readers_ and exit. Ignore `--verbose`.                                                                                                                                                                                                                                 |
-| \-\-force-reader=\<reader\>                           | string<br>-                       | Force a specific [reader](SUPPORTED_FORMATS.md) to be used, disregarding the file extension.                                                                                                                                                                                           |
-| \-\-list-bindings                                     |                                   | List available _bindings_ and exit. Ignore `--verbose`.                                                                                                                                                                                                                                |
-| \-\-list-rendering-backends                           |                                   | List available _rendering backends_ and exit. Ignore `--verbose`.                                                                                                                                                                                                                      |
-| \-\-config=\<config file path/name/stem\>             | string<br>config                  | Specify the [configuration file](CONFIGURATION_FILE.md) to use. Supports absolute/relative path but also filename/filestem to search for in standard configuration file locations.                                                                                                     |
-| \-\-no-config                                         | bool<br>false                     | Do not read any configuration file and consider only the command line options.                                                                                                                                                                                                         |
-| \-\-no-render                                         | bool<br>false                     | Do not render anything and quit just after loading the first file, use with \-\-verbose to recover information about a file.                                                                                                                                                           |
-| \-\-max-size=\<size in MiB\>                          | int<br>-1                         | Prevent F3D to load a file bigger than the provided size in Mib, leave empty for unlimited, useful for thumbnails.                                                                                                                                                                     |
-| \-\-watch                                             | bool<br>false                     | Watch current file and automatically reload it whenever it is modified on disk.                                                                                                                                                                                                        |
-| \-\-frame-rate=\<fps\>                                | double<br>30.0                    | Frame rate used to refresh animation and other repeated tasks (watch, UI). Does not impact rendering frame rate.                                                                                                                                                                       |
-| \-\-load-plugins=\<paths or names\>                   | string<br>-                       | List of plugins to load separated with a comma. Official plugins are `alembic`, `assimp`, `draco`, `hdf`, `occt`, `usd`, `vdb`. See [plugins](PLUGINS.md) for more info.                                                                                                               |
-| \-\-scan-plugins                                      |                                   | Scan standard directories for plugins and display their names, results may be incomplete. See [plugins](PLUGINS.md) for more info.                                                                                                                                                     |
-| \-\-screenshot-filename=\<png file\>                  | string<br>`{app}/{model}_{n}.png` | Filename to save [screenshots](INTERACTIONS.md#taking-screenshots) to. Can use [template variables](#filename-templating). Supports relative paths [as described](INTERACTIONS.md#taking-screenshots).                                                                                 |
-| \-\-rendering-backend=\<auto\|egl\|osmesa\|glx\|wgl\> | string<br>auto                    | Rendering backend to load, `auto` means to let F3D pick the correct one for you depending on your system capabilities. Use `egl` or `osmesa` on linux to force headless rendering.                                                                                                     |
-| -D, \-\-define=\<libf3d.option=value\>                | special<br>-                      | A repeatable option to set [libf3d](../libf3d/OPTIONS.md) and [reader](SUPPORTED_FORMATS.md#reader-options) option manually. May trigger unexpected behavior.                                                                                                                          |
-| -R, \-\-reset=\<libf3d.option\>                       | special<br>-                      | A repeatable option to reset [libf3d options](../libf3d/OPTIONS.md) manually. Useful when overidding option set in [configuration files](CONFIGURATION_FILE.md).                                                                                                                       |
-
-## General Options
-
-| Options                                              | Type<br>Default    | Description                                                                                                                                                                                                            |
-| ---------------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| \-\-verbose=\<[debug\|info\|warning\|error\|quiet]\> | string<br>info     | Set _verbose_ level, in order to provide more information about the loaded data in the output. If no level is provided, assume `debug`. Option parsing may ignore this flag.                                           |
-| \-\-progress                                         | bool<br>false      | Show a _progress bar_ when loading the file.                                                                                                                                                                           |
-| \-\-animation-progress                               | bool<br>false      | Show a _progress bar_ when playing the animation.                                                                                                                                                                      |
-| \-\-multi-file-mode=\<single\|all\| dir>             | string<br>single   | When opening multiple files, select if they should be shown all at once (`all`), one by one (`single`), or by directory (`dir`). Configuration files for all loaded files will be used in the order they are provided. |
-| \-\-up=\<direction\>                                 | direction<br>+Y    | Define the Up direction.                                                                                                                                                                                               |
-| -x, \-\-axis                                         | bool<br>false      | Show _axes_ as a trihedron in the scene.                                                                                                                                                                               |
-| -g, \-\-grid                                         | bool<br>false      | Show _a grid_ aligned with the horizontal (orthogonal to the Up direction) plane.                                                                                                                                      |
-| \-\-grid\-unit=\<length\>                            | double<br>-        | Set the size of the _unit square_ for the grid. If not set (the default) a suitable value will be automatically computed.                                                                                              |
-| \-\-grid\-subdivisions=\<count\>                     | int<br>10          | Set the number of subdivisions for the grid.                                                                                                                                                                           |
-| \-\-grid\-color=\<color\>                            | color<br>0,0,0     | Set the color grid lines.                                                                                                                                                                                              |
-| -e, \-\-edges                                        | bool<br>false      | Show the _cell edges_.                                                                                                                                                                                                 |
-| \-\-armature                                         | bool<br>false      | Show armature if present (glTF only).                                                                                                                                                                                  |
-| \-\-camera-index=\<idx\>                             | int<br>-           | Select the scene camera to use when available in the file. Automatically computed by default.                                                                                                                          |
-| -k, \-\-trackball                                    | bool<br>false      | Enable trackball interaction.                                                                                                                                                                                          |
-| \-\-animation-autoplay                               | bool<br>false      | Automatically start animation.                                                                                                                                                                                         |
-| \-\-animation-indices=\<idx1,idx2\>                  | vector\<int\><br>0 | Select the animations to show.<br>Any negative value all animations.<br>The default scene always has at most one animation.                                                                                            |
-| \-\-animation-speed-factor=\<ratio\>                 | ratio<br>1         | Set the animation speed factor to slow, speed up or even invert animation time.                                                                                                                                        |
-| \-\-animation-time=\<time\>                          | double<br>-        | Set the animation time to load.                                                                                                                                                                                        |
-| \-\-font-file=\<font file\>                          | path<br>-          | Use the provided FreeType compatible font file to display text.<br>Can be useful to display non-ASCII filenames.                                                                                                       |
-| \-\-font-scale=\<ratio\>                             | ratio<br>1.0       | Scale fonts.                                                                                                                                                                                                           |
-| \-\-command-script=\<command script\>                | script<br>-        | Provide a script file containing a list of commands to be executed sequentially.<br>Allows automation of multiple commands or pre-defined tasks.                                                                       |
-
-## Material options
-
-| Options                                     | Type<br>Default  | Description                                                                                                                                                                                                                                                                                                                              |
-| ------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| -o, \-\-point-sprites                       | bool<br>false    | Show sphere _points sprites_ instead of the geometry.                                                                                                                                                                                                                                                                                    |
-| \-\-point-sprites-type=\<sphere\|gaussian\> | string<br>sphere | Set the splat type when showing point sprites.                                                                                                                                                                                                                                                                                           |
-| \-\-point-sprites-size=\<size\>             | double<br>10.0   | Set the _size_ of point sprites.                                                                                                                                                                                                                                                                                                         |
-| \-\-point-size=\<size\>                     | double<br>-      | Set the _size_ of points when showing vertices. Model specified by default.                                                                                                                                                                                                                                                              |
-| \-\-line-width=\<size\>                     | double<br>-      | Set the _width_ of lines when showing edges. Model specified by default.                                                                                                                                                                                                                                                                 |
-| \-\-backface-type=\<visible\|hidden\>       | string<br>-      | Set the Backface type. Model specified by default.                                                                                                                                                                                                                                                                                       |
-| \-\-color=\<color\>                         | color<br>-       | Set a _color_ on the geometry. Multiplied with the base color texture when present. <br>Model specified by default.                                                                                                                                                                                                                      |
-| \-\-opacity=\<opacity\>                     | double<br>-      | Set _opacity_ on the geometry. Multiplied with the base color texture when present. <br>Model specified by default. Usually used with Depth Peeling option.                                                                                                                                                                              |
-| \-\-roughness=\<roughness\>                 | double<br>-      | Set the _roughness coefficient_ on the geometry (0.0-1.0). Multiplied with the material texture when present. <br>Model specified by default.                                                                                                                                                                                            |
-| \-\-metallic=\<metallic\>                   | double<br>-      | Set the _metallic coefficient_ on the geometry (0.0-1.0). Multiplied with the material texture when present. <br>Model specified by default.                                                                                                                                                                                             |
-| \-\-base-ior=\<base-ior\>                   | double<br>-      | Set the _index of refraction of the base layer_ (1.0-2.5). Model specified by default.                                                                                                                                                                                                                                                   |
-| \-\-hdri-file=\<HDRI file\>                 | path<br>-        | Set the _HDRI_ image that can be used as ambient lighting and skybox.<br>Valid file format are hdr, exr, png, jpg, pnm, tiff, bmp. <br> If not set, a default is provided.                                                                                                                                                               |
-| \-\-hdri-ambient                            | string<br>-      | Light the scene using the _HDRI_ image as ambient lighting.<br>The environment act as a light source and is reflected on the material.                                                                                                                                                                                                   |
-| \-\-texture-matcap=\<texture file\>         | path<br>-        | Set the texture file to control the material capture of the object. All other model options for surfaces are ignored if this is set. Must be in linear color space. <br>Model specified by default.                                                                                                                                      |
-| \-\-texture-base-color=\<texture file\>     | path<br>-        | Set the texture file to control the color of the object. Please note this will be multiplied with the color and opacity options. Must be in sRGB color space. <br>Model specified by default.                                                                                                                                            |
-| \-\-texture-material=\<texture file\>       | path<br>-        | Set the texture file to control the occlusion, roughness and metallic values of the object. Please note this will be multiplied with the roughness and metallic options, which have impactful default values. To obtain true results, use \-\-roughness=1 \-\-metallic=1. Must be in linear color space. <br>Model specified by default. |
-| \-\-texture-emissive=\<texture file\>       | path<br>-        | Set the texture file to control the emitted light of the object. Please note this will be multiplied with the emissive factor. Must be in sRGB color space. <br>Model specified by default.                                                                                                                                              |
-| \-\-emissive-factor=\<color\>               | color<br>-       | Set the emissive factor. This value is multiplied with the emissive color when an emissive texture is present. <br>Model specified by default.                                                                                                                                                                                           |
-| \-\-texture-normal=\<texture file\>         | path<br>-        | Set the texture file to control the normal map of the object. Must be in sRGB color space. <br>Model specified by default.                                                                                                                                                                                                               |
-| \-\-normal-scale=\<color\>                  | double<br>-      | Set the normal scale. This value affects the strength of the normal deviation from the normal texture. <br>Model specified by default.                                                                                                                                                                                                   |
-| \-\-textures-transform=\<transform2d\>      | transform2d<br>- | Set the 2d transform to use for all textures applied to the model. <br>Importer may set a default value depending on file type. If a default value exists, the default value is multiplied by the transform.                                                                                                                             |
-
-## Window options
-
-| Options                         | Type<br>Default               | Description                                                                                          |
-| ------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
-| \-\-background-color=\<color\>  | color<br>0.2, 0.2, 0.2        | Set the window _background color_.<br>Ignored if _hdri_ is set.                                      |
-| \-\-resolution=\<width,height\> | vector\<double\><br>1000, 600 | Set the _window resolution_.                                                                         |
-| \-\-position=\<x,y\>            | vector\<double\><br>-         | Set the _window position_ (top left corner) , in pixels, starting from the top left of your screens. |
-| -z, \-\-fps                     | bool<br>false                 | Display a rendering _frame per second counter_.                                                      |
-| -n, \-\-filename                | bool<br>false                 | Display the _name of the file_ on top of the window.                                                 |
-| -m, \-\-metadata                | bool<br>false                 | Display the _metadata_.                                                                              |
-| \-\-hdri-skybox                 | bool<br>false                 | Show the HDRI as a skybox. Overrides \-\-background-color and \-\-no-background.                     |
-| -u, \-\-blur-background         | bool<br>false                 | Blur background.<br>Useful with a HDRI skybox.                                                       |
-| \-\-blur-coc                    | double<br>20                  | Blur circle of confusion radius.                                                                     |
-| \-\-light-intensity             | double<br>1.0                 | _Adjust the intensity_ of every light in the scene.                                                  |
-
-## Scientific visualization options
-
-| Options                                 | Type<br>Default       | Description                                                                                                                                                                                                                                                                               |
-| --------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| -s, \-\-scalar-coloring                 | bool<br>false         | Enable scalar coloring if present in the file. If `--coloring-array` is not set, the first in alphabetical order will be picked if any are available.                                                                                                                                     |
-| \-\-coloring-array=\<array_name\>       | string<br>-           | The coloring array name to use when coloring.<br>Use \-\-verbose to recover the usable array names.                                                                                                                                                                                       |
-| -y, \-\-comp=\<comp_index\>             | int<br>-1             | Specify the _component from the scalar_ array to color with.<br>Use with the scalar option. -1 means _magnitude_. -2 or the short option, -y, means _direct values_.<br>When using _direct values_, components are used as L, LA, RGB, RGBA values depending on the number of components. |
-| -c, \-\-cells                           | bool<br>false         | Specify that the scalar array is to be found _on the cells_ instead of on the points.<br>Use with the scalar option.                                                                                                                                                                      |
-| \-\-range=\<min,max\>                   | vector\<double\><br>- | Set the _coloring range_. Automatically computed by default.<br>Use with the scalar option.                                                                                                                                                                                               |
-| -b, \-\-bar                             | bool<br>false         | Show _scalar bar_ of the coloring by array.<br>Use with the scalar option.                                                                                                                                                                                                                |
-| \-\-colormap\-file=\<name\>             | string<br>-           | Set a _colormap file for the coloring_.<br>See [color maps](COLOR_MAPS.md).<br>Use with the scalar option.                                                                                                                                                                                |
-| \-\-colormap=\<colormap\>               | colormap<br>-         | Set a _custom colormap for the coloring_.See [colormap parsing](PARSING.md#colormap) for details.<br>Ignored if `--colormap-file` option is specified.<br>Use with the scalar option.                                                                                                     |
-| \-\-colormap\-discretization=\<colors\> | int<br>-              | Set How many distinct colors from \[1, N\] will be used in the colormap. Any values outside the valid range will result in smooth shading.                                                                                                                                                |
-| -v, \-\-volume                          | bool<br>false         | Enable _volume rendering_. It is only available for 3D image data (vti, dcm, nrrd, mhd files) and will display nothing with other formats. It forces coloring.                                                                                                                            |
-| -i, \-\-inverse                         | bool<br>false         | Inverse the linear opacity function used for volume rendering.                                                                                                                                                                                                                            |
-
-## Camera configuration options
-
-| Options                              | Type<br>Default       | Description                                                                                    |
-| ------------------------------------ | --------------------- | ---------------------------------------------------------------------------------------------- |
-| \-\-camera-position=\<X,Y,Z\>        | vector\<double\><br>- | Set the camera position, overrides --camera-direction and camera-zoom-factor.                  |
-| \-\-camera-focal-point=\<X,Y,Z\>     | vector\<double\><br>- | Set the camera focal point.                                                                    |
-| \-\-camera-view-up=\<direction\>     | direction<br>-        | Set the camera view up vector. Will be orthogonalized.                                         |
-| \-\-camera-view-angle=\<angle\>      | double<br>-           | Set the camera view angle, a strictly positive value in degrees.                               |
-| \-\-camera-direction=\<direction\>   | direction<br>-        | Set the camera direction, looking at the focal point.                                          |
-| \-\-camera-zoom-factor=\<factor\>    | double<br>-           | Set the camera zoom factor relative to the autozoom on data, a strictly positive value.        |
-| \-\-camera-azimuth-angle=\<angle\>   | double<br>0.0         | Apply an azimuth transformation to the camera, in degrees, added after other camera options.   |
-| \-\-camera-elevation-angle=\<angle\> | double<br>0.0         | Apply an elevation transformation to the camera, in degrees, added after other camera options. |
-| \-\-camera-orthographic              | bool<br>-             | Set the camera to use the orthographic projection. Model specified by default.                 |
-
-## Raytracing options
-
-| Options                 | Type<br>Default | Description                                                                                        |
-| ----------------------- | --------------- | -------------------------------------------------------------------------------------------------- |
-| -r, \-\-raytracing      | bool<br>false   | Enable _OSPRay raytracing_. Requires OSPRay raytracing to be enabled in the linked VTK dependency. |
-| \-\-samples=\<samples\> | int<br>5        | Set the number of _samples per pixel_ when using raytracing.                                       |
-| -d, \-\-denoise         | bool<br>false   | _Denoise_ the image when using raytracing.                                                         |
-
-## PostFX (OpenGL) options
-
-| Options                      | Type<br>Default | Description                                                                                                     |
-| ---------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
-| -p, \-\-translucency-support | bool<br>false   | Enable _translucency support_. This is a technique used to correctly render translucent objects.                |
-| -q, \-\-ambient-occlusion    | bool<br>false   | Enable _ambient occlusion_. This is a technique used to improve the depth perception of the object.             |
-| -a, \-\-anti-aliasing        | bool<br>false   | Enable _anti-aliasing_. This technique is used to reduce aliasing.                                              |
-| \-\-anti-aliasing-mode       | string<br>fxaa  | Anti-aliasing method (`fxaa`: fast, `ssaa`: quality)                                                            |
-| -t, \-\-tone-mapping         | bool<br>false   | Enable generic filmic _Tone Mapping Pass_. This technique is used to map colors properly to the monitor colors. |
-| \-\-final-shader             | string<br>-     | Add a final shader to the output image. See the [dedicated documentation](FINAL_SHADER.md) for more details.    |
-
-## Testing options
-
-| Options                                  | Type<br>Default | Description                                                                                                                                     |
-| ---------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
-| \-\-ref=\<png file\>                     | string<br>-     | Render and compare with the provided _reference image_, for testing purposes. Use with output option to generate new baselines and diff images. |
-| \-\-ref-threshold=\<threshold\>          | double<br>0.04  | Set the _comparison threshold_ to trigger a test failure or success. The default (0.04) correspond to almost visually identical images.         |
-| \-\-interaction-test-record=\<log file\> | string<br>-     | Path to an interaction log file to _record interaction events_ to.                                                                              |
-| \-\-interaction-test-play=\<log file\>   | string<br>-     | Path to an interaction log file to _play interactions events_ from when loading a file.                                                         |
-
-## Rendering options precedence
-
-Some rendering options are not compatible between them, here is the precedence order if several are provided:
-
-- Raytracing (`-r`)
-- Volume (`-v`)
-- Point Sprites (`-o`)
-
-## Options syntax
-
-To turn on/off boolean options, it is possible to write `--option=true` and `--option=false`, eg `--points-sprites=false`.
-
-As documented, the `--option=value` syntax should be preferred. The syntax `--option value` can have unintended effect with positional arguments.
-
-The `-R` short option has a special syntax: `-Rlibf3d.option` but can also be used with `--reset=libf3d.option`
-
-The `-D/--define` option has a special syntax: `-Dlibf3d.option=value` or `--define=libf3d.option=value`.
-
-All options are parsed according to their type, see the [parsing documentation](PARSING.md) for more details.
-
-## Filename templating
-
-The destination filename used by `--output` or to save screenshots using `--screenshot-filename` can use the following template variables:
-
-- `{app}`: application name (ie. `F3D`)
-- `{version}`: application version (eg. `2.4.0`)
-- `{version_full}`: full application version (eg. `2.4.0-abcdefgh`)
-- `{model}`: current model filename without extension (eg. `foo` for `/home/user/foo.glb`)
-- `{model.ext}`: current model filename with extension (eg. `foo.glb` for `/home/user/foo.glb`)
-- `{model_ext}`: current model filename extension (eg. `glb` for `/home/user/foo.glb`)
-- `{date}`: current date in YYYYMMDD format
-- `{date:format}`: current date as per C++'s `std::put_time` format
-- `{n}`: auto-incremented number to make filename unique (up to 1000000)
-- `{n:2}`, `{n:3}`, ...: zero-padded auto-incremented number to make filename unique (up to 1000000)
-- variable names can be escaped by doubling the braces (eg. use `{{model}}.png` to output `{model}.png` without the model name being substituted)
-
-For example the screenshot filename is configured as `{app}/{model}_{n}.png` by default, meaning that, assuming the model `hello.glb` is being viewed,
-consecutive screenshots are going to be saved as `F3D/hello_1.png`, `F3D/hello_2.png`, `F3D/hello_3.png`, ...
-
-Model related variables will be replaced by `no_file` if no file is loaded and `multi_file` if multiple files are loaded using the `multi-file-mode` option.
-
-## HDRI Caches
-
-When using HDRI related options, F3D will create and use a cache directory to store related data in order to speed up rendering.
-These cache files can be safely removed at the cost of recomputing them on next use.
-
-The cache directory location is as follows, in order, using the first defined environment variables:
-
-- Windows: `%LOCALAPPDATA%\f3d`
-- Linux: `${XDG_CACHE_HOME}/f3d`,`${HOME}/.cache/f3d`
-- macOS: `${HOME}/Library/Caches/f3d`
diff -Nru f3d-3.2.0+dfsg/doc/user/PARSING.md f3d-3.4.1/doc/user/PARSING.md
--- f3d-3.2.0+dfsg/doc/user/PARSING.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/PARSING.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,110 +0,0 @@
-# Parsing options
-
-When setting options from the [CLI Options](OPTIONS.md), the [commands](COMMANDS.md) or using the [libf3d options string API](../libf3d/OPTIONS.md#string-api), the values are parsed according to their type. If parsing fails, the value is not changed.
-
-The following types are supported:
-
-- bool: A boolean, true or false.
-- int: A signed integer.
-- double: A floating point number.
-- ratio: A double dividend over a double divisor, stored in a double.
-- string: A string of characters.
-- path: A path to a local file.
-- color: A RGB color.
-- direction: A 3D vector representing a direction.
-
-As well as a list for bool, int, double, ratio, string, noted as
-
-- vector\<type\>
-
-## Bool
-
-The following formats are supported when parsing a bool, case insensitive:
-
-- true/false
-- yes/no
-- on/off
-- 1/0
-
-When formatting a bool into a string, true/false is used.
-
-## Int
-
-Int parsing is supported using [std::stoi](https://en.cppreference.com/w/cpp/string/basic_string/stol) and check
-that the whole string is parsed.
-
-When formatting an int into a string, [std::to_string](https://en.cppreference.com/w/cpp/string/basic_string/to_string) is used.
-
-## Double
-
-Double parsing is supported using [std::stod](https://en.cppreference.com/w/cpp/string/basic_string/stol) and check
-that the whole string is parsed.
-
-When formatting a double into a string, [std::ostringstream](https://en.cppreference.com/w/cpp/io/basic_ostringstream) is used
-with removing the point and precision when the value is exactly an integer.
-
-## Ratio
-
-The following formats are supported when parsing a string into a ratio:
-
-- percent% where percent is a double
-- dividend:divisor where both are doubles
-- dividend/divisor where both are doubles
-- double
-
-Percent, dividend, divisor are then parsed as double.
-
-When formatting a ratio into a string, it is formatted as a double.
-
-## String
-
-String are trimmed of leading and trailing space when parsed.
-String are formatted as is.
-
-## String
-
-Path are formatted by creating a [std::filesystem::path](https://en.cppreference.com/w/cpp/filesystem/path/path)
-and then collapsed using [f3d::utils::collapsePath](https://f3d.app/doc/libf3d/doxygen/api/classf3d_1_1utils.html#_CPPv4N3f3d5utils12collapsePathERKNSt10filesystem4pathERKNSt10filesystem4pathE).
-
-Path are formatted using [std::filesystem::path::string](https://en.cppreference.com/w/cpp/filesystem/path/string).
-
-## Vectors
-
-Vector tokens are separated by `,`, tokens are then parsed using their respective types.
-
-When formatting a vector into a string, individual token are formatted according to their type and separated using `,`.
-
-## Color
-
-The following formats are supported when parsing a color, case insensitive:
-
-- R,G,B where R, G, B are doubles >= 0
-- #RRGGBB where RR, GG, BB are hexadecimal values. Shortened format #RGB is also valid.
-- rgb(R, G, B) where R, G and B are integer [0, 255]
-- hsl(H, S%, L%) where H is integer [0, 360], S and L are integer [0, 100]
-- hsv(H, S%, V%) where H is integer [0, 360], S and V are integer [0, 100]
-- hwb(H, W%, B%) where H is integer [0, 360], W and B are integer [0, 100]
-- cmyk(C%, M%, Y%, K%) where C, M, Y, K are integer [0, 100]
-- [color name](https://htmlpreview.github.io/?https://github.com/Kitware/vtk-examples/blob/gh-pages/VTKNamedColorPatches.html)
-
-See [W3C](https://www.w3.org/TR/css-color-3/#rgb-color) doc for more details on these formats.
-
-When formatting a color into a string, it is formatted as `#RRGGBB` if values are multiple of 255. Otherwise, it is formatted as vector of doubles.
-
-## Direction
-
-The following formats are supported when parsing a string into a direction:
-
-- `[[+|-]X][[+|-]Y][[+|-]Z]` (case insensitive), for example `+X` or `X` for `1,0,0`, `-y+z` for `0,-1,1`
-- vector of three doubles, for example `1,2,3.4`
-
-When formatting a direction into a string, it is formatted in the `±XYZ` form if possible or as a vector of doubles otherwise.
-
-## Colormap
-
-The following formats are supported when parsing a string into a colormap:
-
-- `val, red, green, blue, ...`
-- `val, color, ...`
-
-When formatting a colormap into a string, it is formatted as `val, color, ...`.
diff -Nru f3d-3.2.0+dfsg/doc/user/PLUGINS.md f3d-3.4.1/doc/user/PLUGINS.md
--- f3d-3.2.0+dfsg/doc/user/PLUGINS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/PLUGINS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,51 +0,0 @@
-## Plugins
-
-F3D officially supports plugins for certain file formats. If you installed F3D using the binary
-release, there's no need to manually load these plugins when opening your file.
-F3D will load them automatically.
-
-However, if you installed F3D using a package manager,
-it's possible that the packager chose to bundle the plugins into different packages or
-to list plugin dependencies as optional in order to reduce the number of dependencies of the main package.
-
-In order to open a file that requires a plugin, make sure you've installed all necessary
-dependencies. You can then specify the `--load-plugins=<path or name>` [option](OPTIONS.md)
-in the command line to load your plugin.
-
-Alternatively, you can add your plugin directly in the
-[configuration file](CONFIGURATION_FILE.md) if it isn't there already. You can specify one or
-multiple plugins in a single comma-separated list, like in the example below:
-
-```
-[
-{
-  "match": ".*(file_extension)",
-  "options":
-  {
-    "load-plugins": "plugin1", "plugin2"
-  }
-}
-]
-```
-
-### Supported plugins
-
-F3D supports loading plugins and contains the following plugins:
-`native`, `alembic`, `assimp`, `draco`, `hdf`, `occt`, `usd`, `vdb`.
-
-see [the format list](SUPPORTED_FORMATS.md) for more details.
-
-> Note: Native plugin is always loaded automatically.
-> Note: If you downloaded the binaries from the release page, it's not necessary to specify manually the plugins above. F3D loads them automatically.
-
-Here is how the plugins are searched (in preceding order):
-
-1. Search the static plugins.
-2. Consider the `load-plugins` option given it is a full path.
-3. Search in the paths specified in `F3D_PLUGINS_PATH` environment variable.
-4. Search in a directory relative to the F3D application: `../lib`.
-5. Rely on OS specific paths (e.g. `LD_LIBRARY_PATH` on Linux or `DYLD_LIBRARY_PATH` on macOS).
-
-You can also try plugins maintained by the community. If you have created a plugin and would like it to be listed here, please submit a pull request.
-
-- **Abaqus**: ODB support by @YangShen398 ([repository](https://github.com/YangShen398/F3D-ODB-Reader-Plugin))
diff -Nru f3d-3.2.0+dfsg/doc/user/QUICKSTART.md f3d-3.4.1/doc/user/QUICKSTART.md
--- f3d-3.2.0+dfsg/doc/user/QUICKSTART.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/QUICKSTART.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,102 +0,0 @@
-# Quickstart Guide
-
-This guide will help you get started using F3D.
-
-As an overview, you'll learn how to run F3D and open files, configure basic scene constructions, interact with them, and play animations.
-
-## Prerequisites
-
-To use F3D, you'll need to install F3D. See the [Installation](INSTALLATION.md) page for the latest release package supported by your system.
-
-## Running F3D
-
-Once F3D has been installed, you should be able to open a file in any of the following ways:
-
-- Open a file directly from your file manager.
-- Open the F3D application directly, then drag and drop a file into the application window.
-- Run F3D from the terminal with a set of command-line [options](OPTIONS.md).
-- Configure F3D as a [thumbnailer](DESKTOP_INTEGRATION.md) for supported file formats with certain file managers.
-
-If you choose to run F3D from the terminal, you can open your file by running the following:
-
-```
-f3d /path/to/file.ext
-```
-
-Optionally, you can also save the rendering into an image file:
-
-```
-f3d /path/to/file.ext --output=/path/to/img.png
-```
-
-If you need help, specify the `--help` [option](OPTIONS.md):
-
-```
-f3d --help
-man f3d # Linux only
-```
-
-Once you've opened your file in F3D, you're all set to start visualizing! Press <kbd>H</kbd> to open a list of shortcuts to help you interact with your scene.
-
-See the [supported file formats](SUPPORTED_FORMATS.md) page for the full list of file types that F3D can open.
-
-## Constructing scenes
-
-Certain [formats](SUPPORTED_FORMATS.md) are listed as **full scene** formats (.gltf/.glb, .3ds, .wrl, .obj, .fbx, .dae, .off, .x, .3mf, .usd)
-which means these files contain not only _geometry_ but also some scene information like _lights_, _cameras_, _actors_ in the scene,
-as well as _texture_ properties. By default, all this information will be loaded from the file and displayed.
-For file formats that are not **full scene**, **a default scene** is created.
-
-For **default scene** formats, certain default values are set automatically:
-
-- texture-\*: ""
-- line-width: 1.0
-- point-size: 10.0
-- opacity: 1.0
-- color: 1.0, 1.0, 1.0
-- emissive-factor: 1.0, 1.0, 1.0
-- normal-scale: 1.0
-- metallic: 0.0
-- roughness: 0.3
-- base-ior: 1.5
-- camera-orthographic: false
-
-They will be overridden when using corresponding [options](OPTIONS.md), either from command line or [configuration file](CONFIGURATION_FILE.md).
-
-## Interacting with your scene
-
-Once you've loaded your scene in F3D, you can interact with it by using your mouse and certain hotkeys.
-
-### Moving the camera
-
-- _Click and drag_ with the _left_ mouse button to **rotate** around the focal point of the camera.
-- _Click and drag_ vertically with the _right_ mouse button <u>OR</u> _move the mouse wheel_ to **zoom in/out**.
-- _Click and drag_ with the _middle_ mouse button to **translate** the camera.
-
-### Other shortcuts
-
-- Press <kbd>Enter</kbd> to reset the camera.
-- Press <kbd>Space</kbd> to play animation, if any.
-- Press <kbd>G</kbd> to toggle the horizontal grid.
-- Press <kbd>H</kbd> to display a cheatsheet of hotkey interactions.
-
-For more information, see the [Interactions](INTERACTIONS.md) page.
-
-## Playing animations
-
-F3D can play [animations](ANIMATIONS.md) for any [supported files](SUPPORTED_FORMATS.md) that contain them.
-
-With your file loaded into F3D, press the <kbd>W</kbd> hotkey to cycle through available animations. Then, press <kbd>Space</kbd> to play your selected animation.
-
-Alternatively, you can use the command line to play animations. Use the `--animation-indices` [option](OPTIONS.md) to specify which animation you want to play. To play all animations at once, use `--animation-indices=-1`.
-
-For more information, see the [Animations](ANIMATIONS.md) page.
-
-## Further Reading
-
-- [A list of all F3D command-line options.](OPTIONS.md)
-- [How to interact with scenes in F3D.](INTERACTIONS.MD)
-- [How to play animations in F3D.](ANIMATIONS.md)
-- [How to specify colormaps in F3D.](COLOR_MAPS.md)
-- [How to configure plugins in F3D.](PLUGINS.md)
-- [Limitations and how to troubleshoot F3D.](LIMITATIONS_AND_TROUBLESHOOTING.md)
diff -Nru f3d-3.2.0+dfsg/doc/user/README_USER.md f3d-3.4.1/doc/user/README_USER.md
--- f3d-3.2.0+dfsg/doc/user/README_USER.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/README_USER.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,17 +0,0 @@
-# User Documentation
-
-- [How to use F3D.](QUICKSTART.md)
-- [How to install F3D.](INSTALLATION.md)
-- [List of all supported file formats.](SUPPORTED_FORMATS.md)
-- [List of all F3D command line options.](OPTIONS.md)
-- [The different interactions in F3D.](INTERACTIONS.md)
-- [How to use animations in F3D.](ANIMATIONS.md)
-- [How to configure F3D using a configuration file.](CONFIGURATION_FILE.md)
-- [The different commands available in F3D.](COMMANDS.md)
-- [How are options values are parsed in F3D.](PARSING.md)
-- [How to use colormaps in F3D.](COLOR_MAPS.md)
-- [How to a use custom final shader in F3D.](FINAL_SHADER.md)
-- [How to integrate F3D in your desktop.](DESKTOP_INTEGRATION.md)
-- [How to configure plugins in F3D.](PLUGINS.md)
-- [Limitations and troubleshooting of F3D.](LIMITATIONS_AND_TROUBLESHOOTING.md)
-- [How to sponsor F3D.](SPONSORING.md)
diff -Nru f3d-3.2.0+dfsg/doc/user/SPONSORING.md f3d-3.4.1/doc/user/SPONSORING.md
--- f3d-3.2.0+dfsg/doc/user/SPONSORING.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/SPONSORING.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,50 +0,0 @@
-# Sponsoring
-
-## Sponsors
-
-F3D, a fast and minimalist 3D viewer is at heart a passion project. We (Michael and myself, Mathieu) created it in order to create the best 3D viewer that we ourselves _want_ to use.
-
-I'm very active in the open source world. I'm employed at Kitware (creator of CMake, VTK and ParaView) and work on VTK and ParaView everyday, while also working on F3D when I have any free time! I love software design and the software process.
-
-Michael is a veteran of the rendering and simulation industry. We met at Kitware and decided to start working on F3D together. He is the expert in all things rendering in F3D, and then some.
-
-We are trying to find some sponsors for F3D in order to cover some of the expenses that it incurs.
-
-For starters, we rely on github infrastructure, which has some costs, even for open source, especially in regard to LFS data, costs that scale with the popularity of the project.
-Second, we now have a great website, [f3d.app](https://f3d.app), relying on github pages. We do pay for a domain name and we would like to cover that as well.
-
-If we have enough sponsors, we can consider buying more hardware to develop for and test with, especially Linux mobile and android.
-We also want to promote F3D in open source conferences and summits, being able to do that would be great!
-
-Finally, we want to give back to the open source world, either by sponsoring projects that F3D rely on or even by donating to open source developers that contribute their time to the project.
-
-By sponsoring F3D you get a few perks!
-
-First you get access to a dedicated discord channel where F3D devs will answer any questions you may have.
-Second, if you sponsor at least 5$/month, you also get to vote for which issue should be implemented/fixed each month.
-By sponsoring 20$/month, you also get to pick an image to add to our [gallery](https://f3d.app/doc/GALLERY.html) to showcase your work!
-
-And of course if anyone would like to buy us a beer and talk about F3D then we would be happy to meet up in Lyon, France! Just reach out to us on our [discord](https://discord.f3d.app)!
-
-Please note everything you sponsor gets through the F3D-APP foundation, a non-profit entity we created for this purpose.
-
-We provide different ways to sponsor, choose your preferred one!
-
-- [Github sponsors](https://github.com/sponsors/f3d-app)
-- [Patreon](https://patreon.com/f3d_app_foundation)
-- [ko-fi](https://ko-fi.com/f3d_app)
-- [liberapay](https://liberapay.com/f3d-app/)
-- [Tipee](https://fr.tipeee.com/f3d-app/)
-- [buy me a coffee](https://buymeacoffee.com/f3d.app)
-- [donorbox](https://donorbox.org/support-f3d)
-- Direct Transfer:
-  - IBAN: `FR7616958000018496562230572`
-  - BIC/SWIFT: `QNTOFRP1XXX`
-
-## Industry Sponsors
-
-If you or your company are using F3D in any capacity and you want to ensure F3D keeps growing and being maintained, we would love to hear from you!
-You can sponsor F3D through the non-profit F3D-APP FOUNDATION and we will acknowledge your support by displaying your logo on our [README.md](../../README.md) as
-well as sharing about your support whenever we get the chance.
-
-Please reach out at <contact@f3d.app>.
diff -Nru f3d-3.2.0+dfsg/doc/user/SUPPORTED_FORMATS.md f3d-3.4.1/doc/user/SUPPORTED_FORMATS.md
--- f3d-3.2.0+dfsg/doc/user/SUPPORTED_FORMATS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/doc/user/SUPPORTED_FORMATS.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,89 +0,0 @@
-# Supported File Formats
-
-F3D supports the following file formats:
-
-| Name                                      | File Extension(s)                              | Full scene | Animation Support | Plugin    | Reader Name             |
-| ----------------------------------------- | ---------------------------------------------- | ---------- | ----------------- | --------- | ----------------------- |
-| Legacy VTK                                | `.vtk`                                         | No         | NONE              | `native`  | `VTKLegacy`             |
-| VTK XML                                   | `.vtp`, `.vtu`, `.vtr`, `.vti`, `.vts`, `.vtm` | No         | NONE              | `native`  | `VTKXMLVT<P,U,R,I,S,M>` |
-| VTKHDF                                    | `.vtkhdf`                                      | No         | UNIQUE            | `hdf`     | `VTKHDF`                |
-| EXODUS II                                 | `.e`, `.ex2`, `.exo`, `.g`                     | No         | UNIQUE            | `hdf`     | `ExodusII`              |
-| NetCDF                                    | `.nc`, `.cdf`, `.ncdf`                         | No         | UNIQUE            | `hdf`     | `NetCDF`                |
-| Polygon File Format                       | `.ply`                                         | No         | NONE              | `native`  | `PLYReader`             |
-| Standard Triangle Language                | `.stl`                                         | No         | NONE              | `native`  | `STL`                   |
-| DICOM                                     | `.dcm`                                         | No         | NONE              | `native`  | `DICOM`                 |
-| NRRD ("nearly raw raster data")           | `.nrrd`, `.nhdr`                               | No         | NONE              | `native`  | `Nrrd`                  |
-| MetaHeader MetaIO                         | `.mhd`, `.mha`                                 | No         | NONE              | `native`  | `MetaImage`             |
-| Tag Image File Format 2D/3D               | `.tif`, `.tiff`                                | No         | NONE              | `native`  | `TIFF`                  |
-| QuakeMDL                                  | `.mdl`                                         | Yes        | SINGLE            | `native`  | `QuakeMDL`              |
-| CityGML                                   | `.gml`                                         | No         | NONE              | `native`  | `CityGML`               |
-| Point Cloud                               | `.pts`                                         | No         | NONE              | `native`  | `PTS`                   |
-| Standard for the Exchange of Product Data | `.step`, `.stp`, `.stpnc`, `.p21`, `.210`      | No         | NONE              | `occt`    | `STEP`                  |
-| Initial Graphics Exchange Specification   | `.iges`, `.igs`                                | No         | NONE              | `occt`    | `IGES`                  |
-| Open CASCADE Technology BRep format       | `.brep`                                        | No         | NONE              | `occt`    | `BREP`                  |
-| Open CASCADE Technology XBF format        | `.xbf`                                         | No         | NONE              | `occt`    | `XBF`                   |
-| Alembic                                   | `.abc`                                         | No         | UNIQUE            | `alembic` | `Alembic`               |
-| Wavefront OBJ                             | `.obj`                                         | Yes        | NONE              | `native`  | `OBJ`                   |
-| GL Transmission Format                    | `.gltf`, `.glb`                                | Yes        | MULTI             | `native`  | `GLTF`, `GLTFDraco`     |
-| Draco                                     | `.drc`                                         | No         | NONE              | `draco`   | `Draco`                 |
-| Autodesk 3D Studio                        | `.3ds`                                         | Yes        | NONE              | `native`  | `3DS`                   |
-| Virtual Reality Modeling Language         | `.wrl`, `.vrml`                                | Yes        | NONE              | `native`  | `VRMLReader`            |
-| Autodesk Filmbox                          | `.fbx`                                         | Yes        | SINGLE            | `assimp`  | `FBX`                   |
-| COLLADA                                   | `.dae`                                         | Yes        | SINGLE            | `assimp`  | `COLLADA`               |
-| Object File Format                        | `.off`                                         | Yes        | NONE              | `assimp`  | `OFF`                   |
-| Drawing Exchange Format                   | `.dxf`                                         | Yes        | NONE              | `assimp`  | `DXF`                   |
-| DirectX                                   | `.x`                                           | Yes        | SINGLE            | `assimp`  | `DirectX`               |
-| 3D Manufacturing Format                   | `.3mf`                                         | Yes        | NONE              | `assimp`  | `3MF`                   |
-| Universal Scene Description               | `.usd`, `.usda`, `.usdc`, `.usdz`              | Yes        | SINGLE            | `usd`     | `USD`                   |
-| VDB                                       | `.vdb`                                         | No         | NONE              | `vdb`     | `VDB`                   |
-| 3D Gaussian splatting                     | `.splat`                                       | No         | NONE              | `native`  | `Splat`                 |
-| Compressed 3D Gaussian splatting          | `.spz`                                         | No         | NONE              | `native`  | `SPZ`                   |
-
-## Reader options
-
-Readers can provide option that can be set using the `-D/--define` [command line option](OPTIONS.md).
-eg: `-DVDB.downsampling_factor=0.5`.
-
-For booleans, 0 means false, not 0 means true. Unsigned int will interpret anything that is not a non-negative integer as the default value.
-
-| File extension | Option Name                | Argument Type  | Description                                                                          |
-| -------------- | -------------------------- | -------------- | ------------------------------------------------------------------------------------ |
-| `vdb`          | `VDB.downsampling_factor`  | `double`       | Control the level of downsampling when reading a volume, default is 0.1.             |
-| `occt`         | `STEP.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
-| `occt`         | `STEP.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
-| `occt`         | `STEP.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
-| `occt`         | `STEP.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
-| `occt`         | `IGES.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
-| `occt`         | `IGES.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
-| `occt`         | `IGES.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
-| `occt`         | `IGES.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
-| `occt`         | `BREP.linear_deflection`   | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
-| `occt`         | `BREP.angular_deflection`  | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
-| `occt`         | `BREP.relative_deflection` | `bool`         | Control if the deflection values are relative to object size, default is false.      |
-| `occt`         | `BREP.read_wire`           | `bool`         | Control if lines should be read, default is true.                                    |
-| `occt`         | `XBF.linear_deflection`    | `double`       | Control the distance between a curve and the resulting tessellation, default is 0.1. |
-| `occt`         | `XBF.angular_deflection`   | `double`       | Control the angle between two subsequent segments, default is 0.5.                   |
-| `occt`         | `XBF.relative_deflection`  | `bool`         | Control if the deflection values are relative to object size, default is false.      |
-| `occt`         | `XBF.read_wire`            | `bool`         | Control if lines should be read, default is true.                                    |
-| `mdl`          | `QuakeMDL.skin_index`      | `unsigned int` | Select a particular skin from a `mdl` file. Uses 0-indexing, default is 0.           |
-
-## Format details
-
-### QuakeMDL
-
-- Models texture are loaded with a simple PBR lighting (diffuse color only, no specular, index of refraction set to 1.0).
-- Selecting `skin` is not supported.
-- Loading `groupskin` is not supported.
-- Animation frames are split based on their names, eg: `stand1`, `stand2`, `stand3`, `run1`, `run2`, `run3`.
-
-### 3D Gaussian splatting
-
-Currently, 3 different formats are supported by F3D:
-
-- `.ply`: Original 3DGS format
-- `.splat`: Format specified by https://github.com/antimatter15/splat. Does not support spherical harmonics.
-- `.spz`: Niantic's format specified by https://github.com/nianticlabs/spz
-
-Note that no config files come with the `.ply` format because this format isn't dedicated to 3DGS only so we cannot generalize.
-If you are using `.ply` for 3DGS only, you can set up a config file similar to what is done for `.splat` or `.spz`.
-See configuration file [documentation](./CONFIGURATION_FILE.md)
diff -Nru f3d-3.2.0+dfsg/examples/CMakeLists.txt f3d-3.4.1/examples/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
@@ -1,12 +0,0 @@
-cmake_minimum_required(VERSION 3.19)
-
-project(examples)
-
-find_package(f3d REQUIRED)
-
-if(BUILD_TESTING)
-  enable_testing()
-endif()
-
-add_subdirectory(libf3d)
-add_subdirectory(plugins)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/c/CMakeLists.txt f3d-3.4.1/examples/libf3d/c/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/c/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/c/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,5 @@
+add_subdirectory(interactive-app)
+
+if (BUILD_TESTING)
+  add_subdirectory(tests)
+endif ()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/c/interactive-app/CMakeLists.txt f3d-3.4.1/examples/libf3d/c/interactive-app/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/c/interactive-app/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/c/interactive-app/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(interactive-app-c)
+
+find_package(f3d REQUIRED COMPONENTS c_api)
+
+add_executable(interactive-app-c main.c)
+
+target_link_libraries(interactive-app-c
+  PRIVATE
+  f3d::c_api
+)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/c/interactive-app/main.c f3d-3.4.1/examples/libf3d/c/interactive-app/main.c
--- f3d-3.2.0+dfsg/examples/libf3d/c/interactive-app/main.c	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/c/interactive-app/main.c	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,97 @@
+#include <f3d/engine_c_api.h>
+#include <f3d/interactor_c_api.h>
+#include <f3d/options_c_api.h>
+#include <f3d/scene_c_api.h>
+#include <f3d/window_c_api.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+static void timeout_callback(void* user_data)
+{
+  f3d_interactor_t* interactor = user_data;
+  f3d_interactor_stop(interactor);
+}
+
+int main(int argc, char** argv)
+{
+  if (argc < 2)
+  {
+    fprintf(stderr, "Usage: %s <file>\n", argv[0]);
+    return EXIT_FAILURE;
+  }
+
+  const char* file = argv[1];
+
+  f3d_engine_autoload_plugins();
+
+  f3d_engine_t* engine = f3d_engine_create(0);
+  if (!engine)
+  {
+    fprintf(stderr, "Failed to create engine\n");
+    return EXIT_FAILURE;
+  }
+
+  f3d_options_t* options = f3d_engine_get_options(engine);
+  if (!options)
+  {
+    fprintf(stderr, "Failed to get options\n");
+    f3d_engine_delete(engine);
+    return EXIT_FAILURE;
+  }
+
+  f3d_options_set_as_bool(options, "render.grid.enable", 1);
+  f3d_options_set_as_bool(options, "render.show_edges", 1);
+  f3d_options_set_as_bool(options, "ui.axis", 1);
+  f3d_options_set_as_bool(options, "ui.fps", 1);
+  f3d_options_set_as_bool(options, "ui.animation_progress", 1);
+  f3d_options_set_as_bool(options, "ui.filename", 1);
+
+  f3d_scene_t* scene = f3d_engine_get_scene(engine);
+  if (!scene)
+  {
+    fprintf(stderr, "Failed to get scene\n");
+    f3d_engine_delete(engine);
+    return EXIT_FAILURE;
+  }
+
+  if (!f3d_scene_add(scene, file))
+  {
+    fprintf(stderr, "Failed to load file: %s\n", file);
+    f3d_engine_delete(engine);
+    return EXIT_FAILURE;
+  }
+
+  f3d_window_t* window = f3d_engine_get_window(engine);
+  if (!window)
+  {
+    fprintf(stderr, "Failed to get window\n");
+    f3d_engine_delete(engine);
+    return EXIT_FAILURE;
+  }
+
+  f3d_window_render(window);
+
+  f3d_interactor_t* interactor = f3d_engine_get_interactor(engine);
+  if (!interactor)
+  {
+    fprintf(stderr, "Failed to get interactor\n");
+    f3d_engine_delete(engine);
+    return EXIT_FAILURE;
+  }
+
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    int timeout = atoi(argv[2]);
+    f3d_interactor_start_with_callback(interactor, timeout, timeout_callback, interactor);
+  }
+  else
+  {
+    f3d_interactor_start(interactor, 1.0 / 30.0);
+  }
+
+  f3d_engine_delete(engine);
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/c/tests/CMakeLists.txt f3d-3.4.1/examples/libf3d/c/tests/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/c/tests/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/c/tests/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.10)
+
+# interactive-app
+add_test(NAME test_interactive-app-c COMMAND "$<TARGET_FILE:interactive-app-c>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/c/tests/data/cow.vtp f3d-3.4.1/examples/libf3d/c/tests/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/c/tests/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/c/tests/data/cow.vtp	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5aa4901d186902f90e17bf3b5917541cb6cb8cf223bfeea736631df4c047652
+size 60449
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/CMakeLists.txt f3d-3.4.1/examples/libf3d/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,4 +2,22 @@
 
 project(libf3d-examples)
 
-add_subdirectory(cpp)
+option(BUILD_TESTING "Build the example tests" OFF)
+if(BUILD_TESTING)
+  enable_testing()
+endif()
+
+option(F3D_ENABLE_CPP_EXAMPLES "Enable the C++ examples" ON)
+if (F3D_ENABLE_CPP_EXAMPLES)
+  add_subdirectory(cpp)
+endif ()
+
+option(F3D_ENABLE_C_EXAMPLES "Enable the C examples" OFF)
+if (F3D_ENABLE_C_EXAMPLES)
+  add_subdirectory(c)
+endif ()
+
+option(F3D_ENABLE_JAVA_EXAMPLES "Enable the Java examples" OFF)
+if (F3D_ENABLE_JAVA_EXAMPLES)
+  add_subdirectory(java)
+endif ()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/check-engine/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/check-engine/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/check-engine/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/check-engine/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -15,14 +15,11 @@
 check_variable_defined(f3d_MODULE_RAYTRACING)
 check_variable_defined(f3d_MODULE_EXR)
 check_variable_defined(f3d_MODULE_UI)
+check_variable_defined(f3d_MODULE_WEBP)
+check_variable_defined(f3d_BINDINGS_C)
 check_variable_defined(f3d_BINDINGS_PYTHON)
 check_variable_defined(f3d_BINDINGS_JAVA)
 
 add_executable(check-engine main.cxx)
 target_link_libraries(check-engine f3d::libf3d)
 set_target_properties(check-engine PROPERTIES CXX_STANDARD 17)
-
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_check-engine COMMAND "$<TARGET_FILE:check-engine>")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,13 +1,31 @@
 find_package(f3d REQUIRED COMPONENTS library)
 
-if(BUILD_TESTING)
-  enable_testing()
-endif()
-
 add_subdirectory(check-engine)
+add_subdirectory(interactive-app)
 add_subdirectory(multi-files)
+add_subdirectory(offscreen-thumbnail)
 add_subdirectory(render-image)
 add_subdirectory(render-interact)
 add_subdirectory(use-options-string)
 add_subdirectory(use-options-struct)
 add_subdirectory(use-options-variant)
+
+option(F3D_EXAMPLES_EXTERNAL_GLFW "Enable external-glfw example (requires GLFW)" OFF)
+if(F3D_EXAMPLES_EXTERNAL_GLFW)
+  add_subdirectory(external-glfw)
+endif()
+
+option(F3D_EXAMPLES_EXTERNAL_QT "Enable external-qt example (requires qt)" OFF)
+if (F3D_EXAMPLES_EXTERNAL_QT)
+  add_subdirectory(qml)
+  add_subdirectory(qt6)
+endif ()
+
+option(F3D_EXAMPLES_EXTERNAL_FLTK "Enable external-fltk example (requires fltk)" OFF)
+if (F3D_EXAMPLES_EXTERNAL_FLTK)
+  add_subdirectory(fltk)
+endif ()
+
+if(BUILD_TESTING)
+  add_subdirectory(tests)
+endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/external-glfw/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/external-glfw/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/external-glfw/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/external-glfw/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,10 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(external-glfw)
+
+find_package(f3d REQUIRED COMPONENTS library)
+find_package(glfw3 REQUIRED)
+
+add_executable(external-glfw main.cxx)
+target_link_libraries(external-glfw f3d::libf3d glfw)
+set_target_properties(external-glfw PROPERTIES CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/external-glfw/main.cxx f3d-3.4.1/examples/libf3d/cpp/external-glfw/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/external-glfw/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/external-glfw/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,435 @@
+#include <f3d/engine.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+#include <GLFW/glfw3.h>
+
+#include <chrono>
+#include <iostream>
+
+f3d::interactor::MouseButton translateButton(int button)
+{
+  switch (button)
+  {
+    case GLFW_MOUSE_BUTTON_RIGHT:
+      return f3d::interactor::MouseButton::RIGHT;
+    case GLFW_MOUSE_BUTTON_MIDDLE:
+      return f3d::interactor::MouseButton::MIDDLE;
+    case GLFW_MOUSE_BUTTON_LEFT:
+    default:
+      return f3d::interactor::MouseButton::LEFT;
+  }
+}
+
+std::string translateKey(int key)
+{
+  switch (key)
+  {
+    case GLFW_KEY_SPACE:
+      return "Space";
+    case GLFW_KEY_APOSTROPHE:
+      return "Apostrophe";
+    case GLFW_KEY_COMMA:
+      return "Comma";
+    case GLFW_KEY_MINUS:
+      return "Minus";
+    case GLFW_KEY_PERIOD:
+      return "Period";
+    case GLFW_KEY_SLASH:
+      return "Slash";
+    case GLFW_KEY_0:
+      return "0";
+    case GLFW_KEY_1:
+      return "1";
+    case GLFW_KEY_2:
+      return "2";
+    case GLFW_KEY_3:
+      return "3";
+    case GLFW_KEY_4:
+      return "4";
+    case GLFW_KEY_5:
+      return "5";
+    case GLFW_KEY_6:
+      return "6";
+    case GLFW_KEY_7:
+      return "7";
+    case GLFW_KEY_8:
+      return "8";
+    case GLFW_KEY_9:
+      return "9";
+    case GLFW_KEY_SEMICOLON:
+      return "Semicolon";
+    case GLFW_KEY_EQUAL:
+      return "Equal";
+    case GLFW_KEY_A:
+      return "A";
+    case GLFW_KEY_B:
+      return "B";
+    case GLFW_KEY_C:
+      return "C";
+    case GLFW_KEY_D:
+      return "D";
+    case GLFW_KEY_E:
+      return "E";
+    case GLFW_KEY_F:
+      return "F";
+    case GLFW_KEY_G:
+      return "G";
+    case GLFW_KEY_H:
+      return "H";
+    case GLFW_KEY_I:
+      return "I";
+    case GLFW_KEY_J:
+      return "J";
+    case GLFW_KEY_K:
+      return "K";
+    case GLFW_KEY_L:
+      return "L";
+    case GLFW_KEY_M:
+      return "M";
+    case GLFW_KEY_N:
+      return "N";
+    case GLFW_KEY_O:
+      return "O";
+    case GLFW_KEY_P:
+      return "P";
+    case GLFW_KEY_Q:
+      return "Q";
+    case GLFW_KEY_R:
+      return "R";
+    case GLFW_KEY_S:
+      return "S";
+    case GLFW_KEY_T:
+      return "T";
+    case GLFW_KEY_U:
+      return "U";
+    case GLFW_KEY_V:
+      return "V";
+    case GLFW_KEY_W:
+      return "W";
+    case GLFW_KEY_X:
+      return "X";
+    case GLFW_KEY_Y:
+      return "Y";
+    case GLFW_KEY_Z:
+      return "Z";
+    case GLFW_KEY_LEFT_BRACKET:
+      return "LeftBracket";
+    case GLFW_KEY_BACKSLASH:
+      return "Backslash";
+    case GLFW_KEY_RIGHT_BRACKET:
+      return "RightBracket";
+    case GLFW_KEY_GRAVE_ACCENT:
+      return "GraveAccent";
+    case GLFW_KEY_ESCAPE:
+      return "Escape";
+    case GLFW_KEY_ENTER:
+      return "Return";
+    case GLFW_KEY_TAB:
+      return "Tab";
+    case GLFW_KEY_BACKSPACE:
+      return "BackSpace";
+    case GLFW_KEY_INSERT:
+      return "Insert";
+    case GLFW_KEY_DELETE:
+      return "Delete";
+    case GLFW_KEY_RIGHT:
+      return "Right";
+    case GLFW_KEY_LEFT:
+      return "Left";
+    case GLFW_KEY_DOWN:
+      return "Down";
+    case GLFW_KEY_UP:
+      return "Up";
+    case GLFW_KEY_PAGE_UP:
+      return "Prior";
+    case GLFW_KEY_PAGE_DOWN:
+      return "Next";
+    case GLFW_KEY_HOME:
+      return "Home";
+    case GLFW_KEY_END:
+      return "End";
+    case GLFW_KEY_CAPS_LOCK:
+      return "CapsLock";
+    case GLFW_KEY_SCROLL_LOCK:
+      return "ScrollLock";
+    case GLFW_KEY_NUM_LOCK:
+      return "NumLock";
+    case GLFW_KEY_PRINT_SCREEN:
+      return "PrintScreen";
+    case GLFW_KEY_PAUSE:
+      return "Pause";
+    case GLFW_KEY_F1:
+      return "F1";
+    case GLFW_KEY_F2:
+      return "F2";
+    case GLFW_KEY_F3:
+      return "F3";
+    case GLFW_KEY_F4:
+      return "F4";
+    case GLFW_KEY_F5:
+      return "F5";
+    case GLFW_KEY_F6:
+      return "F6";
+    case GLFW_KEY_F7:
+      return "F7";
+    case GLFW_KEY_F8:
+      return "F8";
+    case GLFW_KEY_F9:
+      return "F9";
+    case GLFW_KEY_F10:
+      return "F10";
+    case GLFW_KEY_F11:
+      return "F11";
+    case GLFW_KEY_F12:
+      return "F12";
+    case GLFW_KEY_F13:
+      return "F13";
+    case GLFW_KEY_F14:
+      return "F14";
+    case GLFW_KEY_F15:
+      return "F15";
+    case GLFW_KEY_F16:
+      return "F16";
+    case GLFW_KEY_F17:
+      return "F17";
+    case GLFW_KEY_F18:
+      return "F18";
+    case GLFW_KEY_F19:
+      return "F19";
+    case GLFW_KEY_F20:
+      return "F20";
+    case GLFW_KEY_F21:
+      return "F21";
+    case GLFW_KEY_F22:
+      return "F22";
+    case GLFW_KEY_F23:
+      return "F23";
+    case GLFW_KEY_F24:
+      return "F24";
+    case GLFW_KEY_F25:
+      return "F25";
+    case GLFW_KEY_KP_0:
+      return "KP_0";
+    case GLFW_KEY_KP_1:
+      return "KP_1";
+    case GLFW_KEY_KP_2:
+      return "KP_2";
+    case GLFW_KEY_KP_3:
+      return "KP_3";
+    case GLFW_KEY_KP_4:
+      return "KP_4";
+    case GLFW_KEY_KP_5:
+      return "KP_5";
+    case GLFW_KEY_KP_6:
+      return "KP_6";
+    case GLFW_KEY_KP_7:
+      return "KP_7";
+    case GLFW_KEY_KP_8:
+      return "KP_8";
+    case GLFW_KEY_KP_9:
+      return "KP_9";
+    case GLFW_KEY_KP_DECIMAL:
+      return "KP_Decimal";
+    case GLFW_KEY_KP_DIVIDE:
+      return "KP_Divide";
+    case GLFW_KEY_KP_MULTIPLY:
+      return "KP_Multiply";
+    case GLFW_KEY_KP_SUBTRACT:
+      return "KP_Subtract";
+    case GLFW_KEY_KP_ADD:
+      return "KP_Add";
+    case GLFW_KEY_KP_ENTER:
+      return "KP_Enter";
+    case GLFW_KEY_KP_EQUAL:
+      return "KP_Equal";
+    case GLFW_KEY_LEFT_SHIFT:
+      return "LeftShift";
+    case GLFW_KEY_LEFT_CONTROL:
+      return "LeftControl";
+    case GLFW_KEY_LEFT_ALT:
+      return "LeftAlt";
+    case GLFW_KEY_LEFT_SUPER:
+      return "LeftSuper";
+    case GLFW_KEY_RIGHT_SHIFT:
+      return "RightShift";
+    case GLFW_KEY_RIGHT_CONTROL:
+      return "RightControl";
+    case GLFW_KEY_RIGHT_ALT:
+      return "RightAlt";
+    case GLFW_KEY_RIGHT_SUPER:
+      return "RightSuper";
+    case GLFW_KEY_MENU:
+      return "Menu";
+    default:
+      return ""; // Unknown key
+  }
+}
+
+f3d::interactor::InputAction translateAction(int action)
+{
+  switch (action)
+  {
+    case GLFW_RELEASE:
+      return f3d::interactor::InputAction::RELEASE;
+    case GLFW_PRESS:
+    default:
+      return f3d::interactor::InputAction::PRESS;
+  }
+}
+
+f3d::interactor::InputModifier translateMod(int mods)
+{
+  bool shift = mods & GLFW_MOD_SHIFT;
+  bool ctrl = mods & GLFW_MOD_CONTROL;
+
+  if (shift && ctrl)
+  {
+    return f3d::interactor::InputModifier::CTRL_SHIFT;
+  }
+
+  if (shift)
+  {
+    return f3d::interactor::InputModifier::SHIFT;
+  }
+
+  if (ctrl)
+  {
+    return f3d::interactor::InputModifier::CTRL;
+  }
+
+  return f3d::interactor::InputModifier::NONE;
+}
+
+int main(int argc, char* argv[])
+{
+  if (argc < 2)
+  {
+    std::cerr << "Usage: " << argv[0] << " <file>" << std::endl;
+    return EXIT_FAILURE;
+  }
+
+  int timeout = 0;
+  if (argc > 2)
+  {
+    timeout = std::atoi(argv[2]);
+  }
+  auto startTime = std::chrono::steady_clock::now();
+
+  // setup glfw window
+  if (!glfwInit())
+  {
+    std::cerr << "Can't initialize GLFW." << std::endl;
+    return EXIT_FAILURE;
+  }
+
+  glfwSetErrorCallback([](int error, const char* desc)
+    { std::cerr << "GLFW error " << error << ": " << desc << std::endl; });
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
+  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+
+  int defaultSize[] = { 1920, 1080 };
+
+  GLFWwindow* window =
+    glfwCreateWindow(defaultSize[0], defaultSize[1], "F3D GLFW External Window", nullptr, nullptr);
+
+  if (!window)
+  {
+    std::cerr << "Can't create GLFW window." << std::endl;
+    return EXIT_FAILURE;
+  }
+
+  glfwMakeContextCurrent(window);
+
+  {
+    f3d::engine::autoloadPlugins();
+
+    f3d::engine eng = f3d::engine::createExternal(glfwGetProcAddress);
+    eng.getWindow().setSize(defaultSize[0], defaultSize[1]);
+
+    glfwSetWindowUserPointer(window, &eng);
+
+    // resize callback
+    glfwSetFramebufferSizeCallback(window,
+      [](GLFWwindow* window, int width, int height)
+      {
+        f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+        pe->getWindow().setSize(width, height);
+      });
+
+    // key callback
+    glfwSetKeyCallback(window,
+      [](GLFWwindow* window, int key, int scancode, int action, int mods)
+      {
+        f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+        pe->getInteractor().triggerModUpdate(translateMod(mods));
+        pe->getInteractor().triggerKeyboardKey(translateAction(action), translateKey(key));
+      });
+
+    glfwSetCharCallback(window,
+      [](GLFWwindow* window, unsigned int codepoint)
+      {
+        f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+        pe->getInteractor().triggerTextCharacter(codepoint);
+      });
+
+    // mouse callback
+    glfwSetMouseButtonCallback(window,
+      [](GLFWwindow* window, int button, int action, int mods)
+      {
+        f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+        pe->getInteractor().triggerModUpdate(translateMod(mods));
+        pe->getInteractor().triggerMouseButton(translateAction(action), translateButton(button));
+      });
+
+    glfwSetCursorPosCallback(window,
+      [](GLFWwindow* window, double xpos, double ypos)
+      {
+        f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+        pe->getInteractor().triggerMousePosition(xpos, ypos);
+        pe->getWindow().render();
+        glfwSwapBuffers(window);
+      });
+
+    glfwSetScrollCallback(window,
+      [](GLFWwindow* window, double xoffset, double yoffset)
+      {
+        if (yoffset != 0.0)
+        {
+          f3d::engine* pe = static_cast<f3d::engine*>(glfwGetWindowUserPointer(window));
+          pe->getInteractor().triggerMouseWheel(yoffset > 0
+              ? f3d::interactor::WheelDirection::FORWARD
+              : f3d::interactor::WheelDirection::BACKWARD);
+          glfwSwapBuffers(window);
+        }
+      });
+
+    eng.getScene().add(argv[1]);
+
+    while (!glfwWindowShouldClose(window))
+    {
+      // For testing purposes only, shutdown the example after `timeout` seconds
+      if (timeout > 0)
+      {
+        auto now = std::chrono::steady_clock::now();
+        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
+
+        if (elapsed >= timeout)
+        {
+          glfwSetWindowShouldClose(window, GLFW_TRUE);
+          break;
+        }
+      }
+
+      eng.getWindow().render();
+      glfwSwapBuffers(window);
+      glfwPollEvents();
+    }
+  }
+
+  glfwDestroyWindow(window);
+  glfwTerminate();
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/fltk/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/fltk/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/fltk/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/fltk/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(minimal_fltk)
+
+if (POLICY CMP0072)
+  cmake_policy(SET CMP0072 NEW)
+endif()
+
+find_package(f3d REQUIRED COMPONENTS library)
+find_package(FLTK REQUIRED)
+
+add_executable(minimal_fltk main.cxx)
+
+target_link_libraries(minimal_fltk
+  PRIVATE
+  f3d::libf3d
+  ${FLTK_LIBRARIES}
+)
+
+set_target_properties(minimal_fltk PROPERTIES
+  CXX_STANDARD 17
+  CXX_STANDARD_REQUIRED ON
+)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/fltk/main.cxx f3d-3.4.1/examples/libf3d/cpp/fltk/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/fltk/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/fltk/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,427 @@
+#include <FL/Fl.H>
+#include <FL/Fl_Gl_Window.H>
+#include <FL/Fl_Window.H>
+#include <FL/gl.h>
+
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+#include <algorithm>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+
+class F3DGLWindow : public Fl_Gl_Window
+{
+public:
+  explicit F3DGLWindow(int X, int Y, int W, int H, std::string filePath)
+    : Fl_Gl_Window(X, Y, W, H, "libf3d + FLTK example")
+    , FilePath(std::move(filePath))
+  {
+    this->mode(FL_DOUBLE | FL_RGB | FL_DEPTH | FL_ALPHA);
+  }
+
+  ~F3DGLWindow() override = default;
+
+  void initialize()
+  {
+    if (this->Initialized)
+    {
+      return;
+    }
+
+    this->make_current();
+
+    f3d::engine::autoloadPlugins();
+
+    f3d::context::function loadFunc;
+#if defined(_WIN32)
+    loadFunc = f3d::context::wgl();
+#elif defined(__APPLE__)
+    loadFunc = f3d::context::cocoa();
+#else
+    const char* sessionType = std::getenv("XDG_SESSION_TYPE");
+
+    if (sessionType && std::string(sessionType) == "wayland")
+    {
+      loadFunc = f3d::context::egl();
+    }
+    else
+    {
+      loadFunc = f3d::context::glx();
+    }
+#endif
+    this->Engine = std::make_unique<f3d::engine>(f3d::engine::createExternal(loadFunc));
+
+    if (!this->Engine)
+    {
+      std::cerr << "Failed to create f3d::engine" << '\n';
+      return;
+    }
+
+    this->Interactor = &this->Engine->getInteractor();
+
+    // Load the model
+    this->Engine->getScene().add(this->FilePath);
+
+    auto& win = this->Engine->getWindow();
+    win.setSize(this->w(), this->h());
+
+    auto& opt = this->Engine->getOptions();
+    opt.set("render.grid.enable", true);
+    opt.set("render.show_edges", false);
+    opt.set("ui.axis", true);
+    opt.set("ui.fps", true);
+    opt.set("ui.filename", true);
+
+    this->Initialized = true;
+
+    Fl::add_timeout(1.0 / 30.0, &F3DGLWindow::TimerCallback, this);
+  }
+
+protected:
+  void draw() override
+  {
+    if (!valid())
+    {
+      this->initialize();
+      this->valid(1);
+    }
+
+    if (!this->Engine)
+    {
+      return;
+    }
+
+    // Process any pending mouse position updates before rendering
+    if (this->HasPendingMouseMove && this->Interactor)
+    {
+      this->Interactor->triggerMousePosition(this->PendingMouseX, this->PendingMouseY);
+      this->HasPendingMouseMove = false;
+    }
+
+    this->Engine->getWindow().render();
+  }
+
+  void resize(int x, int y, int w, int h) override
+  {
+    Fl_Gl_Window::resize(x, y, w, h);
+
+    if (!this->Engine)
+    {
+      return;
+    }
+
+    this->Engine->getWindow().setSize(std::max(1, w), std::max(1, h));
+  }
+
+  void UpdateModifiers(int state)
+  {
+    if (!this->Interactor)
+    {
+      return;
+    }
+
+    using InputModifier = f3d::interactor::InputModifier;
+
+    const bool ctrl = (state & FL_CTRL) != 0;
+    const bool shift = (state & FL_SHIFT) != 0;
+
+    if (ctrl && shift)
+    {
+      this->Interactor->triggerModUpdate(InputModifier::CTRL_SHIFT);
+    }
+    else if (ctrl)
+    {
+      this->Interactor->triggerModUpdate(InputModifier::CTRL);
+    }
+    else if (shift)
+    {
+      this->Interactor->triggerModUpdate(InputModifier::SHIFT);
+    }
+    else
+    {
+      this->Interactor->triggerModUpdate(InputModifier::NONE);
+    }
+  }
+
+  std::optional<f3d::interactor::MouseButton> MapMouseButton(int button)
+  {
+    if (!this->Interactor)
+    {
+      return std::nullopt;
+    }
+
+    using MouseButton = f3d::interactor::MouseButton;
+
+    switch (button)
+    {
+      case FL_LEFT_MOUSE:
+        return MouseButton::LEFT;
+      case FL_RIGHT_MOUSE:
+        return MouseButton::RIGHT;
+      case FL_MIDDLE_MOUSE:
+        return MouseButton::MIDDLE;
+      default:
+        return std::nullopt;
+    }
+  }
+
+  std::string KeySymFromEvent()
+  {
+    int key = Fl::event_key();
+
+    if (std::isalpha(key))
+    {
+      return std::string{ static_cast<char>(std::toupper(key)) };
+    }
+
+    switch (key)
+    {
+      case FL_BackSpace:
+        return "BackSpace";
+      case FL_Escape:
+        return "Escape";
+      case FL_Enter:
+      case FL_KP_Enter:
+        return "Return";
+      case ' ':
+        return "Space";
+      case FL_Tab:
+        return "Tab";
+      default:
+        break;
+    }
+
+    int ch = Fl::event_text()[0];
+    if (ch != 0)
+    {
+      return std::string{ 1, static_cast<char>(ch) };
+    }
+
+    return {};
+  }
+
+  int handle(int event) override
+  {
+    if (!this->Interactor)
+    {
+      return Fl_Gl_Window::handle(event);
+    }
+
+    switch (event)
+    {
+      case FL_PUSH:
+      {
+        this->UpdateModifiers(Fl::event_state());
+        auto button = this->MapMouseButton(Fl::event_button());
+
+        if (button.has_value())
+        {
+          const int x = Fl::event_x();
+          const int y = Fl::event_y();
+          this->Interactor->triggerMousePosition(x, y);
+          this->Interactor->triggerMouseButton(f3d::interactor::InputAction::PRESS, button.value());
+        }
+
+        return 1;
+      }
+
+      case FL_DRAG:
+      case FL_MOVE:
+      {
+        this->UpdateModifiers(Fl::event_state());
+        const int x = Fl::event_x();
+        const int y = Fl::event_y();
+
+        // Store the position for the next draw cycle
+        this->PendingMouseX = x;
+        this->PendingMouseY = y;
+        this->HasPendingMouseMove = true;
+
+        return 1;
+      }
+
+      case FL_RELEASE:
+      {
+        this->UpdateModifiers(Fl::event_state());
+        auto button = this->MapMouseButton(Fl::event_button());
+        if (button.has_value())
+        {
+          const int x = Fl::event_x();
+          const int y = Fl::event_y();
+          this->Interactor->triggerMousePosition(x, y);
+          this->Interactor->triggerMouseButton(
+            f3d::interactor::InputAction::RELEASE, button.value());
+        }
+
+        return 1;
+      }
+
+      case FL_MOUSEWHEEL:
+      {
+        this->UpdateModifiers(Fl::event_state());
+
+        using WheelDirection = f3d::interactor::WheelDirection;
+        int dx = Fl::event_dx();
+        int dy = Fl::event_dy();
+
+        WheelDirection direction;
+        if (std::abs(dy) >= std::abs(dx))
+        {
+          direction = (dy < 0) ? WheelDirection::FORWARD : WheelDirection::BACKWARD;
+        }
+        else
+        {
+          direction = (dx > 0) ? WheelDirection::RIGHT : WheelDirection::LEFT;
+        }
+
+        this->Interactor->triggerMouseWheel(direction);
+        return 1;
+      }
+
+      case FL_KEYDOWN:
+      case FL_SHORTCUT:
+      {
+        this->UpdateModifiers(Fl::event_state());
+        const std::string keySym = this->KeySymFromEvent();
+
+        if (!keySym.empty())
+        {
+          this->Interactor->triggerKeyboardKey(
+            f3d::interactor::InputAction::PRESS, std::string_view(keySym));
+        }
+
+        const char* txt = Fl::event_text();
+        int len = Fl::event_length();
+        for (int i = 0; i < len; ++i)
+        {
+          char16_t ch = static_cast<unsigned char>(txt[i]);
+          this->Interactor->triggerTextCharacter(ch);
+        }
+        return 1;
+      }
+
+      case FL_KEYUP:
+      {
+        this->UpdateModifiers(Fl::event_state());
+        const std::string keySym = this->KeySymFromEvent();
+
+        if (!keySym.empty())
+        {
+          this->Interactor->triggerKeyboardKey(
+            f3d::interactor::InputAction::RELEASE, std::string_view(keySym));
+        }
+        return 1;
+      }
+
+      default:
+        break;
+    }
+
+    return Fl_Gl_Window::handle(event);
+  }
+
+private:
+  static void TimerCallback(void* userdata)
+  {
+    auto* self = static_cast<F3DGLWindow*>(userdata);
+    if (self)
+    {
+      self->onTick();
+      Fl::repeat_timeout(1.0 / 30.0, &F3DGLWindow::TimerCallback, userdata);
+    }
+  }
+
+  void onTick()
+  {
+    if (!this->Engine || !this->Interactor)
+    {
+      return;
+    }
+
+    this->make_current();
+    this->Interactor->triggerEventLoop(1.0 / 30.0);
+    this->redraw();
+  }
+
+  std::string FilePath;
+  std::unique_ptr<f3d::engine> Engine;
+  f3d::interactor* Interactor = nullptr;
+  bool Initialized = false;
+
+  int PendingMouseX = 0;
+  int PendingMouseY = 0;
+  bool HasPendingMouseMove = false;
+};
+
+int main(int argc, char** argv)
+{
+  if (argc < 2 || argc > 3)
+  {
+    std::cerr << "Usage: " << argv[0] << " <file> [timeout]\n";
+    return 1;
+  }
+
+  // Required model file
+  std::string filePath = argv[1];
+
+  if (!std::filesystem::exists(filePath))
+  {
+    std::cerr << "File not found: " << filePath << '\n';
+    return 1;
+  }
+
+  int timeout = 0;
+  if (argc == 3)
+  {
+    try
+    {
+      timeout = std::stoi(argv[2]);
+      if (timeout < 1)
+      {
+        std::cerr << "Timeout must be a positive integer\n";
+        return 1;
+      }
+    }
+    catch (...)
+    {
+      std::cerr << "Invalid timeout value\n";
+      return 1;
+    }
+  }
+
+  Fl::visual(FL_RGB | FL_DOUBLE | FL_DEPTH);
+
+  Fl_Window win(800, 600, "libf3d + FLTK example");
+  F3DGLWindow glwin(0, 0, 800, 600, filePath);
+  win.resizable(&glwin);
+  win.end();
+  win.show();
+
+  glwin.take_focus();
+
+  // For testing purposes only, shutdown the example after `timeout` seconds
+  if (timeout > 0)
+  {
+    Fl::add_timeout(
+      timeout,
+      [](void* data)
+      {
+        auto* w = static_cast<Fl_Window*>(data);
+        if (w)
+        {
+          w->hide();
+        }
+      },
+      &win);
+  }
+
+  return Fl::run();
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/interactive-app/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/interactive-app/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/interactive-app/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/interactive-app/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(interactive-app)
+
+find_package(f3d REQUIRED COMPONENTS library)
+
+add_executable(interactive-app main.cxx)
+target_link_libraries(interactive-app f3d::libf3d)
+set_target_properties(interactive-app PROPERTIES CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/interactive-app/main.cxx f3d-3.4.1/examples/libf3d/cpp/interactive-app/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/interactive-app/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/interactive-app/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,235 @@
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+
+#include <iostream>
+#include <string>
+#include <vector>
+
+namespace
+{
+void AddCustomCommands(f3d::engine& eng)
+{
+  // Get the interactor
+  f3d::interactor& inter = eng.getInteractor();
+  inter.initCommands();
+
+  // Reset options to initial state
+  f3d::options& opt = eng.getOptions();
+  // Keep a copy of the initial options
+  const f3d::options initialOptions = opt;
+  inter.addCommand(
+    "reset_options",
+    [&eng, initialOptions](const std::vector<std::string>&) { eng.getOptions() = initialOptions; },
+    f3d::interactor::command_documentation_t{ "reset_options", "Reset basic render/UI options" });
+
+  // Increase animation speed factor
+  inter.addCommand(
+    "increase_animation_speed_factor",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.scene.animation.speed_factor = f3d::ratio_t(o.scene.animation.speed_factor + 0.05);
+    },
+    f3d::interactor::command_documentation_t{
+      "increase_animation_speed_factor", "Increase animation speed factor" });
+
+  // Decrease animation speed factor
+  inter.addCommand(
+    "decrease_animation_speed_factor",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.scene.animation.speed_factor = f3d::ratio_t(o.scene.animation.speed_factor - 0.05);
+    },
+    f3d::interactor::command_documentation_t{
+      "decrease_animation_speed_factor", "Decrease animation speed factor" });
+
+  // Toggle grid visibility
+  inter.removeCommand("toggle_grid");
+  inter.addCommand(
+    "toggle_grid",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.render.grid.enable = !o.render.grid.enable;
+    },
+    f3d::interactor::command_documentation_t{ "toggle_grid", "Toggle ground grid visibility" });
+
+  // Toggle axis
+  inter.removeCommand("toggle_axis");
+  inter.addCommand(
+    "toggle_axis",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.ui.axis = !o.ui.axis;
+    },
+    f3d::interactor::command_documentation_t{ "toggle_axis", "Toggle axis" });
+
+  // Toggle FXAA anti-aliasing
+  inter.removeCommand("toggle_fxaa");
+  inter.addCommand(
+    "toggle_fxaa",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.render.effect.antialiasing.enable = !o.render.effect.antialiasing.enable;
+      // keep mode at fxaa in this example
+      o.render.effect.antialiasing.mode = "fxaa";
+    },
+    f3d::interactor::command_documentation_t{ "toggle_fxaa", "Toggle FXAA anti-aliasing" });
+
+  // Toggle tone mapping
+  inter.removeCommand("toggle_tonemapping");
+  inter.addCommand(
+    "toggle_tonemapping",
+    [&eng](const std::vector<std::string>&)
+    {
+      auto& o = eng.getOptions();
+      o.render.effect.tone_mapping = !o.render.effect.tone_mapping;
+    },
+    f3d::interactor::command_documentation_t{ "toggle_tonemapping", "Toggle tone mapping" });
+}
+
+void AddCustomBindins(f3d::engine& eng)
+{
+  // Get the interactor
+  f3d::interactor& inter = eng.getInteractor();
+  inter.initBindings();
+
+  f3d::options& opt = eng.getOptions();
+
+  auto docTgl = [](const std::string& doc, const bool& val)
+  { return std::pair(doc, (val ? "ON" : "OFF")); };
+  auto docStr = [](const std::string& doc) { return std::pair(doc, ""); };
+  auto docDblOpt = [](const std::string& doc, const std::optional<double>& val)
+  {
+    std::stringstream valStream;
+    valStream.precision(2);
+    valStream << std::fixed;
+    if (val.has_value())
+    {
+      valStream << val.value();
+    }
+    else
+    {
+      valStream << "Unset";
+    }
+    return std::pair(doc, valStream.str());
+  };
+
+  // R: reset options
+  inter.addBinding({ f3d::interaction_bind_t::ModifierKeys::SHIFT, "R" }, "reset_options",
+    "Example", std::bind(docStr, "Reset Options"), f3d::interactor::BindingType::OTHER);
+
+  // SHIFT + S: Increase animation speed
+  inter.addBinding({ f3d::interaction_bind_t::ModifierKeys::SHIFT, "S" },
+    "increase_animation_speed_factor", "Example",
+    std::bind(docDblOpt, "Increase animation speed", std::cref(opt.scene.animation.speed_factor)),
+    f3d::interactor::BindingType::NUMERICAL);
+
+  // CTRL + S: Decrease animation speed
+  inter.addBinding({ f3d::interaction_bind_t::ModifierKeys::CTRL, "S" },
+    "decrease_animation_speed_factor", "Example",
+    std::bind(docDblOpt, "Decrease animation speed", std::cref(opt.scene.animation.speed_factor)),
+    f3d::interactor::BindingType::NUMERICAL);
+
+  // G: toggle grid
+  inter.removeBinding({ f3d::interaction_bind_t::ModifierKeys::NONE, "G" });
+  inter.addBinding(f3d::interaction_bind_t::parse("G"), "toggle_grid", "Example",
+    std::bind(docTgl, "Toggle grid", std::cref(opt.render.grid.enable)),
+    f3d::interactor::BindingType::TOGGLE);
+
+  // X: toggle axis
+  inter.removeBinding({ f3d::interaction_bind_t::ModifierKeys::NONE, "X" });
+  inter.addBinding(f3d::interaction_bind_t::parse("X"), "toggle_axis", "Example",
+    std::bind(docTgl, "Toggle axis", std::cref(opt.ui.axis)), f3d::interactor::BindingType::TOGGLE);
+
+  // F: toggle FXAA
+  inter.removeBinding({ f3d::interaction_bind_t::ModifierKeys::NONE, "F" });
+  inter.addBinding(f3d::interaction_bind_t::parse("F"), "toggle_fxaa", "Example",
+    std::bind(docTgl, "Toggle FXAA", std::cref(opt.render.effect.antialiasing.enable)),
+    f3d::interactor::BindingType::TOGGLE);
+
+  // T: toggle tone mapping
+  inter.removeBinding({ f3d::interaction_bind_t::ModifierKeys::NONE, "T" });
+  inter.addBinding(f3d::interaction_bind_t::parse("T"), "toggle_tonemapping", "Example",
+    std::bind(docTgl, "Toggle Tone Mapping", std::cref(opt.render.effect.tone_mapping)),
+    f3d::interactor::BindingType::TOGGLE);
+}
+}
+
+int main(int argc, char** argv)
+{
+  if (argc < 2)
+  {
+    std::cerr << "Usage: " << argv[0] << " <file>\n";
+    return EXIT_FAILURE;
+  }
+
+  const std::string file = argv[1];
+
+  // Load static/native plugins
+  f3d::engine::autoloadPlugins();
+
+  // Create a native-window engine
+  f3d::engine eng = f3d::engine::create();
+
+  // Modify options using the struct API
+  f3d::options& opt = eng.getOptions();
+  opt.render.grid.enable = true;
+  opt.render.show_edges = true;
+
+  // UI overlays: axis + some HUD
+  opt.ui.axis = true;
+  opt.ui.fps = true;
+  opt.ui.animation_progress = true;
+  opt.ui.filename = true;
+
+  // FXAA + tone mapping
+  opt.render.effect.antialiasing.enable = true;
+  opt.render.effect.antialiasing.mode = "fxaa";
+  opt.render.effect.tone_mapping = true;
+
+  ::AddCustomCommands(eng);
+  ::AddCustomBindins(eng);
+
+  try
+  {
+    // Add a model to the scene
+    eng.getScene().add(file);
+  }
+  catch (const std::exception& e)
+  {
+    std::cerr << e.what() << '\n';
+  }
+
+  // Initial render
+  f3d::window& win = eng.getWindow();
+  win.render();
+
+  // Start interaction loop
+  f3d::interactor& inter = eng.getInteractor();
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << std::endl;
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/multi-files/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/multi-files/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -11,11 +11,3 @@
 if(UNIX AND NOT APPLE)
   target_link_libraries(multi-files stdc++fs)
 endif()
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_multi-files COMMAND "$<TARGET_FILE:multi-files>" "${CMAKE_CURRENT_SOURCE_DIR}/data/")
-  set_tests_properties(test_multi-files PROPERTIES
-    PASS_REGULAR_EXPRESSION "Scene bounding box: -0.487464,1,-0.487464,1,-0.5,1")
-endif()
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/data/mb_0_0.vtu and /tmp/gJkK3JzSDT/f3d-3.4.1/examples/libf3d/cpp/multi-files/data/mb_0_0.vtu differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/data/mb_1_0.vtp and /tmp/gJkK3JzSDT/f3d-3.4.1/examples/libf3d/cpp/multi-files/data/mb_1_0.vtp differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/data/mb_2_0.vtp and /tmp/gJkK3JzSDT/f3d-3.4.1/examples/libf3d/cpp/multi-files/data/mb_2_0.vtp differ
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/main.cxx f3d-3.4.1/examples/libf3d/cpp/multi-files/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/multi-files/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/multi-files/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,7 +8,7 @@
 int main(int argc, char** argv)
 {
   // Check inputs
-  if (argc != 2)
+  if (argc < 2)
   {
     return EXIT_FAILURE;
   }
@@ -35,10 +35,25 @@
 
   // Start interaction and stop it after one second
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
 
-  // Actual call would look like this
-  // inter.start();
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << std::endl;
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
 
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/offscreen-thumbnail/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/offscreen-thumbnail/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/offscreen-thumbnail/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/offscreen-thumbnail/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(offscreen-thumbnail)
+
+find_package(f3d REQUIRED COMPONENTS library)
+
+add_executable(offscreen-thumbnail main.cxx)
+target_link_libraries(offscreen-thumbnail f3d::libf3d)
+set_target_properties(offscreen-thumbnail PROPERTIES CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/offscreen-thumbnail/main.cxx f3d-3.4.1/examples/libf3d/cpp/offscreen-thumbnail/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/offscreen-thumbnail/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/offscreen-thumbnail/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,148 @@
+#include <f3d/engine.h>
+#include <f3d/image.h>
+#include <f3d/log.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+#include <cstdlib>
+#include <exception>
+#include <iostream>
+#include <string>
+
+namespace
+{
+// Parse optional thumbnail size:
+//   argv[3]      -> square size (N x N)
+//   argv[3,4]    -> width height
+bool ParseSize(int argc, char** argv, int& width, int& height)
+{
+  // Default thumbnail size
+  width = 512;
+  height = 512;
+
+  if (argc == 3)
+  {
+    // Just use the default
+    return true;
+  }
+
+  try
+  {
+    if (argc == 4)
+    {
+      // Single integer: square thumbnail
+      const int size = std::stoi(argv[3]);
+      if (size <= 0)
+      {
+        return false;
+      }
+      width = size;
+      height = size;
+      return true;
+    }
+    if (argc == 5)
+    {
+      width = std::stoi(argv[3]);
+      height = std::stoi(argv[4]);
+      if (width <= 0 || height <= 0)
+      {
+        return false;
+      }
+      return true;
+    }
+  }
+  catch (const std::exception&)
+  {
+    return false;
+  }
+
+  return false;
+}
+
+void PrintUsage(const char* exe)
+{
+  std::cerr << "Usage:\n"
+            << "  " << exe << " <input-model> <output-image> [size]\n"
+            << "  " << exe << " <input-model> <output-image> <width> <height>\n\n"
+            << "Examples:\n"
+            << "  " << exe << " model.glb thumb.png        # 512x512 (default)\n"
+            << "  " << exe << " model.glb thumb.png 256    # 256x256\n"
+            << "  " << exe << " model.glb thumb.png 512 256\n";
+}
+}
+
+int main(int argc, char** argv)
+{
+  try
+  {
+    if (argc < 3 || argc > 5)
+    {
+      ::PrintUsage(argv[0]);
+      return EXIT_FAILURE;
+    }
+
+    const std::string inputFile = argv[1];
+    const std::string outputFile = argv[2];
+
+    int width = 0;
+    int height = 0;
+    if (!::ParseSize(argc, argv, width, height))
+    {
+      f3d::log::error("Invalid thumbnail size.");
+      ::PrintUsage(argv[0]);
+      return EXIT_FAILURE;
+    }
+
+    // Load static/native plugins
+    f3d::engine::autoloadPlugins();
+
+    // Create an offscreen engine
+    f3d::engine eng = f3d::engine::create(true);
+
+    f3d::options& opt = eng.getOptions();
+
+    // No UI overlays in thumbnails
+    opt.ui.axis = false;
+    opt.ui.fps = false;
+    opt.ui.filename = false;
+    opt.ui.metadata = false;
+    opt.ui.console = false;
+    opt.ui.cheatsheet = false;
+
+    // Neutral background, no grid or skybox
+    opt.render.grid.enable = false;
+    opt.render.background.skybox = false;
+    opt.render.background.color = { 0.15, 0.15, 0.15 }; // dark neutral gray
+
+    // Slightly stronger lighting so assets read well at small sizes
+    opt.render.light.intensity = 1.2;
+
+    // Post-processing: AA + AO for better thumbnails
+    opt.render.effect.antialiasing.enable = true;
+    opt.render.effect.antialiasing.mode = "ssaa";
+    opt.render.effect.ambient_occlusion = true;
+
+    // Add a model to the scene
+    eng.getScene().add(inputFile);
+
+    // Render offscreen to an image
+    f3d::window& win = eng.getWindow();
+    win.setSize(width, height);
+
+    // You can make the background transparent by setting noBackground to true in the renderToImage
+    // call.
+    f3d::image img = win.renderToImage(false);
+
+    // Save the resulting image
+    img.save(outputFile);
+
+    return EXIT_SUCCESS;
+  }
+  catch (const std::exception& ex)
+  {
+    f3d::log::error("F3D thumbnail example encountered an unexpected exception:");
+    f3d::log::error(ex.what());
+    return EXIT_FAILURE;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/qml/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(minimal_qml)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTOUIC ON)
+
+find_package(f3d REQUIRED COMPONENTS library)
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
+
+set(SOURCES
+  main.cxx
+  F3DRenderer.cxx
+  F3DRenderer.h
+  F3DView.cxx
+  F3DView.h
+)
+
+add_executable(minimal_qml ${SOURCES})
+target_link_libraries(minimal_qml
+  PRIVATE
+  f3d::libf3d
+  Qt6::Core
+  Qt6::Gui
+  Qt6::Qml
+  Qt6::Quick
+)
+set_target_properties(minimal_qml PROPERTIES CXX_STANDARD 17)
+
+# Copy QML file next to the executable
+configure_file(
+  "${CMAKE_CURRENT_SOURCE_DIR}/Main.qml"
+  "${CMAKE_CURRENT_BINARY_DIR}/Main.qml"
+  COPYONLY
+)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DRenderer.cxx f3d-3.4.1/examples/libf3d/cpp/qml/F3DRenderer.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DRenderer.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/F3DRenderer.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,344 @@
+#include "F3DRenderer.h"
+#include "F3DView.h"
+
+#include <QOpenGLContext>
+
+#include <f3d/engine.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+//----------------------------------------------------------------------------
+F3DRenderer::F3DRenderer(F3DView* item)
+  : View(item)
+{
+  f3d::engine::autoloadPlugins();
+
+  f3d::context::function loadFunc = [](const char* name)
+  { return QOpenGLContext::currentContext()->getProcAddress(name); };
+  this->Engine = std::make_unique<f3d::engine>(f3d::engine::createExternal(loadFunc));
+
+  if (!this->Engine)
+  {
+    return;
+  }
+
+  if (!this->View->GetModelPath().isEmpty())
+  {
+    this->Engine->getScene().add(this->View->GetModelPath().toStdString());
+  }
+
+  auto& window = this->Engine->getWindow();
+  window.setSize(static_cast<int>(this->View->width()), static_cast<int>(this->View->height()));
+
+  auto& options = this->Engine->getOptions();
+
+  options.set("render.grid.enable", true);
+  options.set("render.show_edges", false);
+  options.set("ui.axis", true);
+  options.set("ui.fps", true);
+  options.set("ui.filename", true);
+
+  options.set("render.background.color", f3d::color_t(0.15, 0.15, 0.15));
+
+  this->Interactor = &this->Engine->getInteractor();
+
+  this->FrameTimer.start();
+  this->FirstFrame = true;
+}
+
+//----------------------------------------------------------------------------
+F3DRenderer::~F3DRenderer() = default;
+
+//----------------------------------------------------------------------------
+void F3DRenderer::Enqueue(const Event& ev)
+{
+  std::lock_guard lock(this->Mutex);
+  this->Events.push_back(ev);
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueMousePress(float x, float y, int button, int modifiers)
+{
+  Event event;
+  event.type = EventType::MousePress;
+  event.x = x;
+  event.y = y;
+  event.button = button;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueMouseMove(float x, float y, int buttons, int modifiers)
+{
+  Event event;
+  event.type = EventType::MouseMove;
+  event.x = x;
+  event.y = y;
+  event.buttons = buttons;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueMouseRelease(float x, float y, int button, int modifiers)
+{
+  Event event;
+  event.type = EventType::MouseRelease;
+  event.x = x;
+  event.y = y;
+  event.button = button;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueWheel(int dx, int dy, int modifiers)
+{
+  Event event;
+  event.type = EventType::Wheel;
+  event.dx = dx;
+  event.dy = dy;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueKeyPress(int key, const QString& text, int modifiers)
+{
+  Event event;
+  event.type = EventType::KeyPress;
+  event.key = key;
+  event.text = text;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::QueueKeyRelease(int key, int modifiers)
+{
+  Event event;
+  event.type = EventType::KeyRelease;
+  event.key = key;
+  event.modifiers = modifiers;
+
+  this->Enqueue(event);
+  this->update();
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::UpdateModifiers(int mods)
+{
+  if (this->Interactor == nullptr)
+  {
+    return;
+  }
+
+  auto& inter = *this->Interactor;
+  const auto qMods = Qt::KeyboardModifiers(mods);
+  using InputMod = f3d::interactor::InputModifier;
+
+  if ((qMods & Qt::ControlModifier) && (qMods & Qt::ShiftModifier))
+  {
+    inter.triggerModUpdate(InputMod::CTRL_SHIFT);
+  }
+  else if (qMods & Qt::ControlModifier)
+  {
+    inter.triggerModUpdate(InputMod::CTRL);
+  }
+  else if (qMods & Qt::ShiftModifier)
+  {
+    inter.triggerModUpdate(InputMod::SHIFT);
+  }
+  else
+  {
+    inter.triggerModUpdate(InputMod::NONE);
+  }
+}
+
+//----------------------------------------------------------------------------
+int F3DRenderer::MapMouseButton(int button) const
+{
+  auto qButton = static_cast<Qt::MouseButton>(button);
+  using MouseButton = f3d::interactor::MouseButton;
+
+  if (qButton == Qt::LeftButton)
+  {
+    return static_cast<int>(MouseButton::LEFT);
+  }
+
+  if (qButton == Qt::RightButton)
+  {
+    return static_cast<int>(MouseButton::RIGHT);
+  }
+
+  return static_cast<int>(MouseButton::MIDDLE);
+}
+
+//----------------------------------------------------------------------------
+std::string F3DRenderer::KeySymFromKeyAndText(int key, const QString& text) const
+{
+  if (key >= Qt::Key_A && key <= Qt::Key_Z)
+  {
+    return std::string(1, static_cast<char>(std::toupper(key)));
+  }
+
+  switch (key)
+  {
+    case Qt::Key_Backspace:
+      return "BackSpace";
+    case Qt::Key_Escape:
+      return "Escape";
+    case Qt::Key_Return:
+    case Qt::Key_Enter:
+      return "Return";
+    case Qt::Key_Space:
+      return "Space";
+    case Qt::Key_Tab:
+      return "Tab";
+    default:
+      break;
+  }
+
+  if (!text.isEmpty())
+  {
+    return text.toStdString();
+  }
+
+  return {};
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::HandleEvent(const Event& ev)
+{
+  auto& inter = *this->Interactor;
+
+  switch (ev.type)
+  {
+    case EventType::MousePress:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      auto mouseButtonEnum =
+        static_cast<f3d::interactor::MouseButton>(this->MapMouseButton(ev.button));
+      double yFlipped = this->View->height() - ev.y;
+      inter.triggerMousePosition(ev.x, yFlipped);
+      inter.triggerMouseButton(f3d::interactor::InputAction::PRESS, mouseButtonEnum);
+      break;
+    }
+    case EventType::MouseMove:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      double yFlipped = this->View->height() - ev.y;
+      inter.triggerMousePosition(ev.x, yFlipped);
+      break;
+    }
+    case EventType::MouseRelease:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      auto mouseButtonEnum =
+        static_cast<f3d::interactor::MouseButton>(this->MapMouseButton(ev.button));
+      double yFlipped = this->View->height() - ev.y;
+      inter.triggerMouseButton(f3d::interactor::InputAction::RELEASE, mouseButtonEnum);
+      inter.triggerMousePosition(ev.x, yFlipped);
+      break;
+    }
+    case EventType::Wheel:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      using WheelDirection = f3d::interactor::WheelDirection;
+
+      WheelDirection direction;
+      if (std::abs(ev.dy) >= std::abs(ev.dx))
+      {
+        direction = (ev.dy > 0) ? WheelDirection::FORWARD : WheelDirection::BACKWARD;
+      }
+      else
+      {
+        direction = (ev.dx > 0) ? WheelDirection::RIGHT : WheelDirection::LEFT;
+      }
+
+      inter.triggerMouseWheel(direction);
+      break;
+    }
+    case EventType::KeyPress:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      std::string keySym = this->KeySymFromKeyAndText(ev.key, ev.text);
+      if (!keySym.empty())
+      {
+        inter.triggerKeyboardKey(f3d::interactor::InputAction::PRESS, keySym);
+      }
+      if (!ev.text.isEmpty())
+      {
+        for (QChar ch : ev.text)
+        {
+          inter.triggerTextCharacter(ch.unicode());
+        }
+      }
+      break;
+    }
+    case EventType::KeyRelease:
+    {
+      this->UpdateModifiers(ev.modifiers);
+      std::string keySym = this->KeySymFromKeyAndText(ev.key, {});
+      if (!keySym.empty())
+      {
+        inter.triggerKeyboardKey(f3d::interactor::InputAction::RELEASE, keySym);
+      }
+      break;
+    }
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DRenderer::render()
+{
+  if (!this->Engine || !this->Interactor)
+  {
+    return;
+  }
+
+  auto& window = this->Engine->getWindow();
+  window.setSize(static_cast<int>(this->View->width()), static_cast<int>(this->View->height()));
+
+  std::deque<Event> localQueue;
+  {
+    std::lock_guard lock(this->Mutex);
+    localQueue.swap(this->Events);
+  }
+
+  for (const Event& event : localQueue)
+  {
+    this->HandleEvent(event);
+  }
+
+  double dt = 1.0 / 30.0;
+
+  if (this->FirstFrame)
+  {
+    this->FrameTimer.restart();
+    this->FirstFrame = false;
+  }
+  else
+  {
+    qint64 nSec = this->FrameTimer.nsecsElapsed();
+    this->FrameTimer.restart();
+    dt = static_cast<double>(nSec) / 1e9;
+  }
+  this->Interactor->triggerEventLoop(dt);
+
+  window.render();
+
+  this->update();
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DRenderer.h f3d-3.4.1/examples/libf3d/cpp/qml/F3DRenderer.h
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DRenderer.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/F3DRenderer.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,77 @@
+#ifndef F3DRENDERER_H
+#define F3DRENDERER_H
+
+#include <QElapsedTimer>
+#include <QString>
+#include <QtQuick/QQuickFramebufferObject>
+
+#include <deque>
+#include <memory>
+#include <mutex>
+
+namespace f3d
+{
+class engine;
+class interactor;
+}
+
+class F3DView;
+
+class F3DRenderer : public QQuickFramebufferObject::Renderer
+{
+public:
+  explicit F3DRenderer(F3DView* item);
+  ~F3DRenderer() override;
+
+  void QueueMousePress(float x, float y, int button, int modifiers);
+  void QueueMouseMove(float x, float y, int buttons, int modifiers);
+  void QueueMouseRelease(float x, float y, int button, int modifiers);
+  void QueueWheel(int dx, int dy, int modifiers);
+  void QueueKeyPress(int key, const QString& text, int modifiers);
+  void QueueKeyRelease(int key, int modifiers);
+
+  void render() override;
+
+private:
+  enum class EventType
+  {
+    MousePress,
+    MouseMove,
+    MouseRelease,
+    Wheel,
+    KeyPress,
+    KeyRelease
+  };
+
+  struct Event
+  {
+    EventType type;
+    double x = 0.0;
+    double y = 0.0;
+    int button = 0;
+    int buttons = 0;
+    int modifiers = 0;
+    int dx = 0;
+    int dy = 0;
+    int key = 0;
+    QString text;
+  };
+
+  void Enqueue(const Event& ev);
+
+  void UpdateModifiers(int mods);
+  int MapMouseButton(int button) const;
+  std::string KeySymFromKeyAndText(int key, const QString& text) const;
+  void HandleEvent(const Event& ev);
+
+  F3DView* View = nullptr;
+  std::unique_ptr<f3d::engine> Engine;
+  f3d::interactor* Interactor = nullptr;
+
+  std::deque<Event> Events;
+  std::mutex Mutex;
+  QElapsedTimer FrameTimer;
+  bool FirstFrame = true;
+};
+
+#endif // F3DRENDERER_H
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DView.cxx f3d-3.4.1/examples/libf3d/cpp/qml/F3DView.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DView.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/F3DView.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,101 @@
+#include "F3DView.h"
+
+#include "F3DRenderer.h"
+
+#include <QtCore/QSysInfo>
+#include <QtGui/QWindow>
+
+#include <f3d/interactor.h>
+#include <f3d/scene.h>
+
+//----------------------------------------------------------------------------
+F3DView::F3DView(QQuickItem* parent)
+  : QQuickFramebufferObject(parent)
+{
+}
+
+//----------------------------------------------------------------------------
+QString F3DView::GetModelPath() const
+{
+  return this->modelPath;
+}
+
+//----------------------------------------------------------------------------
+void F3DView::SetModelPath(const QString& path)
+{
+  if (this->modelPath == path)
+  {
+    return;
+  }
+
+  this->modelPath = path;
+  emit this->ModelPathChanged();
+}
+
+//----------------------------------------------------------------------------
+QQuickFramebufferObject::Renderer* F3DView::createRenderer() const
+{
+  auto* renderer = new F3DRenderer(const_cast<F3DView*>(this));
+  this->Renderer = renderer;
+  return renderer;
+}
+
+//----------------------------------------------------------------------------
+void F3DView::releaseResources()
+{
+  this->Renderer = nullptr;
+}
+
+//----------------------------------------------------------------------------
+void F3DView::MousePress(float x, float y, int button, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueMousePress(x, y, button, modifiers);
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DView::MouseMove(float x, float y, int buttons, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueMouseMove(x, y, buttons, modifiers);
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DView::MouseRelease(float x, float y, int button, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueMouseRelease(x, y, button, modifiers);
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DView::MouseWheel(int dx, int dy, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueWheel(dx, dy, modifiers);
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DView::KeyPress(int key, const QString& text, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueKeyPress(key, text, modifiers);
+  }
+}
+
+//----------------------------------------------------------------------------
+void F3DView::KeyRelease(int key, int modifiers)
+{
+  if (this->Renderer != nullptr)
+  {
+    this->Renderer->QueueKeyRelease(key, modifiers);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DView.h f3d-3.4.1/examples/libf3d/cpp/qml/F3DView.h
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/F3DView.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/F3DView.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,40 @@
+#ifndef F3DVIEW_H
+#define F3DVIEW_H
+
+#include <QtQuick/QQuickFramebufferObject>
+
+class F3DRenderer;
+
+class F3DView : public QQuickFramebufferObject
+{
+  Q_OBJECT
+  Q_PROPERTY(QString modelPath READ GetModelPath WRITE SetModelPath NOTIFY ModelPathChanged)
+
+public:
+  explicit F3DView(QQuickItem* parent = nullptr);
+
+  QString GetModelPath() const;
+  void SetModelPath(const QString& path);
+
+  Renderer* createRenderer() const override;
+  void releaseResources() override;
+
+public slots:
+  void MousePress(float x, float y, int button, int modifiers);
+  void MouseMove(float x, float y, int buttons, int modifiers);
+  void MouseRelease(float x, float y, int button, int modifiers);
+
+  void MouseWheel(int dx, int dy, int modifiers);
+
+  void KeyPress(int key, const QString& text, int modifiers);
+  void KeyRelease(int key, int modifiers);
+
+signals:
+  void ModelPathChanged();
+
+private:
+  QString modelPath;
+  mutable F3DRenderer* Renderer = nullptr;
+};
+
+#endif // F3DVIEW_H
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/main.cxx f3d-3.4.1/examples/libf3d/cpp/qml/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,78 @@
+#include <QCommandLineParser>
+#include <QDebug>
+#include <QFileInfo>
+#include <QGuiApplication>
+#include <QTimer>
+#include <QtQml/QQmlApplicationEngine>
+#include <QtQml/QQmlContext>
+
+#include "F3DView.h"
+
+int main(int argc, char* argv[])
+{
+  QGuiApplication app(argc, argv);
+  QCoreApplication::setApplicationName("libf3d + QML example");
+  QCoreApplication::setApplicationVersion("1.0");
+
+  QCommandLineParser parser;
+  parser.setApplicationDescription("libf3d + QML example");
+  parser.addHelpOption();
+  parser.addVersionOption();
+
+  QCommandLineOption timeoutOpt(
+    "timeout", "Optional timeout (in seconds) before closing the viewer.", "seconds");
+  parser.addOption(timeoutOpt);
+
+  parser.addPositionalArgument("file", "3D model file to open");
+
+  parser.process(app);
+
+  const QStringList pos = parser.positionalArguments();
+  if (pos.isEmpty())
+  {
+    qWarning("No model file provided");
+    return 1;
+  }
+
+  QString filePath = pos.first();
+  QFileInfo fi(filePath);
+  if (!fi.exists())
+  {
+    qWarning() << "File not found:" << fi.absoluteFilePath();
+    return 1;
+  }
+
+  bool okTimeout = false;
+  int timeout = parser.value(timeoutOpt).toInt(&okTimeout);
+
+  qmlRegisterType<F3DView>("F3D", 1, 0, "F3DView");
+
+  QQmlApplicationEngine engine;
+  engine.rootContext()->setContextProperty("initialModelPath", fi.absoluteFilePath());
+
+  const QUrl url = QUrl::fromLocalFile("Main.qml");
+  QObject::connect(
+    &engine, &QQmlApplicationEngine::objectCreated, &app,
+    [url](QObject* obj, const QUrl& objUrl)
+    {
+      if (!obj && url == objUrl)
+      {
+        QCoreApplication::exit(-1);
+      }
+    },
+    Qt::QueuedConnection);
+
+  engine.load(url);
+
+  if (engine.rootObjects().isEmpty())
+  {
+    return 1;
+  }
+
+  if (okTimeout && timeout > 0)
+  {
+    QTimer::singleShot(timeout * 1000, &app, &QCoreApplication::quit);
+  }
+
+  return app.exec();
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/Main.qml f3d-3.4.1/examples/libf3d/cpp/qml/Main.qml
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qml/Main.qml	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qml/Main.qml	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,77 @@
+import QtQuick
+import QtQuick.Window
+import F3D 1.0
+
+Window {
+    id: root
+    width: 800
+    height: 600
+    visible: true
+    title: "libf3d + QML (C++)"
+    color: "#202020"
+
+    property string modelPath: ""
+    Component.onCompleted: {
+        if (typeof initialModelPath === "string") {
+            modelPath = initialModelPath
+        }
+    }
+
+    F3DView {
+        id: view
+        anchors.fill: parent
+        focus: true
+
+        modelPath: root.modelPath
+
+        transform: Scale {
+            yScale: -1
+            origin.y: view.height / 2
+        }
+
+        Keys.enabled: true
+        Keys.onPressed: function (event) {
+            view.KeyPress(event.key, event.text, event.modifiers)
+            event.accepted = true
+        }
+
+        Keys.onReleased: function (event) {
+            view.KeyRelease(event.key, event.modifiers)
+            event.accepted = true
+        }
+
+        MouseArea {
+            anchors.fill: parent
+            hoverEnabled: true
+            acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+
+            onPressed: function (mouse) {
+                view.MousePress(mouse.x, mouse.y,
+                    mouse.button, mouse.modifiers)
+                mouse.accepted = true
+            }
+
+            onReleased: function (mouse) {
+                view.MouseRelease(mouse.x, mouse.y,
+                    mouse.button, mouse.modifiers)
+                mouse.accepted = true
+            }
+
+            onPositionChanged: function (mouse) {
+                view.MouseMove(mouse.x, mouse.y,
+                    mouse.buttons, mouse.modifiers)
+            }
+        }
+
+        WheelHandler {
+            id: wheelHandler
+            target: view
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+
+            onWheel: function (event) {
+                view.MouseWheel(event.angleDelta.x, event.angleDelta.y, event.modifiers)
+                event.accepted = true
+            }
+        }
+    }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qt6/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/qt6/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qt6/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qt6/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(minimal_qt6)
+
+find_package(f3d REQUIRED COMPONENTS library)
+find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGLWidgets)
+
+# This example only works if libf3d was built against a VTK >= 9.3.20240914
+
+add_executable(minimal_qt6 main.cxx)
+target_link_libraries(minimal_qt6 f3d::libf3d Qt6::Widgets Qt6::OpenGLWidgets)
+set_target_properties(minimal_qt6 PROPERTIES CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/qt6/main.cxx f3d-3.4.1/examples/libf3d/cpp/qt6/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/qt6/main.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/qt6/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,429 @@
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QGuiApplication>
+#include <QMainWindow>
+#include <QOpenGLContext>
+#include <QOpenGLWidget>
+#include <QTimer>
+#include <QWheelEvent>
+
+#include <f3d/engine.h>
+#include <f3d/interactor.h>
+#include <f3d/options.h>
+#include <f3d/scene.h>
+#include <f3d/window.h>
+
+#include <algorithm>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <utility>
+
+class F3DWidget : public QOpenGLWidget
+{
+public:
+  explicit F3DWidget(QString filePath, QWidget* parent = nullptr)
+    : QOpenGLWidget(parent)
+    , FilePath(std::move(filePath))
+  {
+    // Allow this widget to receive key events
+    this->setFocusPolicy(Qt::StrongFocus);
+  }
+
+protected:
+  // ------------------------------------------------------------------
+  // OpenGL / F3D setup
+  // ------------------------------------------------------------------
+
+  void initializeGL() override
+  {
+    f3d::engine::autoloadPlugins();
+
+    f3d::context::function loadFunc = [this](const char* name)
+    { return this->context()->getProcAddress(name); };
+    this->Engine = std::make_unique<f3d::engine>(f3d::engine::createExternal(loadFunc));
+
+    if (!this->Engine)
+    {
+      std::cerr << "Failed to create f3d::engine" << '\n';
+      return;
+    }
+
+    this->Interactor = &this->Engine->getInteractor();
+
+    this->Timer = new QTimer(this);
+    this->Timer->setInterval(30);
+    connect(this->Timer, &QTimer::timeout, this, &F3DWidget::onTick);
+    this->Timer->start();
+
+    this->Engine->getScene().add(this->FilePath.toStdString());
+
+    auto& win = this->Engine->getWindow();
+    win.setSize(this->width(), this->height());
+
+    auto& opt = this->Engine->getOptions();
+    opt.set("render.grid.enable", true);
+    opt.set("render.show_edges", false);
+    opt.set("ui.axis", true);
+    opt.set("ui.fps", true);
+    opt.set("ui.filename", true);
+  }
+
+  void resizeGL(int width, int height) override
+  {
+    if (!this->Engine)
+    {
+      return;
+    }
+
+    this->Engine->getWindow().setSize(std::max(1, width), std::max(1, height));
+  }
+
+  void paintGL() override
+  {
+    if (!this->Engine)
+    {
+      return;
+    }
+
+    this->Engine->getWindow().render();
+  }
+
+  // ------------------------------------------------------------------
+  // Forwarding helpers
+  // ------------------------------------------------------------------
+
+  void UpdateModifiers(Qt::KeyboardModifiers mods)
+  {
+    if (!this->Interactor)
+    {
+      return;
+    }
+
+    using InputModifier = f3d::interactor::InputModifier;
+
+    if ((mods & Qt::ControlModifier) && (mods & Qt::ShiftModifier))
+    {
+      this->Interactor->triggerModUpdate(InputModifier::CTRL_SHIFT);
+    }
+    else if (mods & Qt::ControlModifier)
+    {
+      this->Interactor->triggerModUpdate(InputModifier::CTRL);
+    }
+    else if (mods & Qt::ShiftModifier)
+    {
+      this->Interactor->triggerModUpdate(InputModifier::SHIFT);
+    }
+    else
+    {
+      this->Interactor->triggerModUpdate(InputModifier::NONE);
+    }
+  }
+
+  std::optional<f3d::interactor::MouseButton> MapMouseButton(Qt::MouseButton button)
+  {
+    if (!this->Interactor)
+    {
+      return std::nullopt;
+    }
+
+    using MouseButton = f3d::interactor::MouseButton;
+
+    switch (button)
+    {
+      case Qt::LeftButton:
+        return MouseButton::LEFT;
+      case Qt::RightButton:
+        return MouseButton::RIGHT;
+      case Qt::MiddleButton:
+        return MouseButton::MIDDLE;
+      default:
+        return std::nullopt;
+    }
+  }
+
+  std::string KeySymFromEvent(QKeyEvent* event)
+  {
+    int key = event->key();
+
+    // A–Z -> "A"..."Z"
+    if (key >= Qt::Key_A && key <= Qt::Key_Z)
+    {
+      char c = static_cast<char>(key);
+      if (c >= 'a' && c <= 'z')
+      {
+        c = static_cast<char>(c - 'a' + 'A');
+      }
+      return std::string{ c };
+    }
+
+    // Special keys
+    switch (key)
+    {
+      case Qt::Key_Backspace:
+        return "BackSpace";
+      case Qt::Key_Escape:
+        return "Escape";
+      case Qt::Key_Return:
+      case Qt::Key_Enter:
+        return "Return";
+      case Qt::Key_Space:
+        return "Space";
+      case Qt::Key_Tab:
+        return "Tab";
+      default:
+        break;
+    }
+
+    const QString text = event->text();
+    if (!text.isEmpty())
+    {
+      return text.toStdString();
+    }
+
+    return {};
+  }
+
+  // ------------------------------------------------------------------
+  // Mouse interaction forwarding
+  // ------------------------------------------------------------------
+
+  void mousePressEvent(QMouseEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::mousePressEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+    auto button = this->MapMouseButton(event->button());
+
+    if (button.has_value())
+    {
+      const QPointF pos = event->position();
+      this->Interactor->triggerMousePosition(pos.x(), pos.y());
+      this->Interactor->triggerMouseButton(f3d::interactor::InputAction::PRESS, button.value());
+      this->update();
+    }
+
+    this->doneCurrent();
+  }
+
+  void mouseMoveEvent(QMouseEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::mouseMoveEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+    const QPointF pos = event->position();
+    this->Interactor->triggerMousePosition(pos.x(), pos.y());
+    this->update();
+
+    this->doneCurrent();
+  }
+
+  void mouseReleaseEvent(QMouseEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::mouseReleaseEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+    auto button = this->MapMouseButton(event->button());
+    if (button.has_value())
+    {
+      this->Interactor->triggerMouseButton(f3d::interactor::InputAction::RELEASE, button.value());
+      const QPointF pos = event->position();
+      this->Interactor->triggerMousePosition(pos.x(), pos.y());
+      this->update();
+    }
+
+    this->doneCurrent();
+  }
+
+  void wheelEvent(QWheelEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::wheelEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+
+    using WheelDirection = f3d::interactor::WheelDirection;
+    const QPoint delta = event->angleDelta();
+    WheelDirection direction;
+
+    if (std::abs(delta.y()) >= std::abs(delta.x()))
+    {
+      direction = (delta.y() > 0) ? WheelDirection::FORWARD : WheelDirection::BACKWARD;
+    }
+    else
+    {
+      direction = (delta.x() > 0) ? WheelDirection::RIGHT : WheelDirection::LEFT;
+    }
+
+    this->Interactor->triggerMouseWheel(direction);
+    this->update();
+
+    this->doneCurrent();
+  }
+
+  // ------------------------------------------------------------------
+  // Keyboard interaction forwarding
+  // ------------------------------------------------------------------
+
+  void keyPressEvent(QKeyEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::keyPressEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+    const std::string keySym = this->KeySymFromEvent(event);
+
+    if (!keySym.empty())
+    {
+      this->Interactor->triggerKeyboardKey(
+        f3d::interactor::InputAction::PRESS, std::string_view(keySym));
+    }
+
+    const QString text = event->text();
+    if (!text.isEmpty())
+    {
+      const std::u16string u16 = text.toStdU16String();
+      for (char16_t ch : u16)
+      {
+        this->Interactor->triggerTextCharacter(ch);
+      }
+    }
+
+    this->update();
+    this->doneCurrent();
+  }
+
+  void keyReleaseEvent(QKeyEvent* event) override
+  {
+    if (!this->Interactor)
+    {
+      QOpenGLWidget::keyReleaseEvent(event);
+      return;
+    }
+
+    this->makeCurrent();
+
+    this->UpdateModifiers(event->modifiers());
+    const std::string keySym = this->KeySymFromEvent(event);
+
+    if (!keySym.empty())
+    {
+      this->Interactor->triggerKeyboardKey(
+        f3d::interactor::InputAction::RELEASE, std::string_view(keySym));
+    }
+
+    this->update();
+    this->doneCurrent();
+  }
+
+private slots:
+  void onTick()
+  {
+    if (!this->Engine || !this->Interactor)
+    {
+      return;
+    }
+
+    this->makeCurrent();
+    this->Interactor->triggerEventLoop(1.0 / 30.0);
+    this->update();
+    this->doneCurrent();
+  }
+
+private:
+  QString FilePath;
+  std::unique_ptr<f3d::engine> Engine;
+  f3d::interactor* Interactor = nullptr;
+  QTimer* Timer = nullptr;
+};
+
+class MainWindow : public QMainWindow
+{
+public:
+  explicit MainWindow(const QString& filePath, QWidget* parent = nullptr)
+    : QMainWindow(parent)
+  {
+    this->setWindowTitle(QStringLiteral("libf3d + Qt6 example"));
+    auto viewer = std::make_unique<F3DWidget>(filePath, this);
+    this->setCentralWidget(viewer.get());
+    viewer.release();
+    this->resize(800, 600);
+  }
+};
+
+int main(int argc, char** argv)
+{
+  QApplication app(argc, argv);
+
+  QCommandLineParser parser;
+  parser.setApplicationDescription("libf3d + Qt6 example");
+  parser.addHelpOption();
+
+  QCommandLineOption timeoutOpt(
+    "timeout", "Optional timeout (in seconds) before closing the viewer.", "seconds");
+  parser.addOption(timeoutOpt);
+
+  parser.addPositionalArgument("file", "Model file to open.");
+
+  parser.process(app);
+
+  const QStringList positional = parser.positionalArguments();
+  if (positional.isEmpty())
+  {
+    std::cerr << "Usage: " << argv[0] << " <file> [--timeout N]" << '\n';
+    return 1;
+  }
+
+  const QString& filePath = positional.first();
+
+  if (!std::filesystem::exists(filePath.toStdString()))
+  {
+    std::cerr << "File not found: " << filePath.toStdString() << '\n';
+    return 1;
+  }
+
+  MainWindow win(filePath);
+  win.show();
+
+  // For testing purposes only, shutdown the example after `timeout` seconds
+  if (parser.isSet(timeoutOpt))
+  {
+    bool ok = false;
+    int timeoutSec = parser.value(timeoutOpt).toInt(&ok);
+    if (ok && timeoutSec > 0)
+    {
+      QTimer::singleShot(timeoutSec * 1000, &app, &QCoreApplication::quit);
+    }
+  }
+
+  return app.exec();
+}
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/baselines/cow.png and /tmp/gJkK3JzSDT/f3d-3.4.1/examples/libf3d/cpp/render-image/baselines/cow.png differ
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/check.cxx f3d-3.4.1/examples/libf3d/cpp/render-image/check.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/check.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-image/check.cxx	1970-01-01 02:00:00.000000000 +0200
@@ -1,17 +0,0 @@
-#include <f3d/image.h>
-
-int main(int argc, char** argv)
-{
-  // Check inputs
-  if (argc != 3)
-  {
-    return EXIT_FAILURE;
-  }
-
-  // Open images
-  f3d::image img0(argv[1]);
-  f3d::image img1(argv[2]);
-
-  // Compare them
-  return img0.compare(img1) <= 0.05 ? EXIT_SUCCESS : EXIT_FAILURE;
-}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/render-image/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-image/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,15 +7,3 @@
 add_executable(render-image main.cxx)
 target_link_libraries(render-image f3d::libf3d)
 set_target_properties(render-image PROPERTIES CXX_STANDARD 17)
-
-add_executable(check-image check.cxx)
-target_link_libraries(check-image f3d::libf3d)
-set_target_properties(check-image PROPERTIES CXX_STANDARD 17)
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_render-image COMMAND "$<TARGET_FILE:render-image>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" "${CMAKE_CURRENT_BINARY_DIR}/cow.png")
-  add_test(NAME test_check-image COMMAND "$<TARGET_FILE:check-image>" "${CMAKE_CURRENT_BINARY_DIR}/cow.png" "${CMAKE_CURRENT_SOURCE_DIR}/baselines/cow.png")
-  set_tests_properties(test_check-image PROPERTIES DEPENDS "test_render-image")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/render-image/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-image/data/cow.vtp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-image/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
@@ -1,33 +0,0 @@
-<?xml version="1.0"?>
-<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor">
-  <PolyData>
-    <Piece NumberOfPoints="2903"                 NumberOfVerts="0"                    NumberOfLines="0"                    NumberOfStrips="0"                    NumberOfPolys="3263"                >
-      <PointData>
-      </PointData>
-      <CellData>
-      </CellData>
-      <Points>
-        <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="1.3772239036"         RangeMax="6.1258021248"         offset="0"                   />
-      </Points>
-      <Verts>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29520"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29536"               />
-      </Verts>
-      <Lines>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29552"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29568"               />
-      </Lines>
-      <Strips>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29584"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29600"               />
-      </Strips>
-      <Polys>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29616"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="52104"               />
-      </Polys>
-    </Piece>
-  </PolyData>
-  <AppendedData encoding="base64">
-   _AgAAAACAAAAUCAAAC08AAFoHAAA=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efyQx0ceH3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8ZHHD3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMev5LHD3n8kMcPefyQxw95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMeH3l85PGRxw95/JDHD3n8kMcPefyQxw95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PGRx0ceP+TxQx4/5PFDHj/k8UMeP+TxQx4feXzk8ZHHD3l85PFDHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHRx4feXzk8ZHHRx4fefyQxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQxw95/JDHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8UMeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHD3n8kMcPeXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHr+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3n8Sh4/5PFDHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/EoeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcEPGfyQwUcGHxl8ZPCRwUcGHxl8ZPCRwUcGHxl8ZPCRuw+5+5C7D7n7kLsPufuQu0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuPuTuQ+4euXvk7pG7R+4+5O5D7j7k7kPuHrl75O6Ru0fuHrn7kLsPuXvk7pG7D7n7kLsPuXvk7pG7R+4euXvk7pG7D7n7kLsPufuQuw+5+5C7D7n7kLtH7j7k7pG7D7l75O5D7h65+5C7R+4+5O5D7j7k7kPuPuTuQ+4+5O5D7j7k7kPuPuTukbtH7h65e+TukbtH7h65e+TukbtH7h65+5C7D7l75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O5D7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+Tu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AQAAAACAAAD8MgAAGBIAAA==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
-  </AppendedData>
-</VTKFile>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/render-interact/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-interact/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,11 +7,3 @@
 add_executable(render-interact main.cxx)
 target_link_libraries(render-interact f3d::libf3d)
 set_target_properties(render-interact PROPERTIES CXX_STANDARD 17)
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_render-interact COMMAND "$<TARGET_FILE:render-interact>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp")
-  set_tests_properties(test_render-interact PROPERTIES
-    PASS_REGULAR_EXPRESSION "Number of points: 2903")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/render-interact/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/data/cow.vtp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-interact/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
@@ -1,33 +0,0 @@
-<?xml version="1.0"?>
-<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor">
-  <PolyData>
-    <Piece NumberOfPoints="2903"                 NumberOfVerts="0"                    NumberOfLines="0"                    NumberOfStrips="0"                    NumberOfPolys="3263"                >
-      <PointData>
-      </PointData>
-      <CellData>
-      </CellData>
-      <Points>
-        <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="1.3772239036"         RangeMax="6.1258021248"         offset="0"                   />
-      </Points>
-      <Verts>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29520"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29536"               />
-      </Verts>
-      <Lines>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29552"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29568"               />
-      </Lines>
-      <Strips>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29584"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29600"               />
-      </Strips>
-      <Polys>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29616"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="52104"               />
-      </Polys>
-    </Piece>
-  </PolyData>
-  <AppendedData encoding="base64">
-   _AgAAAACAAAAUCAAAC08AAFoHAAA=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efyQx0ceH3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8ZHHD3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMev5LHD3n8kMcPefyQxw95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMeH3l85PGRxw95/JDHD3n8kMcPefyQxw95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PGRx0ceP+TxQx4/5PFDHj/k8UMeP+TxQx4feXzk8ZHHD3l85PFDHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHRx4feXzk8ZHHRx4fefyQxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQxw95/JDHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8UMeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHD3n8kMcPeXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHr+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3n8Sh4/5PFDHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/EoeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcEPGfyQwUcGHxl8ZPCRwUcGHxl8ZPCRwUcGHxl8ZPCRuw+5+5C7D7n7kLsPufuQu0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuPuTuQ+4euXvk7pG7R+4+5O5D7j7k7kPuHrl75O6Ru0fuHrn7kLsPuXvk7pG7D7n7kLsPuXvk7pG7R+4euXvk7pG7D7n7kLsPufuQuw+5+5C7D7n7kLtH7j7k7pG7D7l75O5D7h65+5C7R+4+5O5D7j7k7kPuPuTuQ+4+5O5D7j7k7kPuPuTukbtH7h65e+TukbtH7h65e+TukbtH7h65+5C7D7l75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O5D7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+Tu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AQAAAACAAAD8MgAAGBIAAA==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
-  </AppendedData>
-</VTKFile>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/main.cxx f3d-3.4.1/examples/libf3d/cpp/render-interact/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/render-interact/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/render-interact/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,12 +1,11 @@
 #include <f3d/engine.h>
-#include <f3d/image.h>
 #include <f3d/log.h>
 #include <f3d/window.h>
 
 int main(int argc, char** argv)
 {
   // Check inputs
-  if (argc != 2)
+  if (argc < 2)
   {
     return EXIT_FAILURE;
   }
@@ -29,10 +28,25 @@
 
   // Start interaction and stop it after one second
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
 
-  // Actual call would look like this
-  // inter.start();
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << '\n';
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
 
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/baselines/cow.png f3d-3.4.1/examples/libf3d/cpp/tests/baselines/cow.png
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/baselines/cow.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/baselines/cow.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c9590019f4b9aaac43246b920d916bb765268c83b4e40c0a9728e92af87ab99
+size 18647
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/baselines/thumbnail.png f3d-3.4.1/examples/libf3d/cpp/tests/baselines/thumbnail.png
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/baselines/thumbnail.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/baselines/thumbnail.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:46b4551a9c33f4d5a8a5232339cae75ba12e994ef5b64bb9b5771f607d627ca0
+size 24905
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/check.cxx f3d-3.4.1/examples/libf3d/cpp/tests/check.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/check.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/check.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,17 @@
+#include <f3d/image.h>
+
+int main(int argc, char** argv)
+{
+  // Check inputs
+  if (argc != 3)
+  {
+    return EXIT_FAILURE;
+  }
+
+  // Open images
+  f3d::image img0(argv[1]);
+  f3d::image img1(argv[2]);
+
+  // Compare them
+  return img0.compare(img1) <= 0.05 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/tests/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,67 @@
+cmake_minimum_required(VERSION 3.10)
+
+# for image comparisons
+add_executable(check-image check.cxx)
+target_link_libraries(check-image f3d::libf3d)
+set_target_properties(check-image PROPERTIES CXX_STANDARD 17)
+
+# check-engine
+add_test(NAME test_check-engine COMMAND "$<TARGET_FILE:check-engine>")
+
+# interactive-app
+add_test(NAME test_interactive-app COMMAND "$<TARGET_FILE:interactive-app>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+
+# multi-files
+add_test(NAME test_multi-files COMMAND "$<TARGET_FILE:multi-files>" "${CMAKE_CURRENT_SOURCE_DIR}/data/mb" 1)
+set_tests_properties(test_multi-files PROPERTIES
+        PASS_REGULAR_EXPRESSION "Scene bounding box: -0.487464 ≤ x ≤ 1, -0.487464 ≤ y ≤ 1, -0.5 ≤ z ≤ 1")
+
+# offscreen-thumbnail
+add_test(NAME test_offscreen-thumbnail COMMAND "$<TARGET_FILE:offscreen-thumbnail>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" "${CMAKE_CURRENT_BINARY_DIR}/thumbnail.png" 256 256)
+add_test(NAME test_check-offscreen-thumbnail COMMAND "$<TARGET_FILE:check-image>" "${CMAKE_CURRENT_BINARY_DIR}/thumbnail.png" "${CMAKE_CURRENT_SOURCE_DIR}/baselines/thumbnail.png")
+set_tests_properties(test_check-offscreen-thumbnail PROPERTIES DEPENDS "test_offscreen-thumbnail")
+
+# render-image
+add_test(NAME test_render-image COMMAND "$<TARGET_FILE:render-image>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" "${CMAKE_CURRENT_BINARY_DIR}/cow.png")
+add_test(NAME test_check-image COMMAND "$<TARGET_FILE:check-image>" "${CMAKE_CURRENT_BINARY_DIR}/cow.png" "${CMAKE_CURRENT_SOURCE_DIR}/baselines/cow.png")
+set_tests_properties(test_check-image PROPERTIES DEPENDS "test_render-image")
+
+# render-interact
+add_test(NAME test_render-interact COMMAND "$<TARGET_FILE:render-interact>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+set_tests_properties(test_render-interact PROPERTIES
+        PASS_REGULAR_EXPRESSION "Number of points: 2903")
+
+# use-options-string
+add_test(NAME test_use-options-string COMMAND "$<TARGET_FILE:use-options-string>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+set_tests_properties(test_use-options-string PROPERTIES
+        PASS_REGULAR_EXPRESSION "Number of points: 2903")
+
+# use-options-struct
+add_test(NAME test_use-options-struct COMMAND "$<TARGET_FILE:use-options-struct>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+set_tests_properties(test_use-options-struct PROPERTIES
+        PASS_REGULAR_EXPRESSION "Number of points: 2903")
+
+# use-options-variant
+add_test(NAME test_use-options-variant COMMAND "$<TARGET_FILE:use-options-variant>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+set_tests_properties(test_use-options-variant PROPERTIES
+        PASS_REGULAR_EXPRESSION "Number of points: 2903")
+
+# glfw -- only works if libf3d was built against a VTK >= 9.3.20240914
+if (F3D_EXAMPLES_EXTERNAL_GLFW)
+  add_test(NAME test_external-glfw COMMAND "$<TARGET_FILE:external-glfw>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+endif ()
+
+# qt6 & qml -- only works if libf3d was built against a VTK >= 9.3.20240914
+if (F3D_EXAMPLES_EXTERNAL_QT)
+  add_test(NAME test_minimal_qt6 COMMAND "$<TARGET_FILE:minimal_qt6>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" "--timeout" 1)
+  add_test(
+    NAME test_minimal_qml
+    COMMAND "$<TARGET_FILE:minimal_qml>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" "--timeout" 1
+    WORKING_DIRECTORY "$<TARGET_FILE_DIR:minimal_qml>"
+  )
+endif ()
+
+# fltk -- only works if libf3d was built against a VTK >= 9.3.20240914
+if (F3D_EXAMPLES_EXTERNAL_FLTK)
+  add_test(NAME test_minimal_fltk COMMAND "$<TARGET_FILE:minimal_fltk>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp" 1)
+endif ()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/tests/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/data/cow.vtp	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5aa4901d186902f90e17bf3b5917541cb6cb8cf223bfeea736631df4c047652
+size 60449
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_0_0.vtu f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_0_0.vtu
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_0_0.vtu	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_0_0.vtu	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b4cd1dc8f61b57a4bf6da245ce5697b394c2243d2b50e6009248118c156152a
+size 2305
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_1_0.vtp f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_1_0.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_1_0.vtp	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_1_0.vtp	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da39f580f748e2fbd03ad270b8e3fee354f7165f3652a002610c80172616e02c
+size 7084
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_2_0.vtp f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_2_0.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/tests/data/mb/mb_2_0.vtp	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/cpp/tests/data/mb/mb_2_0.vtp	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a35af390f451b6729124015a8b79e6a56bfc1104c98bc5b8e0e01d8776f9f402
+size 6860
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/use-options-string/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-string/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,11 +7,3 @@
 add_executable(use-options-string main.cxx)
 target_link_libraries(use-options-string f3d::libf3d)
 set_target_properties(use-options-string PROPERTIES CXX_STANDARD 17)
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_use-options-string COMMAND "$<TARGET_FILE:use-options-string>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp")
-  set_tests_properties(test_use-options-string PROPERTIES
-    PASS_REGULAR_EXPRESSION "Number of points: 2903")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/use-options-string/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/data/cow.vtp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-string/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
@@ -1,33 +0,0 @@
-<?xml version="1.0"?>
-<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor">
-  <PolyData>
-    <Piece NumberOfPoints="2903"                 NumberOfVerts="0"                    NumberOfLines="0"                    NumberOfStrips="0"                    NumberOfPolys="3263"                >
-      <PointData>
-      </PointData>
-      <CellData>
-      </CellData>
-      <Points>
-        <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="1.3772239036"         RangeMax="6.1258021248"         offset="0"                   />
-      </Points>
-      <Verts>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29520"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29536"               />
-      </Verts>
-      <Lines>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29552"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29568"               />
-      </Lines>
-      <Strips>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29584"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29600"               />
-      </Strips>
-      <Polys>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29616"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="52104"               />
-      </Polys>
-    </Piece>
-  </PolyData>
-  <AppendedData encoding="base64">
-   _AgAAAACAAAAUCAAAC08AAFoHAAA=eJwsm3k81c/3xy1ZIyRLShEl2YvKduckUpGy0yaUVKjQQir7mp1IWVJaEaKQO2daqKSISqsiRcnSvql+9+vzu389H+cx73nPnDlzznndx72PH/uD5C854ON92iWioWeNOvkfk2nrwPKl+Jj9yd590HernfM/Dn+4EvZXjhuz/+0IgKUvOGN2B2kCfTf/jD0rN8UD3OVk6P/41QpdmC35+T97sDN882Jj9q9X5OCmV9+Y3S3VGgIMDfF/HDBPFAze947ZIy4KwK2Q12NcHVRPLuT8Z7dQ40Bi6a6x8b3HTSEhN22M364xBre+M2Psn24IkWaXx7iHo8Nb8/UxbtKfAVtG741x/lJ5uPKpdYxfVInDFqeWMb7c9pVsL7o1xi8NLOHih2VjfPCZKRl80DW2hsAd/iQ97OaYvViihPR+axzjJxo7OCHBL8bGGBb3ckzUHo3ZWzdtpbtH/9/+aBrlCL0fswcqSuDon//mXNwkgr26Q2P2B/XrUOTIqzH7q4Zg3FX5bszO//Mwvq/4zw8LTldhZ8nzMfvQQClOtP3Pn905j3Bz57sxNi0VZL8vDIyx9rLZrCbu6xhv3WzB+CfzjZ1jg5o7Sw7tGbP/2r+B5Rc9GeMVcvfw2qRnY/Nf2fAWr/u8GmO9ycbs7K/XYzxVfA7L0h/8z29W8kwChsd42ych1rB3YIznu3sz054NY3Fi+MSL9eoMj7HH2k1smpIL9388s9WXaWy5NBYbnlO3MfnYhWPPuu3cxtoW+4+x/IktrME0Y4ydt3qyiMHy/951ZTUTXoZjfMLNni39c3eMZ0tZs6WJj8e4ZNlG9kd39ti+rF19WPcDmzF2HdzMpNduGWP9B5uZSUX4f/zZhxmPZI1xfYQPOx5ZMsYilW/Ir47//H8y4C4ps/kvBlYfXMl81/OP+fOwQT7c9BUHL9pQK5VzHCwVBeDRm6yFMcM58NJIFhSr0+qCyktgrct3or9pg/EnpVKIWNtDhOb3GV/KLIMau+fkn5CAyazOKrgddJuUP5hnUnCtBn5ZXyXp4+1MzOS4UH27kjwJjTV5dp+C9qbT5Mb7VJOOe5dhYzOXlMh5mPg01IPw48tkdHSPyYKHFKIwn8QMZZi8PseFf81p5FlChgmdxJsnLX7M/uftFdCgkWR0R5pJCquHR0qBZKdqqonpaC04aG0kM+8lmuzCGgh550KmTYkw4SRUQUuIC0nq3moS/OAUzHVZQyKnTjKZlncC5i1wJZFnfhsPuJ6DK9XryFNBHZMVIhcg7JILeaVvb1KyJx+Cvq4k5nWnjEN+ZsEVSQviHnN34Ve9Q/BtvSE5tkq0rkUzEb5XzIQptovrJVJToGW/KpRMnFYfKxsLlzpnwWcTr3qJt2mg8kEZajNF69uTI+FttjrE24bUr20Kh9O3VcF4fUT9fM5BsHGZDl3TouvbvAPhvftkOPwup74yPgAGG2Qh8NRkKl63HTSFZeBgwx6aG7sDbEwk4aNPCR2aFQwTvv4mb9p66JnAvXDzcTfZ2fOZ6tTsBaFtTSTRjB9x8UGInHaKTJ4ujoucI6BOPoak+v2lk/UiYWusLVm34C69tS4KrHS1yZUjR+jIiyhYdUiF7H9nTpud1vH8IwbB0M59GrQOSgqFYWPVQnq7fD3YLvhDQg8V045iLxCp+kBu3G+kunc2gT33CekeaaepZ7fAwzVXyHLJr/TS0+3Qcz6HiN4ch9blwYCeq8lC2w/01prdsHvyHHKf00Q3z94DP2OkyOq6Qlo8vBciXT9zYMV+qqK5H65vesEZdVxOA3/awf3bglDtksXdob4Kzl/gh9JGBbrzgj0opw0R0XOHqUueM3Q+f0n49cvpYXU3EDW/Q7Tm3aXutuvhx4tyMnyrn45v9wROVRxZkvuLTr3rDWZdhuR8VR91O7MZBCs+cnwzblFXsS3Qlt/GGRdYR4N0t4FdWBnHMD+fYrsfaMjHc1Yl7qbmWtth3zwTjri0EZ2aDRCzbpRYzZHgch0tIOfPFyL4XodaNSwGvj89JMUsl44kWcETofsk6GAZ3e5lDSeEkZCyazR153JY9yqPlN74QIPVbGGagy/5J8KHZd/sYeC6AGmr+Udv5TpBTlI85/buIfpG3BX8d2lygmw6KIfjBm8FcswFPtXQawproXr6We7enCga9HUDrC/8yXVWAGpyTwfkBN+T/itaNMVcD+J+dRIr6RxKTxpA0JcbhP9UNf2qawgnnSqI2NkOWl06Hw4LHyI3LP5QjyozkE9cSjqfCuLb/sVg5HmHs+rvRAyrWwpX3fQ4RfPFcX3xcliZUW6mPO09bWy0hWv+ylToxDV6z8kBpv0+Qi32JdPhN87w9/MNul3XjspNVYD3qj1EetdKemu8EkzNaycuj8opd4Eq/JlLSWjKY1oYqAbG3ieJ92xB1GyZDZlGkWRX7ySc16UH813mk7Zjylg+PA/ydUY4enazsTPaGDIXJnH2XJmFs16awKf2MnOLvero/9cMtK4KUD8ihCtVCZy71EG1jj+jl3wWw4RJUzHc+zRt714ChW/18X1IHJ0UIgqUviLfchdSh0vjIb22g8iPltM9y2Qhtr+KnLw5HrvXTYYK/kKyctxkdLg8A5b+PkhUxGehSJgWLPitR0L2LMSaefqwy/guZ0vafIzOmwupgRs4cq8WoAqP5ZKNzG3KFmB+vh58+D6O8nUvwC5lPdhsWUkfNs9GV3Ft2LtvHCos1cbQAE1ITJmK6721ULRfBWyn7id3y7TxwPlZ4JtlQjTFrPAhr384FHObY3TAHRXv64KjxQbOe85GfDaqC5yWaeZ1JetQ+Ls2tKgvp3PKbNBnrjb82NlAdzN7dMvQAJt0CTxjaIeLImfA2ZUmqDodULl/Gqht4u2rfR5uj1AHJyVz4pfthKl+GjDH9A0n+/MufNI6BzaoOnLePYrA0VRNeDyH3zxaOwpnvJoF3znOdLVPCC6RmAUnDz2mkob78GjTDLhmI4GaA9sxeLESfJxjjWccXLFfRxjCHz8iJx1P072zJGBIpIF8Ch2HBryc9+VnMVFp08SdJvLgtzue2HAsMCd1MkxbsJTUrdyK/VemQtNJEbLpZjR2X5wO2yzDOaL9aSgiqgIZoVJc/1vZaPh9KggOhVClG+moMmsq9G1vpiatafguXxH44xVwMDIGDWbJg0UrBy9cjEC7WikIs16HKwcOYPf6PhL+6BV5+tqI+pu9JXxBz8jt29E0zugdCVFtJUG196iJwBAJPVVHVjxRRZkzv0htaRq5WbgWiwdGyZ8vjuTCmkiUCueHqkgRcn5fBmaaCIGgcDpHaP5xlJcVg6R4L67swxIsrhWFaC2gHjkncOsPYdj35BLvvh5H+oIPKg5p4s3IXAyYd4V82PaSLLriTY2GuOT68idE6VkmXViMZLFxKxFZf5eaFTMy+TWXxArIo57BNZLYco7MvmyORhK3ybz5O4je/P14PKaZVG2yItPr43H3qTairdnJ0Zc+jPayHcT+8SbOXdWjGBX7gOx6W2D2N6wQ5wg+JD+MLKiOejHKv3hEoj/fp8uteGseuk+qL2jjkpxC9A9uJ+H5a3DoQh7mlE0ARbUDOKdnLYZdngiVBh74vdcTV8sowfLcEAxUssMoO1Vo9gtGS1tzdG2dDXqnLfHPJDUMfzgH/pEADCiewItDFbj+KQHP9Bri4yuKAM9S8HO/BdrHjAeFS2lo5eWAJ0rl4XxLLrobmWGksjgceZCHayqWosDmSUBPluCySG20nCQKsovPoU7WQvycLgMRYZfxLUcG16hNB5h7Aj/WqmAIKEOOw0XeXRBHhdlqoNp/Dkf/iWL7IQ0YP7sA7XWFsKBSBZQcs3Bmtg76v5kD/V3JeN1yPAaH6cDjcCfcbS+KfL3zYad5AF5afovqR+qD351DmB7+kc43nQ+XliWjz6+7dMuOeWDPijDO7xUt26oH9dvOY8S6QToaMAe6PWvxU+sX2v9LBXb2MWzf/p2+FZ4C5zZdxVrBP3TfLyn4Ko24il8UtRSFoCCnAXvKNDAz0Bwyp+zER7fTKdlpCvuuJmNSYyENm20MO8tOoZPAOQrXDOG7SjVa3qihshq6IJV/DY8l1NGUNzNg0cc76N/cSAW8JsOi4Tu45dxN6pEtBVpPm7GJPqc1coKw7UAzCthMQBfvhVC8f5RWFQrh3T1GQEv86JERWRRfbgIyO9VQpvE5PfXQEO7YyuC/3ZKYmEJgaI0RUt8L9N8HCyhKtsQBgVhq3WdGnrrcJRVmaTTosTHp1+eSzJ0faBm1IIWcPDLgMg9jQtYQEc0wso55oNKzbaQrcBFReHAQpYN3kNjka5zTzTl4LtKfSL+z5qx+kI/LFwWR0il65o3NRbivaCePW7lVWifwSOcu8rc2n/6eVogtoSHEZq8wyn9Px7tvAolBiBrW6ySjWvEOki+5CZUSInHF02CyRDgBbzdvwwYSQFouHcXFt53Qcr4f2dBbgk6BJnjy1BaSR+rxebgsnqrbQmLeIb6494gmNV4gI4nSuONIMop4FpHQm/74yCgcdwbnk3PDidjItwnjJY+R45+OokiVA97IyiWW9ueQog7u+ZxHrF/X49Pzsvgi9xhR072B50O66BaBTZzG1E7SdkSdnhAO4vguaiH3WzLpwGgmJykJiZb+c/ou/Bgnbu4pYmmkiqOrQjhHNNOJ9IA9Bn0/zlEXX0bSxZNwm4Q42WhewzHJLMIGIXHy974C54/8WayzH0dWlp9t+FJShq1PxEkWBRrke4GnHyTIcs1b9NaBcvzwXYJYSc5AH99z+EFRhPD1rsXusCI8HTKe6Iin47LNh3BFpwhJDj+DKuIB+H5AjExcUY1G9xdjyu1fnD0G17FHVRvXdX/llD9sw4d371KFh1502dNO4i02hbbVxtMdi9vIrbB4GvR5P612uUUKzW/SgGcLaI5jAzHcNB5vzrGijVNOk50L5+Ghz6207lQ4MT6yFxv6B7ifLr3n6K0qxbrtNaaxOzU4ukENqKCZZVZ3zdRU78c1bC0Yb961y4euPtiIIXe1zJ7k8KHSjds4K6jULOCnE/qGXkU7qznc574ZqJpdhfecM7nztpbidq8CFJ6YxT1+vA479JJwu0QEd3tRM47bvQ67q4u4SpMf46tntvj0NuNu2dmDGRkTcDv3LXcmpw/HL75Be5dKYe/Tx+RhBocGsgnoI9dOZP4k0seeKmg+3Ezqenqojuhk7DpPyZkp6uhwSRsFis6R6w8scL7yFkxUySOqu11wndxe9KjxIdsvZ6BLAeAjh1ccxfYraEJm4ty0BZyu5mZM2yKGbECWejp24vJsUVwhLYcnbz7B48v5MX1kC2490Y6RwT+paWMuDkYx5DcUxllTLqKAzBm0qBBGzbWIG/Zm41xe7ot/fA9Tz25Djc3SqDj+DQ7mr0BNkfGYxe3DRy7SSJSEcejeB3x6pok+erMRdRc+I6kpU6hqcDBer3tEkkUPU/0lkahz9y5ZSMWx8E0MKsy5Sh7w9nX7TgpGGleRtnZ7VLx4gufvXGK3MRBvVB3BSVmhZMA0GacOZ6D6IzvinFCEhUGpWKT4hHPqQD3OfZOAz77P5mQ/bMLBR7E4xfsC/ez+GEe7E9D++Hp8IX4fr3fGoYhmBu6JRryuHY6lf8+hrkkZlj8Owr8ba9HxcDYuEQnGTd2NeFbfBzue7kUJtdfIf9USiVEoasS8wRtr/tGMnKOY0v+KnK4SpX4z8rHo+lNS6phMRUJKcL/oA7I3RxAzHc9jV8o9En7JGG0qL6K16Q1SOn0zbs5tQiHDi6TOLxY3NTZhw540Yu2ciQK69fhZYAN5m1GAYYMX8enbicRyUhmqe3NRyC+HY+tYi7vKKSasb2oQ9r2KNwurcEbXXFS534QJsyqQ2x2Iy+/VoXb4RTRZl4bKqadRTbcaleYX4IHgFKz6Wo5ih87jKhdffMEbL9zwAD0kF+LbvZX46usznFh4kXp1lmN0ZA9xUxajAleq8PXJ58RDNIFeDapBmeRH5N1+EbTOvIKSPR1E78VCnBLTjAtKbhHjfftQfeNzHJlURR6Hp+Dvdy9wQkMiETXNwQz3PtxlMpu43svDnf1vML+gj1OlV4gmLX1oQTI5Zb4l2N70Bp2/i5prR15AcuMtPjuVRpevv4jfOQMYu1scA7tKcHR+L5o6WOEs48Oo8eMVHuUPxBfVCeg6oxd9iiNxW8RO7IjvRNt/vST64Q9uZOAzFOa+JEsT9vPi/iXGS3WRz1O6qD//dxzH10oyt9vh9fB/uPVUJUk1jMc1+//g7NnphDhl4LOXfIybtJLsdMrCU0P87Ku+BJm+PBfPr+Zn74baOd/ECvDOlu84Py2A0zR6Cu+9+IQXxaeY720vx9jzP7FPIZkGW1TgZg0BVndGGMsSTuDoAkHW5LcIhfNT0KRZgI0YbcCVVVHoqCbA6r/74vTO3ThNUYjplvUTnSpJqrdFmMlMe0MMcSs1uCvKrHxfkeYbL6jjuPHsDm0lDpp2WK4lyTQzykhfUCJmf5rIZovHk56+bIySl2UhlZbkpWIe/vSRZqnKEuRUeAGaqImzEPfrnG0jxXgqYhw7xpw4V3vPYfDMcWzjwBEz4dpqnCsgwSJNGV0nV40ZF2SYea4+ltkV41DFRBYotx2v+6Th6iJZNvEnL6Zkd+NNaS1W8vQz2ZCawD17XJvtiR0mUhIb6O1/2uzZ6XdE3q6Z5jTpsKHslySxWQkNBnRY2IPb5ATZgG8earG+hOMkQT0FnYvUGd9rZ/LAugjPrpzBlCbPJhBdjHNdVdnoVCnSI3QCP9hPY3vk8ji7E86gVcMU9mxukTmNuYAjiUrs+pcX3I9Hq/B6hCITaHhIQ/grcGfyFDYlygH/ZRRhupU6WyJxFNO9s7H3mz4zfnATG5sTUdhnMfvOP0oCHOy5cT8s2ZHwYVKbvZ5237dk9cs+kHNDdbTo1xKWLfmGCNRJ4/J9VqxW7QEp6FiKwk8XsT8Tr5Bjv3agaaIpm2qRTKqnpuG+p4bsSOQi8vpqHl44Mpe9EBnh/Ko+ht7r57LX2+I4yZ4FqPVDi60h+eZmBiV4NUWT6cXsocEh53FusAZzfSeAyRKlePKPBpP19MYFAcdROUiXt7ajGLE+FwMNF7BCvibc8jcFH3x0YR6X+kjb0Wv0wwp3tvDHc+IrNAE/lzmzR2dbyGiSPkq32PPO/TL5e2w1Op1byYJFjpDKeVvwd7otm61jS+5sDEMlQ2umr/OeA88Scd8zC7a5ehMndWo2tupwmLWJoPnIyaN4RNKEqYxUUCnxk5hTaszacqah4FoeC5ixSavDsV7uGM7p5TChV8exdHI6eohbsim8vLrnRBKKLPZkp2o6yYv1xXQ615NtrGgmQxxB/PhgA6t2ryN3oqZiceh6lv7oJEnaZoiGtmtZengyKVm5GC8sdmOTDluS4jYv7DzhyOiWa5xzD8LQXXYVu7x4HmdZZyyqLFrOIrfnNdgmpeDDvUuY0okaev92FpprLmF5olqoM5TO06fWbPjcQdziEI/i+cuYrslx3KUajh0KtizMohrnX9iNeoZ+7Kv03bHvS6XEt7PEwDvksnoOPXrej9U3XCF337+gLW3b2JbXpeTwOj78NLiFndmRS/a9mIjXP/uy/I8R5NN+ZaxcsYn1CJgRreK5qLZ8A9t0upVj/8QNx2usYXyBMzjOk7bhoJcr63Pp5+5vC8K72Q4sdOETGvRxHw4N2TEXCT20HY3AYbOVLKg2FIN0dmPdNXu25OJhVLLfij17nFhbbTl23/fCe/7+zCKkZmzNPn7+bPBzKSnvcKMZfX5s7tti8j40kWol+LFnCllky4JiOlVsG5txNoYovaqnd6K3MsfWAF7N5VKFzb4sZ9c8slW3k2pHbGQlvo0cB0thvL7ck2nESXHGrZ6Go47rmNhPPhpqb4qiwqvZ9KcXKFUgOHW2AzutYopvzWyw7rwIiwlLw0dpe9FVRIA17jqEl0P2oEKhMitSOo3nNu3B6KNyTOn5OTy1fw+eWCPORGrOov/UXahYKcCOSJ3BpJc7sNL/NdbMP4G/NLfirKan+AlO4k3uJiwNqscHHSX4/sNGvPN4JlvvfRU1y73weYMS07zbhL9VvPGM0wRmtbQR9RQ8caGBIKv5fRPBzglXJbzGq8kNOC3eDadWv8YPfk1Y0bgEjz/pROPxNWiauh533rmD1LYa20+54M4LTXhd4iaudTXDz4EPsLCsEU2bluEr3yOY7FnP68+8cGbFEfw1eBsP59hi4fkk7I98ivHaJrw6k4/wvAMvfdXDjSFJ2DLwChf56GEITzMXpj/GXx0T8Jj8KmyK70U1riUeVNDFELceNLlgjy5Rj6mDxit8escBbw0kY9/pHvQy+ETDJE/j/fKn+O/DdxrwqorXa/HmmTAR2R4u7h3/AI/bTkEJgbv4kafdpMSm4MWQazhc3oGTA/kwekI1flr0BC/mvaXN7Da+V3mC79dfpmXf9mLzqne8d1Fq0tKNigPdKK7dRrMvv0Td4Ic42XgKvhzVZbVvnmFiL8GMdBV2Y9ZzrMSF6Kwgwx4cfIU3D2riso0CbPyiLjRQm43e4z/iPMP3aJB3l75t/Y69+u9xnuMv6kz52U3pfrygNgHj5EXZhndDWM/lw3zb8czwyTAmmvfSZw6T2O03w3hzZIT6pk9lG1w+4ejvUVrzUpn53xrEK2ITcfoENZZW9B53j05BST9NFiP6AVU0FJHTos1an3zAqSMy+HFkHkOttzi8az4+99FhSsUjOIkIoIOVGUts7kWpIz1U+p4Wmyj7CcWl31CNFRrMsPMzOjm00hSOCosN/oJ3+57TFVvF2N+jn7Ewq45WCI5jP3U/4sw1+TRF5SfeqhnG84sq6McGa7Yo/ylWKr6gDweWsi3/OnlnooYts5xYRXkLnpXvpu8iHJklr2dqq1XH6TZrWcl3LpZIt9HGW25sxnyKBaCNKtXebHvtWZyVfJVma3kxd9tzmLFXBn8N+bIXvBz2sO8cNd/ky8QMs/DMdn58N7yN0dhgvD3uJDWkW1ks/x70GxykPiu2saJGE949OEvHH9vKzn8wxSMHe+n93ZuZ8caLtMOujPqp+zDN1ZVUwu0pfX5gI5ukvptbnVZBr771ZN8047j9T1/SRZZebBefJeePbRntl/VkvzMVOePcntNENy82I7CHo/fiJDVRXc+u6Rdxua/GY8IlT/Yiq5gKZkrhjdCNrNjbFD0WzUItI2+2UiUM6TNtpLEezHd6Dm4yNkDZcGd24145XjlgjAp5q9lZ/QJ69LYWvhm/lj2pWohBm02wk7qwUqW9vD7XEif127MmmoVlz5ZiBy//p3ED8WWPPR4q8GbzgwxJQ3kq5fJtYv7BLiThUibNmT8B1k/ahZzWCLSYJQ5vLwTjKK9GrK8ThPxNATh7KAu9pIbJ0fJteCssD81s75M6xT0oWpaDz4/dIB5eYXjyag6eeHOJXM4Jw7tq6bifnCeBSoE4WBeJrmvGw9HVaegUFIK9EWKwwiAZ13jEY0oWP/ClJKCz7GF8t2GAhGsmIffmEZx7rp2sMEjCuZzD2Dj9OjEWT0M9owxsnFZL8j9lYGzyIfRvPk34gtKxY+5WLBAXAybK05rfduByD2FItj6OHLU4vN7wm7gb5uP30kwUlukj99cU41RNnkYTukt+Zx1HSaUM9C9DMkROorhtEgrOvExeJ5/CS/pRuDOjlJQqneTN54ZMTQROkArMj9mERzPGAR6oxrfN4Tjp4igRkGpA74541D/+ljhmXcFZh9Lwr9c94qJNcfT6IV4/eY0k/anDGY3xGLignlySuYz+fKE4S/Ec6ZlRho5mFvjZ/B9JK9Zh/XKOeHXyL2KSrcxajzvh23R+WB0uwVZWrMRgSUHYbSTIvqetwuk+gjA9vAvTNR0x7IEgVD25iUd716DqhQEi26HDJoER1s74TlLPKbNuyU1oM/0vUZgzgWmUbMRDj/ggzkGQKZtsQfTkh5Vlr3DwAE/f2gmAzfRmNMrZg2euj5Adg7psVq83njoyRC7JTmOHorbjsOAX8lF6InsksAdFDn8lh1cLsZua4die9IO8NurGme4HeXftO7ki2YY526PQKekt0RSazo5b78CPDf1EQ34Sy/q5Byu3DRDTv8JsKPMgfmrtJzm9r1BRKAqfiPSRvfubsXNrEl7j9pCKBQbs2LxADNjaRWLEp7MRhW2YPfSUKPnJMGobzKt5z0nRVWF25txe9L3zjJQX9KF8bDgWmncQ09J2lH0Qi/jxGbE6bsA6fm1DwXePiHmnCvNZthFPje8gbECGvXjvj3WjrcT1sBC7U7QbN7jeJuFD7/DN+jCUdmsk7zzb8VdbBE751kG6XFSZ4Aw3TJe4QzRiZJhenCdasuukoleIpWt6Y6LyVRJ2sA815m5HiOGSx8P3UUkuGD0GHxAhpels8o7lOPF7C6lUlGGBp5ZhyclGkjxHiB1zXIa2ZxrIoo5+FKy0xc0P68i2R7fxc988XOf4nhydqsdiyGYc3/aK3OXl+qHDhhh+5REpNJFibxbNRdE17aTh7TjmHKePLl4tZOu255hspYdzbreQuSYtKJkwHYs/9pAp3AmsWWkmXrj4kixwGMfkktSw71oXOT+uE50E5uDJVc+JXtIdXKCohDvHfSZ56y3Z8KEDmFfzhbg5GbMukyA8svQ3meRxB3f1TkVH4S7yvs2cBUWH4ZwFfWTXLTNmMrAPV3/7RcILdVh9kQ9KrecHvYCFbJl0II4rFwST2wuZ6nQ35FfoIjuDF7H5wiFYUP2WLGqwYl2mu1Hd6zmZtM+aCZ0Oxe/9fcStfCnzfBmAOlkfiImbKZtjfBCL54zydP8ydtU1Hm+Ji8D4/cvYt3vxCHFi8FXXjgldTcC5fALwwX8VI8Yp+KFLCsJHnFnWqjQs6haFE0HuTD3sKJb9kIPk8A3M7W8uBllLQ7CFN3NXLECr4/yw4oQ3M12YhxE5f8h387WsdEoODh/6RgT3O7Cuf0n4ffdHEm5ly6InxOKZfzLwusSZ5aTFYMYHCVhywI61Kx3EgW5xuHx0Kft2OBSbnglBkKwFO7AjEAWsREDrGrA8odW44Z0k+P9ZxmrTvHH02gCJoiuZcm8E0oYholTiwgrE4vBo+VeyTNuDp7FT8E/0H6LTasHMHu/G82ufklw/YOYazqjb9JQExSxlsx/aodvCPiKpaMMeFzjji9z35M9PB8Z2rUP+xO+k9asXS8jegrMlheDAjk3Mumkr7t81QOxG3JmVthdO/NpFqJ012+w5D79ZfCaZO5ey/lgT3MT/nSyd7MjSYngxtv8HaetYw1YlzMOf2nxw+aAni9HQx8DJovBRx5tJ75qHP85JwGaPTcylfjtOKZIC3/de7FCBKV51ek2u1HHYCTUj1FncRSJy9RicMMPLF5+Qly/mMgkBRxTdNUiOOVqxz4Fh6HJ5HGQ6L2KCa5fiwL/xkOOynIW+tEV+eT74BvPZKsPFaKMqBevk7NiktRux6aoiSGSvYzp70/Hd4CRYd8iFddjtwgfaU6Hq2AZmERaJMS2yMGuqCzuutwKn6CrCyd/rWWqPLU+PSQFf6kp2ecgBj10dJMcfmzHnaC2sufqPFKZYsB7hhbhX/CcZN3UhS/o3D50NPpITa3WZ0aV5WHVwlASW6DLba1Zo0ywI6XV27OgtbTR5Jwxo6cqcV+ng/UwjFun6BlMGtuHuX1psk2IjXh4Jw1WNJmxFw2c8nxuOL6qAmS74iLsqErE+2prZfxtCqQfJmDHdnAXnIr7jaQa513ZsZfN7nMRJQt93LkzzXR+mXgvBvl5nxtxvYNvwNvT0c2WSShQ/u3lj+UYP5ubcjR0Gm7Gxz4M1JXXjOoPFuN5nOTt1VI19ColDXzErtjNYjZ35G4N7PW3YjxcyTEkhFr09LZnYaWmmvT8a+x8vZ93iIiz4QQIKTFzMZo4TYedV4rFtyQo2mCfMYmYkIJm8im2arM6711G4RMyBCcXKskTzcNT/5sTo43EspzwcTU1cmMMldVYTH4wNA25sbaosE1gexOvX17JLn8axJ9kB2LVrEfP0U2U7z4ZiXigwN0cZVvOTl1ukOOycpTBTP7oPq5WNmXr1Twxp2YAXa8zZ4JI/6Hd3CTbe4TCbGWJMmLsGt39dxKQHZFmQtxee/W3BllSpsL+5PtidbcHa/4mx2rd2eKjemvUpirPXhpZ4YMEyZr1aju0PdcJqY0vGx/cL581diOvigd3R68bpTzVwSsQK1snrn7O95uGC+auYb7Mok6cWmKS4kunulmMTJjqg+aaVrPGQGvszbw26HFzKLj9UZb7gjkd83NhkgffYvmMuhkmuYR/ixjHdaUtQv96ZOfHJsWvPHDFN2pHNVFNjKdlrUbV8Hfs8VYhNNnNDB4/VzL9Znn0oWYdDra5MyV+d/SnczMt134invDjPRwZo/+0vybjzEoenaGDU6A+yVoCfnVygjZ/IT/J+3VTWnmiKe2Tek5eHprF4X22s2TEO1DVfEPmn7vTUbFn2flEU2tXGoP1yOzZHGVjCrDQ8eJV3pocMWf3yJFwW7sR+fjVkzrrx+HC1IyuYuYjNCkpDaxXCXl8zZTLGSVjvu5gZftFnyyJi0FFkGVO3mMv+zYjG1qdLmORZM7bmXhK+zTRjw8pm7Fd4GApILmLwXZ+xPXuwyN6Rqd62ZCLVKehh4cRutS5n26NTkZNhx75KL2PP7JLRtX8FkxC0ZflFCZjrz2FF1xyZ1tYifKI6n52NsmcfPx7FpS8XMrxvy17/y8Nn1IL99LVlZfF56PV4PnO7tZZJl5XhDnU9ZmTlzsIjitGkUpdp5vgwq2UVaLltNlv00JtZ6Z3HO7z4OZ/hwXJtz6LhIw7j/+HDNLaUomynFbt835nd0S/AqJNLmVHeSraD5GKL91z2q92eOdN0VOTprNxfbiyK1xs3nzBk20Zs2OawVNRaYcBmOKxgS39G4ZTCheylvCUL2nwAF+qYMA1fK6a1JQktZi5nrT/dWaLJEeS7bcMWyjoyMbkszLVfwv6Ge7FuzyLk4+PlJLUlTDw/GQ9n2rJVO+eyC20xyHW2YcOlHBZxNx09Zjozjac2jG9BEEZ4ODPbq4uZfEIInltmx6752bEKXi+61N+WVdq5sJaBcByUNmXzGn2ZdFYcHrexYtHLfVjl71i89NmG/bVcx6Sso1DstDWvbtgw1r8erTodWMCJ5Qyuu+Lbw1Zs1N+F+fY5YVKjJQuf7cE0B1wxcgewzQ82snUJTvi3egErifRh7x654tktekyR+LBNfBtwculcNrXalylMSMSlAavY9gkWbF+MO+Y38Or23PnsjfcetDF2ZEaDc5mikCceuG3N7C5as4170rHzyXK26sxS5rM1DfN95zGb8yuY46KdGLndlC0+Z8mec/2wPQHY+Wxjpnl4My8n6LGJW+3Zt/XRuMF9JmPvNrDJZ0+gYLYq2/zRm+mWZeHidE0m0+3OXB4fwmmjM5n7Ki+22XInGrTNYU93uLOQ9J3Y/kePVb92YKmr9uDpC8uZvQ4w3bcr8cYeS1a3w5R5e7rhphvAnF5bsXrftfitw4bd0jBgH467Y0/zUuZaacAkFd1wNNWCafwyYDPVt2JDsBmzmuDAgubY4+Jxxiz4yGqmU2WPLh272cW1f8d+Y/mqOIRJOvz3m0Ybgb3MI4w7xuKF/ux95ZsxPlKwnhk+HR3jlvG72aD0P87HIneubvNeVn4gnJRP9OOWKexhaZu5ZH3UAa7+FT8Gz3pJcmg8t01qPUub+Jt4VKVzJ5GVLMWND24M5HHz3gextgN8pGVfA1czdDfL+hFO5ga3cIsPBzOqyyUn/Z9zS2y3suq0HqKw+QO3LGEtSzL6RsI8Rrn+qXbMVOk32aMtTNv5dzLXVn7y9OwTrrt9EJtSGEGUp37gDs7ZyeS6G4hnyl/uvF8+zH/7S/I4XJRWBK/m9UfDJKhOhr5WDGBd0wWJ5KEG7o7ZO9g5q0giFNvCnYj+bGNmAxGPeM7NPe3N/qx4ThL2feAWeLix4oF+krJ7lOv+fRtzlx9Hoh+7cyc89mdnMZIcXOnHPW2+jUWsbiChTQe41Qs9mbLgU1JfEM81e+LCBiV7SdWTdO56vW28Mx1HuqZF1/b99WNrPkQSxeq02iiHrbw1NJAg/aO105e7MHOrHiJiUVUrdfA7Zk75h9cvv6JnbUZ5dfEfei7gw7RlAswj5w/6LpJAt9vCTNmYnyldFUAHaTE2K4WfxTWO0BX1Iiz2tQDjj2mn7W8E2f++H1SPoHSDyR88EsHPXPvv0O6P05nMDgHWNpsfv3SpsHPtfGyukBRmJqmzIN9/qCUrj9ulNFnnPD7GWSOLi4O1mNQBPvbr0wTs69Zi3g/5mUO7IPKzOWy+mwC7v/oj9U/RYHE/BNiv4W5qmzyD2evz9Gb6AI1S/Isb9oiwD89u08p541jHTxHWdauBTh0RYQVtokxsZRttMBdj+VEiLOL+IJUWEmFn9UXYZyF+PHRcgIW3j2P5y8XxuN4frEgRYgGKf6h2xA98lCrEzs3uohMe82qyhyjzGP5LFbzVWOQUMZZF+2mL5Ww2+Y0om+n+ktp5abErS0WZivcQVdyvzZRvi7B90gKoNlGbGYQKsyUDEujSoMlaDYRZ+wIZfOQ0k9nVCLGTXbK4AlVZQ4sw+zEqgTnKgux3pjh78+AtNTw2i9WtGs+06wTwcf0xOKT/k7jdUsEHx/Lhk+Uvns6chqIxBaA+9JMoFChg5uRCWLj0G7krJI/WkQXgtuALMRCbghX5+TAU+pUUiypjv1oheMn+IybpopgjVQR/xb+S1/N+0rjthWBza5jgYjH0OpEPqycMkSFhOXRYehQ8ez6SVa+V0GljHvQa/CT/3FTQPuMoPFXig0qj6ThtJB98hflh+4gc7nHPBbu2EbL961TcwBsj4zBIQkyn4dsfeRCzjh+OOqvhlU/5sM9OEA6emoheDYXwqogPHowIYfG6IiiZOEKMNAepqGkBPM4bIJdsZFGON+a5zAeSu0oID0zIh9LeN2T7QXlczVvbrt39RGGTEq5/WACFnF6ieVESx+0qBDK/jzSE8qPsxnyY4ScE3R6i+OBVATT5C8Kyz/9oyMkiKDP8Q5jFN5oakAdSNUKgEyqLz4yOQbCkMEw2Ece5pkcg9g4/BN+bgoESuXAtQQCOuszAU0lHYGeaCLjbK2DnwFFYYSAO/b/E0awyHyYJiUMMZxz+kS2CpBvCYNYwTPk6j4OsDh+obemjSQHFMOz4mTxa+Jqq/iiCrv53ZKiim5ZxC+GxVzc5c0YAF33Jh/5tXeTk2QkY/uoovH7VRcqKFXD2tDywVnlLgo4r4SrJE/BE7DeJyLxLf9Ni2FP/kZhL36d5z4+D9etewi4+pAeLi2BJTRfJPvqDim3IB65JJ7m2ajxezjsKohmdpPyrDM6UzYVfOc+JjocCtjgcho6VPUS/SBYFfxfBpkRhMFn9gsbMKoa+Rn6Im/+YVggWwPjc8TBx1U86e+Ex2BIkAV+YCAY9OQLHDMTAWmginnPMgQWrBMB4sTL6XM2BXx9GSLD6dLRozQGL0PHA3CegqNBhePZdACqvKuHWO3ngLicF/Nu+0m15x0BSeQKEdj2kki8LoSpGDK77VNDNn45D5xZBuOxRQGdMOQF+ar/IfdczdHTDCfDIGyIttZVUyO4wPPcdIr3FShh+NgcOz5SFkQAuFTvNi702aYi3Tadn5QrBzlsM/mr7UJ2VxfCwWQBeEBdaKnMSnEp/kLClbrSv5yQoOb4nWt8z6IjFScjIeUZeqJ2nS1aegL0fWomNexe1DiqE4xsZ2fuJH21K8yHUiZL1XGHMPHQEZqY1EKFJEzHj7WEYtGaEkyOHfpWZcO3lfXK0XxZLP6bD4MFesvS3FF4rSYcAOkBqCS8HSqXDk5y/pGbBJOxenwWWz6Qh5+xHqvktHTSiReF//9swEywBwcofpJd+5WbznYY7T9+Sl/K2NHTkFGTvv0+SF6VS3d2n4WBxLUl7Ukftr2TD0Wm15MFneXSXz4TUs1dJj8tkvDyQDkWr7pK4Lnn8vSwVrox/TM4NK+LlmhQIqXpNbDLl8WNnMrw7MEL2jvBy1LNkeJ/FD/ecpPG1fRZkFyrAw1ApGrovg5d/FGG6dhQNU06F3/cmgZjRa3p69yF4ricBfK6fqKf6aRiXfIH8rmiiP6VLeHe/gFwRe0RXZhTAT9sCctWFH8U8cmH+nnJiZzURcck5+PXgDfn7+Qt398EyiK7rJJvF/3DNF5eCpH0ryVq5ktaMnAd1y0YysTiM3q86B5MPXSRnV56jdeFnoc3qLClfdplauZ6B3xk5ZO+iavpT8AxsxVSy0byRKvPyidOrLPJOih+nhebAAaV8ontTAVPisqAm/iQpNJ+O5FIaNP2qIicVZiJtToaUzBtkTooaeg4fgqPND4jGU2U0WXcI7p7vIUqoiHNkDoFN1jDJj1REzpqLsPJtI2nnf8cNml4Fpx4gyQtbTN++rYD7rrXExjWciildgA7dUuJxuYiOKyqDj4+Ok4aWM3RmQykErDhMRNMqqXFAKUxflkoE5lRTg9EakP2C5PrjHm5pw2V453WFPG36xz3jcRl+XL9MNvW40KmDNXC1u45ss15EsaYagiKrSIVANM2uuwTdmuXEPjqLXg67CEMXzhCjqhP0YlsNSGmeJOofz1Et4SqYNa2ACPkX05TIGpBXyCY/l9XQ3uwqmLD8MFnNvUhV3S+AL0khT3bX022f6kENL5Go50L0tTUXDr+vIElKEnRoQwPcWFdJgsL16SuherhmUEPCP1nTXhcE0fNniZsbH70/nQJxO0f4TBdQx0l14FBzntyqSaf1M67Age7TRO9VDD1xjwuNPfnk9O211GUEQaI8nwzdf8ad0UQhzjCFNP+u5c7v50KKYBwpasrnbq9vAHGDCHJRtITLVW6ApYlh5Nd6QfoolQshZlFE8tpUyu9HoeFwIpn4Yh79WVgH26USyeeUE5SuagCzb3GkLv4grQi8ApPND5JVQYHUSqsWvsSFE9ud+dQmsBb0P8eR1V5ldKU+F2oUUsnUWZH0/UUK41SyyJrDhO43r4XhhbuIarMrzT1SA/kbg4h7wBb6srUedk7NIJMWHaNNWAuC746ReO8SKhJSC0vunyCdv/Op4fkGqPLOIf5nZtODBlfgy/Vc4r3RmJ6pqYO9nsfJDXFXWv+2HnJ9j5GwAkJ1o2tg2mg4iftTTnPXVoHG8yAC3BBqzzsvhQcHyQ3Vi9REpRImDYYS87pyuk/uElT3JhHJV1y66w7v3MXiyNJ5l6jKzIu8mnKITFJvojnSF6DOOILof66h+ulVcFA2lgi2Uxp2rAzS86LJv70X6Nz+Mvjgt5cUe56loZ4XgPt3B7EYOE6T1EvB+UUM0Zp4kT55fh6mfA4lr6+X0v4v52HktBfvvjjTGP9q6I3dTApfe9OyfZXwPGIr8asOoye+XAazkm0kRMiBnl9fAT+l1vFiwIi22l2EyhWryUzTWdTp0CXY5uVOogwFqUL9aVDQjSFSOTfoAY3T8N09hPhsrqNB6Weh+KIvGTCKoQEOp+DuDD9CX+TRas4V2NvhT0RvdXI3RtfBzWWeREsui6s5rwrazzuTD4YZ3GSPcpjww41w7j3gnh88DZEm7mT9ii7ugNdJyJd1JLvtJ9H6beeB77U7aT0pSefvPgMXhdeQv5Im1GfgJEzucSIFhz2oIa8PMVG1JcPdE6nEG17fEr6SLLiaShfdPgxLwIrsrzpHlQKyAX6YkfNtQPtnFsH2Y76k8CavxjnkQPSXDWRWYyu9u7oIBpLiyYlb76lyeRF8iA8ioadaafWtHDDIiiNrzaRx9GEOzNgZRJS2/6aG2hnQqp1JHoaoY8srXh5WjyKx7pPwumMyfO3cRj59eE8nlCaCVXgQ2ZTwjV6/fQjKL7uThcdvUcf4BNjltoUkr39DHfYngbPdPJJ6ag69Y5oIS9RMCXdyNk34EAtac1eTw/daqe36FJjuc5yI05m4gZsE0rfTyDV/VSyMjobE6RHk6tl/dPzLeMj+nEBCBCTxY0Mk6P06SQzWj0fz/Bgo7ywl9rrKGDAYB5FNFeSDzSxUP5QIRw5cIXKumoh/EmBjSCfx6FDDBZcTINv7LfG2mIo/aQL4in0hwQcmouiFOPjO03Fh++VQaXIcTMp6QJbKK6LxqyhwPnWPfLGXx2xN3ntX3CZpX0Xx1PVo+GvZTRb38OryrkgoTHtJJIRFMPFLIvwxmAkWF6Toh5AUWJuhCumeQtS/KBH642eCQ641PS2XApGlKvCiNYEKdiXCk8GZsDY5ml68mgzh9dNhvmo+Hfc3BnSbZkKl0nh6IiIWjGtmQpzfBvq0MRZGU2dBkHYcfXQ5Fvx3zILoJ6X0Y1QiRCmqQ/7EClo8MxkaBFSgzKia/tGPg4U/NGDr89/UWiAJxu+bCSYqH+k0iWTYKDwDqvR+0oyueDh4VRO+eitgT3gS9G/RAL7birg/IRk+fVAD8Sgl/LAlCYwuacFZ2YXoppUMimFzoD11PqqapcK2jllwwsQAJ/omg9QSHfjiuBRDX6fA4whtgOeL8NaZNBDtnw2dzzi4xC4N7GV04cI7e5x3IB3aPHUg7sYKLPXKALGXc+BlozXm66RCzk0lCBGoo0KGKRBxZyosPvuH1iilwCfP6fBFURU3RqZCoOEMuBBihAYladBSowZbdRcj/ZYBb/eqg/Sa5ZgtkA18KVqgUGuLE3OyYanjLNj3xA6bK7Ogo1gXBiMcMNUzC4xE9UF5zRp8OZwDu69rwYVWWzzumgMSLbNAu84ON/P6z33zdOHdLGcMEsyBVzL60HNhHdoszYPVS7RhSt1y9OJxUZMGr0dcjvDrCFTy5g+iq3BdyREoFdQHyZ41WCJ7AhYV6sNJpyXoop0CQj/kYEqEKDrw9pUePhniJ6iiqn4qyDQpw4t9gMckEuGlnBxUdcpinkUiHChVhGtcXZxxMAGunpMGHQ8RvGl8CMTpFHDdZ4u4MB1uPlUB0btLcIVpCvgHTYNLy9fiMZIJv1VngNkqG1Sl6TDhigoIoTt+eZENa/aqQesWB6y8nw3almpg+8oL24/xcoKaOmzIcsL4ObkQvVkdBpw34L2kPLipNRMWeTvgRp+jMI5/Fij9WIfv9XnPHlUD15YduGowFd6LqYGf+A608M8F47aZMPe1P15edAy0l2jw7tJGjF2aBDdyVKH/pzd2+saD4ebpMHu/PTppxcL5dUowWVAL3xvGQneNAni+UsY1JVGw2UsO7jUJo0VsDixdqQlx/kHYuuYoaC6aw+sd/TDiYiY4ps+G5etCUZ6bAnovNEDubzAW/07k7Xcm5BZvxE8dceAdpgZL7q5ElR3RYOoyDQ4EqOOGvigIWKUMfoMyqBEVCYprlGDp9Y9UKyEORC7Phuy6Fdj5MxFuHpwDy6I24zWaAj+jtWGdQRCu+5sJP5x1wNJ2PyYFREPBRTVQm6SBa12iYF7ADKiYNh7bOiKhpUoFUlz7aZNQJGQXqMDfoxfoTZ8IaG1QBF2+u7TmRgRkuqiAu1Eufa0ewdNTk2HGlAp6UTEX7t3UhrbZIRhx4ijULdCFz7e245LqSOhLVIftmz1oxqIo2HRaDaynxdPjJlFg260Gpzry6N6rUZDgpc7zBa+uPYqGFVIz4Z/ZH+pzJROyvuuB6t4dWHsnBRp368Kfb56Y6ZwEdSk6EJzriK0YD49HtODmCKBjcQxUXpsNW+2mYR9fOPjOnAIPvIroxcFwmGOiCrfWH6Q34nNg2Rc92Fy3A5vD8ng6RR9sKjfjjz/BMC5NHj6OBFCpy3vgReVEeNLfQFllMGhMFINQ+2e0L3U/BC+Sh84upLdaw6HxrzzMVb1HN+3j5dt4WYgqEcDa3v2g9k0GfhaLYfmTELgaLAVKBoI4t383GEuJwamNQvgrJwom2kyEikBFfO4WDpdsZGC21XTMCdwHFU4TYHO+MtZNDQGbNDFojVbC7NJo8GuRAWd5IwwIiIAJy6RhXzQHV/seBKlZknD2mxHuiwyDVEdRCIjRxwttsbB+syyIr5BH3duxENkjA8xMBStc4qAxXQasjbVxbmQMtG6VhUtbrVErPhIm9UlDpoMbujWHQ4+QBJzZsQp3NcRDzUtZqFZejJd6Y8EpQB4CrnqhxJooWKAoC8f8t6LOuwhwDZGAoXXe2PY7AnofTwDjh4dRdkkUJM2VhcCVUbjkVzR0vlQEt8YkzFGOgg27FeD55QKcvSAWZs5Qgiy5CLzYkQAd0QqwMm0DmsTEg9OoEpwWjEOpVXEQ3TkFphQcwgPBMTDZVRlavxQgnyDvri2ZAmcaK/DCiwjY3TQRVludR7cDEXD2sRxEfKjD1dER8N5CEqSGI/C1w0H4lCcCZc8Xo2xdOMi9EoFC8824/FQ4GMqIwcEV4Zj1PgJS1IYI58II/S4QBd86PhL7SZPQrDUa8kTfkbjrW/GWQjScF3lP3mYnYVJXNAgZDpOulmNoYRcFT/YIwHBKOT46HQkbYTzI1tbhed0IcPr3l6fLEvDEnXAQbBeGP8V5qC0QAcnDv8lclW1oJLYP8kJGSUb2d7r1xf7/a+LOn76q7gOOYwDZFERkEzVjMXXJOAhNglHu531AFFdUSMEGTW1QlEVR2UQUER4eNgFFwIILGo2NFtQZNJrK8z1HLSCLS0QQ29QFDW5xCxUUQfpMZjrz/g/uufdzzpz7w+udZrffx5D7D8kHPpzaOL/fU7+xex67cFqaNP0Aqy44Pa89fG76h66tUuurW+f3589OD73SIj095ug8ZOi8NPDug9OOCRfkw3venr4999D0/IYhucnWeWldxw6Jg8bkF1bPSWf07pQGPzEzd3l7Vqo/qEua0WlRPnJwfdrat2v6zZYVuel/1qX33+2SFlVP5qGNe7b5/g7pPzo+m1cNm5d6d2uW1v96QN6xfVbaOK9JennTmXlgkznpmjubpq03npOfP3dOmn1Ki5TeOj/vYE76rH2bNHDLsFzdMid1WN4u7fhkTD7mtdlp8OQj0tgvZ+bl+xtndVundM/pi/Khg+rTaZd0Ss8MfyD3eqkuLdrUsfGfdlUedkLj87zQIf38ltX5qi4z0xVLv+WhIWPyk1fVpdFffsNvXp2Vdy+rSxvfbZKarV+Wv/9qRmq2sHnat+63ucM9M9LQmw9Jb+5+PO9ZPCP1bNEuzeq5Oo/YMzfd16NF+uzMZvmztXPSyo1N0+JOrfMLo2emFss7pGOHPZxXTalLb3x5eBrQ5Il87py6tPWT9mn1y7/Nr1xen0bf2iH9YujS/Pam2enbI9qmP715cj5lfX26+IjD0/U/mJnzn2enXk33s21hq9z92FnpxuVt04IPx+XXGtf4g2mt03PThuVXd9alhmuapzbPnJ+f21qX5i89OLF+eL6t8c42qr5V+t3Cm/NP/lqXtpxySDpxyR05v1eXvm7fLp300j35s1MbZ5Km6fnvR+d922ak0qpZmnRefb6tVV26uKFF6t5xaf6mX13a97M26daeD+YhY2elLv+6i12bWmZ5/JDHD3n8kMcPefyQx0ceH3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8ZHHD3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMev5LHD3n8kMcPefyQxw95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMeH3l85PGRxw95/JDHD3n8kMcPefyQxw95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PGRx0ceP+TxQx4/5PFDHj/k8UMeP+TxQx4feXzk8ZHHD3l85PFDHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHRx4feXzk8ZHHRx4fefyQxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQxw95/JDHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8UMeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHD3n8kMcPeXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHr+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3n8Sh4/5PFDHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/EoeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcEPGfyQwUcGHxl8ZPCRwUcGHxl8ZPCRwUcGHxl8ZPCRuw+5+5C7D7n7kLsPufuQu0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuPuTuQ+4euXvk7pG7R+4+5O5D7j7k7kPuHrl75O6Ru0fuHrn7kLsPuXvk7pG7D7n7kLsPuXvk7pG7R+4euXvk7pG7D7n7kLsPufuQuw+5+5C7D7n7kLtH7j7k7pG7D7l75O5D7h65+5C7R+4+5O5D7j7k7kPuPuTuQ+4+5O5D7j7k7kPuPuTukbtH7h65e+TukbtH7h65e+TukbtH7h65+5C7D7l75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O5D7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+Tu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AQAAAACAAAD8MgAAGBIAAA==eJwl2oVzkMfWB2AIvbcllOCuAYJ7cZcGdydAihUo7u4OBYq7a4Hixd3d3d3dKdDwPd/cmff5A94zvz1nd3a/ixQp0g9EIwZxSEASUpCKtGQgM9nJRT4KUZQShFKWClSmOrUJI5zGNKMlbelAF3rQhwEMYQSjGcckpjGLeSxkCctYwWrWsYHNbGcXeznIUU5yhvNc4io3ucsDHvOMl7zlI1+IIHLkSJH+Q1SiE4u4JCQpKUhFCBnIQg5ykY+CFKUEoZSlApWpTm3CaEAjmtKCVrSlA53pRk/6MIDBDOd3xjCeyUxnNvNZzDJWspYNbGYbO9nLQY5ykrNc5Co3uctDnvKSt3zkC9+IEhAp0vdEIwaxiUcikhFMCBnIQg5yk59CFONnylCBKtSgDvVpSFNa0Ip2dKIbvejHQIYwnFGMZSJTmM5s5rOYZaxkLevZxFZ2speDHOUkZ7nIVW5yl4c84Tmvec8//EvkKHJCVKITi3gkIhnBhJCBLOQgN/kpTHFCKUsFqlCd2tTjF5rQnFa0oxPd6EkfBjCEEYxmHJOYxizmsZAlLGMFa1jPZrazm/0c5jinOc9lrnObezziGa94xye+Euk7Hz/wIzGIQwKSkIJUpCUjWclJHgpQhBKUohyVqEYtwginMc1oSVs60pWe9GUgQxnJGMYzmenMZj6LWcZK1rKBLexgDwc4wgnOcIEr3OAOD3jCC97wgc/8S8B/IkX6L4EEEZv4JCY5qUhLRrKSkzwUoAglKEU5KlGNWoQRTmOa0ZK2dKQrPenLQIYykjGMZzLTmc18FrOMlaxlA1vYwR4OcIQTnOECV7jBHR7whBe84QOfiSDgv/6fQKITi3gkIhnBhJCBLOQgN/kpTHFCKUtFqlKTOtSnIU1pQWva05lu9KIfgxjG7/zBBKYwgzksYAnLWcU6NrCFHezhAEc4wRkucIXr3OY+j3nGK97ygc9EEPC9OhBIELGJT2KSEUwIGchCDnKTn8IUJ5SyVKQqNalLAxrxK7/Rhg50oQd9GMAQhjOKsUxkCjOYwwKWsJxVrGMjW9nJXg5ylJOc5SJXucldHvKUl7zlI1/4RhQbi++JRgzikIAkpCA16chENn4iLwUpSklKU57KVKc29fiFJjSnFe3oRDd60Y9BDGUkYxjPZKYzm3ksYikrWMN6NrOdXezlAIc5zmnOc4lr3OQuD3jCC17znk98IYKAqPJAIEHEJj6JSUYwIWQgCznITX4KUYyfKU15KlGNWoQRTmOa0ZK2dKQrPenLQIYykjGMZxLTmMU8FrGUFaxhPZvZzm72c5jjnOY8l7jKDe7wgCe84A0f+EwEAYHqQCBBxCY+iUlOKtKSkazkJA8FKEIJSlGOSlSjFmGE05hfaUFr2tOZ7vSmP4MZzijGMpGpzGQuC/mTv1jN32xiG7vYxyGOcYpzXOIat7jHI57xind84mvg/zba3/EDPxKTuCQkKSlJQ3oyk51c5KMQxfiZMlSgCjWoQ30a0pQWtKY9nelOb/ozmOGMYiwTmcpM5rKQP/mL1fzNJraxi30c4hinOMclrnGLezziGa94y0e+8I0oP8oAgQQRm3gkIikpSUN6MpONnOQmP4UpTihlqUhValKXBjSiKc1pRTs60Y1e9GMQw/idP5jAFGYwhwUsYTmrWMdGtrKTvRzgCMc5zXkuc53b3Ocxz3nNez7xhW9Eia5HEo0YxCEBSUhBKkJITyay8RN5KUBRSlKa8lSmOrUII5zGNKMlbelIV3rShwEMYQSjGcckpjKDOSxgCctZxTo2spWd7OUgRznJWS5ylZvc4QFPeMEbPvCZCAKCZIJAgohNPBKRlBSkIoQMZCEHuclPYYoTSlkqUpWa1KUBjfiV32hDB7rQgz4MYAgjGM04JjGNWcxjEUtZwWrWsYHNbGMne9jPIY5ygtOc4yJXuM4tHvCEF7zmPf/wNeh/B/Lv+IFoxCAOCUhCcoIJIT2ZyMZP5KUgRSlJacpTiWrUIoxwGtGUFrSmPZ3pTm/6M5jhjGIsE5nCDOawgCUsZxXr2MBmtrOb/RzmOKc5z2Wuc4t7POIZr3jLRz4TQUBMGSGQ6MQkLglIQgpSk45MZCMnechPYYoTSlkqUIXq1KYev9CE5rSiLR3pSk/6MpChjGQM45nMdGYzn8UsYyVrWM9mtrOb/RzmOKc5z2Wuc5v7POY5r3nPJ77yjSix9A2iEYM4JCQpKUlDejKTnVzkoyBFKUkpylKRKtSgDvUIpxG/8hutaUcnutGTvgxkCMP5nTGMZxJTmclcFvInf7Gav9nIVnayhwMc4QSnOc8lrnGLezziKS95ywf+4SuRYlsv/MCPxCQuCUlKStKQnsxkJxf5KEQxfqYM5alMdWoTRjiN+ZXfaEMHutCDPgxgCCMYzTgmMY1ZzGUhf/IXq/mbTWxjF/s4xDFOcY5LXOMW93jEM17xlk98IYLIcSJF+g9RiU4s4pGIZAQTQgaykIPc5KcQRSlBKGWoQBVqUIf6NKQpLWhNezrTnd70ZzDDGcVYJjKVmcxlIX+ynFWsYyNb2cleDnCEE5zhAle4wR0e8IQXvOEDn4kgIK5+QSBBxCY+iUlOKtKSkazkJA8FKEIJSlGOSlSjJnVpQCOa0oLWtKcT3ehFPwYylJGMYTyTmc5s5rGIpaxgDevZzHZ2s5/DHOMkZ7nIVW5yhwc85jmvec8nvvCNgHgyQVSiE4t4JCIZwaQhPZnIRk5yk59CFKUEoZShPJWpTi3CCKcxzWhJG9rTia70oDf9GcxwRjGWiUxlJnNYwBKWs4p1bGQLO9jDAY5wnNOc5zLXuMU9HvGMV7zlA5+JICC+jBCVH4lBbOKTiGSkJDXpyEQ2fiIvBSlKSUpTnkpUoyZ1qc8vNKE5LWlLR7rSk74MZAjDGcVYJjCFGcxhAUtYzirWsYHNbGMne9jPIY5ygjNc4Ao3uMN9HvOMV7zjE1/j/+8S4Du+JxoxiEMCkpCcVISQnsxkJxf5KEgRihNKGcpTmerUIoxwGtOMlrShA13oQR8GMIQRjGYck5jGLOaygMUs5S9WsZb1bGYbO9nLQY5ygtOc4yJXuckd7vOIp7zkLR/5zL9ETmg9EZXoxCQuCUlCcoIJIQNZyE4u8lKQIhQnlDKUpzLVqUVdGtCQpjSnJW1oTye60oM+9GcwwxjJGMYzmenMZj6LWcZK1rKeTWxlJ3s5yFFOcpaLXOUmd3nIU17ylo984RtREskN0QgiFvFIRDKCCSEDWchBbvJRiGL8TBkqUIUa1KE+DWlKc1rRjk50oxd9GcBghjOKsUxgMtOZzXwWs4yVrOFvNrKVnezlIEc5yVkucpWb3OUBj3nOa97zD/8SObGcEJXoxCIeiUhGMCFkIAs5yE1+ClOcUMpQgcpUpzb1+IUmNKcV7ehEV3rQhwEMYQSjGcckpjGLuSxgMUtZwRrWs5nt7GY/hznOac5zmevc4h6PeMYr3vGJr4n/d6H4HT8QjRjEIQFJSE4q0pKRrOQkDwUoQglKUY5KVKMWYYTTmGa0pC0d6UpP+jKQoYxkDOOZzHRmM5/FLGMla9nAFnawhwMc4QRnuMAVbnCHBzzhBW/4wD9EEJDUrCGQIGITn8QkJxVpyUhWcpKHAhShBKUoRyWqUYswwmlMM1rSlo50pSd9GchQRjKG8UxmOrOZz2KWsZK1bGALO9jDAY5wgjNc4Ao3uMMDnvCCN3zgMxEEJPP/RCU6sYhHIpIRTAgZyEIOcpOfwhQnlLJUpCo1qEN9GtKUFrSmPZ3oRi/6MYhh/M4fTGAKM5jDApawnFWsZQNb2MEeDnCEE5zhApe5zm3u84hnvOQNH/hMBAHJ1YFAgohNfBKRjGBCyEAWcpCb/BSmOKGUpSJVqUldGtCIX/mNNnSgCz3owwAGM5xRjGUCU5jBHBawhOWsYh0b2cpO9nKQo5zkLBe5yk3u8pCnvOQtH/nCN6KkMCOIRgzikIAkpCA16chENn4iLwUpSklKU57KVKc29fiFJjSnFe3oRDd60Y+BDGUkYxjPZKYzi3ksYikrWMN6NrONnexhP4c5zmnOcYmr3OQOD3jCc17zjo98JoKAlPJAIEHEJj6JSEYwIWQgCznITT4KUYySlKYclahGLcIIpzHNaElbOtKVnvRlIEMZyRjGMYlpzGIei1jKCtawns1sZzf7OcxxTnOOi1zhBnd4wBNe8IYPfCaCgGB1IJAgYhOfxCQnFWnJSFZykocCFKEEpShHJapRizDCaURTWtCa9nSmO73pz2CGM4qxTGQqM5nLQv7kL1bzN5vYxi72cYhjnOIcl7jGLe7xiGe84h2f+Mr/P7b5jh/4kZjEJSFJSUka0pOZ7OQiH4Uoxs+UoQJVqEEd6tOQprSgNe3pTHd605/BDGcUY5nIVGYyl4X8yV+s5m82sY1d7OMQxzjFOS5xjVvc4xHPeMlbPvKFbwSklgECCSIW8UhIUlKShvRkIis5yE1+ClOcUMpSkarUpC4NaEgTmtOKdnSiG73oxyCG8Tt/MIEpzGAOC1jCclaxjo1sZSd7OMBhjnOa81zmOre5z2Oe85p3fOQL34iSRo8kGjGIQwKSkJxg0pCOTGTjJ/JQkKKUpDTlqUw1ahFGOI1pRkva0pGu9KAPAxjCCEYzjolMYQZzWMBilvIXq1jLejaxlR3sZh8HOcJxTnGWC1zmGje5w30e8ZQXvOYdH/nMv0QKUSf+S1R+JAaxiUdCkpCcYNKQjoxkITs/kYf8FKIoJQilDOWpRFVqUJswGtCQJjTjN1rTjo50oTu96MsABjOMkYxmLBOYzDRmMof5LOJPlrOSNfzNRrawnV3s5QCHOcZJznCeS1zlBre5xyOe8Yp3fOIr///Q7zt+4EdiEpeEJCUlaUhPZrKTi7wUpCglKU15KlOd2tTjF5rQnFa0oxPd6EU/BjGM3/mDCUxhBnNYwBKWs4p1bGAz29jJHvZziKOc4DTnuMgVrnOLuzzgMc94yRve84kvRBA5ndrwPYFEJyZxiE8ikpKCVISQnkxkJQe5yEsBilCcnylNOSpRjVqEEU5jmtGStnSkKz3py0CGMpIxjGcy05nNfBazjJWsZQNb2MEeDnCEE5zhPJe4yg1uc4+HPOE5r3jLB/7hK98ISO+8SzRiEIf4JCY5wYSQgSxkJxf5KEQxSlKKclSkCjWoQ30a0pQWtKY9nehGL/oxiKGMYDTjmMQ0ZjGPRSxlBWtYz2a2sYt9HOIYpzjHJa5ygzs84AkveMMHPhNBQAa1ICrRiUU8EpKUFKQmHZnIRk5yk59CFONnylCBKtSgNvUIpzHNaElbOtCF7vSmP4MZzijGMoEpzGAOC1jCclaxjo1sZSd7OchRTnKWC1zhBnd4wBNe8IYPfCaCgIx6NIEEEZv4JCYZwaQhPZnJTi7yUZjihFKWilSlJnVpQEOa0oJWtKUjXehBH/oziKGMZAzjmMhUZjKHBSxhGStYzTo2soXt7GY/hznOac5zmWvc4h4PecpL3vKBz/xL5EzyQVSiE5O4JCQJyQkmhAxkIQe5yU9hihNKWSpSlZrUpQGN+JXfaEN7OtOd3vRjEMMYyRjGM5npzGY+i1nGStaygS3sYA/7OcxxTnOey1znNvd5zHNe855/+JfImdWBqEQnFvFISDJSkpq0ZCQrOclDAYpQglKUoxLVqEUY4TSiKc1pSRs60IUe9GEAQxjBaMYxiWnMYh6LWMoK1rCezWxnN/s5zHFOcY5LXOMW93jEU17ylo984RtRspgxRCMGcUhAElKQmnRkIhs/kZeCFKUkpSlPZapTm3r8QhOa04p2dKIbPenLQIYygtGMYxJTmclcFrKE5axiHRvZyk72coAjnOAMF7jCDe7wgCe84DXv+MRXImU1a/meaAQRm/gkJhkpSUM6MpKVnOShAEUoQSnKUpEq1KA2YYTTiKY0pyVtaE9nutOLfgxiGL/zB+OZxFRmMJt5LGIpK1jDejaznd3s4xDHOMU5LnGNm9zlIU95yRs+8JkIImezVohKdGIRj4QkIQWpSUcmspKDXOSlIEUoQShlqEAValCH+jSkKS1oTXs60Y2e9GUAgxnOKP5gAlOYwRwWsIRlrGAN69nENnaxj0Mc4xTnuMRVbnCbezzkCc95xVs+8oVvRMkuMwQSRCzikpCkpCQN6cjI/wEqE4o9
-  </AppendedData>
-</VTKFile>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/main.cxx f3d-3.4.1/examples/libf3d/cpp/use-options-string/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-string/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-string/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,4 @@
 #include <f3d/engine.h>
-#include <f3d/image.h>
 #include <f3d/log.h>
 #include <f3d/options.h>
 #include <f3d/window.h>
@@ -7,7 +6,7 @@
 int main(int argc, char** argv)
 {
   // Check inputs
-  if (argc != 2)
+  if (argc < 2)
   {
     return EXIT_FAILURE;
   }
@@ -36,10 +35,24 @@
 
   // Start interaction and stop it after one second
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
-
-  // Actual call would look like this
-  // inter.start();
 
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << '\n';
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/use-options-struct/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-struct/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,11 +7,3 @@
 add_executable(use-options-struct main.cxx)
 target_link_libraries(use-options-struct f3d::libf3d)
 set_target_properties(use-options-struct PROPERTIES CXX_STANDARD 17)
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_use-options-struct COMMAND "$<TARGET_FILE:use-options-struct>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp")
-  set_tests_properties(test_use-options-struct PROPERTIES
-    PASS_REGULAR_EXPRESSION "Number of points: 2903")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/use-options-struct/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/data/cow.vtp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-struct/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
@@ -1,33 +0,0 @@
-<?xml version="1.0"?>
-<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor">
-  <PolyData>
-    <Piece NumberOfPoints="2903"                 NumberOfVerts="0"                    NumberOfLines="0"                    NumberOfStrips="0"                    NumberOfPolys="3263"                >
-      <PointData>
-      </PointData>
-      <CellData>
-      </CellData>
-      <Points>
-        <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="1.3772239036"         RangeMax="6.1258021248"         offset="0"                   />
-      </Points>
-      <Verts>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29520"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29536"               />
-      </Verts>
-      <Lines>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29552"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29568"               />
-      </Lines>
-      <Strips>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29584"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29600"               />
-      </Strips>
-      <Polys>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29616"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="52104"               />
-      </Polys>
-    </Piece>
-  </PolyData>
-  <AppendedData encoding="base64">
-   _AgAAAACAAAAUCAAAC08AAFoHAAA=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efyQx0ceH3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8ZHHD3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMev5LHD3n8kMcPefyQxw95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMeH3l85PGRxw95/JDHD3n8kMcPefyQxw95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PGRx0ceP+TxQx4/5PFDHj/k8UMeP+TxQx4feXzk8ZHHD3l85PFDHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHRx4feXzk8ZHHRx4fefyQxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQxw95/JDHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8UMeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHD3n8kMcPeXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHr+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3n8Sh4/5PFDHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/EoeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcEPGfyQwUcGHxl8ZPCRwUcGHxl8ZPCRwUcGHxl8ZPCRuw+5+5C7D7n7kLsPufuQu0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuPuTuQ+4euXvk7pG7R+4+5O5D7j7k7kPuHrl75O6Ru0fuHrn7kLsPuXvk7pG7D7n7kLsPuXvk7pG7R+4euXvk7pG7D7n7kLsPufuQuw+5+5C7D7n7kLtH7j7k7pG7D7l75O5D7h65+5C7R+4+5O5D7j7k7kPuPuTuQ+4+5O5D7j7k7kPuPuTukbtH7h65e+TukbtH7h65e+TukbtH7h65+5C7D7l75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O5D7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+Tu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AQAAAACAAAD8MgAAGBIAAA==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
-  </AppendedData>
-</VTKFile>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/main.cxx f3d-3.4.1/examples/libf3d/cpp/use-options-struct/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-struct/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-struct/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,4 @@
 #include <f3d/engine.h>
-#include <f3d/image.h>
 #include <f3d/log.h>
 #include <f3d/options.h>
 #include <f3d/window.h>
@@ -7,7 +6,7 @@
 int main(int argc, char** argv)
 {
   // Check inputs
-  if (argc != 2)
+  if (argc < 2)
   {
     return EXIT_FAILURE;
   }
@@ -36,10 +35,25 @@
 
   // Start interaction and stop it after one second
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
 
-  // Actual call would look like this
-  // inter.start();
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << '\n';
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
 
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/CMakeLists.txt f3d-3.4.1/examples/libf3d/cpp/use-options-variant/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-variant/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,11 +7,3 @@
 add_executable(use-options-variant main.cxx)
 target_link_libraries(use-options-variant f3d::libf3d)
 set_target_properties(use-options-variant PROPERTIES CXX_STANDARD 17)
-
-# Simple testing
-if(BUILD_TESTING)
-  enable_testing()
-  add_test(NAME test_use-options-variant COMMAND "$<TARGET_FILE:use-options-variant>" "${CMAKE_CURRENT_SOURCE_DIR}/data/cow.vtp")
-  set_tests_properties(test_use-options-variant PROPERTIES
-    PASS_REGULAR_EXPRESSION "Number of points: 2903")
-endif()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/data/cow.vtp f3d-3.4.1/examples/libf3d/cpp/use-options-variant/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/data/cow.vtp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-variant/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
@@ -1,33 +0,0 @@
-<?xml version="1.0"?>
-<VTKFile type="PolyData" version="0.1" byte_order="LittleEndian" compressor="vtkZLibDataCompressor">
-  <PolyData>
-    <Piece NumberOfPoints="2903"                 NumberOfVerts="0"                    NumberOfLines="0"                    NumberOfStrips="0"                    NumberOfPolys="3263"                >
-      <PointData>
-      </PointData>
-      <CellData>
-      </CellData>
-      <Points>
-        <DataArray type="Float32" Name="Points" NumberOfComponents="3" format="appended" RangeMin="1.3772239036"         RangeMax="6.1258021248"         offset="0"                   />
-      </Points>
-      <Verts>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29520"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29536"               />
-      </Verts>
-      <Lines>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29552"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29568"               />
-      </Lines>
-      <Strips>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29584"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="29600"               />
-      </Strips>
-      <Polys>
-        <DataArray type="Int32" Name="connectivity" format="appended" RangeMin=""                     RangeMax=""                     offset="29616"               />
-        <DataArray type="Int32" Name="offsets" format="appended" RangeMin=""                     RangeMax=""                     offset="52104"               />
-      </Polys>
-    </Piece>
-  </PolyData>
-  <AppendedData encoding="base64">
-   _AgAAAACAAAAUCAAAC08AAFoHAAA=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efyQx0ceH3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8ZHHD3n8kMcPefyQxw95/EoeP+TxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMev5LHD3n8kMcPefyQxw95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PFDHj/k8UMeH3l85PGRxw95/JDHD3n8kMcPefyQxw95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8UMeP+TxQx4/5PGRx0ceP+TxQx4/5PFDHj/k8UMeP+TxQx4feXzk8ZHHD3l85PFDHj/k8UMeP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/JDHRx4feXzk8ZHHRx4fefyQxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3n8kMdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxQx4/5PFDHj/k8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQxw95/JDHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8UMeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHD3n8kMcPeXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHr+TxQx4/5PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRxw95/JDHD3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceH3n8Sh4/5PFDHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95/EoeP+TxQx4feXzk8ZHHRx4feXzk8ZHHRx4feXzk8ZHHRx4fefyQx0ceH3l85PGRx0ceH3l85PGRx0ceH3l85PGRx0ceP+TxkcdHHh95fOTxkcdHHh95fOTxkcdHHh95fOTxkcEPGfyQwUcGHxl8ZPCRwUcGHxl8ZPCRwUcGHxl8ZPCRuw+5+5C7D7n7kLsPufuQu0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuPuTuQ+4euXvk7pG7R+4+5O5D7j7k7kPuHrl75O6Ru0fuHrn7kLsPuXvk7pG7D7n7kLsPuXvk7pG7R+4euXvk7pG7D7n7kLsPufuQuw+5+5C7D7n7kLtH7j7k7pG7D7l75O5D7h65+5C7R+4+5O5D7j7k7kPuPuTuQ+4+5O5D7j7k7kPuPuTukbtH7h65e+TukbtH7h65e+TukbtH7h65+5C7D7l75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O6Ru0fuHrl75O5D7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+TukbtH7h65e+Tu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AQAAAACAAAD8MgAAGBIAAA==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
-  </AppendedData>
-</VTKFile>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/main.cxx f3d-3.4.1/examples/libf3d/cpp/use-options-variant/main.cxx
--- f3d-3.2.0+dfsg/examples/libf3d/cpp/use-options-variant/main.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/cpp/use-options-variant/main.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,4 @@
 #include <f3d/engine.h>
-#include <f3d/image.h>
 #include <f3d/log.h>
 #include <f3d/options.h>
 #include <f3d/window.h>
@@ -7,7 +6,7 @@
 int main(int argc, char** argv)
 {
   // Check inputs
-  if (argc != 2)
+  if (argc < 2)
   {
     return EXIT_FAILURE;
   }
@@ -34,10 +33,24 @@
 
   // Start interaction and stop it after one second
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
-
-  // Actual call would look like this
-  // inter.start();
+  if (argc > 2)
+  {
+    // For testing purposes only, shutdown the example after `timeout` seconds
+    try
+    {
+      int timeout = std::stoi(argv[2]);
+      inter.start(timeout, [&inter]() { inter.stop(); });
+    }
+    catch (const std::exception& e)
+    {
+      std::cout << e.what() << '\n';
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    inter.start();
+  }
 
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/java/CMakeLists.txt f3d-3.4.1/examples/libf3d/java/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/java/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/java/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.10)
+
+add_subdirectory(interactive-app)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/java/interactive-app/CMakeLists.txt f3d-3.4.1/examples/libf3d/java/interactive-app/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/libf3d/java/interactive-app/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/java/interactive-app/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,34 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(interactive-app-java)
+
+find_package(Java REQUIRED)
+include(UseJava)
+
+find_package(f3d REQUIRED COMPONENTS library)
+
+find_file(F3D_JAR
+  NAMES f3d.jar
+  PATHS
+  ${f3d_DIR}/../../../share/java
+  ${CMAKE_INSTALL_PREFIX}/share/java
+  /usr/share/java
+  /usr/local/share/java
+  REQUIRED
+)
+
+message(STATUS "Found f3d.jar: ${F3D_JAR}")
+
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/MANIFEST.MF"
+  "Manifest-Version: 1.0\n"
+  "Main-Class: InteractiveApp\n"
+  "Class-Path: ${F3D_JAR}\n"
+)
+
+set(CMAKE_JAVA_COMPILE_FLAGS -source 8 -target 8)
+add_jar(interactive-app-java
+  InteractiveApp.java
+  INCLUDE_JARS ${F3D_JAR}
+  OUTPUT_NAME interactive-app
+  MANIFEST "${CMAKE_CURRENT_BINARY_DIR}/MANIFEST.MF"
+)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/java/interactive-app/InteractiveApp.java f3d-3.4.1/examples/libf3d/java/interactive-app/InteractiveApp.java
--- f3d-3.2.0+dfsg/examples/libf3d/java/interactive-app/InteractiveApp.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/java/interactive-app/InteractiveApp.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,69 @@
+import app.f3d.F3D.*;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class InteractiveApp {
+
+    static {
+        if (System.getProperty("os.name").startsWith("Windows")) {
+            System.loadLibrary("opengl32");
+        }
+    }
+
+    public static void main(String[] args) {
+        if (args.length < 1) {
+            System.err.println("Usage: InteractiveApp <file> [timeout]");
+            System.exit(1);
+        }
+
+        String file = args[0];
+
+        Engine.autoloadPlugins();
+
+        try (Engine engine = Engine.create(false)) {
+            Options options = engine.getOptions();
+
+            options.setAsBool("render.grid.enable", true);
+            options.setAsBool("render.show_edges", true);
+
+            options.setAsBool("ui.axis", true);
+            options.setAsBool("ui.fps", true);
+            options.setAsBool("ui.animation_progress", true);
+            options.setAsBool("ui.filename", true);
+
+            Scene scene = engine.getScene();
+            try {
+                scene.add(file);
+            } catch (Exception e) {
+                System.err.println("Failed to load file: " + file);
+                System.err.println(e.getMessage());
+                System.exit(1);
+            }
+
+            Window window = engine.getWindow();
+            window.render();
+
+            Interactor interactor = engine.getInteractor();
+
+            if (args.length > 1) {
+                // For testing purposes, stop after timeout seconds
+                int timeout = Integer.parseInt(args[1]);
+                Timer timer = new Timer();
+                timer.schedule(new TimerTask() {
+                    @Override
+                    public void run() {
+                        interactor.requestStop();
+                        timer.cancel();
+                    }
+                }, timeout * 1000L);
+                interactor.start();
+            } else {
+                interactor.start();
+            }
+        } catch (Exception e) {
+            System.err.println("Error: " + e.getMessage());
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/check-engine/check_engine.py f3d-3.4.1/examples/libf3d/python/check-engine/check_engine.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/check-engine/check_engine.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/check-engine/check_engine.py	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,10 @@
 import f3d
 
-if __name__ == "__main__":
-    eng = f3d.Engine.create_none()
+
+def main():
+    _eng = f3d.Engine.create_none()
     print("F3D engine is loaded")
+
+
+if __name__ == "__main__":
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/check-engine/test_check_engine.py f3d-3.4.1/examples/libf3d/python/check-engine/test_check_engine.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/check-engine/test_check_engine.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/check-engine/test_check_engine.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,5 @@
+from check_engine import main
+
+
+def test_check_engine():
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/img-cmp.py f3d-3.4.1/examples/libf3d/python/img-cmp/img-cmp.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/img-cmp.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/img-cmp/img-cmp.py	1970-01-01 02:00:00.000000000 +0200
@@ -1,19 +0,0 @@
-from argparse import ArgumentParser
-
-import f3d
-
-if __name__ == "__main__":
-    argparser = ArgumentParser()
-    argparser.add_argument("image1")
-    argparser.add_argument("image2")
-
-    args = argparser.parse_args()
-
-    img1 = f3d.Image(args.image1)
-    img2 = f3d.Image(args.image2)
-
-    error = img1.compare(img2)
-    if error > 0.05:
-        print(f"Images are different ({error=})")
-    else:
-        print(f"Images are identical ({error=})")
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/img_cmp.py f3d-3.4.1/examples/libf3d/python/img-cmp/img_cmp.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/img_cmp.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/img-cmp/img_cmp.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,27 @@
+import sys
+from argparse import ArgumentParser
+
+import f3d
+
+
+def main(argv: list[str] | None = None):
+    argparser = ArgumentParser()
+    argparser.add_argument("image1")
+    argparser.add_argument("image2")
+
+    args = argparser.parse_args(argv)
+
+    img1 = f3d.Image(args.image1)
+    img2 = f3d.Image(args.image2)
+
+    error = img1.compare(img2)
+    if error > 0.05:
+        print(f"Images are different ({error=})")
+        return 1
+
+    print(f"Images are identical ({error=})")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/test_img_cmp.py f3d-3.4.1/examples/libf3d/python/img-cmp/test_img_cmp.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/img-cmp/test_img_cmp.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/img-cmp/test_img_cmp.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+from img_cmp import main
+
+
+def test_img_cmp():
+    img = Path(__file__).parent.parent / "tests" / "baselines" / "cow.png"
+    assert main([str(img), str(img)]) == 0
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/interactive-app/interactive_app.py f3d-3.4.1/examples/libf3d/python/interactive-app/interactive_app.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/interactive-app/interactive_app.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/interactive-app/interactive_app.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,243 @@
+import re
+from argparse import ArgumentParser
+
+import f3d
+
+
+def main(argv: list[str] | None = None):
+    parser = ArgumentParser()
+    parser.add_argument("file")
+    parser.add_argument(
+        "--timeout",
+        type=int,
+        help="Optional timeout (in seconds) before closing the viewer.",
+    )
+
+    args = parser.parse_args(argv)
+
+    f3d.Engine.autoload_plugins()
+
+    # Create a native-window engine
+    eng = f3d.Engine.create(False)
+
+    # Modify options using the struct-like API
+    opt = eng.options
+    opt.update(
+        {
+            "render.grid.enable": True,
+            "render.show_edges": True,
+            # UI overlays: axis + some HUD
+            "ui.axis": True,
+            "ui.fps": True,
+            "ui.animation_progress": True,
+            "ui.filename": True,
+            # FXAA + tone mapping
+            "render.effect.antialiasing.enable": True,
+            "render.effect.antialiasing.mode": "fxaa",
+            "render.effect.tone_mapping": True,
+        }
+    )
+
+    add_custom_commands(eng)
+    add_custom_bindings(eng)
+
+    # Add a model to the scene
+    try:
+        eng.scene.add(args.file)
+    except Exception as e:
+        print(e)
+        return 1
+
+    # Initial render
+    win = eng.window
+    win.render()
+
+    if args.timeout:
+        # For testing purposes only, exit after `timeout` seconds
+        eng.interactor.start(args.timeout, eng.interactor.stop)
+    else:
+        eng.interactor.start()
+
+
+def parse_size(size: str):
+    if m := re.fullmatch(r"(\d+)([,x](\d+))?", size):
+        w = int(m.group(1))
+        h = int(m.group(3)) if m.group(3) else w
+        return w, h
+    else:
+        raise ValueError(f"cannot parse size from {size!r}")
+
+
+def add_custom_commands(eng: f3d.Engine):
+    # Get the interactor
+    inter = eng.interactor
+    inter.init_commands()
+
+    # Reset options to initial state
+    opt = eng.options
+
+    # Keep a copy of the initial options
+    initial_options = dict(opt)
+
+    def reset_options(_args: list[str]):
+        opt.update(initial_options)
+
+    inter.add_command(
+        "reset_options",
+        reset_options,
+        ("reset_options", "Reset basic render/UI options"),
+    )
+
+    # Increase animation speed factor
+    def increase_animation_speed_factor(_args: list[str]):
+        val = opt["scene.animation.speed_factor"]
+        opt["scene.animation.speed_factor"] = val + 0.05
+
+    inter.add_command(
+        "increase_animation_speed_factor",
+        increase_animation_speed_factor,
+        ("increase_animation_speed_factor", "Increase animation speed factor"),
+    )
+
+    # Decrease animation speed factor
+    def decrease_animation_speed_factor(_args: list[str]):
+        val = opt["scene.animation.speed_factor"]
+        opt["scene.animation.speed_factor"] = val - 0.05
+
+    inter.add_command(
+        "decrease_animation_speed_factor",
+        decrease_animation_speed_factor,
+        ("decrease_animation_speed_factor", "Decrease animation speed factor"),
+    )
+
+    # Toggle grid visibility
+    inter.remove_command("toggle_grid")
+
+    def toggle_grid(_args: list[str]):
+        opt["render.grid.enable"] = not opt["render.grid.enable"]
+
+    inter.add_command(
+        "toggle_grid",
+        toggle_grid,
+        ("toggle_grid", "Toggle ground grid visibility"),
+    )
+
+    # Toggle axis
+    inter.remove_command("toggle_axis")
+
+    def toggle_axis(_args: list[str]):
+        opt["ui.axis"] = not opt["ui.axis"]
+
+    inter.add_command(
+        "toggle_axis",
+        toggle_axis,
+        ("toggle_axis", "Toggle axis"),
+    )
+
+    # Toggle FXAA anti-aliasing
+    inter.remove_command("toggle_fxaa")
+
+    def toggle_fxaa(_):
+        opt["render.effect.antialiasing.enable"] = not opt[
+            "render.effect.antialiasing.enable"
+        ]
+        # keep mode at fxaa in this example
+        opt["render.effect.antialiasing.mode"] = "fxaa"
+
+    inter.add_command(
+        "toggle_fxaa",
+        toggle_fxaa,
+        ("toggle_fxaa", "Toggle FXAA anti-aliasing"),
+    )
+
+
+def add_custom_bindings(eng: f3d.Engine):
+    # Get the interactor
+    inter = eng.interactor
+    inter.init_bindings()
+
+    opt = eng.options
+
+    # Helpers for documentation
+    def docTgl(doc: str, key: str):
+        return doc, "ON" if opt[key] else "OFF"
+
+    def docStr(doc: str):
+        return doc, ""
+
+    def docDblOpt(doc: str, key: str):
+        val = opt[key]
+        try:
+            return doc, f"{val:.2f}"
+        except (ValueError, TypeError):
+            return doc, str(val)
+
+    # R: reset options
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "R"),
+        "reset_options",
+        "Example",
+        lambda: docStr("Reset Options"),
+        f3d.Interactor.BindingType.OTHER,
+    )
+
+    # SHIFT + S: Increase animation speed
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.SHIFT, "S"),
+        "increase_animation_speed_factor",
+        "Example",
+        lambda: docDblOpt("Increase animation speed", "scene.animation.speed_factor"),
+        f3d.Interactor.BindingType.NUMERICAL,
+    )
+
+    # SHIFT + S: Increase animation speed
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.CTRL, "S"),
+        "decrease_animation_speed_factor",
+        "Example",
+        lambda: docDblOpt("Decrease animation speed", "scene.animation.speed_factor"),
+        f3d.Interactor.BindingType.NUMERICAL,
+    )
+
+    # G: toggle grid
+    inter.remove_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "G"),
+    )
+
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "G"),
+        "toggle_grid",
+        "Example",
+        lambda: docTgl("Toggle grid", "render.grid.enable"),
+        f3d.Interactor.BindingType.TOGGLE,
+    )
+
+    # X: toggle axis
+    inter.remove_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "X"),
+    )
+
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "X"),
+        "toggle_axis",
+        "Example",
+        lambda: docTgl("Toggle axis", "ui.axis"),
+        f3d.Interactor.BindingType.TOGGLE,
+    )
+
+    # F: toggle FXAA
+    inter.remove_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "F"),
+    )
+
+    inter.add_binding(
+        f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.NONE, "F"),
+        "toggle_fxaa",
+        "Example",
+        lambda: docTgl("Toggle FXAA", "render.effect.antialiasing.enable"),
+        f3d.Interactor.BindingType.TOGGLE,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/interactive-app/test_interactive_app.py f3d-3.4.1/examples/libf3d/python/interactive-app/test_interactive_app.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/interactive-app/test_interactive_app.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/interactive-app/test_interactive_app.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+from interactive_app import main
+
+
+def test_interactive_app():
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+    main([str(file), "--timeout", "1"])
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/offscreen-thumbnail/offscreen_thumbnail.py f3d-3.4.1/examples/libf3d/python/offscreen-thumbnail/offscreen_thumbnail.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/offscreen-thumbnail/offscreen_thumbnail.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/offscreen-thumbnail/offscreen_thumbnail.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,75 @@
+import re
+import sys
+from argparse import ArgumentParser
+
+import f3d
+
+
+def main(argv: list[str] | None = None):
+    parser = ArgumentParser()
+    parser.add_argument("inputFile")
+    parser.add_argument("outputFile")
+    parser.add_argument("size", nargs="?", type=parse_size, default=(512, 512))
+
+    args = parser.parse_args(argv)
+
+    # Load static/native plugins
+    f3d.Engine.autoload_plugins()
+
+    # Create an offscreen engine
+    eng = f3d.Engine.create(True)
+
+    opt = eng.options
+
+    # No UI overlays in thumbnails
+    opt["ui.axis"] = False
+    opt["ui.fps"] = False
+    opt["ui.filename"] = False
+    opt["ui.metadata"] = False
+    opt["ui.console"] = False
+    opt["ui.cheatsheet"] = False
+
+    # Neutral background, no grid or skybox
+    opt["render.grid.enable"] = False
+    opt["render.background.skybox"] = False
+    opt["render.background.color"] = [0.15, 0.15, 0.15]  # dark neutral gray
+
+    # Slightly stronger lighting so assets read well at small sizes
+    opt["render.light.intensity"] = 1.2
+
+    # Post-processing: AA + AO for better thumbnails
+    opt["render.effect.antialiasing.enable"] = True
+    opt["render.effect.antialiasing.mode"] = "ssaa"
+    opt["render.effect.ambient_occlusion"] = True
+
+    try:
+        # Add a model to the scene
+        eng.scene.add(args.inputFile)
+    except Exception as e:
+        print(e)
+        return 1
+
+    # Render offscreen to an image
+    eng.window.size = args.size[0], args.size[1]
+
+    # You can make the background transparent by setting no_background to True in the
+    # render_to_image call.
+    img = eng.window.render_to_image(False)
+
+    # Save the resulting image
+    img.save(args.outputFile)
+
+    return 0
+
+
+def parse_size(size: str):
+    if m := re.fullmatch(r"(\d+)([,x](\d+))?", size):
+        w = int(m.group(1))
+        h = int(m.group(3)) if m.group(3) else w
+        return w, h
+    else:
+        raise ValueError(f"cannot parse size from {size!r}")
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/offscreen-thumbnail/test_offscreen_thumbnail.py f3d-3.4.1/examples/libf3d/python/offscreen-thumbnail/test_offscreen_thumbnail.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/offscreen-thumbnail/test_offscreen_thumbnail.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/offscreen-thumbnail/test_offscreen_thumbnail.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,14 @@
+from pathlib import Path
+
+import f3d
+
+from offscreen_thumbnail import main
+
+
+def test_offscreen_thumbnail(tmp_path: Path):
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+    img = tmp_path / "thumbnail.png"
+    assert main([str(file), str(img), "256"]) == 0
+    assert img.exists()
+    thumb = f3d.Image(img)
+    assert (thumb.width, thumb.height) == (256, 256)
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/qml/Main.qml f3d-3.4.1/examples/libf3d/python/qml/Main.qml
--- f3d-3.2.0+dfsg/examples/libf3d/python/qml/Main.qml	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/qml/Main.qml	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,73 @@
+import QtQuick 6.5
+import QtQuick.Window 6.5
+import F3D 1.0
+
+Window {
+    id: root
+    width: 800
+    height: 600
+    visible: true
+    title: "libf3d + QML"
+    color: "#202020"
+
+    property string modelPath: ""
+    Component.onCompleted: {
+        if (typeof initialModelPath === "string") {
+            modelPath = initialModelPath
+        }
+    }
+
+    F3DView {
+        id: view
+        anchors.fill: parent
+        focus: true
+
+        modelPath: root.modelPath
+
+        Keys.enabled: true
+        Keys.onPressed: function (event) {
+            view.keyPress(event.key, event.text, event.modifiers)
+            event.accepted = true
+        }
+
+        Keys.onReleased: function (event) {
+            view.keyRelease(event.key, event.modifiers)
+            event.accepted = true
+        }
+
+        MouseArea {
+            anchors.fill: parent
+            hoverEnabled: true
+            acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+
+            onPressed: function (mouse) {
+                view.mousePress(mouse.x, mouse.y,
+                    mouse.button, mouse.modifiers)
+                mouse.accepted = true
+            }
+
+            onReleased: function (mouse) {
+                view.mouseRelease(mouse.x, mouse.y,
+                    mouse.button, mouse.modifiers)
+                mouse.accepted = true
+            }
+
+            onPositionChanged: function (mouse) {
+                view.mouseMove(mouse.x, mouse.y,
+                    mouse.buttons, mouse.modifiers)
+            }
+        }
+
+
+        WheelHandler {
+            id: wheelHandler
+            target: view
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+
+            onWheel: function (event) {
+                view.mouseWheel(event.angleDelta.x, event.angleDelta.y, event.modifiers)
+                event.accepted = true
+            }
+        }
+    }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/qml/minimal_qml.py f3d-3.4.1/examples/libf3d/python/qml/minimal_qml.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/qml/minimal_qml.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/qml/minimal_qml.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,317 @@
+import sys
+from argparse import ArgumentParser
+from pathlib import Path
+from collections import deque
+
+import f3d
+
+from PySide6.QtGui import QGuiApplication, QOpenGLContext
+from PySide6.QtCore import (
+    QTimer,
+    QCoreApplication,
+    Property,
+    Slot,
+    Qt,
+)
+from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
+from PySide6.QtQuick import QQuickFramebufferObject, QQuickItem
+
+
+def main(argv: list[str] | None = None) -> int:
+    if argv is None:
+        argv = sys.argv
+
+    parser = ArgumentParser()
+    parser.add_argument("file", help="3D model file to open")
+    parser.add_argument(
+        "--timeout",
+        type=int,
+        help="Optional timeout (in seconds) before closing the viewer.",
+    )
+
+    # Let argparse handle our options, and leave the rest to Qt
+    args, qt_args = parser.parse_known_args(argv[1:])
+
+    model_path = Path(args.file)
+    if not model_path.exists():
+        print(f"File not found: {model_path}")
+        return 1
+
+    app = QGuiApplication([argv[0]] + qt_args)
+
+    qmlRegisterType(F3DView, "F3D", 1, 0, "F3DView")
+
+    engine = QQmlApplicationEngine()
+    # expose initial model path to QML
+    engine.rootContext().setContextProperty("initialModelPath", str(model_path))
+
+    qml_file = Path(__file__).parent / "Main.qml"
+    engine.load(qml_file.as_uri())
+
+    if not engine.rootObjects():
+        print("Failed to load QML")
+        return 1
+
+    # For testing purposes only, shutdown the example after `timeout` seconds
+    if args.timeout:
+        QTimer.singleShot(args.timeout * 1000, QCoreApplication.quit)
+
+    return app.exec()
+
+
+class F3DView(QQuickFramebufferObject):
+    """
+    QML-exposed item that uses libf3d to render a 3D model
+    inside Qt Quick via QQuickFramebufferObject, with event
+    forwarding to libf3d::interactor.
+    """
+
+    def __init__(self, parent: QQuickItem | None = None):
+        super().__init__(parent)
+        self._model_path: str = ""
+        self._renderer: F3DRenderer | None = None
+
+        self.setMirrorVertically(True)
+        # Request depth buffer in the texture format
+        self.setTextureFollowsItemSize(True)
+
+    @Property(str)
+    def modelPath(self) -> str:
+        return self._model_path
+
+    @modelPath.setter
+    def modelPath(self, path: str) -> None:
+        self._model_path = path
+
+    def createRenderer(self):
+        self._renderer = F3DRenderer(self)
+        return self._renderer
+
+    def releaseResources(self):
+        if self._renderer is not None:
+            self._renderer = None
+
+    # Mouse
+    @Slot(float, float, int, int)
+    def mousePress(self, x: float, y: float, button: int, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_mouse_press(x, y, button, modifiers)
+
+    @Slot(float, float, int, int)
+    def mouseMove(self, x: float, y: float, buttons: int, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_mouse_move(x, y, buttons, modifiers)
+
+    @Slot(float, float, int, int)
+    def mouseRelease(self, x: float, y: float, button: int, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_mouse_release(x, y, button, modifiers)
+
+    # Wheel
+    @Slot(int, int, int)
+    def mouseWheel(self, delta_x: int, delta_y: int, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_wheel(delta_x, delta_y, modifiers)
+
+    # Keyboard
+    @Slot(int, str, int)
+    def keyPress(self, key: int, text: str, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_key_press(key, text, modifiers)
+
+    @Slot(int, int)
+    def keyRelease(self, key: int, modifiers: int) -> None:
+        if self._renderer is not None:
+            self._renderer.queue_key_release(key, modifiers)
+
+
+class F3DRenderer(QQuickFramebufferObject.Renderer):
+    """
+    Actual OpenGL renderer for F3DView.
+    """
+
+    def __init__(self, item: F3DView):
+        super().__init__()
+        self.item = item
+
+        f3d.Engine.autoload_plugins()
+
+        self.engine = f3d.Engine.create_external(
+            QOpenGLContext.currentContext().getProcAddress
+        )
+
+        if self.item.modelPath:
+            self.engine.scene.add(self.item.modelPath)
+
+        win = self.engine.window
+        win.size = (int(self.item.width()), int(self.item.height()))
+
+        opt = self.engine.options
+        opt.update(
+            {
+                "render.grid.enable": True,
+                "render.show_edges": False,
+                "ui.axis": True,
+                "ui.fps": True,
+                "ui.filename": True,
+                "render.background.color": [0.15, 0.15, 0.15],
+            }
+        )
+
+        self.interactor = self.engine.interactor
+
+        self._events = deque()
+
+    def queue_mouse_press(self, x: float, y: float, button: int, modifiers: int):
+        self._events.append(("mouse_press", (x, y, button, modifiers)))
+        self.update()
+
+    def queue_mouse_move(self, x: float, y: float, buttons: int, modifiers: int):
+        self._events.append(("mouse_move", (x, y, buttons, modifiers)))
+        self.update()
+
+    def queue_mouse_release(self, x: float, y: float, button: int, modifiers: int):
+        self._events.append(("mouse_release", (x, y, button, modifiers)))
+        self.update()
+
+    def queue_wheel(self, dx: int, dy: int, modifiers: int):
+        self._events.append(("wheel", (dx, dy, modifiers)))
+        self.update()
+
+    def queue_key_press(self, key: int, text: str, modifiers: int):
+        self._events.append(("key_press", (key, text, modifiers)))
+        self.update()
+
+    def queue_key_release(self, key: int, modifiers: int):
+        self._events.append(("key_release", (key, modifiers)))
+        self.update()
+
+    # --------------------------
+    # Event mapping helpers
+    # --------------------------
+
+    def _update_modifiers(self, mods: int) -> None:
+        inter = self.interactor
+        m = Qt.KeyboardModifier(mods)
+
+        if (m & Qt.KeyboardModifier.ControlModifier) and (
+            m & Qt.KeyboardModifier.ShiftModifier
+        ):
+            inter.trigger_mod_update(inter.InputModifier.CTRL_SHIFT)
+        elif m & Qt.KeyboardModifier.ControlModifier:
+            inter.trigger_mod_update(inter.InputModifier.CTRL)
+        elif m & Qt.KeyboardModifier.ShiftModifier:
+            inter.trigger_mod_update(inter.InputModifier.SHIFT)
+        else:
+            inter.trigger_mod_update(inter.InputModifier.NONE)
+
+    def _map_mouse_button(self, button: int):
+        inter = self.interactor
+
+        b = Qt.MouseButton(button)
+        if b == Qt.MouseButton.LeftButton:
+            return inter.MouseButton.LEFT
+        if b == Qt.MouseButton.RightButton:
+            return inter.MouseButton.RIGHT
+        if b == Qt.MouseButton.MiddleButton:
+            return inter.MouseButton.MIDDLE
+        return None
+
+    def _key_sym_from_key_and_text(self, key: int, text: str) -> str:
+        # A–Z
+        if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
+            return chr(key).upper()
+
+        special = {
+            Qt.Key.Key_Backspace: "BackSpace",
+            Qt.Key.Key_Escape: "Escape",
+            Qt.Key.Key_Return: "Return",
+            Qt.Key.Key_Enter: "Return",
+            Qt.Key.Key_Space: "Space",
+            Qt.Key.Key_Tab: "Tab",
+        }
+        if key in special:
+            return special[key]
+
+        if text:
+            return text
+        return ""
+
+    # --------------------------
+    # Main render function
+    # --------------------------
+
+    def render(self) -> None:
+        win = self.engine.window
+        win.size = (int(self.item.width()), int(self.item.height()))
+
+        inter = self.interactor
+
+        while self._events:
+            kind, data = self._events.popleft()
+
+            if kind == "mouse_press":
+                x, y, button, mods = data
+                self._update_modifiers(mods)
+                mb = self._map_mouse_button(button)
+                if mb is not None:
+                    inter.trigger_mouse_position(x, y)
+                    inter.trigger_mouse_button(inter.InputAction.PRESS, mb)
+
+            elif kind == "mouse_move":
+                x, y, buttons, mods = data
+                self._update_modifiers(mods)
+                inter.trigger_mouse_position(x, y)
+
+            elif kind == "mouse_release":
+                x, y, button, mods = data
+                self._update_modifiers(mods)
+                mb = self._map_mouse_button(button)
+                if mb is not None:
+                    inter.trigger_mouse_button(inter.InputAction.RELEASE, mb)
+                    inter.trigger_mouse_position(x, y)
+
+            elif kind == "wheel":
+                dx, dy, mods = data
+                self._update_modifiers(mods)
+
+                if abs(dy) >= abs(dx):
+                    direction = (
+                        inter.WheelDirection.FORWARD
+                        if dy > 0
+                        else inter.WheelDirection.BACKWARD
+                    )
+                else:
+                    direction = (
+                        inter.WheelDirection.RIGHT
+                        if dx > 0
+                        else inter.WheelDirection.LEFT
+                    )
+                inter.trigger_mouse_wheel(direction)
+
+            elif kind == "key_press":
+                key, text, mods = data
+                self._update_modifiers(mods)
+                key_sym = self._key_sym_from_key_and_text(key, text)
+                if key_sym:
+                    inter.trigger_keyboard_key(inter.InputAction.PRESS, key_sym)
+                if text:
+                    for ch in text:
+                        inter.trigger_text_character(ord(ch))
+
+            elif kind == "key_release":
+                key, mods = data
+                self._update_modifiers(mods)
+                key_sym = self._key_sym_from_key_and_text(key, "")
+                if key_sym:
+                    inter.trigger_keyboard_key(inter.InputAction.RELEASE, key_sym)
+
+        inter.trigger_event_loop(1.0 / 30.0)
+
+        win.render()
+
+        self.update()
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/qml/test_minimal_qml.py f3d-3.4.1/examples/libf3d/python/qml/test_minimal_qml.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/qml/test_minimal_qml.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/qml/test_minimal_qml.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,14 @@
+from pathlib import Path
+
+from minimal_qml import main
+
+from PySide6.QtWidgets import QApplication
+
+
+def test_minimal_qml():
+    example_script = Path(__file__).parent.parent / "qml" / "minimal_qml.py"
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+
+    if app := QApplication.instance():
+        app.shutdown()
+    main([str(example_script), str(file), "--timeout", "1"])
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/qt6/minimal_qt6.py f3d-3.4.1/examples/libf3d/python/qt6/minimal_qt6.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/qt6/minimal_qt6.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/qt6/minimal_qt6.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,311 @@
+import sys
+from argparse import ArgumentParser
+from pathlib import Path
+
+import f3d
+from PySide6.QtGui import QKeyEvent, QMouseEvent, QWheelEvent
+from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
+from PySide6.QtOpenGLWidgets import QOpenGLWidget
+from PySide6.QtCore import Qt, QPointF, QTimer, QCoreApplication
+
+
+def main(argv: list[str] | None = None) -> int:
+    if argv is None:
+        argv = sys.argv
+
+    parser = ArgumentParser()
+    parser.add_argument("file")
+    parser.add_argument(
+        "--timeout",
+        type=int,
+        help="Optional timeout (in seconds) before closing the viewer.",
+    )
+
+    # Let argparse handle our options, and leave the rest to Qt
+    args, qt_args = parser.parse_known_args(argv[1:])
+
+    app = QApplication([argv[0]] + qt_args)
+
+    model_path = Path(args.file)
+    if not model_path.exists():
+        print(f"File not found: {model_path}")
+        return 1
+
+    win = MainWindow(str(model_path))
+    win.show()
+
+    # For testing purposes only, shutdown the example after `timeout` seconds
+    if args.timeout:
+        QTimer.singleShot(args.timeout * 1000, QCoreApplication.quit)
+
+    return app.exec()
+
+
+class MainWindow(QMainWindow):
+    def __init__(self, file_path: str, parent: QWidget | None = None):
+        super().__init__(parent)
+        self.setWindowTitle("libf3d + PySide6 example")
+
+        self.viewer = F3DWidget(file_path, self)
+        self.setCentralWidget(self.viewer)
+        self.resize(800, 600)
+
+
+class F3DWidget(QOpenGLWidget):
+    """
+    QOpenGLWidget that uses libf3d to render a 3D model
+    inside Qt's OpenGL context.
+    """
+
+    def __init__(self, file_path: str, parent: QWidget | None = None):
+        super().__init__(parent)
+        self.file_path = file_path
+        self.engine: f3d.Engine | None = None
+        self.interactor: f3d.Interactor | None = None
+        self.timer: QTimer | None = None
+
+        # Allow this widget to receive key events
+        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+
+    # ------------------------------------------------------------------
+    # OpenGL / F3D setup
+    # ------------------------------------------------------------------
+
+    def initializeGL(self) -> None:
+        """Called once when the OpenGL context is created."""
+        f3d.Engine.autoload_plugins()
+
+        self.engine = f3d.Engine.create_external(self.context().getProcAddress)
+
+        # Retrieve the interactor for event forwarding
+        self.interactor = self.engine.interactor
+
+        self.timer = QTimer(self)
+        self.timer.setInterval(30)
+        self.timer.timeout.connect(self._on_tick)
+        self.timer.start()
+
+        self.engine.scene.add(self.file_path)
+
+        win = self.engine.window
+        win.size = (self.width(), self.height())
+
+        opt = self.engine.options
+        opt.update(
+            {
+                "render.grid.enable": True,
+                "render.show_edges": False,
+                "ui.axis": True,
+                "ui.fps": True,
+                "ui.filename": True,
+            }
+        )
+
+    def _on_tick(self) -> None:
+        if self.engine is None or self.interactor is None:
+            return
+        self.makeCurrent()
+        self.interactor.trigger_event_loop(1.0 / 30.0)
+        self.update()
+        self.doneCurrent()
+
+    def resizeGL(self, width: int, height: int) -> None:
+        if self.engine is not None:
+            self.engine.window.size = (max(1, width), max(1, height))
+
+    def paintGL(self) -> None:
+        if self.engine is not None:
+            self.engine.window.render()
+
+    # ------------------------------------------------------------------
+    # Forwarding helpers
+    # ------------------------------------------------------------------
+
+    def _update_modifiers(self, mods: Qt.KeyboardModifier) -> None:
+        """
+        Translate Qt modifiers (Shift/Ctrl) into f3d::interactor::InputModifier
+        and forward them.
+        """
+        if self.interactor is None:
+            return
+
+        inter = self.interactor
+
+        if (mods & Qt.KeyboardModifier.ControlModifier) and (
+            mods & Qt.KeyboardModifier.ShiftModifier
+        ):
+            inter.trigger_mod_update(inter.InputModifier.CTRL_SHIFT)
+        elif mods & Qt.KeyboardModifier.ControlModifier:
+            inter.trigger_mod_update(inter.InputModifier.CTRL)
+        elif mods & Qt.KeyboardModifier.ShiftModifier:
+            inter.trigger_mod_update(inter.InputModifier.SHIFT)
+        else:
+            inter.trigger_mod_update(inter.InputModifier.NONE)
+
+    def _map_mouse_button(self, button: Qt.MouseButton):
+        """Map a Qt mouse button to f3d::interactor::MouseButton."""
+        if self.interactor is None:
+            return None
+
+        inter = self.interactor
+        if button == Qt.MouseButton.LeftButton:
+            return inter.MouseButton.LEFT
+        if button == Qt.MouseButton.RightButton:
+            return inter.MouseButton.RIGHT
+        if button == Qt.MouseButton.MiddleButton:
+            return inter.MouseButton.MIDDLE
+        return None
+
+    def _key_sym_from_event(self, event: QKeyEvent) -> str:
+        """
+        Convert a QKeyEvent into a keySym string expected by libf3d.
+        Mainly covers common F3D bindings (G, X, etc.).
+        """
+        key: int = event.key()
+
+        # A–Z -> "A"..."Z"
+        if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
+            return chr(key).upper()
+
+        # Special keys
+        special = {
+            Qt.Key.Key_Backspace: "BackSpace",
+            Qt.Key.Key_Escape: "Escape",
+            Qt.Key.Key_Return: "Return",
+            Qt.Key.Key_Enter: "Return",
+            Qt.Key.Key_Space: "Space",
+            Qt.Key.Key_Tab: "Tab",
+        }
+        try:
+            return special[key]
+        except KeyError:
+            return event.text()
+
+    # ------------------------------------------------------------------
+    # Mouse interaction forwarding
+    # ------------------------------------------------------------------
+
+    def mousePressEvent(self, event: QMouseEvent) -> None:
+        if self.interactor is None:
+            return None
+        self.makeCurrent()
+
+        self._update_modifiers(event.modifiers())
+        inter = self.interactor
+        button = self._map_mouse_button(event.button())
+
+        if button is not None:
+            pos: QPointF = event.position()
+            inter.trigger_mouse_position(pos.x(), pos.y())
+            inter.trigger_mouse_button(inter.InputAction.PRESS, button)
+            self.update()
+
+        self.doneCurrent()
+        return None
+
+    def mouseMoveEvent(self, event: QMouseEvent) -> None:
+        if self.interactor is None:
+            return None
+
+        self.makeCurrent()
+
+        self._update_modifiers(event.modifiers())
+        inter = self.interactor
+        pos: QPointF = event.position()
+        inter.trigger_mouse_position(pos.x(), pos.y())
+        self.update()
+
+        self.doneCurrent()
+        return None
+
+    def mouseReleaseEvent(self, event: QMouseEvent) -> None:
+        if self.interactor is None:
+            return None
+
+        self.makeCurrent()
+
+        self._update_modifiers(event.modifiers())
+        button = self._map_mouse_button(event.button())
+        if button is not None:
+            inter = self.interactor
+            inter.trigger_mouse_button(inter.InputAction.RELEASE, button)
+            pos: QPointF = event.position()
+            inter.trigger_mouse_position(pos.x(), pos.y())
+            self.update()
+
+        self.doneCurrent()
+        return None
+
+    def wheelEvent(self, event: QWheelEvent) -> None:
+        if self.interactor is None:
+            return None
+
+        self.makeCurrent()
+
+        self._update_modifiers(event.modifiers())
+        inter = self.interactor
+
+        delta = event.angleDelta()
+        if abs(delta.y()) >= abs(delta.x()):
+            direction = (
+                inter.WheelDirection.FORWARD
+                if delta.y() > 0
+                else inter.WheelDirection.BACKWARD
+            )
+        else:
+            direction = (
+                inter.WheelDirection.RIGHT
+                if delta.x() > 0
+                else inter.WheelDirection.LEFT
+            )
+
+        inter.trigger_mouse_wheel(direction)
+        self.update()
+
+        self.doneCurrent()
+        return None
+
+    # ------------------------------------------------------------------
+    # Keyboard interaction forwarding
+    # ------------------------------------------------------------------
+
+    def keyPressEvent(self, event: QKeyEvent) -> None:
+        if self.interactor is None:
+            return None
+
+        self.makeCurrent()
+        self._update_modifiers(event.modifiers())
+        inter = self.interactor
+        key_sym = self._key_sym_from_event(event)
+
+        if key_sym:
+            inter.trigger_keyboard_key(inter.InputAction.PRESS, key_sym)
+
+        text = event.text()
+        if text:
+            for ch in text:
+                inter.trigger_text_character(ord(ch))
+
+        self.update()
+        self.doneCurrent()
+        return None
+
+    def keyReleaseEvent(self, event: QKeyEvent) -> None:
+        if self.interactor is None:
+            return None
+
+        self.makeCurrent()
+        self._update_modifiers(event.modifiers())
+        inter = self.interactor
+        key_sym = self._key_sym_from_event(event)
+
+        if key_sym:
+            inter.trigger_keyboard_key(inter.InputAction.RELEASE, key_sym)
+
+        self.update()
+        self.doneCurrent()
+        return None
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/qt6/test_minimal_qt6.py f3d-3.4.1/examples/libf3d/python/qt6/test_minimal_qt6.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/qt6/test_minimal_qt6.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/qt6/test_minimal_qt6.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,15 @@
+from pathlib import Path
+
+from minimal_qt6 import main
+
+from PySide6.QtWidgets import QApplication
+
+
+def test_minimal_qt6():
+    example_script = Path(__file__).parent.parent / "qt6" / "minimal_qt6.py"
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+    if app := QApplication.instance():
+        app.shutdown()
+    main(
+        [str(example_script), str(file), "--timeout", "1"],
+    )
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-image/render_image.py f3d-3.4.1/examples/libf3d/python/render-image/render_image.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-image/render_image.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/render-image/render_image.py	2026-01-08 08:41:55.000000000 +0200
@@ -7,12 +7,13 @@
 
 TEST_DATA_DIR = Path(__file__).parent.parent.parent.parent.parent / "testing/data"
 
-if __name__ == "__main__":
+
+def main(argv: list[str] | None = None):
     argparser = ArgumentParser()
     argparser.add_argument("models", nargs="*")
     argparser.add_argument("--into", default=gettempdir())
 
-    args = argparser.parse_args()
+    args = argparser.parse_args(argv)
 
     input_paths = args.models or [TEST_DATA_DIR / "suzanne.obj"]
     output_path = Path(args.into) / "f3d.png"
@@ -31,3 +32,7 @@
         print("F3D encountered an unexpected exception:")
         print(e)
         sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-image/test_render_image.py f3d-3.4.1/examples/libf3d/python/render-image/test_render_image.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-image/test_render_image.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/render-image/test_render_image.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,9 @@
+from pathlib import Path
+
+from render_image import main
+
+
+def test_render_image(tmp_path):
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+    into = tmp_path
+    main([str(file), "--into", str(into)])
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-interact/render_interact.py f3d-3.4.1/examples/libf3d/python/render-interact/render_interact.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-interact/render_interact.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/render-interact/render_interact.py	2026-01-08 08:41:55.000000000 +0200
@@ -5,11 +5,18 @@
 
 TEST_DATA_DIR = Path(__file__).parent.parent.parent.parent.parent / "testing/data"
 
-if __name__ == "__main__":
+
+def main(argv: list[str] | None = None):
     argparser = ArgumentParser()
     argparser.add_argument("models", nargs="*")
+    argparser.add_argument(
+        "--timeout",
+        type=int,
+        default=None,
+        help="Optional timeout (in seconds) before closing the viewer.",
+    )
 
-    args = argparser.parse_args()
+    args = argparser.parse_args(argv)
 
     input_paths = args.models or [TEST_DATA_DIR / "suzanne.obj"]
 
@@ -25,4 +32,12 @@
     # Render
     eng.window.render()
 
-    eng.interactor.start()
+    if args.timeout:
+        # For testing purposes only, exit after `timeout` seconds
+        eng.interactor.start(args.timeout, eng.interactor.stop)
+    else:
+        eng.interactor.start()
+
+
+if __name__ == "__main__":
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-interact/test_render_interact.py f3d-3.4.1/examples/libf3d/python/render-interact/test_render_interact.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-interact/test_render_interact.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/render-interact/test_render_interact.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+from render_interact import main
+
+
+def test_render_interact():
+    file = Path(__file__).parent.parent / "tests" / "data" / "cow.vtp"
+    main([str(file), "--timeout", "1"])
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-terminal/render_terminal.py f3d-3.4.1/examples/libf3d/python/render-terminal/render_terminal.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-terminal/render_terminal.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/render-terminal/render_terminal.py	2026-01-08 08:41:55.000000000 +0200
@@ -15,7 +15,7 @@
         "scene.up_direction": "+Y",
         "render.effect.tone_mapping": True,
         "render.effect.ambient_occlusion": True,
-        "render.effect.translucency_support": True,
+        "render.effect.blending.enable": True,
         "render.effect.antialiasing.enable": True,
     }
     anim_fps = 30
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/render-terminal/test_render_terminal.py f3d-3.4.1/examples/libf3d/python/render-terminal/test_render_terminal.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/render-terminal/test_render_terminal.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/render-terminal/test_render_terminal.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,5 @@
+from render_terminal import main
+
+
+def test_render_terminal():
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/tests/baselines/cow.png f3d-3.4.1/examples/libf3d/python/tests/baselines/cow.png
--- f3d-3.2.0+dfsg/examples/libf3d/python/tests/baselines/cow.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/tests/baselines/cow.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c9590019f4b9aaac43246b920d916bb765268c83b4e40c0a9728e92af87ab99
+size 18647
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/tests/data/cow.vtp f3d-3.4.1/examples/libf3d/python/tests/data/cow.vtp
--- f3d-3.2.0+dfsg/examples/libf3d/python/tests/data/cow.vtp	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/tests/data/cow.vtp	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5aa4901d186902f90e17bf3b5917541cb6cb8cf223bfeea736631df4c047652
+size 60449
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/tkinter/minimal_tkinter.py f3d-3.4.1/examples/libf3d/python/tkinter/minimal_tkinter.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/tkinter/minimal_tkinter.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/libf3d/python/tkinter/minimal_tkinter.py	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,5 @@
 import tkinter as tk  # Works also on CustomTKinter
+from argparse import ArgumentParser
 
 import f3d
 from pyopengltk import OpenGLFrame
@@ -28,7 +29,17 @@
         self.engine.window.render()
 
 
-if __name__ == "__main__":
+def main(argv: list[str] | None = None):
+    argparser = ArgumentParser()
+    argparser.add_argument(
+        "--timeout",
+        type=int,
+        default=None,
+        help="Optional timeout (in seconds) before closing the viewer.",
+    )
+
+    args = argparser.parse_args(argv)
+
     # Create main window and define size, position and title
     root = tk.Tk()
     root.geometry("640x480+100+100")
@@ -37,5 +48,12 @@
     F3D = Frame()
     F3D.pack(fill=tk.BOTH, expand=tk.YES)
 
+    if args.timeout:
+        # For testing purposes only, exit after `timeout` seconds
+        root.after(args.timeout * 1000, root.destroy)
     # Run TKinter mainloop
     root.mainloop()
+
+
+if __name__ == "__main__":
+    main()
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/python/tkinter/test_minimal_tkinter.py f3d-3.4.1/examples/libf3d/python/tkinter/test_minimal_tkinter.py
--- f3d-3.2.0+dfsg/examples/libf3d/python/tkinter/test_minimal_tkinter.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/python/tkinter/test_minimal_tkinter.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,10 @@
+import pytest
+
+pytest.importorskip("tkinter")  # tkinter is missing on linux for now
+pytest.importorskip("pyopengltk")
+
+from minimal_tkinter import main
+
+
+def test_minimal_tkinter():
+    main(["--timeout", "1"])
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/web/index.html f3d-3.4.1/examples/libf3d/web/index.html
--- f3d-3.2.0+dfsg/examples/libf3d/web/index.html	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/web/index.html	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,133 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>F3D Web example</title>
+    <style>
+      #main {
+        min-height: 70vh;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app">
+      <section class="section">
+        <div class="columns">
+          <div class="column">
+            <h1 class="title">F3D Web</h1>
+            <p class="subtitle">A WebAssembly example using libf3d</p>
+          </div>
+          <div class="column">
+            <div class="field is-pulled-right">
+              <input
+                id="dark"
+                type="checkbox"
+                name="dark"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="dark">Dark theme</label>
+            </div>
+          </div>
+        </div>
+      </section>
+      <section class="section">
+        <div class="columns">
+          <aside class="menu">
+            <div class="file has-name">
+              <label class="file-label">
+                <input
+                  class="file-input"
+                  type="file"
+                  id="file-selector"
+                  accept=".gml,.gltf,.glb,.obj,.ply,.pts,.stl,.vtk,.vtp,.vtu,.vtkhdf,.3ds,.wrl,.vrml,.fbx,.off,.x,.dae,.ex2,.exo,.e,.g,.stp,.step,.igs,.iges,.brep,.xbf,.drc,.mdl,.nc,.cdf,.ncdf"
+                />
+                <span class="file-cta">
+                  <span class="file-label">Open a file...</span>
+                </span>
+                <span class="file-name" id="file-name"></span>
+              </label>
+            </div>
+            <div class="is-flex is-flex-direction-row is-align-items-center">
+              <button id="y-up" class="button mx-1 is-small">+Y</button>
+              <button id="z-up" class="button mx-1 is-small is-active">
+                +Z
+              </button>
+              <label class="mx-1">Up</label>
+            </div>
+            <p class="menu-label">Widgets</p>
+            <div class="field">
+              <input
+                id="grid"
+                type="checkbox"
+                name="grid"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="grid">Grid</label>
+            </div>
+            <div class="field">
+              <input
+                id="axis"
+                type="checkbox"
+                name="axis"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="axis">Axis</label>
+            </div>
+            <p class="menu-label">Rendering</p>
+            <div class="field">
+              <input
+                id="fxaa"
+                type="checkbox"
+                name="fxaa"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="fxaa">Anti-aliasing</label>
+            </div>
+            <div class="field">
+              <input
+                id="tone"
+                type="checkbox"
+                name="tone"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="tone">Tone mapping</label>
+            </div>
+            <div class="field">
+              <input
+                id="ssao"
+                type="checkbox"
+                name="ssao"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="ssao">Ambient occlusion</label>
+            </div>
+            <p class="menu-label">Lighting</p>
+            <div class="field">
+              <input
+                id="ambient"
+                type="checkbox"
+                name="ambient"
+                class="switch is-rounded"
+                checked
+              />
+              <label for="ambient">Ambient light</label>
+            </div>
+          </aside>
+          <div class="column">
+            <div class="container" id="main">
+              <canvas id="canvas"></canvas>
+            </div>
+          </div>
+        </div>
+      </section>
+    </div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/web/package.json f3d-3.4.1/examples/libf3d/web/package.json
--- f3d-3.2.0+dfsg/examples/libf3d/web/package.json	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/web/package.json	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,19 @@
+{
+  "name": "f3d-example",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "vite": "^7.1.2"
+  },
+  "dependencies": {
+    "bulma": "^1.0.4",
+    "bulma-switch": "^2.0.4",
+    "f3d": "file:../../.."
+  }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/web/package-lock.json f3d-3.4.1/examples/libf3d/web/package-lock.json
--- f3d-3.2.0+dfsg/examples/libf3d/web/package-lock.json	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/web/package-lock.json	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,1043 @@
+{
+  "name": "f3d-example",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "f3d-example",
+      "version": "0.0.0",
+      "dependencies": {
+        "bulma": "^1.0.4",
+        "bulma-switch": "^2.0.4",
+        "f3d": "file:../../.."
+      },
+      "devDependencies": {
+        "vite": "^7.1.2"
+      }
+    },
+    "../../..": {
+      "version": "3.2.0",
+      "license": "BSD-3-Clause",
+      "funding": {
+        "url": "https://github.com/sponsors/f3d-app"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+      "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+      "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+      "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+      "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+      "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+      "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+      "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+      "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+      "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+      "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+      "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+      "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+      "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+      "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+      "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.9",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+      "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+      "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+      "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+      "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+      "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz",
+      "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz",
+      "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz",
+      "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz",
+      "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz",
+      "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz",
+      "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz",
+      "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz",
+      "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz",
+      "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz",
+      "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz",
+      "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz",
+      "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz",
+      "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz",
+      "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz",
+      "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.50.0",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz",
+      "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz",
+      "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz",
+      "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz",
+      "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz",
+      "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/bulma": {
+      "version": "1.0.4",
+      "license": "MIT"
+    },
+    "node_modules/bulma-switch": {
+      "version": "2.0.4",
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.9",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.9",
+        "@esbuild/android-arm": "0.25.9",
+        "@esbuild/android-arm64": "0.25.9",
+        "@esbuild/android-x64": "0.25.9",
+        "@esbuild/darwin-arm64": "0.25.9",
+        "@esbuild/darwin-x64": "0.25.9",
+        "@esbuild/freebsd-arm64": "0.25.9",
+        "@esbuild/freebsd-x64": "0.25.9",
+        "@esbuild/linux-arm": "0.25.9",
+        "@esbuild/linux-arm64": "0.25.9",
+        "@esbuild/linux-ia32": "0.25.9",
+        "@esbuild/linux-loong64": "0.25.9",
+        "@esbuild/linux-mips64el": "0.25.9",
+        "@esbuild/linux-ppc64": "0.25.9",
+        "@esbuild/linux-riscv64": "0.25.9",
+        "@esbuild/linux-s390x": "0.25.9",
+        "@esbuild/linux-x64": "0.25.9",
+        "@esbuild/netbsd-arm64": "0.25.9",
+        "@esbuild/netbsd-x64": "0.25.9",
+        "@esbuild/openbsd-arm64": "0.25.9",
+        "@esbuild/openbsd-x64": "0.25.9",
+        "@esbuild/openharmony-arm64": "0.25.9",
+        "@esbuild/sunos-x64": "0.25.9",
+        "@esbuild/win32-arm64": "0.25.9",
+        "@esbuild/win32-ia32": "0.25.9",
+        "@esbuild/win32-x64": "0.25.9"
+      }
+    },
+    "node_modules/f3d": {
+      "resolved": "../../..",
+      "link": true
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.50.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.50.0",
+        "@rollup/rollup-android-arm64": "4.50.0",
+        "@rollup/rollup-darwin-arm64": "4.50.0",
+        "@rollup/rollup-darwin-x64": "4.50.0",
+        "@rollup/rollup-freebsd-arm64": "4.50.0",
+        "@rollup/rollup-freebsd-x64": "4.50.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.50.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.50.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.50.0",
+        "@rollup/rollup-linux-arm64-musl": "4.50.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.50.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.50.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.50.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.50.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.50.0",
+        "@rollup/rollup-linux-x64-gnu": "4.50.0",
+        "@rollup/rollup-linux-x64-musl": "4.50.0",
+        "@rollup/rollup-openharmony-arm64": "4.50.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.50.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.50.0",
+        "@rollup/rollup-win32-x64-msvc": "4.50.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.14",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.1.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.14"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/web/src/main.js f3d-3.4.1/examples/libf3d/web/src/main.js
--- f3d-3.2.0+dfsg/examples/libf3d/web/src/main.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/web/src/main.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,153 @@
+import "bulma/css/bulma.min.css";
+import "bulma-switch/dist/css/bulma-switch.min.css";
+import f3d from "f3d";
+
+const settings = {
+  canvas: document.getElementById("canvas"),
+  setupOptions: (options) => {
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // setup coloring
+    options.toggle("model.scivis.enable");
+    options.setAsString("model.scivis.array_name", "Colors");
+    options.setAsString("model.scivis.component", "-2");
+    options.toggle("model.scivis.cells");
+
+    // make it look nice
+    options.toggle("render.effect.antialiasing.enable");
+    options.toggle("render.effect.tone_mapping");
+    options.toggle("render.effect.ambient_occlusion");
+    options.toggle("render.hdri.ambient");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+
+    // default to +Z
+    options.setAsString("scene.up_direction", "+Z");
+  },
+};
+
+f3d(settings)
+  .then(async (Module) => {
+    // write in the filesystem
+    const defaultFile = await fetch("f3d.vtp").then((b) => b.arrayBuffer());
+    Module.FS.writeFile("f3d.vtp", new Uint8Array(defaultFile));
+
+    // automatically load all supported file format readers
+    Module.Engine.autoloadPlugins();
+
+    Module.engineInstance = Module.Engine.create();
+
+    const openFile = (name) => {
+      document.getElementById("file-name").innerHTML = name;
+      const filePath = "/" + name;
+      const scene = Module.engineInstance.getScene();
+      if (scene.supports(filePath)) {
+        scene.clear();
+        scene.add(filePath);
+      } else {
+        console.error("File " + filePath + " cannot be opened");
+      }
+      Module.engineInstance.getWindow().getCamera().resetToBounds(0.9);
+      Module.engineInstance.getWindow().render();
+    };
+
+    // setup file open event
+    const progressEl = document.querySelector("#progressEl");
+    const fileSelector = document.querySelector("#file-selector");
+    fileSelector.addEventListener("change", (evt) => {
+      for (const file of evt.target.files) {
+        const reader = new FileReader();
+        reader.addEventListener("loadend", (e) => {
+          Module.FS.writeFile(file.name, new Uint8Array(reader.result));
+          openFile(file.name);
+        });
+        reader.readAsArrayBuffer(file);
+      }
+    });
+
+    Module.setupOptions(Module.engineInstance.getOptions());
+
+    // Storing DOM element ids to f3d option mappings since also useful for url-param parsing
+    const idOptionMappings = [
+      ["grid", "render.grid.enable"],
+      ["axis", "ui.axis"],
+      ["fxaa", "render.effect.antialiasing.enable"],
+      ["tone", "render.effect.tone_mapping"],
+      ["ssao", "render.effect.ambient_occlusion"],
+      ["ambient", "render.hdri.ambient"],
+    ];
+
+    // toggle callback
+    const mapToggleIdToOption = (id, option) => {
+      document.querySelector("#" + id).addEventListener("change", (evt) => {
+        Module.engineInstance.getOptions().toggle(option);
+        Module.engineInstance.getWindow().render();
+      });
+    };
+
+    // This assumes all toggles are 'on' before mapping their state to options
+    // Ok after f3d(settings) where settings = {..., setupOptions} which toggles some options
+    for (let [id, option] of idOptionMappings) {
+      mapToggleIdToOption(id, option);
+    }
+
+    const switchDark = () => {
+      document.documentElement.classList.add("theme-dark");
+      document.documentElement.classList.remove("theme-light");
+      Module.engineInstance
+        .getOptions()
+        .set_color("render.grid.color", 0.25, 0.27, 0.33);
+      Module.engineInstance.getWindow().render();
+    };
+
+    const switchLight = () => {
+      document.documentElement.classList.add("theme-light");
+      document.documentElement.classList.remove("theme-dark");
+      Module.engineInstance
+        .getOptions()
+        .set_color("render.grid.color", 0.67, 0.69, 0.75);
+      Module.engineInstance.getWindow().render();
+    };
+
+    // theme switch
+    document.querySelector("#dark").addEventListener("change", (evt) => {
+      if (evt.target.checked) switchDark();
+      else switchLight();
+    });
+
+    switchDark();
+
+    // up callback
+    document.querySelector("#z-up").addEventListener("click", (evt) => {
+      Module.engineInstance
+        .getOptions()
+        .set_as_string("scene.up_direction", "+Z");
+      document.getElementById("z-up").classList.add("is-active");
+      document.getElementById("y-up").classList.remove("is-active");
+      openFile(document.getElementById("file-name").innerHTML);
+    });
+
+    document.querySelector("#y-up").addEventListener("click", (evt) => {
+      Module.engineInstance
+        .getOptions()
+        .set_as_string("scene.up_direction", "+Y");
+      document.getElementById("y-up").classList.add("is-active");
+      document.getElementById("z-up").classList.remove("is-active");
+      openFile(document.getElementById("file-name").innerHTML);
+    });
+
+    // setup the window size based on the canvas size
+    const main = document.getElementById("main");
+    const scale = window.devicePixelRatio;
+    Module.engineInstance
+      .getWindow()
+      .setSize(scale * main.clientWidth, scale * main.clientHeight);
+
+    // do a first render and start the interactor
+    Module.engineInstance.getWindow().render();
+    Module.engineInstance.getInteractor().start();
+  })
+  .catch((error) => console.error("Internal exception: " + error));
diff -Nru f3d-3.2.0+dfsg/examples/libf3d/web/vite.config.js f3d-3.4.1/examples/libf3d/web/vite.config.js
--- f3d-3.2.0+dfsg/examples/libf3d/web/vite.config.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/examples/libf3d/web/vite.config.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,32 @@
+import { defineConfig } from "vite";
+import fs from "fs";
+import path from "path";
+
+// Custom middleware to serve wasm files with the correct MIME type
+// See https://stackoverflow.com/questions/78095780/web-assembly-wasm-errors-in-a-vite-vue-app-using-realm-web-sdk
+const wasmMiddleware = () => {
+  return {
+    name: "wasm-middleware",
+    configureServer(server) {
+      server.middlewares.use((req, res, next) => {
+        if (req.url && req.url.endsWith(".wasm")) {
+          const wasmPath = path.join(
+            __dirname,
+            "node_modules/f3d/dist",
+            path.basename(req.url),
+          );
+          const wasmFile = fs.readFileSync(wasmPath);
+          res.setHeader("Content-Type", "application/wasm");
+          res.end(wasmFile);
+          return;
+        }
+        next();
+      });
+    },
+  };
+};
+
+export default defineConfig({
+  plugins: [wasmMiddleware()],
+  base: "./",
+});
diff -Nru f3d-3.2.0+dfsg/examples/plugins/CMakeLists.txt f3d-3.4.1/examples/plugins/CMakeLists.txt
--- f3d-3.2.0+dfsg/examples/plugins/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/plugins/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -4,6 +4,7 @@
 
 find_package(f3d REQUIRED COMPONENTS pluginsdk)
 
+option(BUILD_TESTING "Build the plugin example tests" OFF)
 if(BUILD_TESTING)
   enable_testing()
 endif()
diff -Nru f3d-3.2.0+dfsg/examples/README.md f3d-3.4.1/examples/README.md
--- f3d-3.2.0+dfsg/examples/README.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/examples/README.md	2026-01-08 08:41:55.000000000 +0200
@@ -13,11 +13,25 @@
 
 ## Building
 
-Each example is intended to be built as a separate CMake project, however top level `CMakeLists.txt` are provided if needed.
+Each example (C++, C, Java) is intended to be built as a separate CMake project, however there are top level `CMakeLists.txt` in each directory for easier usage.
+
+eg:
 
 ```
-cmake -Df3d_DIR=/path/to/f3d/install/lib/cmake/f3d /path/to/example/dir
+cmake -Df3d_DIR=<path_to_f3d_install>/lib/cmake/f3d <path_to_examples_dir>/libf3d
 cmake --build .
 ```
 
-Then run the example
+Then run the example individually.
+
+## Testing
+
+When building from a top level `CMakeLists.txt`, one can enable the cmake option `BUILD_TESTING` to enable tests.
+
+## Python
+
+Python example can be run using python directly or with pytest
+
+## Web
+
+Web examples have to be run independently using npm
diff -Nru f3d-3.2.0+dfsg/external/.clang-tidy f3d-3.4.1/external/.clang-tidy
--- f3d-3.2.0+dfsg/external/.clang-tidy	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/.clang-tidy	2026-01-08 08:41:55.000000000 +0200
@@ -1,2 +1,2 @@
 # Disabling all checks is not allowed by clang-format 14
-Checks: '-*,performance-*,-performance-no-int-to-ptr'
+Checks: '-*,performance-*,-performance-no-int-to-ptr,-performance-enum-size'
diff -Nru f3d-3.2.0+dfsg/external/cxxopts/cxxopts.hpp f3d-3.4.1/external/cxxopts/cxxopts.hpp
--- f3d-3.2.0+dfsg/external/cxxopts/cxxopts.hpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/cxxopts/cxxopts.hpp	2026-01-08 08:41:55.000000000 +0200
@@ -27,6 +27,7 @@
 #ifndef CXXOPTS_HPP_INCLUDED
 #define CXXOPTS_HPP_INCLUDED
 
+#include <cstdint>
 #include <cstdlib>
 #include <cstring>
 #include <exception>
@@ -72,6 +73,12 @@
 #      define CXXOPTS_HAS_OPTIONAL
 #    endif
 #  endif
+#  if __has_include(<filesystem>)
+#    include <filesystem>
+#    ifdef __cpp_lib_filesystem
+#      define CXXOPTS_HAS_FILESYSTEM
+#    endif
+#  endif
 #endif
 
 #define CXXOPTS_FALLTHROUGH
@@ -93,8 +100,8 @@
 #endif
 
 #define CXXOPTS__VERSION_MAJOR 3
-#define CXXOPTS__VERSION_MINOR 2
-#define CXXOPTS__VERSION_PATCH 0
+#define CXXOPTS__VERSION_MINOR 3
+#define CXXOPTS__VERSION_PATCH 1
 
 #if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6
   #define CXXOPTS_NULL_DEREF_IGNORE
@@ -976,11 +983,11 @@
     US limit = 0;
     if (negative)
     {
-      limit = static_cast<US>(std::abs(static_cast<intmax_t>(std::numeric_limits<T>::min())));
+      limit = static_cast<US>(std::abs(static_cast<intmax_t>((std::numeric_limits<T>::min)())));
     }
     else
     {
-      limit = std::numeric_limits<T>::max();
+      limit = (std::numeric_limits<T>::max)();
     }
 
     if (base != 0 && result > limit / base)
@@ -1062,6 +1069,37 @@
   stringstream_parser(text, value);
 }
 
+#ifdef CXXOPTS_HAS_OPTIONAL
+template <typename T>
+void
+parse_value(const std::string& text, std::optional<T>& value)
+{
+  T result;
+  parse_value(text, result);
+  value = std::move(result);
+}
+#endif
+
+#ifdef CXXOPTS_HAS_FILESYSTEM
+inline
+void
+parse_value(const std::string& text, std::filesystem::path& value)
+{
+  value.assign(text);
+}
+#endif
+
+inline
+void parse_value(const std::string& text, char& c)
+{
+  if (text.length() != 1)
+  {
+    throw_or_mimic<exceptions::incorrect_argument_type>(text);
+  }
+
+  c = text[0];
+}
+
 template <typename T>
 void
 parse_value(const std::string& text, std::vector<T>& value)
@@ -1097,28 +1135,6 @@
   value.emplace_back(std::move(v));
 }
 
-#ifdef CXXOPTS_HAS_OPTIONAL
-template <typename T>
-void
-parse_value(const std::string& text, std::optional<T>& value)
-{
-  T result;
-  parse_value(text, result);
-  value = std::move(result);
-}
-#endif
-
-inline
-void parse_value(const std::string& text, char& c)
-{
-  if (text.length() != 1)
-  {
-    throw_or_mimic<exceptions::incorrect_argument_type>(text);
-  }
-
-  c = text[0];
-}
-
 template <typename T>
 struct type_is_container
 {
@@ -1538,6 +1554,18 @@
     return CXXOPTS_RTTI_CAST<const values::standard_value<T>&>(*m_value).get();
   }
 
+#ifdef CXXOPTS_HAS_OPTIONAL
+  template <typename T>
+  std::optional<T>
+  as_optional() const
+  {
+    if (m_value == nullptr) {
+      return std::nullopt;
+    }
+    return as<T>();
+  }
+#endif
+
   private:
   void
   ensure_value(const std::shared_ptr<const OptionDetails>& details)
@@ -1730,6 +1758,12 @@
     return viter->second.count();
   }
 
+  bool
+  contains(const std::string& o) const
+  {
+    return static_cast<bool>(count(o));
+  }
+
   const OptionValue&
   operator[](const std::string& option) const
   {
@@ -1750,6 +1784,24 @@
     return viter->second;
   }
 
+#ifdef CXXOPTS_HAS_OPTIONAL
+  template <typename T>
+  std::optional<T>
+  as_optional(const std::string& option) const
+  {
+    auto iter = m_keys.find(option);
+    if (iter != m_keys.end())
+    {
+      auto viter = m_values.find(iter->second);
+      if (viter != m_values.end())
+      {
+        return viter->second.as_optional<T>();
+      }
+    }
+    return std::nullopt;
+  }
+#endif
+
   const std::vector<KeyValue>&
   arguments() const
   {
diff -Nru f3d-3.2.0+dfsg/external/dmon/dmon.h f3d-3.4.1/external/dmon/dmon.h
--- f3d-3.2.0+dfsg/external/dmon/dmon.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/dmon/dmon.h	2026-01-08 08:41:55.000000000 +0200
@@ -85,6 +85,7 @@
 //      1.3.5       Got rid of volatile for quit variable
 //      1.3.6       Fix deadlock when watch/unwatch API is called from the OnChange callback
 //      1.3.7       Fix deadlock caused by constantly locking the mutex in the thread loop (recent change)
+//      1.3.8       Fix a cpp compatiblity compiler bug after recent changes
 //      
 
 #include <stdbool.h>
@@ -648,6 +649,7 @@
     HANDLE dir_handle = INVALID_HANDLE_VALUE;
     int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches;
     int index = _dmon.freelist[num_freelist - 1];
+    size_t rootdir_len;
 
     {
         _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH);
@@ -694,7 +696,7 @@
 
     _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir);
     _dmon_unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir);
-    size_t rootdir_len = strlen(watch->rootdir);
+    rootdir_len = strlen(watch->rootdir);
     if (watch->rootdir[rootdir_len - 1] != '/') {
         watch->rootdir[rootdir_len] = '/';
         watch->rootdir[rootdir_len + 1] = '\0';
diff -Nru f3d-3.2.0+dfsg/external/imgui/imconfig.h f3d-3.4.1/external/imgui/imconfig.h
--- f3d-3.2.0+dfsg/external/imgui/imconfig.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imconfig.h	2026-01-08 08:41:55.000000000 +0200
@@ -83,6 +83,7 @@
 
 //---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
 // Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
+// Note that imgui_freetype.cpp may be used _without_ this define, if you manually call ImFontAtlas::SetFontLoader(). The define is simply a convenience.
 // On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
 //#define IMGUI_ENABLE_FREETYPE
 
@@ -129,6 +130,10 @@
 //#define IM_DEBUG_BREAK  IM_ASSERT(0)
 //#define IM_DEBUG_BREAK  __debugbreak()
 
+//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set.
+// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use)
+//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
+
 //---- Debug Tools: Enable slower asserts
 //#define IMGUI_DEBUG_PARANOID
 
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui.cpp f3d-3.4.1/external/imgui/imgui.cpp
--- f3d-3.2.0+dfsg/external/imgui/imgui.cpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui.cpp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91.9b
+// dear imgui, v1.92.4
 // (main code and documentation)
 
 // Help:
@@ -21,9 +21,10 @@
 // - Issues & support ........... https://github.com/ocornut/imgui/issues
 // - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps)
 
-// For first-time users having issues compiling/linking/running/loading fonts:
+// For first-time users having issues compiling/linking/running:
 // please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above.
 // Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there.
+// Since 1.92, we encourage font loading questions to also be posted in 'Issues'.
 
 // Copyright (c) 2014-2025 Omar Cornut
 // Developed by Omar Cornut and every direct or indirect contributors to the GitHub.
@@ -52,7 +53,7 @@
   - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI
   - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE
   - HOW A SIMPLE APPLICATION MAY LOOK LIKE
-  - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE
+  - USING CUSTOM BACKEND / CUSTOM ENGINE
 - API BREAKING CHANGES (read me when you update!)
 - FREQUENTLY ASKED QUESTIONS (FAQ)
   - Read all answers online: https://www.dearimgui.com/faq, or in docs/FAQ.md (with a Markdown viewer)
@@ -77,6 +78,7 @@
 // [SECTION] RENDER HELPERS
 // [SECTION] INITIALIZATION, SHUTDOWN
 // [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!)
+// [SECTION] FONTS, TEXTURES
 // [SECTION] ID STACK
 // [SECTION] INPUTS
 // [SECTION] ERROR CHECKING, STATE RECOVERY
@@ -272,7 +274,8 @@
 
  HOW A SIMPLE APPLICATION MAY LOOK LIKE
  --------------------------------------
- EXHIBIT 1: USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder).
+
+ USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder).
  The sub-folders in examples/ contain examples applications following this structure.
 
      // Application init: create a dear imgui context, setup some options, load fonts
@@ -297,7 +300,7 @@
          // Any application code here
          ImGui::Text("Hello, world!");
 
-         // Render dear imgui into screen
+         // Render dear imgui into framebuffer
          ImGui::Render();
          ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
          g_pSwapChain->Present(1, 0);
@@ -308,26 +311,36 @@
      ImGui_ImplWin32_Shutdown();
      ImGui::DestroyContext();
 
- EXHIBIT 2: IMPLEMENTING CUSTOM BACKEND / CUSTOM ENGINE
+ To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application,
+ you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags!
+ Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this.
 
-     // Application init: create a dear imgui context, setup some options, load fonts
+
+USING CUSTOM BACKEND / CUSTOM ENGINE
+------------------------------------
+
+IMPLEMENTING YOUR PLATFORM BACKEND:
+ -> see https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md for basic instructions.
+ -> the Platform backends in impl_impl_XXX.cpp files contain many implementations.
+
+IMPLEMENTING YOUR RenderDrawData() function:
+ -> see https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md
+ -> the Renderer Backends in impl_impl_XXX.cpp files contain many implementations of a ImGui_ImplXXXX_RenderDrawData() function.
+
+IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures:
+ -> see https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md
+ -> the Renderer Backends in impl_impl_XXX.cpp files contain many implementations of a ImGui_ImplXXXX_UpdateTexture() function.
+
+ Basic application/backend skeleton:
+
+     // Application init: create a Dear ImGui context, setup some options, load fonts
      ImGui::CreateContext();
      ImGuiIO& io = ImGui::GetIO();
-     // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls.
-     // TODO: Fill optional fields of the io structure later.
-     // TODO: Load TTF/OTF fonts if you don't want to use the default font.
+     // TODO: set io.ConfigXXX values, e.g.
+     io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;  // Enable keyboard controls
 
-     // Build and load the texture atlas into a texture
-     // (In the examples/ app this is usually done within the ImGui_ImplXXX_Init() function from one of the demo Renderer)
-     int width, height;
-     unsigned char* pixels = nullptr;
-     io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
-
-     // At this point you've got the texture data and you need to upload that to your graphic system:
-     // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'.
-     // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ for details about ImTextureID.
-     MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA32)
-     io.Fonts->SetTexID((void*)texture);
+     // TODO: Load TTF/OTF fonts if you don't want to use the default font.
+     io.Fonts->AddFontFromFileTTF("NotoSans.ttf");
 
      // Application main loop
      while (true)
@@ -350,77 +363,25 @@
         MyGameUpdate(); // may use any Dear ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End();
         MyGameRender(); // may use any Dear ImGui functions as well!
 
-        // Render dear imgui, swap buffers
+        // End the dear imgui frame
         // (You want to try calling EndFrame/Render as late as you can, to be able to use Dear ImGui in your own game rendering code)
-        ImGui::EndFrame();
+        ImGui::EndFrame(); // this is automatically called by Render(), but available
         ImGui::Render();
+
+        // Update textures
         ImDrawData* draw_data = ImGui::GetDrawData();
-        MyImGuiRenderFunction(draw_data);
+        for (ImTextureData* tex : *draw_data->Textures)
+            if (tex->Status != ImTextureStatus_OK)
+                MyImGuiBackend_UpdateTexture(tex);
+
+        // Render dear imgui contents, swap buffers
+        MyImGuiBackend_RenderDrawData(draw_data);
         SwapBuffers();
      }
 
      // Shutdown
      ImGui::DestroyContext();
 
- To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application,
- you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags!
- Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this.
-
-
- HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE
- ---------------------------------------------
- The backends in impl_impl_XXX.cpp files contain many working implementations of a rendering function.
-
-    void MyImGuiRenderFunction(ImDrawData* draw_data)
-    {
-       // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled
-       // TODO: Setup texture sampling state: sample with bilinear filtering (NOT point/nearest filtering). Use 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines;' to allow point/nearest filtering.
-       // TODO: Setup viewport covering draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize
-       // TODO: Setup orthographic projection matrix cover draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize
-       // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color.
-       ImVec2 clip_off = draw_data->DisplayPos;
-       for (int n = 0; n < draw_data->CmdListsCount; n++)
-       {
-          const ImDrawList* cmd_list = draw_data->CmdLists[n];
-          const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data;  // vertex buffer generated by Dear ImGui
-          const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data;   // index buffer generated by Dear ImGui
-          for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++)
-          {
-             const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i];
-             if (pcmd->UserCallback)
-             {
-                 pcmd->UserCallback(cmd_list, pcmd);
-             }
-             else
-             {
-                 // Project scissor/clipping rectangles into framebuffer space
-                 ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y);
-                 ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y);
-                 if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
-                     continue;
-
-                 // We are using scissoring to clip some objects. All low-level graphics API should support it.
-                 // - If your engine doesn't support scissoring yet, you may ignore this at first. You will get some small glitches
-                 //   (some elements visible outside their bounds) but you can fix that once everything else works!
-                 // - Clipping coordinates are provided in imgui coordinates space:
-                 //   - For a given viewport, draw_data->DisplayPos == viewport->Pos and draw_data->DisplaySize == viewport->Size
-                 //   - In a single viewport application, draw_data->DisplayPos == (0,0) and draw_data->DisplaySize == io.DisplaySize, but always use GetMainViewport()->Pos/Size instead of hardcoding those values.
-                 //   - In the interest of supporting multi-viewport applications (see 'docking' branch on github),
-                 //     always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space.
-                 // - Note that pcmd->ClipRect contains Min+Max bounds. Some graphics API may use Min+Max, other may use Min+Size (size being Max-Min)
-                 MyEngineSetScissor(clip_min.x, clip_min.y, clip_max.x, clip_max.y);
-
-                 // The texture for the draw call is specified by pcmd->GetTexID().
-                 // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization.
-                 MyEngineBindTexture((MyTexture*)pcmd->GetTexID());
-
-                 // Render 'pcmd->ElemCount/3' indexed triangles.
-                 // By default the indices ImDrawIdx are 16-bit, you can change them to 32-bit in imconfig.h if your engine doesn't support 16-bit indices.
-                 MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset, vtx_buffer, pcmd->VtxOffset);
-             }
-          }
-       }
-    }
 
 
  API BREAKING CHANGES
@@ -431,7 +392,104 @@
  When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files.
  You can read releases logs https://github.com/ocornut/imgui/releases for more details.
 
+ - 2025/10/14 (1.92.4) - TreeNode, Selectable, Clipper: commented out legacy names which were obsoleted in 1.89.7 (July 2023) and 1.89.9 (Sept 2023);
+                         - ImGuiTreeNodeFlags_AllowItemOverlap       --> ImGuiTreeNodeFlags_AllowOverlap
+                         - ImGuiSelectableFlags_AllowItemOverlap     --> ImGuiSelectableFlags_AllowOverlap
+                         - ImGuiListClipper::IncludeRangeByIndices() --> ImGuiListClipper::IncludeItemsByIndex()
+ - 2025/08/08 (1.92.2) - Backends: SDL_GPU3: Changed ImTextureID type from SDL_GPUTextureSamplerBinding* to SDL_GPUTexture*, which is more natural and easier for user to manage. If you need to change the current sampler, you can access the ImGui_ImplSDLGPU3_RenderState struct. (#8866, #8163, #7998, #7988)
+ - 2025/07/31 (1.92.2) - Tabs: Renamed ImGuiTabBarFlags_FittingPolicyResizeDown to ImGuiTabBarFlags_FittingPolicyShrink. Kept inline redirection enum (will obsolete).
+ - 2025/06/25 (1.92.0) - Layout: commented out legacy ErrorCheckUsingSetCursorPosToExtendParentBoundaries() fallback obsoleted in 1.89 (August 2022) which allowed a SetCursorPos()/SetCursorScreenPos() call WITHOUT AN ITEM
+                         to extend parent window/cell boundaries. Replaced with assert/tooltip that would already happens if previously using IMGUI_DISABLE_OBSOLETE_FUNCTIONS. (#5548, #4510, #3355, #1760, #1490, #4152, #150)
+                         - Incorrect way to make a window content size 200x200:
+                              Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End();
+                         - Correct ways to make a window content size 200x200:
+                              Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End();
+                              Begin(...) + Dummy(ImVec2(200,200)) + End();
+                         - TL;DR; if the assert triggers, you can add a Dummy({0,0}) call to validate extending parent boundaries.
+ - 2025/06/11 (1.92.0) - THIS VERSION CONTAINS THE LARGEST AMOUNT OF BREAKING CHANGES SINCE 2015! I TRIED REALLY HARD TO KEEP THEM TO A MINIMUM, REDUCE THE AMOUNT OF INTERFERENCES, BUT INEVITABLY SOME USERS WILL BE AFFECTED.
+                         IN ORDER TO HELP US IMPROVE THE TRANSITION PROCESS, INCL. DOCUMENTATION AND COMMENTS, PLEASE REPORT **ANY** DOUBT, CONFUSION, QUESTIONS, FEEDBACK TO: https://github.com/ocornut/imgui/issues/
+                         As part of the plan to reduce impact of API breaking changes, several unfinished changes/features/refactors related to font and text systems and scaling will be part of subsequent releases (1.92.1+).
+                         If you are updating from an old version, and expecting a massive or difficult update, consider first updating to 1.91.9 to reduce the amount of changes.
+                       - Hard to read? Refer to 'docs/Changelog.txt' for a less compact and more complete version of this!
+                       - Fonts: **IMPORTANT**: if your app was solving the OSX/iOS Retina screen specific logical vs display scale problem by setting io.DisplayFramebufferScale (e.g. to 2.0f) + setting io.FontGlobalScale (e.g. to 1.0f/2.0f) + loading fonts at scaled sizes (e.g. size X * 2.0f):
+                         This WILL NOT map correctly to the new system! Because font will rasterize as requested size.
+                         - With a legacy backend (< 1.92): Instead of setting io.FontGlobalScale = 1.0f/N -> set ImFontCfg::RasterizerDensity = N. This already worked before, but is now pretty much required.
+                         - With a new backend (1.92+): This should be all automatic. FramebufferScale is automatically used to set current font RasterizerDensity. FramebufferScale is a per-viewport property provided by backend through the Platform_GetWindowFramebufferScale() handler in 'docking' branch.
+                       - Fonts: **IMPORTANT** on Font Sizing: Before 1.92, fonts were of a single size. They can now be dynamically sized.
+                         - PushFont() API now has a REQUIRED size parameter.
+                         - Before 1.92: PushFont() always used font "default" size specified in AddFont() call. It is equivalent to calling PushFont(font, font->LegacySize).
+                         - Since  1.92: PushFont(font, 0.0f) preserve the current font size which is a shared value.
+                         - To use old behavior: use 'ImGui::PushFont(font, font->LegacySize)' at call site.
+                         - Kept inline single parameter function. Will obsolete.
+                       - Fonts: **IMPORTANT** on Font Merging:
+                         - When searching for a glyph in multiple merged fonts: we search for the FIRST font source which contains the desired glyph.
+                           Because the user doesn't need to provide glyph ranges any more, it is possible that a glyph that you expected to fetch from a secondary/merged icon font may be erroneously fetched from the primary font.
+                         - When searching for a glyph in multiple merged fonts: we now search for the FIRST font source which contains the desired glyph. This is technically a different behavior than before!
+                         - e.g. If you are merging fonts you may have glyphs that you expected to load from Font Source 2 which exists in Font Source 1.
+                           After the update and when using a new backend, those glyphs may now loaded from Font Source 1!
+                         - We added `ImFontConfig::GlyphExcludeRanges[]` to specify ranges to exclude from a given font source:
+                             // Add Font Source 1 but ignore ICON_MIN_FA..ICON_MAX_FA range
+                             static ImWchar exclude_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
+                             ImFontConfig cfg1;
+                             cfg1.GlyphExcludeRanges = exclude_ranges;
+                             io.Fonts->AddFontFromFileTTF("segoeui.ttf", 0.0f, &cfg1);
+                             // Add Font Source 2, which expects to use the range above
+                             ImFontConfig cfg2;
+                             cfg2.MergeMode = true;
+                             io.Fonts->AddFontFromFileTTF("FontAwesome4.ttf", 0.0f, &cfg2);
+                         - You can use `Metrics/Debugger->Fonts->Font->Input Glyphs Overlap Detection Tool` to see list of glyphs available in multiple font sources. This can facilitate unde
+                       - Fonts: **IMPORTANT** on Thread Safety:
+                          - A few functions such as font->CalcTextSizeA() were, by sheer luck (== accidentally) thread-safe even thou we had never provided that guarantee. They are definitively not thread-safe anymore as new glyphs may be loaded.
+                       - Fonts: ImFont::FontSize was removed and does not make sense anymore. ImFont::LegacySize is the size passed to AddFont().
+                       - Fonts: Removed support for PushFont(NULL) which was a shortcut for "default font".
+                       - Fonts: Renamed/moved 'io.FontGlobalScale' to 'style.FontScaleMain'.
+                       - Textures: all API functions taking a 'ImTextureID' parameter are now taking a 'ImTextureRef'. Affected functions are: ImGui::Image(), ImGui::ImageWithBg(), ImGui::ImageButton(), ImDrawList::AddImage(), ImDrawList::AddImageQuad(), ImDrawList::AddImageRounded().
+                       - Fonts: obsoleted ImFontAtlas::GetTexDataAsRGBA32(), GetTexDataAsAlpha8(), Build(), SetTexID(), IsBuilt() functions. The new protocol for backends to handle textures doesn't need them. Kept redirection functions (will obsolete).
+                       - Fonts: ImFontConfig::OversampleH/OversampleV default to automatic (== 0) since v1.91.8. It is quite important you keep it automatic until we decide if we want to provide a way to express finer policy, otherwise you will likely waste texture space when using large glyphs. Note that the imgui_freetype backend doesn't use and does not need oversampling.
+                       - Fonts: specifying glyph ranges is now unnecessary. The value of ImFontConfig::GlyphRanges[] is only useful for legacy backends. All GetGlyphRangesXXXX() functions are now marked obsolete: GetGlyphRangesDefault(), GetGlyphRangesGreek(), GetGlyphRangesKorean(), GetGlyphRangesJapanese(), GetGlyphRangesChineseSimplifiedCommon(), GetGlyphRangesChineseFull(), GetGlyphRangesCyrillic(), GetGlyphRangesThai(), GetGlyphRangesVietnamese().
+                       - Fonts: removed ImFontAtlas::TexDesiredWidth to enforce a texture width. (#327)
+                       - Fonts: if you create and manage ImFontAtlas instances yourself (instead of relying on ImGuiContext to create one), you'll need to call ImFontAtlasUpdateNewFrame() yourself. An assert will trigger if you don't.
+                       - Fonts: obsolete ImGui::SetWindowFontScale() which is not useful anymore. Prefer using 'PushFont(NULL, style.FontSizeBase * factor)' or to manipulate other scaling factors.
+                       - Fonts: obsoleted ImFont::Scale which is not useful anymore.
+                       - Fonts: generally reworked Internals of ImFontAtlas and ImFont. While in theory a vast majority of users shouldn't be affected, some use cases or extensions might be. Among other things:
+                          - ImDrawCmd::TextureId has been changed to ImDrawCmd::TexRef.
+                          - ImFontAtlas::TexID has been changed to ImFontAtlas::TexRef.
+                          - ImFontAtlas::ConfigData[] has been renamed to ImFontAtlas::Sources[]
+                          - ImFont::ConfigData[], ConfigDataCount has been renamed to Sources[], SourceCount.
+                          - Each ImFont has a number of ImFontBaked instances corresponding to actively used sizes. ImFont::GetFontBaked(size) retrieves the one for a given size.
+                          - Fields moved from ImFont to ImFontBaked: IndexAdvanceX[], Glyphs[], Ascent, Descent, FindGlyph(), FindGlyphNoFallback(), GetCharAdvance().
+                          - Fields moved from ImFontAtlas to ImFontAtlas->Tex: ImFontAtlas::TexWidth => TexData->Width, ImFontAtlas::TexHeight => TexData->Height, ImFontAtlas::TexPixelsAlpha8/TexPixelsRGBA32 => TexData->GetPixels().
+                          - Widget code may use ImGui::GetFontBaked() instead of ImGui::GetFont() to access font data for current font at current font size (and you may use font->GetFontBaked(size) to access it for any other size.)
+                       - Fonts: (users of imgui_freetype): renamed ImFontAtlas::FontBuilderFlags to ImFontAtlas::FontLoaderFlags. Renamed ImFontConfig::FontBuilderFlags to ImFontConfig::FontLoaderFlags. Renamed ImGuiFreeTypeBuilderFlags to ImGuiFreeTypeLoaderFlags.
+                         If you used runtime imgui_freetype selection rather than the default IMGUI_ENABLE_FREETYPE compile-time option: Renamed/reworked ImFontBuilderIO into ImFontLoader. Renamed ImGuiFreeType::GetBuilderForFreeType() to ImGuiFreeType::GetFontLoader().
+                           - old:  io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()
+                           - new:  io.Fonts->FontLoader = ImGuiFreeType::GetFontLoader()
+                           - new:  io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader()) to change dynamically at runtime [from 1.92.1]
+                       - Fonts: (users of custom rectangles, see #8466): Renamed AddCustomRectRegular() to AddCustomRect(). Added GetCustomRect() as a replacement for GetCustomRectByIndex() + CalcCustomRectUV().
+                           - The output type of GetCustomRect() is now ImFontAtlasRect, which include UV coordinates. X->x, Y->y, Width->w, Height->h.
+                           - old:
+                                const ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(custom_rect_id);
+                                ImVec2 uv0, uv1;
+                                atlas->GetCustomRectUV(r, &uv0, &uv1);
+                                ImGui::Image(atlas->TexRef, ImVec2(r->w, r->h), uv0, uv1);
+                           - new;
+                                ImFontAtlasRect r;
+                                atlas->GetCustomRect(custom_rect_id, &r);
+                                ImGui::Image(atlas->TexRef, ImVec2(r.w, r.h), r.uv0, r.uv1);
+                           - We added a redirecting typedef but haven't attempted to magically redirect the field names, as this API is rarely used and the fix is simple.
+                           - Obsoleted AddCustomRectFontGlyph() as the API does not make sense for scalable fonts. Kept existing function which uses the font "default size" (Sources[0]->LegacySize). Added a helper AddCustomRectFontGlyphForSize() which is immediately marked obsolete, but can facilitate transitioning old code.
+                           - Prefer adding a font source (ImFontConfig) using a custom/procedural loader.
+                       - DrawList: Renamed ImDrawList::PushTextureID()/PopTextureID() to PushTexture()/PopTexture().
+                       - Backends: removed ImGui_ImplXXXX_CreateFontsTexture()/ImGui_ImplXXXX_DestroyFontsTexture() for all backends that had them. They should not be necessary any more.
+ - 2025/05/23 (1.92.0) - Fonts: changed ImFont::CalcWordWrapPositionA() to ImFont::CalcWordWrapPosition()
+                            - old:  const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, ....);
+                            - new:  const char* ImFont::CalcWordWrapPosition (float size,  const char* text, ....);
+                         The leading 'float scale' parameters was changed to 'float size'. This was necessary as 'scale' is assuming standard font size which is a concept we aim to eliminate in an upcoming update. Kept inline redirection function.
+ - 2025/05/15 (1.92.0) - TreeNode: renamed ImGuiTreeNodeFlags_NavLeftJumpsBackHere to ImGuiTreeNodeFlags_NavLeftJumpsToParent for clarity. Kept inline redirection enum (will obsolete).
+ - 2025/05/15 (1.92.0) - Commented out PushAllowKeyboardFocus()/PopAllowKeyboardFocus() which was obsoleted in 1.89.4. Use PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop)/PopItemFlag() instead. (#3092)
+ - 2025/05/15 (1.92.0) - Commented out ImGuiListClipper::ForceDisplayRangeByIndices() which was obsoleted in 1.89.6. Use ImGuiListClipper::IncludeItemsByIndex() instead.
  - 2025/03/05 (1.91.9) - BeginMenu(): Internals: reworked mangling of menu windows to use "###Menu_00" etc. instead of "##Menu_00", allowing them to also store the menu name before it. This shouldn't affect code unless directly accessing menu window from their mangled name.
+ - 2025/04/16 (1.91.9) - Internals: RenderTextEllipsis() function removed the 'float clip_max_x' parameter directly preceding 'float ellipsis_max_x'. Values were identical for a vast majority of users. (#8387)
  - 2025/02/27 (1.91.9) - Image(): removed 'tint_col' and 'border_col' parameter from Image() function. Added ImageWithBg() replacement. (#8131, #8238)
                             - old: void Image      (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1), ImVec4 tint_col = (1,1,1,1), ImVec4 border_col = (0,0,0,0));
                             - new: void Image      (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1));
@@ -472,12 +530,12 @@
                          in doubt it is almost always better to do an intermediate intptr_t cast, since it allows casting any pointer/integer type without warning:
                             - May warn:    ImGui::Image((void*)MyTextureData, ...);
                             - May warn:    ImGui::Image((void*)(intptr_t)MyTextureData, ...);
-                            - Won't warn:  ImGui::Image((ImTextureID)(intptr_t)MyTextureData), ...);
+                            - Won't warn:  ImGui::Image((ImTextureID)(intptr_t)MyTextureData, ...);
   -                      note that you can always define ImTextureID to be your own high-level structures (with dedicated constructors) if you like.
  - 2024/10/03 (1.91.3) - drags: treat v_min==v_max as a valid clamping range when != 0.0f. Zero is a still special value due to legacy reasons, unless using ImGuiSliderFlags_ClampZeroRange. (#7968, #3361, #76)
                        - drags: extended behavior of ImGuiSliderFlags_AlwaysClamp to include _ClampZeroRange. It considers v_min==v_max==0.0f as a valid clamping range (aka edits not allowed).
                          although unlikely, it you wish to only clamp on text input but want v_min==v_max==0.0f to mean unclamped drags, you can use _ClampOnInput instead of _AlwaysClamp. (#7968, #3361, #76)
- - 2024/09/10 (1.91.2) - internals: using multiple overlayed ButtonBehavior() with same ID will now have io.ConfigDebugHighlightIdConflicts=true feature emit a warning. (#8030)
+ - 2024/09/10 (1.91.2) - internals: using multiple overlaid ButtonBehavior() with same ID will now have io.ConfigDebugHighlightIdConflicts=true feature emit a warning. (#8030)
                          it was one of the rare case where using same ID is legal. workarounds: (1) use single ButtonBehavior() call with multiple _MouseButton flags, or (2) surround the calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
  - 2024/08/23 (1.91.1) - renamed ImGuiChildFlags_Border to ImGuiChildFlags_Borders for consistency. kept inline redirection flag.
  - 2024/08/22 (1.91.1) - moved some functions from ImGuiIO to ImGuiPlatformIO structure:
@@ -667,7 +725,7 @@
  - 2022/01/10 (1.87) - inputs: reworked keyboard IO. Removed io.KeyMap[], io.KeysDown[] in favor of calling io.AddKeyEvent(), ImGui::IsKeyDown(). Removed GetKeyIndex(), now unnecessary. All IsKeyXXX() functions now take ImGuiKey values. All features are still functional until IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Read Changelog and Release Notes for details.
                         - IsKeyPressed(MY_NATIVE_KEY_XXX)              -> use IsKeyPressed(ImGuiKey_XXX)
                         - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX))      -> use IsKeyPressed(ImGuiKey_XXX)
-                        - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() (+ call io.SetKeyEventNativeData() if you want legacy user code to stil function with legacy key codes).
+                        - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() (+ call io.SetKeyEventNativeData() if you want legacy user code to still function with legacy key codes).
                         - Backend writing to io.KeyCtrl, io.KeyShift.. -> backend should call io.AddKeyEvent() with ImGuiMod_XXX values. *IF YOU PULLED CODE BETWEEN 2021/01/10 and 2021/01/27: We used to have a io.AddKeyModsEvent() function which was now replaced by io.AddKeyEvent() with ImGuiMod_XXX values.*
                      - one case won't work with backward compatibility: if your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") because those values are now larger than the legacy KeyDown[] array. Will assert.
                      - inputs: added ImGuiKey_ModCtrl/ImGuiKey_ModShift/ImGuiKey_ModAlt/ImGuiKey_ModSuper values to submit keyboard modifiers using io.AddKeyEvent(), instead of writing directly to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper.
@@ -759,7 +817,7 @@
                        - ShowTestWindow()                    -> use ShowDemoWindow()
                        - IsRootWindowFocused()               -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow)
                        - IsRootWindowOrAnyChildFocused()     -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)
-                       - SetNextWindowContentWidth(w)        -> use SetNextWindowContentSize(ImVec2(w, 0.0f)
+                       - SetNextWindowContentWidth(w)        -> use SetNextWindowContentSize(ImVec2(w, 0.0f))
                        - GetItemsLineHeightWithSpacing()     -> use GetFrameHeightWithSpacing()
                        - ImGuiCol_ChildWindowBg              -> use ImGuiCol_ChildBg
                        - ImGuiStyleVar_ChildWindowRounding   -> use ImGuiStyleVar_ChildRounding
@@ -876,7 +934,7 @@
                      - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete).
                      - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete).
  - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency.
- - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix.
+ - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicitly to fix.
  - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame type.
  - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely.
  - 2017/08/13 (1.51) - renamed ImGuiCol_Column to ImGuiCol_Separator, ImGuiCol_ColumnHovered to ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive to ImGuiCol_SeparatorActive. Kept redirection enums (will obsolete).
@@ -1001,6 +1059,7 @@
       associated with it.
 
  Q: What is this library called?
+ Q: What is the difference between Dear ImGui and traditional UI toolkits?
  Q: Which version should I get?
  >> This library is called "Dear ImGui", please don't call it "ImGui" :)
  >> See https://www.dearimgui.com/faq for details.
@@ -1178,6 +1237,9 @@
 #define IMGUI_DEBUG_NAV_SCORING     0   // Display navigation scoring preview when hovering items. Hold CTRL to display for all candidates. CTRL+Arrow to change last direction.
 #define IMGUI_DEBUG_NAV_RECTS       0   // Display the reference navigation rectangle for each window
 
+// Default font size if unspecified in both style.FontSizeBase and AddFontXXX() calls.
+static const float FONT_DEFAULT_SIZE = 20.0f;
+
 // When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch.
 static const float NAV_WINDOWING_HIGHLIGHT_DELAY            = 0.20f;    // Time before the highlight and screen dimming starts fading in
 static const float NAV_WINDOWING_LIST_APPEAR_DELAY          = 0.15f;    // Time before the window list starts to appear
@@ -1234,7 +1296,7 @@
 static inline void      NavUpdateAnyRequestFlag();
 static void             NavUpdateCreateWrappingRequest();
 static void             NavEndFrame();
-static bool             NavScoreItem(ImGuiNavItemData* result);
+static bool             NavScoreItem(ImGuiNavItemData* result, const ImRect& nav_bb);
 static void             NavApplyItemToResult(ImGuiNavItemData* result);
 static void             NavProcessItem();
 static void             NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags);
@@ -1260,6 +1322,10 @@
 static void             UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt);
 
 // Misc
+static void             UpdateFontsNewFrame();
+static void             UpdateFontsEndFrame();
+static void             UpdateTexturesNewFrame();
+static void             UpdateTexturesEndFrame();
 static void             UpdateSettings();
 static int              UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect);
 static void             RenderWindowOuterBorders(ImGuiWindow* window);
@@ -1324,6 +1390,10 @@
 
 ImGuiStyle::ImGuiStyle()
 {
+    FontSizeBase                = 0.0f;             // Will default to io.Fonts->Fonts[0] on first frame.
+    FontScaleMain               = 1.0f;             // Main scale factor. May be set by application once, or exposed to end-user.
+    FontScaleDpi                = 1.0f;             // Additional scale factor from viewport/monitor contents scale. When io.ConfigDpiScaleFonts is enabled, this is automatically overwritten when changing monitor DPI.
+
     Alpha                       = 1.0f;             // Global alpha applies to everything in Dear ImGui.
     DisabledAlpha               = 0.60f;            // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha.
     WindowPadding               = ImVec2(8,8);      // Padding within a window
@@ -1348,18 +1418,24 @@
     ColumnsMinSpacing           = 6.0f;             // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1).
     ScrollbarSize               = 14.0f;            // Width of the vertical scrollbar, Height of the horizontal scrollbar
     ScrollbarRounding           = 9.0f;             // Radius of grab corners rounding for scrollbar
+    ScrollbarPadding            = 2.0f;             // Padding of scrollbar grab within its frame (same for both axises)
     GrabMinSize                 = 12.0f;            // Minimum width/height of a grab box for slider/scrollbar
     GrabRounding                = 0.0f;             // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs.
     LogSliderDeadzone           = 4.0f;             // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero.
     ImageBorderSize             = 0.0f;             // Thickness of border around tabs.
     TabRounding                 = 5.0f;             // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs.
     TabBorderSize               = 0.0f;             // Thickness of border around tabs.
+    TabMinWidthBase             = 1.0f;             // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected.
+    TabMinWidthShrink           = 80.0f;            // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy.
     TabCloseButtonMinWidthSelected   = -1.0f;       // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width.
     TabCloseButtonMinWidthUnselected = 0.0f;        // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected.
     TabBarBorderSize            = 1.0f;             // Thickness of tab-bar separator, which takes on the tab active color to denote focus.
     TabBarOverlineSize          = 1.0f;             // Thickness of tab-bar overline, which highlights the selected tab-bar.
     TableAngledHeadersAngle     = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees).
     TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell
+    TreeLinesFlags              = ImGuiTreeNodeFlags_DrawLinesNone;
+    TreeLinesSize               = 1.0f;             // Thickness of outlines when using ImGuiTreeNodeFlags_DrawLines.
+    TreeLinesRounding           = 0.0f;             // Radius of lines connecting child nodes to the vertical line.
     ColorButtonPosition         = ImGuiDir_Right;   // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
     ButtonTextAlign             = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text.
     SelectableTextAlign         = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
@@ -1382,14 +1458,20 @@
     HoverFlagsForTooltipMouse   = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled;    // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse.
     HoverFlagsForTooltipNav     = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled;  // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad.
 
+    // [Internal]
+    _MainScale                  = 1.0f;
+    _NextFrameFontSizeBase      = 0.0f;
+
     // Default theme
     ImGui::StyleColorsDark(this);
 }
 
-// To scale your entire UI (e.g. if you want your app to use High DPI or generally be DPI aware) you may use this helper function. Scaling the fonts is done separately and is up to you.
+
+// Scale all spacing/padding/thickness values. Do not scale fonts.
 // Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times.
 void ImGuiStyle::ScaleAllSizes(float scale_factor)
 {
+    _MainScale *= scale_factor;
     WindowPadding = ImTrunc(WindowPadding * scale_factor);
     WindowRounding = ImTrunc(WindowRounding * scale_factor);
     WindowMinSize = ImTrunc(WindowMinSize * scale_factor);
@@ -1406,14 +1488,18 @@
     ColumnsMinSpacing = ImTrunc(ColumnsMinSpacing * scale_factor);
     ScrollbarSize = ImTrunc(ScrollbarSize * scale_factor);
     ScrollbarRounding = ImTrunc(ScrollbarRounding * scale_factor);
+    ScrollbarPadding = ImTrunc(ScrollbarPadding * scale_factor);
     GrabMinSize = ImTrunc(GrabMinSize * scale_factor);
     GrabRounding = ImTrunc(GrabRounding * scale_factor);
     LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor);
     ImageBorderSize = ImTrunc(ImageBorderSize * scale_factor);
     TabRounding = ImTrunc(TabRounding * scale_factor);
+    TabMinWidthBase = ImTrunc(TabMinWidthBase * scale_factor);
+    TabMinWidthShrink = ImTrunc(TabMinWidthShrink * scale_factor);
     TabCloseButtonMinWidthSelected = (TabCloseButtonMinWidthSelected > 0.0f && TabCloseButtonMinWidthSelected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthSelected * scale_factor) : TabCloseButtonMinWidthSelected;
     TabCloseButtonMinWidthUnselected = (TabCloseButtonMinWidthUnselected > 0.0f && TabCloseButtonMinWidthUnselected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthUnselected * scale_factor) : TabCloseButtonMinWidthUnselected;
     TabBarOverlineSize = ImTrunc(TabBarOverlineSize * scale_factor);
+    TreeLinesRounding = ImTrunc(TreeLinesRounding * scale_factor);
     SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor);
     DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor);
     DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor);
@@ -1437,9 +1523,11 @@
     UserData = NULL;
 
     Fonts = NULL;
-    FontGlobalScale = 1.0f;
     FontDefault = NULL;
     FontAllowUserScaling = false;
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    FontGlobalScale = 1.0f; // Use style.FontScaleMain instead!
+#endif
     DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
 
     // Keyboard/Gamepad Navigation options
@@ -1748,7 +1836,7 @@
     // On MacOS X: Convert Ctrl(Super)+Left click into Right-click: handle held button.
     if (ConfigMacOSXBehaviors && mouse_button == 0 && MouseCtrlLeftAsRightClick)
     {
-        // Order of both statements matterns: this event will still release mouse button 1
+        // Order of both statements matters: this event will still release mouse button 1
         mouse_button = 1;
         if (!down)
             MouseCtrlLeftAsRightClick = false;
@@ -1983,7 +2071,7 @@
     if (count < 1)
         return;
     if (count > 1)
-        strncpy(dst, src, count - 1);
+        strncpy(dst, src, count - 1); // FIXME-OPT: strncpy not only doesn't guarantee 0-termination, it also always writes the whole array
     dst[count - 1] = 0;
 }
 
@@ -1994,6 +2082,12 @@
     return (char*)memcpy(buf, (const void*)str, len + 1);
 }
 
+void* ImMemdup(const void* src, size_t size)
+{
+    void* dst = IM_ALLOC(size);
+    return memcpy(dst, src, size);
+}
+
 char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src)
 {
     size_t dst_buf_size = p_dst_size ? *p_dst_size : ImStrlen(dst) + 1;
@@ -2298,6 +2392,17 @@
     return ~crc;
 }
 
+// Skip to the "###" marker if any. We don't skip past to match the behavior of GetID()
+// FIXME-OPT: This is not designed to be optimal. Use with care.
+const char* ImHashSkipUncontributingPrefix(const char* label)
+{
+    const char* result = label;
+    while (unsigned char c = *label++)
+        if (c == '#' && label[0] == '#' && label[1] == '#')
+            result = label - 1;
+    return result;
+}
+
 //-----------------------------------------------------------------------------
 // [SECTION] MISC HELPERS/UTILITIES (File functions)
 //-----------------------------------------------------------------------------
@@ -2503,11 +2608,11 @@
     return 0;
 }
 
-const char* ImTextCharToUtf8(char out_buf[5], unsigned int c)
+int ImTextCharToUtf8(char out_buf[5], unsigned int c)
 {
     int count = ImTextCharToUtf8_inline(out_buf, 5, c);
     out_buf[count] = 0;
-    return out_buf;
+    return count;
 }
 
 // Not optimal but we very rarely use this function.
@@ -2974,19 +3079,21 @@
     va_end(args_copy);
 }
 
+IM_MSVC_RUNTIME_CHECKS_OFF
 void ImGuiTextIndex::append(const char* base, int old_size, int new_size)
 {
     IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset);
     if (old_size == new_size)
         return;
     if (EndOffset == 0 || base[EndOffset - 1] == '\n')
-        LineOffsets.push_back(EndOffset);
+        Offsets.push_back(EndOffset);
     const char* base_end = base + new_size;
     for (const char* p = base + old_size; (p = (const char*)ImMemchr(p, '\n', base_end - p)) != 0; )
         if (++p < base_end) // Don't push a trailing offset on last \n
-            LineOffsets.push_back((int)(intptr_t)(p - base));
+            Offsets.push_back((int)(intptr_t)(p - base));
     EndOffset = ImMax(EndOffset, new_size);
 }
+IM_MSVC_RUNTIME_CHECKS_RESTORE
 
 //-----------------------------------------------------------------------------
 // [SECTION] ImGuiListClipper
@@ -3024,7 +3131,7 @@
     }
 }
 
-static void ImGuiListClipper_SeekCursorAndSetupPrevLine(float pos_y, float line_height)
+static void ImGuiListClipper_SeekCursorAndSetupPrevLine(ImGuiListClipper* clipper, float pos_y, float line_height)
 {
     // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor.
     // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue.
@@ -3042,10 +3149,13 @@
     {
         if (table->IsInsideRow)
             ImGui::TableEndRow(table);
-        table->RowPosY2 = window->DC.CursorPos.y;
         const int row_increase = (int)((off_y / line_height) + 0.5f);
-        //table->CurrentRow += row_increase; // Can't do without fixing TableEndRow()
-        table->RowBgColorCounter += row_increase;
+        if (row_increase > 0 && (clipper->Flags & ImGuiListClipperFlags_NoSetTableRowCounters) == 0) // If your clipper item height is != from actual table row height, consider using ImGuiListClipperFlags_NoSetTableRowCounters. See #8886.
+        {
+            table->CurrentRow += row_increase;
+            table->RowBgColorCounter += row_increase;
+        }
+        table->RowPosY2 = window->DC.CursorPos.y;
     }
 }
 
@@ -3128,7 +3238,7 @@
     // - StartPosY starts from ItemsFrozen, by adding SeekOffsetY we generally cancel that out (SeekOffsetY == LossynessOffset - ItemsFrozen * ItemsHeight).
     // - The reason we store SeekOffsetY instead of inferring it, is because we want to allow user to perform Seek after the last step, where ImGuiListClipperData is already done.
     float pos_y = (float)((double)StartPosY + StartSeekOffsetY + (double)item_n * ItemsHeight);
-    ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, ItemsHeight);
+    ImGuiListClipper_SeekCursorAndSetupPrevLine(this, pos_y, ItemsHeight);
 }
 
 static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper)
@@ -3181,10 +3291,17 @@
         if (table)
             IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y);
 
-        clipper->ItemsHeight = (window->DC.CursorPos.y - clipper->StartPosY) / (float)(clipper->DisplayEnd - clipper->DisplayStart);
-        bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision(clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y);
+        bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision((float)clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y);
         if (affected_by_floating_point_precision)
+        {
+            // Mitigation/hack for very large range: assume last time height constitute line height.
             clipper->ItemsHeight = window->DC.PrevLineSize.y + g.Style.ItemSpacing.y; // FIXME: Technically wouldn't allow multi-line entries.
+            window->DC.CursorPos.y = (float)(clipper->StartPosY + clipper->ItemsHeight);
+        }
+        else
+        {
+            clipper->ItemsHeight = (float)(window->DC.CursorPos.y - clipper->StartPosY) / (float)(clipper->DisplayEnd - clipper->DisplayStart);
+        }
         if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode.
             return false;
         IM_ASSERT(clipper->ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!");
@@ -3208,7 +3325,10 @@
             // Add range selected to be included for navigation
             const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav);
             if (is_nav_request)
+            {
+                data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringRect.Min.y, g.NavScoringRect.Max.y, 0, 0));
                 data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0));
+            }
             if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && g.NavTabbingDir == -1)
                 data->Ranges.push_back(ImGuiListClipperRange::FromIndices(clipper->ItemsCount - 1, clipper->ItemsCount));
 
@@ -3217,7 +3337,6 @@
             if (g.NavId != 0 && window->NavLastIds[0] == g.NavId)
                 data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0));
 
-            // Add visible range
             float min_y = window->ClipRect.Min.y;
             float max_y = window->ClipRect.Max.y;
 
@@ -3236,6 +3355,7 @@
                     data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0));
             }
 
+            // Add main visible range
             const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0;
             const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0;
             data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max));
@@ -3301,6 +3421,13 @@
     return ret;
 }
 
+// Generic helper, equivalent to old ImGui::CalcListClipping() but statelesss
+void ImGui::CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end)
+{
+    *out_visible_start = ImMax((int)((clip_rect.Min.y - pos.y) / items_height), 0);
+    *out_visible_end = ImMax((int)ImCeil((clip_rect.Max.y - pos.y) / items_height), *out_visible_start);
+}
+
 //-----------------------------------------------------------------------------
 // [SECTION] STYLING
 //-----------------------------------------------------------------------------
@@ -3406,15 +3533,20 @@
     { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, CellPadding) },               // ImGuiStyleVar_CellPadding
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) },             // ImGuiStyleVar_ScrollbarSize
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) },         // ImGuiStyleVar_ScrollbarRounding
+    { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarPadding) },          // ImGuiStyleVar_ScrollbarPadding
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabMinSize) },               // ImGuiStyleVar_GrabMinSize
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabRounding) },              // ImGuiStyleVar_GrabRounding
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ImageBorderSize) },           // ImGuiStyleVar_ImageBorderSize
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabRounding) },               // ImGuiStyleVar_TabRounding
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBorderSize) },             // ImGuiStyleVar_TabBorderSize
+    { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabMinWidthBase) },           // ImGuiStyleVar_TabMinWidthBase
+    { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabMinWidthShrink) },         // ImGuiStyleVar_TabMinWidthShrink
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) },          // ImGuiStyleVar_TabBarBorderSize
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarOverlineSize) },        // ImGuiStyleVar_TabBarOverlineSize
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)},    // ImGuiStyleVar_TableAngledHeadersAngle
     { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign
+    { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesSize)},              // ImGuiStyleVar_TreeLinesSize
+    { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TreeLinesRounding)},          // ImGuiStyleVar_TreeLinesRounding
     { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) },           // ImGuiStyleVar_ButtonTextAlign
     { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) },       // ImGuiStyleVar_SelectableTextAlign
     { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)},    // ImGuiStyleVar_SeparatorTextBorderSize
@@ -3544,6 +3676,7 @@
     case ImGuiCol_ResizeGrip: return "ResizeGrip";
     case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered";
     case ImGuiCol_ResizeGripActive: return "ResizeGripActive";
+    case ImGuiCol_InputTextCursor: return "InputTextCursor";
     case ImGuiCol_TabHovered: return "TabHovered";
     case ImGuiCol_Tab: return "Tab";
     case ImGuiCol_TabSelected: return "TabSelected";
@@ -3562,7 +3695,9 @@
     case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt";
     case ImGuiCol_TextLink: return "TextLink";
     case ImGuiCol_TextSelectedBg: return "TextSelectedBg";
+    case ImGuiCol_TreeLines: return "TreeLines";
     case ImGuiCol_DragDropTarget: return "DragDropTarget";
+    case ImGuiCol_UnsavedMarker: return "UnsavedMarker";
     case ImGuiCol_NavCursor: return "NavCursor";
     case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight";
     case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg";
@@ -3636,7 +3771,7 @@
 
 // Default clip_rect uses (pos_min,pos_max)
 // Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges)
-// FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especally for text above draw_list->DrawList.
+// FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especially for text above draw_list->DrawList.
 // Effectively as this is called from widget doing their own coarse clipping it's not very valuable presently. Next time function will take
 // better advantage of the render function taking size into account for coarse clipping.
 void ImGui::RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect)
@@ -3683,18 +3818,19 @@
 }
 
 // Another overly complex function until we reorganize everything into a nice all-in-one helper.
-// This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display.
+// This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) from 'ellipsis_max_x' which may be beyond it.
 // This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move.
-void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known)
+// (BREAKING) On 2025/04/16 we removed the 'float clip_max_x' parameters which was preceding 'float ellipsis_max' and was the same value for 99% of users.
+void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known)
 {
     ImGuiContext& g = *GImGui;
     if (text_end_full == NULL)
         text_end_full = FindRenderedTextEnd(text);
     const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f);
 
-    //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 4), IM_COL32(0, 0, 255, 255));
-    //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y-2), ImVec2(ellipsis_max_x, pos_max.y+2), IM_COL32(0, 255, 0, 255));
-    //draw_list->AddLine(ImVec2(clip_max_x, pos_min.y), ImVec2(clip_max_x, pos_max.y), IM_COL32(255, 0, 0, 255));
+    //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 6), IM_COL32(0, 0, 255, 255));
+    //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y - 2), ImVec2(ellipsis_max_x, pos_max.y + 3), IM_COL32(0, 255, 0, 255));
+
     // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels.
     if (text_size.x > pos_max.x - pos_min.x)
     {
@@ -3707,17 +3843,12 @@
         const float font_size = draw_list->_Data->FontSize;
         const float font_scale = draw_list->_Data->FontScale;
         const char* text_end_ellipsis = NULL;
-        const float ellipsis_width = font->EllipsisWidth * font_scale;
+        ImFontBaked* baked = font->GetFontBaked(font_size);
+        const float ellipsis_width = baked->GetCharAdvance(font->EllipsisChar) * font_scale;
 
         // We can now claim the space between pos_max.x and ellipsis_max.x
         const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_width) - pos_min.x, 1.0f);
         float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x;
-        if (text == text_end_ellipsis && text_end_ellipsis < text_end_full)
-        {
-            // Always display at least 1 character if there's no room for character + ellipsis
-            text_end_ellipsis = text + ImTextCountUtf8BytesFromChar(text, text_end_full);
-            text_size_clipped_x = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text, text_end_ellipsis).x;
-        }
         while (text_end_ellipsis > text && ImCharIsBlankA(text_end_ellipsis[-1]))
         {
             // Trim trailing space before ellipsis (FIXME: Supporting non-ascii blanks would be nice, for this we need a function to backtrack in UTF-8 text)
@@ -3726,15 +3857,14 @@
         }
 
         // Render text, render ellipsis
-        RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f));
+        RenderTextClippedEx(draw_list, pos_min, pos_max, text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f));
+        ImVec4 cpu_fine_clip_rect(pos_min.x, pos_min.y, pos_max.x, pos_max.y);
         ImVec2 ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y));
-        if (ellipsis_pos.x + ellipsis_width <= ellipsis_max_x)
-            for (int i = 0; i < font->EllipsisCharCount; i++, ellipsis_pos.x += font->EllipsisCharStep * font_scale)
-                font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar);
+        font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar, &cpu_fine_clip_rect);
     }
     else
     {
-        RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_full, &text_size, ImVec2(0.0f, 0.0f));
+        RenderTextClippedEx(draw_list, pos_min, pos_max, text, text_end_full, &text_size, ImVec2(0.0f, 0.0f));
     }
 
     if (g.LogEnabled)
@@ -3806,7 +3936,7 @@
     ImGuiContext& g = *GImGui;
     if (mouse_cursor <= ImGuiMouseCursor_None || mouse_cursor >= ImGuiMouseCursor_COUNT) // We intentionally accept out of bound values.
         mouse_cursor = ImGuiMouseCursor_Arrow;
-    ImFontAtlas* font_atlas = g.DrawListSharedData.Font->ContainerAtlas;
+    ImFontAtlas* font_atlas = g.DrawListSharedData.FontAtlas;
     for (ImGuiViewportP* viewport : g.Viewports)
     {
         // We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor.
@@ -3818,12 +3948,12 @@
         if (!viewport->GetMainRect().Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale)))
             continue;
         ImDrawList* draw_list = GetForegroundDrawList(viewport);
-        ImTextureID tex_id = font_atlas->TexID;
-        draw_list->PushTextureID(tex_id);
-        draw_list->AddImage(tex_id, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow);
-        draw_list->AddImage(tex_id, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow);
-        draw_list->AddImage(tex_id, pos,                        pos + size * scale,                  uv[2], uv[3], col_border);
-        draw_list->AddImage(tex_id, pos,                        pos + size * scale,                  uv[0], uv[1], col_fill);
+        ImTextureRef tex_ref = font_atlas->TexRef;
+        draw_list->PushTexture(tex_ref);
+        draw_list->AddImage(tex_ref, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow);
+        draw_list->AddImage(tex_ref, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow);
+        draw_list->AddImage(tex_ref, pos,                        pos + size * scale,                  uv[2], uv[3], col_border);
+        draw_list->AddImage(tex_ref, pos,                        pos + size * scale,                  uv[0], uv[1], col_fill);
         if (mouse_cursor == ImGuiMouseCursor_Wait || mouse_cursor == ImGuiMouseCursor_Progress)
         {
             float a_min = ImFmod((float)g.Time * 5.0f, 2.0f * IM_PI);
@@ -3831,7 +3961,7 @@
             draw_list->PathArcTo(pos + ImVec2(14, -1) * scale, 6.0f * scale, a_min, a_max);
             draw_list->PathStroke(col_fill, ImDrawFlags_None, 3.0f * scale);
         }
-        draw_list->PopTextureID();
+        draw_list->PopTexture();
     }
 }
 
@@ -3913,10 +4043,13 @@
     InputTextState.Ctx = this;
 
     Initialized = false;
-    FontAtlasOwnedByContext = shared_font_atlas ? false : true;
     Font = NULL;
-    FontSize = FontBaseSize = FontScale = CurrentDpiScale = 0.0f;
+    FontBaked = NULL;
+    FontSize = FontSizeBase = FontBakedScale = CurrentDpiScale = 0.0f;
+    FontRasterizerDensity = 1.0f;
     IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)();
+    if (shared_font_atlas == NULL)
+        IO.Fonts->OwnerContext = this;
     Time = 0.0f;
     FrameCount = 0;
     FrameCountEnded = FrameCountRendered = -1;
@@ -3941,8 +4074,8 @@
     WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1;
     WheelingWindowReleaseTimer = 0.0f;
 
-    DebugDrawIdConflicts = 0;
-    DebugHookIdInfo = 0;
+    DebugDrawIdConflictsId = 0;
+    DebugHookIdInfoId = 0;
     HoveredId = HoveredIdPreviousFrame = 0;
     HoveredIdPreviousFrameItemCount = 0;
     HoveredIdAllowOverlap = false;
@@ -3962,6 +4095,7 @@
     ActiveIdClickOffset = ImVec2(-1, -1);
     ActiveIdWindow = NULL;
     ActiveIdSource = ImGuiInputSource_None;
+    ActiveIdDisabledId = 0;
     ActiveIdMouseButton = -1;
     ActiveIdPreviousFrame = 0;
     memset(&DeactivatedItemData, 0, sizeof(DeactivatedItemData));
@@ -4015,9 +4149,11 @@
 
     // All platforms use Ctrl+Tab but Ctrl<>Super are swapped on Mac...
     // FIXME: Because this value is stored, it annoyingly interfere with toggling io.ConfigMacOSXBehaviors updating this..
+    ConfigNavWindowingWithGamepad = true;
     ConfigNavWindowingKeyNext = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiKey_Tab);
     ConfigNavWindowingKeyPrev = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab);
     NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL;
+    NavWindowingInputSource = ImGuiInputSource_None;
     NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f;
     NavWindowingToggleLayer = false;
     NavWindowingToggleKey = ImGuiKey_None;
@@ -4029,6 +4165,7 @@
     DragDropSourceFrameCount = -1;
     DragDropMouseButton = -1;
     DragDropTargetId = 0;
+    DragDropTargetFullViewport = 0;
     DragDropAcceptFlags = ImGuiDragDropFlags_None;
     DragDropAcceptIdCurrRectSurface = 0.0f;
     DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0;
@@ -4050,6 +4187,7 @@
     MouseCursor = ImGuiMouseCursor_Arrow;
     MouseStationaryTimer = 0.0f;
 
+    InputTextPasswordFontBackupFlags = ImFontFlags_None;
     TempInputId = 0;
     memset(&DataTypeZeroValue, 0, sizeof(DataTypeZeroValue));
     BeginMenuDepth = BeginComboDepth = 0;
@@ -4169,6 +4307,18 @@
 #ifdef IMGUI_HAS_DOCK
 #endif
 
+    // Print a debug message when running with debug feature IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS because it is very slow.
+    // DO NOT COMMENT OUT THIS MESSAGE. IT IS DESIGNED TO REMIND YOU THAT IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS SHOULD ONLY BE TEMPORARILY ENABLED.
+#ifdef IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
+    DebugLog("IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS is enabled.\nMust disable after use! Otherwise Dear ImGui will run slower.\n");
+#endif
+
+    // ImDrawList/ImFontAtlas are designed to function without ImGui, and 99% of it works without an ImGui context.
+    // But this link allows us to facilitate/handle a few edge cases better.
+    ImFontAtlas* atlas = g.IO.Fonts;
+    g.DrawListSharedData.Context = &g;
+    RegisterFontAtlas(atlas);
+
     g.Initialized = true;
 }
 
@@ -4180,12 +4330,15 @@
     IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == NULL, "Forgot to shutdown Renderer backend?");
 
     // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame)
-    if (g.IO.Fonts && g.FontAtlasOwnedByContext)
+    for (ImFontAtlas* atlas : g.FontAtlases)
     {
-        g.IO.Fonts->Locked = false;
-        IM_DELETE(g.IO.Fonts);
+        UnregisterFontAtlas(atlas);
+        if (atlas->RefCount == 0)
+        {
+            atlas->Locked = false;
+            IM_DELETE(atlas);
+        }
     }
-    g.IO.Fonts = NULL;
     g.DrawListSharedData.TempBuffer.clear();
 
     // Cleanup of other data are conditional on actually having initialized Dear ImGui.
@@ -4237,6 +4390,7 @@
     g.ClipboardHandlerData.clear();
     g.MenusIdSubmittedThisFrame.clear();
     g.InputTextState.ClearFreeMemory();
+    g.InputTextLineIndex.clear();
     g.InputTextDeactivatedState.ClearFreeMemory();
 
     g.SettingsWindows.clear();
@@ -4314,7 +4468,7 @@
     SettingsOffset = -1;
     DrawList = &DrawListInst;
     DrawList->_OwnerName = Name;
-    DrawList->_Data = &Ctx->DrawListSharedData;
+    DrawList->_SetDrawListSharedData(&Ctx->DrawListSharedData);
     NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX);
 }
 
@@ -4334,8 +4488,15 @@
     g.CurrentDpiScale = 1.0f; // FIXME-DPI: WIP this is modified in docking
     if (window)
     {
-        g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize();
-        g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize;
+        bool backup_skip_items = window->SkipItems;
+        window->SkipItems = false;
+        if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures)
+        {
+            ImGuiViewport* viewport = window->Viewport;
+            g.FontRasterizerDensity = (viewport->FramebufferScale.x != 0.0f) ? viewport->FramebufferScale.x : g.IO.DisplayFramebufferScale.x; // == SetFontRasterizerDensity()
+        }
+        ImGui::UpdateCurrentFontSize(0.0f);
+        window->SkipItems = backup_skip_items;
         ImGui::NavUpdateCurrentWindowIsScrollPushableX();
     }
 }
@@ -4345,9 +4506,12 @@
     ImGuiContext& g = *GImGui;
     g.ItemFlagsStack.clear();
     g.GroupStack.clear();
+    g.InputTextLineIndex.clear();
     g.MultiSelectTempDataStacked = 0;
     g.MultiSelectTempData.clear_destruct();
     TableGcCompactSettings();
+    for (ImFontAtlas* atlas : g.FontAtlases)
+        atlas->CompactCache();
 }
 
 // Free up/compact internal window buffers, we can use this when a window becomes unused.
@@ -4383,15 +4547,6 @@
     // Clear previous active id
     if (g.ActiveId != 0)
     {
-        // While most behaved code would make an effort to not steal active id during window move/drag operations,
-        // we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch
-        // may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that.
-        if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId)
-        {
-            IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n");
-            g.MovingWindow = NULL;
-        }
-
         // Store deactivate data
         ImGuiDeactivatedItemData* deactivated_data = &g.DeactivatedItemData;
         deactivated_data->ID = g.ActiveId;
@@ -4404,6 +4559,15 @@
         // One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveID()
         if (g.InputTextState.ID == g.ActiveId)
             InputTextDeactivateHook(g.ActiveId);
+
+        // While most behaved code would make an effort to not steal active id during window move/drag operations,
+        // we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch
+        // may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that.
+        if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId)
+        {
+            IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n");
+            StopMouseMovingWindow();
+        }
     }
 
     // Set active id
@@ -4427,6 +4591,7 @@
     g.ActiveIdWindow = window;
     g.ActiveIdHasBeenEditedThisFrame = false;
     g.ActiveIdFromShortcut = false;
+    g.ActiveIdDisabledId = 0;
     if (id)
     {
         g.ActiveIdIsAlive = id;
@@ -4478,7 +4643,8 @@
 
     // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343)
     // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714)
-    IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive));
+    // FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction.
+    IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive));
 
     //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id);
     g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited;
@@ -4570,8 +4736,17 @@
         const ImGuiID id = g.LastItemData.ID;
         if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0)
             if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap)
-                if (g.ActiveId != window->MoveId)
+            {
+                // When ActiveId == MoveId it means that either:
+                // - (1) user clicked on void _or_ an item with no id, which triggers moving window (ActiveId is set even when window has _NoMove flag)
+                //   - the (id == 0) test handles it, however, IsItemHovered() will leak between id==0 items (mostly visible when using _NoMove). // FIXME: May be fixed.
+                // - (2) user clicked a disabled item. UpdateMouseMovingWindowEndFrame() uses ActiveId == MoveId to avoid interference with item logic + sets ActiveIdDisabledId.
+                bool cancel_is_hovered = true;
+                if (g.ActiveId == window->MoveId && (id == 0 || g.ActiveIdDisabledId == id))
+                    cancel_is_hovered = false;
+                if (cancel_is_hovered)
                     return false;
+            }
 
         // Test if interactions on this window are blocked by an active popup or modal.
         // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here.
@@ -4618,7 +4793,8 @@
     return true;
 }
 
-// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered().
+// Internal facing ItemHoverable() used when submitting widgets. THIS IS A SUBMISSION NOT A HOVER CHECK.
+// Returns whether the item was hovered, logic differs slightly from IsItemHovered().
 // (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call)
 // FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28.
 // If you used this in your legacy/custom widgets code:
@@ -4630,11 +4806,12 @@
     ImGuiWindow* window = g.CurrentWindow;
 
     // Detect ID conflicts
+    // (this is specifically done here by comparing on hover because it allows us a detection of duplicates that is algorithmically extra cheap, 1 u32 compare per item. No O(log N) lookup whatsoever)
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     if (id != 0 && g.HoveredIdPreviousFrame == id && (item_flags & ImGuiItemFlags_AllowDuplicateId) == 0)
     {
         g.HoveredIdPreviousFrameItemCount++;
-        if (g.DebugDrawIdConflicts == id)
+        if (g.DebugDrawIdConflictsId == id)
             window->DrawList->AddRect(bb.Min - ImVec2(1,1), bb.Max + ImVec2(1,1), IM_COL32(255, 0, 0, 255), 0.0f, ImDrawFlags_None, 2.0f);
     }
 #endif
@@ -4650,7 +4827,7 @@
         if (!g.ActiveIdFromShortcut)
             return false;
 
-    // Done with rectangle culling so we can perform heavier checks now.
+    // We are done with rectangle culling so we can perform heavier checks now.
     if (!(item_flags & ImGuiItemFlags_NoWindowHoverableCheck) && !IsWindowContentHoverable(window, ImGuiHoveredFlags_None))
     {
         g.HoveredIdIsDisabled = true;
@@ -4900,7 +5077,7 @@
     if (viewport->BgFgDrawListsLastFrame[drawlist_no] != g.FrameCount)
     {
         draw_list->_ResetForNewFrame();
-        draw_list->PushTextureID(g.IO.Fonts->TexID);
+        draw_list->PushTexture(g.IO.Fonts->TexRef);
         draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false);
         viewport->BgFgDrawListsLastFrame[drawlist_no] = g.FrameCount;
     }
@@ -4955,6 +5132,19 @@
         g.MovingWindow = window;
 }
 
+// This is not 100% symetric with StartMouseMovingWindow().
+// We do NOT clear ActiveID, because:
+// - It would lead to rather confusing recursive code paths. Caller can call ClearActiveID() if desired.
+// - Some code intentionally cancel moving but keep the ActiveID to lock inputs (e.g. code path taken when clicking a disabled item).
+void ImGui::StopMouseMovingWindow()
+{
+    ImGuiContext& g = *GImGui;
+
+    // [nb: docking branch has more stuff in this function]
+
+    g.MovingWindow = NULL;
+}
+
 // Handle mouse moving window
 // Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing()
 // FIXME: We don't have strong guarantee that g.MovingWindow stay synced with g.ActiveId == g.MovingWindow->MoveId.
@@ -4978,7 +5168,7 @@
         }
         else
         {
-            g.MovingWindow = NULL;
+            StopMouseMovingWindow();
             ClearActiveID();
         }
     }
@@ -5020,6 +5210,9 @@
         {
             StartMouseMovingWindow(g.HoveredWindow); //-V595
 
+            // FIXME: In principal we might be able to call StopMouseMovingWindow() below.
+            // Please note how StartMouseMovingWindow() and StopMouseMovingWindow() and not entirely symetrical, at the later doesn't clear ActiveId.
+
             // Cancel moving if clicked outside of title bar
             if (g.IO.ConfigWindowsMoveFromTitleBarOnly)
                 if (!(root_window->Flags & ImGuiWindowFlags_NoTitleBar))
@@ -5027,9 +5220,12 @@
                         g.MovingWindow = NULL;
 
             // Cancel moving if clicked over an item which was disabled or inhibited by popups
-            // (when g.HoveredIdIsDisabled == true && g.HoveredId == 0 we are inhibited by popups, when g.HoveredIdIsDisabled == true && g.HoveredId != 0 we are over a disabled item)0 already)
+            // (when g.HoveredIdIsDisabled == true && g.HoveredId == 0 we are inhibited by popups, when g.HoveredIdIsDisabled == true && g.HoveredId != 0 we are over a disabled item)
             if (g.HoveredIdIsDisabled)
+            {
                 g.MovingWindow = NULL;
+                g.ActiveIdDisabledId = g.HoveredId;
+            }
         }
         else if (root_window == NULL && g.NavWindow != NULL)
         {
@@ -5057,7 +5253,7 @@
 }
 
 // The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app)
-void ImGui::UpdateHoveredWindowAndCaptureFlags()
+void ImGui::UpdateHoveredWindowAndCaptureFlags(const ImVec2& mouse_pos)
 {
     ImGuiContext& g = *GImGui;
     ImGuiIO& io = g.IO;
@@ -5071,7 +5267,7 @@
     // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point of the frame.
     // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms.
     bool clear_hovered_windows = false;
-    FindHoveredWindowEx(g.IO.MousePos, false, &g.HoveredWindow, &g.HoveredWindowUnderMovingWindow);
+    FindHoveredWindowEx(mouse_pos, false, &g.HoveredWindow, &g.HoveredWindowUnderMovingWindow);
     g.HoveredWindowBeforeClear = g.HoveredWindow;
 
     // Modal windows prevents mouse from hovering behind them.
@@ -5184,7 +5380,6 @@
     UpdateSettings();
 
     g.Time += g.IO.DeltaTime;
-    g.WithinFrameScope = true;
     g.FrameCount += 1;
     g.TooltipOverrideCount = 0;
     g.WindowsActiveCount = 0;
@@ -5204,11 +5399,14 @@
     // Update viewports (after processing input queue, so io.MouseHoveredViewport is set)
     UpdateViewportsNewFrame();
 
+    // Update texture list (collect destroyed textures, etc.)
+    UpdateTexturesNewFrame();
+
     // Setup current font and draw list shared data
-    g.IO.Fonts->Locked = true;
     SetupDrawListSharedData();
-    SetCurrentFont(GetDefaultFont());
-    IM_ASSERT(g.Font->IsLoaded());
+    UpdateFontsNewFrame();
+
+    g.WithinFrameScope = true;
 
     // Mark rendering data as invalid to prevent user who may have a handle on it to use it.
     for (ImGuiViewportP* viewport : g.Viewports)
@@ -5220,9 +5418,9 @@
 
     // [DEBUG]
     if (!g.IO.ConfigDebugHighlightIdConflicts || !g.IO.KeyCtrl) // Count is locked while holding CTRL
-        g.DebugDrawIdConflicts = 0;
+        g.DebugDrawIdConflictsId = 0;
     if (g.IO.ConfigDebugHighlightIdConflicts && g.HoveredIdPreviousFrameItemCount > 1)
-        g.DebugDrawIdConflicts = g.HoveredIdPreviousFrame;
+        g.DebugDrawIdConflictsId = g.HoveredIdPreviousFrame;
 
     // Update HoveredId data
     if (!g.HoveredIdPreviousFrame)
@@ -5342,7 +5540,7 @@
     // Find hovered window
     // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame)
     // (currently needs to be done after the WasActive=Active loop and FindHoveredWindowEx uses ->Active)
-    UpdateHoveredWindowAndCaptureFlags();
+    UpdateHoveredWindowAndCaptureFlags(g.IO.MousePos);
 
     // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering)
     UpdateMouseMovingWindowNewFrame();
@@ -5358,7 +5556,7 @@
 
     // Platform IME data: reset for the frame
     g.PlatformImeDataPrev = g.PlatformImeData;
-    g.PlatformImeData.WantVisible = false;
+    g.PlatformImeData.WantVisible = g.PlatformImeData.WantTextInput = false;
 
     // Mouse wheel scrolling, scale
     UpdateMouseWheel();
@@ -5516,6 +5714,7 @@
     draw_data->DisplaySize = viewport->Size;
     draw_data->FramebufferScale = io.DisplayFramebufferScale;
     draw_data->OwnerViewport = viewport;
+    draw_data->Textures = &ImGui::GetPlatformIO().Textures;
 }
 
 // Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering.
@@ -5524,7 +5723,7 @@
 // - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect():
 //   some frequently called functions which to modify both channels and clipping simultaneously tend to use the
 //   more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds.
-// - This is analoguous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack,
+// - This is analogous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack,
 //   which in the case of ClipRect is not so problematic but tends to be more restrictive for fonts.
 void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect)
 {
@@ -5617,7 +5816,7 @@
         if (window->DrawList->CmdBuffer.Size == 0)
             window->DrawList->AddDrawCmd();
         window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size);
-        window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f);
+        window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); // FIXME-DPI
         window->DrawList->PopClipRect();
     }
 }
@@ -5631,7 +5830,11 @@
     // Don't process EndFrame() multiple times.
     if (g.FrameCountEnded == g.FrameCount)
         return;
-    IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?");
+    if (!g.WithinFrameScope)
+    {
+        IM_ASSERT_USER_ERROR(g.WithinFrameScope, "Forgot to call ImGui::NewFrame()?");
+        return;
+    }
 
     CallContextHooks(&g, ImGuiContextHookType_EndFramePre);
 
@@ -5646,9 +5849,11 @@
     if (g.PlatformIO.Platform_SetImeDataFn != NULL && memcmp(ime_data, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0)
     {
         IMGUI_DEBUG_LOG_IO("[io] Calling Platform_SetImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f)\n", ime_data->WantVisible, ime_data->InputPos.x, ime_data->InputPos.y);
+        IM_ASSERT(ime_data->ViewportId == IMGUI_VIEWPORT_DEFAULT_ID); // master branch
         ImGuiViewport* viewport = GetMainViewport();
         g.PlatformIO.Platform_SetImeDataFn(&g, viewport, ime_data);
     }
+    g.WantTextInputNextFrame = ime_data->WantTextInput ? 1 : 0;
 
     // Hide implicit/fallback "Debug" window if it hasn't been used
     g.WithinFrameScopeWithImplicitWindow = false;
@@ -5683,6 +5888,7 @@
     // End frame
     g.WithinFrameScope = false;
     g.FrameCountEnded = g.FrameCount;
+    UpdateFontsEndFrame();
 
     // Initiate moving window + handle left-click and right-click focus
     UpdateMouseMovingWindowEndFrame();
@@ -5703,8 +5909,11 @@
     g.Windows.swap(g.WindowsTempSortBuffer);
     g.IO.MetricsActiveWindows = g.WindowsActiveCount;
 
+    UpdateTexturesEndFrame();
+
     // Unlock font atlas
-    g.IO.Fonts->Locked = false;
+    for (ImFontAtlas* atlas : g.FontAtlases)
+        atlas->Locked = false;
 
     // Clear Input data for next frame
     g.IO.MousePosPrev = g.IO.MousePos;
@@ -5716,7 +5925,7 @@
 }
 
 // Prepare the data for rendering so you can call GetDrawData()
-// (As with anything within the ImGui:: namspace this doesn't touch your GPU or graphics API at all:
+// (As with anything within the ImGui:: namespace this doesn't touch your GPU or graphics API at all:
 // it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend)
 void ImGui::Render()
 {
@@ -5781,6 +5990,12 @@
         g.IO.MetricsRenderIndices += draw_data->TotalIdxCount;
     }
 
+#ifndef IMGUI_DISABLE_DEBUG_TOOLS
+    if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures)
+        for (ImFontAtlas* atlas : g.FontAtlases)
+            ImFontAtlasDebugLogTextureRequests(atlas);
+#endif
+
     CallContextHooks(&g, ImGuiContextHookType_RenderPost);
 }
 
@@ -6371,10 +6586,10 @@
         return;
     }
 
-    content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x);
-    content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y);
-    content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x);
-    content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y);
+    content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : ImTrunc64(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x);
+    content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : ImTrunc64(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y);
+    content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : ImTrunc64(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x);
+    content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : ImTrunc64(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y);
 }
 
 static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents)
@@ -6385,24 +6600,27 @@
     const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y;
     ImVec2 size_pad = window->WindowPadding * 2.0f;
     ImVec2 size_desired = size_contents + size_pad + ImVec2(decoration_w_without_scrollbars, decoration_h_without_scrollbars);
+
+    // Determine maximum window size
+    // Child windows are layed within their parent (unless they are also popups/menus) and thus have no restriction
+    ImVec2 size_max = ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) ? ImVec2(FLT_MAX, FLT_MAX) : ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f;
+
     if (window->Flags & ImGuiWindowFlags_Tooltip)
     {
-        // Tooltip always resize
-        return size_desired;
+        // Tooltip always resize (up to maximum size)
+        return ImMin(size_desired, size_max);
     }
     else
     {
-        // Maximum window size is determined by the viewport size or monitor size
         ImVec2 size_min = CalcWindowMinSize(window);
-        ImVec2 size_max = ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) ? ImVec2(FLT_MAX, FLT_MAX) : ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f;
-        ImVec2 size_auto_fit = ImClamp(size_desired, size_min, size_max);
+        ImVec2 size_auto_fit = ImClamp(size_desired, ImMin(size_min, size_max), size_max);
 
         // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one axis may be auto-fit when calculating scrollbars,
         // we may need to compute/store three variants of size_auto_fit, for x/y/xy.
         // Here we implement a workaround for child windows only, but a full solution would apply to normal windows as well:
-        if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && !(window->ChildFlags & ImGuiChildFlags_ResizeY))
+        if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && !(window->ChildFlags & (ImGuiChildFlags_ResizeY | ImGuiChildFlags_AutoResizeY)))
             size_auto_fit.y = window->SizeFull.y;
-        else if (!(window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->ChildFlags & ImGuiChildFlags_ResizeY))
+        else if ((window->ChildFlags & ImGuiChildFlags_ResizeY) && !(window->ChildFlags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_AutoResizeX)))
             size_auto_fit.x = window->SizeFull.x;
 
         // When the window cannot fit all contents (either because of constraints, either because screen is too small),
@@ -6437,8 +6655,20 @@
     return ImGuiCol_WindowBg;
 }
 
-static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size)
+static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target_arg, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size)
 {
+    ImVec2 corner_target = corner_target_arg;
+    if (window->Flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent
+    {
+        ImGuiWindow* parent_window = window->ParentWindow;
+        ImGuiWindowFlags parent_flags = parent_window->Flags;
+        ImRect limit_rect = parent_window->InnerRect;
+        limit_rect.Expand(ImVec2(-ImMax(parent_window->WindowPadding.x, parent_window->WindowBorderSize), -ImMax(parent_window->WindowPadding.y, parent_window->WindowBorderSize)));
+        if ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar))
+            corner_target.x = ImClamp(corner_target.x, limit_rect.Min.x, limit_rect.Max.x);
+        if (parent_flags & ImGuiWindowFlags_NoScrollbar)
+            corner_target.y = ImClamp(corner_target.y, limit_rect.Min.y, limit_rect.Max.y);
+    }
     ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm);                // Expected window upper-left
     ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right
     ImVec2 size_expected = pos_max - pos_min;
@@ -6522,7 +6752,9 @@
     ImGuiContext& g = *GImGui;
     ImGuiWindowFlags flags = window->Flags;
 
-    if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0)
+    if ((flags & ImGuiWindowFlags_NoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0)
+        return false;
+    if ((flags & ImGuiWindowFlags_AlwaysAutoResize) && (window->ChildFlags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0)
         return false;
     if (window->WasActive == false) // Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window.
         return false;
@@ -6581,7 +6813,8 @@
         }
 
         // Only lower-left grip is visible before hovering/activating
-        if (resize_grip_n == 0 || held || hovered)
+        const bool resize_grip_visible = held || hovered || (resize_grip_n == 0 && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0);
+        if (resize_grip_visible)
             resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip);
     }
 
@@ -6656,17 +6889,6 @@
             ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX);
             ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX);
             border_target = ImClamp(border_target, clamp_min, clamp_max);
-            if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent
-            {
-                ImGuiWindow* parent_window = window->ParentWindow;
-                ImGuiWindowFlags parent_flags = parent_window->Flags;
-                ImRect border_limit_rect = parent_window->InnerRect;
-                border_limit_rect.Expand(ImVec2(-ImMax(parent_window->WindowPadding.x, parent_window->WindowBorderSize), -ImMax(parent_window->WindowPadding.y, parent_window->WindowBorderSize)));
-                if ((axis == ImGuiAxis_X) && ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar)))
-                    border_target.x = ImClamp(border_target.x, border_limit_rect.Min.x, border_limit_rect.Max.x);
-                if ((axis == ImGuiAxis_Y) && (parent_flags & ImGuiWindowFlags_NoScrollbar))
-                    border_target.y = ImClamp(border_target.y, border_limit_rect.Min.y, border_limit_rect.Max.y);
-            }
             if (!ignore_resize)
                 CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target);
         }
@@ -6914,8 +7136,13 @@
 
     // Close button
     if (has_close_button)
+    {
+        ImGuiItemFlags backup_item_flags = g.CurrentItemFlags;
+        g.CurrentItemFlags |= ImGuiItemFlags_NoFocus;
         if (CloseButton(window->GetID("#CLOSE"), close_button_pos))
             *p_open = false;
+        g.CurrentItemFlags = backup_item_flags;
+    }
 
     window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
     g.CurrentItemFlags = item_flags_backup;
@@ -6948,7 +7175,7 @@
         marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5f;
         if (marker_pos.x > layout_r.Min.x)
         {
-            RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_Text));
+            RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_UnsavedMarker));
             clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - (int)(marker_size_x * 0.5f));
         }
     }
@@ -6965,7 +7192,7 @@
         window->RootWindow = parent_window->RootWindow;
     if (parent_window && (flags & ImGuiWindowFlags_Popup))
         window->RootWindowPopupTree = parent_window->RootWindowPopupTree;
-    if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)))
+    if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)))
         window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight;
     while (window->RootWindowForNav->ChildFlags & ImGuiChildFlags_NavFlattened)
     {
@@ -7074,7 +7301,7 @@
 
     // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack
     ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window;
-    ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow;
+    ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) ? parent_window_in_stack : NULL) : window->ParentWindow;
     IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow));
 
     // We allow window memory to be compacted so recreate the base stack when needed.
@@ -7123,7 +7350,7 @@
     }
 
     // Process SetNextWindow***() calls
-    // (FIXME: Consider splitting the HasXXX flags into X/Y components
+    // (FIXME: Consider splitting the HasXXX flags into X/Y components)
     bool window_pos_set_by_api = false;
     bool window_size_x_set_by_api = false, window_size_y_set_by_api = false;
     if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos)
@@ -7430,12 +7657,25 @@
         }
 #endif
 
+        // Decide if we are going to handle borders and resize grips
+        // 'window->SkipItems' is not updated yet so for child windows we rely on ParentWindow to avoid submitting decorations. (#8815)
+        // Whenever we add support for full decorated child windows we will likely make this logic more general.
+        bool handle_borders_and_resize_grips = true;
+        if ((flags & ImGuiWindowFlags_ChildWindow) && window->ParentWindow->SkipItems)
+            handle_borders_and_resize_grips = false;
+
         // Handle manual resize: Resize Grips, Borders, Gamepad
+        // Child windows can only be resized when they have the flags set. The resize grip allows resizing in both directions, so it should appear only if both flags are set.
         int border_hovered = -1, border_held = -1;
         ImU32 resize_grip_col[4] = {};
-        const int resize_grip_count = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) ? 0 : g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it.
+        int resize_grip_count;
+        if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup))
+            resize_grip_count = ((window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->ChildFlags & ImGuiChildFlags_ResizeY)) ? 1 : 0;
+        else
+            resize_grip_count = g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it.
+
         const float resize_grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f));
-        if (!window->Collapsed)
+        if (handle_borders_and_resize_grips && !window->Collapsed)
             if (int auto_fit_mask = UpdateWindowManualResize(window, size_auto_fit, &border_hovered, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect))
             {
                 if (auto_fit_mask & (1 << ImGuiAxis_X))
@@ -7531,12 +7771,6 @@
         window->InnerClipRect.Max.y = ImFloor(window->InnerRect.Max.y - window->WindowBorderSize * 0.5f);
         window->InnerClipRect.ClipWithFull(host_rect);
 
-        // Default item width. Make it proportional to window size if window manually resizes
-        if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize))
-            window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f);
-        else
-            window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f);
-
         // SCROLLING
 
         // Lock down maximum scrolling
@@ -7554,7 +7788,7 @@
 
         // Setup draw list and outer clipping rectangle
         IM_ASSERT(window->DrawList->CmdBuffer.Size == 1 && window->DrawList->CmdBuffer[0].ElemCount == 0);
-        window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID);
+        window->DrawList->PushTexture(g.Font->ContainerAtlas->TexRef);
         PushClipRect(host_rect.Min, host_rect.Max, false);
 
         // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71)
@@ -7578,7 +7812,6 @@
             // Handle title bar, scrollbar, resize grips and resize borders
             const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow;
             const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight);
-            const bool handle_borders_and_resize_grips = true; // This exists to facilitate merge with 'docking' branch.
             RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_count, resize_grip_col, resize_grip_draw_size);
 
             if (render_decorations_in_parent)
@@ -7642,13 +7875,18 @@
         window->DC.MenuBarAppending = false;
         window->DC.MenuColumns.Update(style.ItemSpacing.x, window_just_activated_by_user);
         window->DC.TreeDepth = 0;
-        window->DC.TreeHasStackDataDepthMask = 0x00;
+        window->DC.TreeHasStackDataDepthMask = window->DC.TreeRecordsClippedNodesY2Mask = 0x00;
         window->DC.ChildWindows.resize(0);
         window->DC.StateStorage = &window->StateStorage;
         window->DC.CurrentColumns = NULL;
         window->DC.LayoutType = ImGuiLayoutType_Vertical;
         window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical;
 
+        // Default item width. Make it proportional to window size if window manually resizes
+        if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize))
+            window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f);
+        else
+            window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f);
         window->DC.ItemWidth = window->ItemWidthDefault;
         window->DC.TextWrapPos = -1.0f; // disabled
         window->DC.ItemWidthStack.resize(0);
@@ -7850,56 +8088,6 @@
     SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window);
 }
 
-// Important: this alone doesn't alter current ImDrawList state. This is called by PushFont/PopFont only.
-void ImGui::SetCurrentFont(ImFont* font)
-{
-    ImGuiContext& g = *GImGui;
-    IM_ASSERT(font && font->IsLoaded());    // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ?
-    IM_ASSERT(font->Scale > 0.0f);
-    g.Font = font;
-    g.FontBaseSize = ImMax(1.0f, g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale);
-    g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f;
-    g.FontScale = g.FontSize / g.Font->FontSize;
-
-    ImFontAtlas* atlas = g.Font->ContainerAtlas;
-    g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel;
-    g.DrawListSharedData.TexUvLines = atlas->TexUvLines;
-    g.DrawListSharedData.Font = g.Font;
-    g.DrawListSharedData.FontSize = g.FontSize;
-    g.DrawListSharedData.FontScale = g.FontScale;
-}
-
-// Use ImDrawList::_SetTextureID(), making our shared g.FontStack[] authorative against window-local ImDrawList.
-// - Whereas ImDrawList::PushTextureID()/PopTextureID() is not to be used across Begin() calls.
-// - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did...
-//   - Some code paths never really fully worked with multiple atlas textures.
-//   - The right-ish solution may be to remove _SetTextureID() and make AddText/RenderText lazily call PushTextureID()/PopTextureID()
-//     the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem
-//     because we have a concrete need and a test bed for multiple atlas textures.
-void ImGui::PushFont(ImFont* font)
-{
-    ImGuiContext& g = *GImGui;
-    if (font == NULL)
-        font = GetDefaultFont();
-    g.FontStack.push_back(font);
-    SetCurrentFont(font);
-    g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID);
-}
-
-void  ImGui::PopFont()
-{
-    ImGuiContext& g = *GImGui;
-    if (g.FontStack.Size <= 0)
-    {
-        IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!");
-        return;
-    }
-    g.FontStack.pop_back();
-    ImFont* font = g.FontStack.Size == 0 ? GetDefaultFont() : g.FontStack.back();
-    SetCurrentFont(font);
-    g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID);
-}
-
 void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled)
 {
     ImGuiContext& g = *GImGui;
@@ -8227,8 +8415,10 @@
         return;
     window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing);
 
-    // Set
-    window->Collapsed = collapsed;
+    // Queue applying in Begin()
+    if (window->WantCollapseToggle)
+        window->Collapsed ^= 1;
+    window->WantCollapseToggle = (window->Collapsed != collapsed);
 }
 
 void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size)
@@ -8300,7 +8490,7 @@
 }
 
 // Content size = inner scrollable rectangle, padded with WindowPadding.
-// SetNextWindowContentSize(ImVec2(100,100) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item.
+// SetNextWindowContentSize(ImVec2(100,100)) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item.
 void ImGui::SetNextWindowContentSize(const ImVec2& size)
 {
     ImGuiContext& g = *GImGui;
@@ -8350,6 +8540,14 @@
     return GImGui->Font;
 }
 
+ImFontBaked* ImGui::GetFontBaked()
+{
+    return GImGui->FontBaked;
+}
+
+// Get current font size (= height in pixels) of current font, with global scale factors applied.
+// - Use style.FontSizeBase to get value before global scale factors.
+// - recap: ImGui::GetFontSize() == style.FontSizeBase * (style.FontScaleMain * style.FontScaleDpi * other_scaling_factors)
 float ImGui::GetFontSize()
 {
     return GImGui->FontSize;
@@ -8360,15 +8558,16 @@
     return GImGui->DrawListSharedData.TexUvWhitePixel;
 }
 
+// Prefer using PushFont(NULL, style.FontSizeBase * factor), or use style.FontScaleMain to scale all windows.
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
 void ImGui::SetWindowFontScale(float scale)
 {
     IM_ASSERT(scale > 0.0f);
-    ImGuiContext& g = *GImGui;
     ImGuiWindow* window = GetCurrentWindow();
     window->FontWindowScale = scale;
-    g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize();
-    g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize;
+    UpdateCurrentFontSize(0.0f);
 }
+#endif
 
 void ImGui::PushFocusScope(ImGuiID id)
 {
@@ -8523,6 +8722,272 @@
 }
 
 //-----------------------------------------------------------------------------
+// [SECTION] FONTS, TEXTURES
+//-----------------------------------------------------------------------------
+// Most of the relevant font logic is in imgui_draw.cpp.
+// Those are high-level support functions.
+//-----------------------------------------------------------------------------
+// - UpdateTexturesNewFrame() [Internal]
+// - UpdateTexturesEndFrame() [Internal]
+// - UpdateFontsNewFrame() [Internal]
+// - UpdateFontsEndFrame() [Internal]
+// - GetDefaultFont() [Internal]
+// - RegisterUserTexture() [Internal]
+// - UnregisterUserTexture() [Internal]
+// - RegisterFontAtlas() [Internal]
+// - UnregisterFontAtlas() [Internal]
+// - SetCurrentFont() [Internal]
+// - UpdateCurrentFontSize() [Internal]
+// - SetFontRasterizerDensity() [Internal]
+// - PushFont()
+// - PopFont()
+//-----------------------------------------------------------------------------
+
+static void ImGui::UpdateTexturesNewFrame()
+{
+    // Cannot update every atlases based on atlas's FrameCount < g.FrameCount, because an atlas may be shared by multiple contexts with different frame count.
+    ImGuiContext& g = *GImGui;
+    const bool has_textures = (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures) != 0;
+    for (ImFontAtlas* atlas : g.FontAtlases)
+    {
+        if (atlas->OwnerContext == &g)
+        {
+            ImFontAtlasUpdateNewFrame(atlas, g.FrameCount, has_textures);
+        }
+        else
+        {
+            // (1) If you manage font atlases yourself, e.g. create a ImFontAtlas yourself you need to call ImFontAtlasUpdateNewFrame() on it.
+            // Otherwise, calling ImGui::CreateContext() without parameter will create an atlas owned by the context.
+            // (2) If you have multiple font atlases, make sure the 'atlas->RendererHasTextures' as specified in the ImFontAtlasUpdateNewFrame() call matches for that.
+            // (3) If you have multiple imgui contexts, they also need to have a matching value for ImGuiBackendFlags_RendererHasTextures.
+            IM_ASSERT(atlas->Builder != NULL && atlas->Builder->FrameCount != -1);
+            IM_ASSERT(atlas->RendererHasTextures == has_textures);
+        }
+    }
+}
+
+// Build a single texture list
+static void ImGui::UpdateTexturesEndFrame()
+{
+    ImGuiContext& g = *GImGui;
+    g.PlatformIO.Textures.resize(0);
+    for (ImFontAtlas* atlas : g.FontAtlases)
+        for (ImTextureData* tex : atlas->TexList)
+        {
+            // We provide this information so backends can decide whether to destroy textures.
+            // This means in practice that if N imgui contexts are created with a shared atlas, we assume all of them have a backend initialized.
+            tex->RefCount = (unsigned short)atlas->RefCount;
+            g.PlatformIO.Textures.push_back(tex);
+        }
+    for (ImTextureData* tex : g.UserTextures)
+        g.PlatformIO.Textures.push_back(tex);
+}
+
+void ImGui::UpdateFontsNewFrame()
+{
+    ImGuiContext& g = *GImGui;
+    if ((g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures) == 0)
+        for (ImFontAtlas* atlas : g.FontAtlases)
+            atlas->Locked = true;
+
+    if (g.Style._NextFrameFontSizeBase != 0.0f)
+    {
+        g.Style.FontSizeBase = g.Style._NextFrameFontSizeBase;
+        g.Style._NextFrameFontSizeBase = 0.0f;
+    }
+
+    // Apply default font size the first time
+    ImFont* font = ImGui::GetDefaultFont();
+    if (g.Style.FontSizeBase <= 0.0f)
+        g.Style.FontSizeBase = (font->LegacySize > 0.0f ? font->LegacySize : FONT_DEFAULT_SIZE);
+
+    // Set initial font
+    g.Font = font;
+    g.FontSizeBase = g.Style.FontSizeBase;
+    g.FontSize = 0.0f;
+    ImFontStackData font_stack_data = { font, g.Style.FontSizeBase, g.Style.FontSizeBase };           // <--- Will restore FontSize
+    SetCurrentFont(font_stack_data.Font, font_stack_data.FontSizeBeforeScaling, 0.0f); // <--- but use 0.0f to enable scale
+    g.FontStack.push_back(font_stack_data);
+    IM_ASSERT(g.Font->IsLoaded());
+}
+
+void ImGui::UpdateFontsEndFrame()
+{
+    PopFont();
+}
+
+ImFont* ImGui::GetDefaultFont()
+{
+    ImGuiContext& g = *GImGui;
+    ImFontAtlas* atlas = g.IO.Fonts;
+    if (atlas->Builder == NULL || atlas->Fonts.Size == 0)
+        ImFontAtlasBuildMain(atlas);
+    return g.IO.FontDefault ? g.IO.FontDefault : atlas->Fonts[0];
+}
+
+// EXPERIMENTAL: DO NOT USE YET.
+void ImGui::RegisterUserTexture(ImTextureData* tex)
+{
+    ImGuiContext& g = *GImGui;
+    tex->RefCount++;
+    g.UserTextures.push_back(tex);
+}
+
+void ImGui::UnregisterUserTexture(ImTextureData* tex)
+{
+    ImGuiContext& g = *GImGui;
+    IM_ASSERT(tex->RefCount > 0);
+    tex->RefCount--;
+    g.UserTextures.find_erase(tex);
+}
+
+void ImGui::RegisterFontAtlas(ImFontAtlas* atlas)
+{
+    ImGuiContext& g = *GImGui;
+    if (g.FontAtlases.Size == 0)
+        IM_ASSERT(atlas == g.IO.Fonts);
+    atlas->RefCount++;
+    g.FontAtlases.push_back(atlas);
+    ImFontAtlasAddDrawListSharedData(atlas, &g.DrawListSharedData);
+    for (ImTextureData* tex : atlas->TexList)
+        tex->RefCount = (unsigned short)atlas->RefCount;
+}
+
+void ImGui::UnregisterFontAtlas(ImFontAtlas* atlas)
+{
+    ImGuiContext& g = *GImGui;
+    IM_ASSERT(atlas->RefCount > 0);
+    ImFontAtlasRemoveDrawListSharedData(atlas, &g.DrawListSharedData);
+    g.FontAtlases.find_erase(atlas);
+    atlas->RefCount--;
+    for (ImTextureData* tex : atlas->TexList)
+        tex->RefCount = (unsigned short)atlas->RefCount;
+}
+
+// Use ImDrawList::_SetTexture(), making our shared g.FontStack[] authoritative against window-local ImDrawList.
+// - Whereas ImDrawList::PushTexture()/PopTexture() is not to be used across Begin() calls.
+// - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did...
+//   - Some code paths never really fully worked with multiple atlas textures.
+//   - The right-ish solution may be to remove _SetTexture() and make AddText/RenderText lazily call PushTexture()/PopTexture()
+//     the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem
+//     because we have a concrete need and a test bed for multiple atlas textures.
+// FIXME-NEWATLAS-V2: perhaps we can now leverage ImFontAtlasUpdateDrawListsTextures() ?
+void ImGui::SetCurrentFont(ImFont* font, float font_size_before_scaling, float font_size_after_scaling)
+{
+    ImGuiContext& g = *GImGui;
+    g.Font = font;
+    g.FontSizeBase = font_size_before_scaling;
+    UpdateCurrentFontSize(font_size_after_scaling);
+
+    if (font != NULL)
+    {
+        IM_ASSERT(font && font->IsLoaded());    // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ?
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+        IM_ASSERT(font->Scale > 0.0f);
+#endif
+        ImFontAtlas* atlas = font->ContainerAtlas;
+        g.DrawListSharedData.FontAtlas = atlas;
+        g.DrawListSharedData.Font = font;
+        ImFontAtlasUpdateDrawListsSharedData(atlas);
+        if (g.CurrentWindow != NULL)
+            g.CurrentWindow->DrawList->_SetTexture(atlas->TexRef);
+    }
+}
+
+void ImGui::UpdateCurrentFontSize(float restore_font_size_after_scaling)
+{
+    ImGuiContext& g = *GImGui;
+    ImGuiWindow* window = g.CurrentWindow;
+
+    g.Style.FontSizeBase = g.FontSizeBase;
+
+    // Early out to avoid hidden window keeping bakes referenced and out of GC reach.
+    // However this would leave a pretty subtle and damning error surface area if g.FontBaked was mismatching.
+    // FIXME: perhaps g.FontSize should be updated?
+    if (window != NULL && window->SkipItems)
+    {
+        ImGuiTable* table = g.CurrentTable;
+        if (table == NULL || (table->CurrentColumn != -1 && table->Columns[table->CurrentColumn].IsSkipItems == false)) // See 8465#issuecomment-2951509561 and #8865. Ideally the SkipItems=true in tables would be amended with extra data.
+            return;
+    }
+
+    // Restoring is pretty much only used by PopFont()
+    float final_size = (restore_font_size_after_scaling > 0.0f) ? restore_font_size_after_scaling : 0.0f;
+    if (final_size == 0.0f)
+    {
+        final_size = g.FontSizeBase;
+
+        // Global scale factors
+        final_size *= g.Style.FontScaleMain;    // Main global scale factor
+        final_size *= g.Style.FontScaleDpi;     // Per-monitor/viewport DPI scale factor, automatically updated when io.ConfigDpiScaleFonts is enabled.
+
+        // Window scale (mostly obsolete now)
+        if (window != NULL)
+            final_size *= window->FontWindowScale;
+
+        // Legacy scale factors
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+        final_size *= g.IO.FontGlobalScale; // Use style.FontScaleMain instead!
+        if (g.Font != NULL)
+            final_size *= g.Font->Scale;    // Was never really useful.
+#endif
+    }
+
+    // Round font size
+    // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet.
+    // - We may support it better later and remove this rounding.
+    final_size = GetRoundedFontSize(final_size);
+    final_size = ImClamp(final_size, 1.0f, IMGUI_FONT_SIZE_MAX);
+    if (g.Font != NULL && (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures))
+        g.Font->CurrentRasterizerDensity = g.FontRasterizerDensity;
+    g.FontSize = final_size;
+    g.FontBaked = (g.Font != NULL && window != NULL) ? g.Font->GetFontBaked(final_size) : NULL;
+    g.FontBakedScale = (g.Font != NULL && window != NULL) ? (g.FontSize / g.FontBaked->Size) : 0.0f;
+    g.DrawListSharedData.FontSize = g.FontSize;
+    g.DrawListSharedData.FontScale = g.FontBakedScale;
+}
+
+// Exposed in case user may want to override setting density.
+// IMPORTANT: Begin()/End() is overriding density. Be considerate of this you change it.
+void ImGui::SetFontRasterizerDensity(float rasterizer_density)
+{
+    ImGuiContext& g = *GImGui;
+    IM_ASSERT(g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures);
+    if (g.FontRasterizerDensity == rasterizer_density)
+        return;
+    g.FontRasterizerDensity = rasterizer_density;
+    UpdateCurrentFontSize(0.0f);
+}
+
+// If you want to scale an existing font size! Read comments in imgui.h!
+void ImGui::PushFont(ImFont* font, float font_size_base)
+{
+    ImGuiContext& g = *GImGui;
+    if (font == NULL) // Before 1.92 (June 2025), PushFont(NULL) == PushFont(GetDefaultFont())
+        font = g.Font;
+    IM_ASSERT(font != NULL);
+    IM_ASSERT(font_size_base >= 0.0f);
+
+    g.FontStack.push_back({ g.Font, g.FontSizeBase, g.FontSize });
+    if (font_size_base == 0.0f)
+        font_size_base = g.FontSizeBase; // Keep current font size
+    SetCurrentFont(font, font_size_base, 0.0f);
+}
+
+void  ImGui::PopFont()
+{
+    ImGuiContext& g = *GImGui;
+    if (g.FontStack.Size <= 0)
+    {
+        IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!");
+        return;
+    }
+    ImFontStackData* font_stack_data = &g.FontStack.back();
+    SetCurrentFont(font_stack_data->Font, font_stack_data->FontSizeBeforeScaling, font_stack_data->FontSizeAfterScaling);
+    g.FontStack.pop_back();
+}
+
+//-----------------------------------------------------------------------------
 // [SECTION] ID STACK
 //-----------------------------------------------------------------------------
 
@@ -8535,7 +9000,7 @@
     ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *Ctx;
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end);
 #endif
     return id;
@@ -8547,7 +9012,7 @@
     ImGuiID id = ImHashData(&ptr, sizeof(void*), seed);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *Ctx;
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL);
 #endif
     return id;
@@ -8559,7 +9024,7 @@
     ImGuiID id = ImHashData(&n, sizeof(n), seed);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *Ctx;
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL);
 #endif
     return id;
@@ -8622,7 +9087,7 @@
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL);
 #endif
     window->IDStack.push_back(id);
@@ -8636,7 +9101,7 @@
     ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *GImGui;
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         DebugHookIdInfo(id, ImGuiDataType_String, str, str_end);
 #endif
     return id;
@@ -8647,7 +9112,7 @@
     ImGuiID id = ImHashData(&n, sizeof(n), seed);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *GImGui;
-    if (g.DebugHookIdInfo == id)
+    if (g.DebugHookIdInfoId == id)
         DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL);
 #endif
     return id;
@@ -8918,7 +9383,7 @@
             routing_entry->RoutingCurrScore = routing_entry->RoutingNextScore;
             routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry
             routing_entry->RoutingNext = ImGuiKeyOwner_NoOwner;
-            routing_entry->RoutingNextScore = 255;
+            routing_entry->RoutingNextScore = 0;
             if (routing_entry->RoutingCurr == ImGuiKeyOwner_NoOwner)
                 continue;
             rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer
@@ -8987,23 +9452,24 @@
     return routing_data;
 }
 
-// Current score encoding (lower is highest priority):
-//  -   0: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverActive
-//  -   1: ImGuiInputFlags_ActiveItem or ImGuiInputFlags_RouteFocused (if item active)
-//  -   2: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused
-//  -  3+: ImGuiInputFlags_RouteFocused (if window in focus-stack)
-//  - 254: ImGuiInputFlags_RouteGlobal
-//  - 255: never route
+// Current score encoding
+//  -        0: Never route
+//  -        1: ImGuiInputFlags_RouteGlobal    (lower priority)
+//  - 100..199: ImGuiInputFlags_RouteFocused   (if window in focus-stack)
+//         200: ImGuiInputFlags_RouteGlobal  | ImGuiInputFlags_RouteOverFocused
+//         300: ImGuiInputFlags_RouteActive or ImGuiInputFlags_RouteFocused (if item active)
+//         400: ImGuiInputFlags_RouteGlobal  | ImGuiInputFlags_RouteOverActive
+//  - 500..599: ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteOverActive (if window in focus-stack) (higher priority)
 // 'flags' should include an explicit routing policy
 static int CalcRoutingScore(ImGuiID focus_scope_id, ImGuiID owner_id, ImGuiInputFlags flags)
 {
     ImGuiContext& g = *GImGui;
     if (flags & ImGuiInputFlags_RouteFocused)
     {
-        // ActiveID gets top priority
+        // ActiveID gets high priority
         // (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it)
         if (owner_id != 0 && g.ActiveId == owner_id)
-            return 1;
+            return 300;
 
         // Score based on distance to focused window (lower is better)
         // Assuming both windows are submitting a routing request,
@@ -9013,25 +9479,32 @@
         // - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score.
         // This essentially follow the window->ParentWindowForFocusRoute chain.
         if (focus_scope_id == 0)
-            return 255;
+            return 0;
         for (int index_in_focus_path = 0; index_in_focus_path < g.NavFocusRoute.Size; index_in_focus_path++)
             if (g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id)
-                return 3 + index_in_focus_path;
-        return 255;
+            {
+                if (flags & ImGuiInputFlags_RouteOverActive) // && g.ActiveId != 0 && g.ActiveId != owner_id)
+                    return 599 - index_in_focus_path;
+                else
+                    return 199 - index_in_focus_path;
+            }
+        return 0;
     }
     else if (flags & ImGuiInputFlags_RouteActive)
     {
         if (owner_id != 0 && g.ActiveId == owner_id)
-            return 1;
-        return 255;
+            return 300;
+        return 0;
     }
     else if (flags & ImGuiInputFlags_RouteGlobal)
     {
         if (flags & ImGuiInputFlags_RouteOverActive)
-            return 0;
+            return 400;
+        if (owner_id != 0 && g.ActiveId == owner_id)
+            return 300;
         if (flags & ImGuiInputFlags_RouteOverFocused)
-            return 2;
-        return 254;
+            return 200;
+        return 1;
     }
     IM_ASSERT(0);
     return 0;
@@ -9071,8 +9544,10 @@
     else
         IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiInputFlags_RouteTypeMask_)); // Check that only 1 routing flag is used
     IM_ASSERT(owner_id != ImGuiKeyOwner_Any && owner_id != ImGuiKeyOwner_NoOwner);
-    if (flags & (ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused))
+    if (flags & (ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteUnlessBgFocused))
         IM_ASSERT(flags & ImGuiInputFlags_RouteGlobal);
+    if (flags & ImGuiInputFlags_RouteOverActive)
+        IM_ASSERT(flags & (ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteFocused));
 
     // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified.
     key_chord = FixupKeyChord(key_chord);
@@ -9127,17 +9602,17 @@
 
     const int score = CalcRoutingScore(focus_scope_id, owner_id, flags);
     IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> score %d\n", GetKeyChordName(key_chord), flags, owner_id, score);
-    if (score == 255)
+    if (score == 0)
         return false;
 
     // Submit routing for NEXT frame (assuming score is sufficient)
-    // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <).
+    // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using >= instead of >).
     ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord);
-    //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore);
-    if (score < routing_data->RoutingNextScore)
+    //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score >= routing_data->RoutingNextScore) : (score > routing_data->RoutingNextScore);
+    if (score > routing_data->RoutingNextScore)
     {
         routing_data->RoutingNext = owner_id;
-        routing_data->RoutingNextScore = (ImU8)score;
+        routing_data->RoutingNextScore = (ImU16)score;
     }
 
     // Return routing state for CURRENT frame
@@ -9773,13 +10248,17 @@
 static const char* GetInputSourceName(ImGuiInputSource source)
 {
     const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad" };
-    IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT);
+    IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT);
+    if (source < 0 || source >= ImGuiInputSource_COUNT)
+        return "Unknown";
     return input_source_names[source];
 }
 static const char* GetMouseSourceName(ImGuiMouseSource source)
 {
     const char* mouse_source_names[] = { "Mouse", "TouchScreen", "Pen" };
-    IM_ASSERT(IM_ARRAYSIZE(mouse_source_names) == ImGuiMouseSource_COUNT && source >= 0 && source < ImGuiMouseSource_COUNT);
+    IM_ASSERT(IM_ARRAYSIZE(mouse_source_names) == ImGuiMouseSource_COUNT);
+    if (source < 0 || source >= ImGuiMouseSource_COUNT)
+        return "Unknown";
     return mouse_source_names[source];
 }
 static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e)
@@ -9867,12 +10346,16 @@
             if (trickle_interleaved_nonchar_keys_and_text && (text_inputted && !key_is_potentially_for_char_input))
                 break;
 
+            if (key_data->Down != e->Key.Down) // Analog change only do not trigger this, so it won't block e.g. further mouse pos events testing key_changed.
+            {
+                key_changed = true;
+                key_changed_mask.SetBit(key_data_index);
+                if (trickle_interleaved_nonchar_keys_and_text && !key_is_potentially_for_char_input)
+                    key_changed_nonchar = true;
+            }
+
             key_data->Down = e->Key.Down;
             key_data->AnalogValue = e->Key.AnalogValue;
-            key_changed = true;
-            key_changed_mask.SetBit(key_data_index);
-            if (trickle_interleaved_nonchar_keys_and_text && !key_is_potentially_for_char_input)
-                key_changed_nonchar = true;
         }
         else if (e->Type == ImGuiInputEventType_Text)
         {
@@ -10169,36 +10652,44 @@
     return !error;
 }
 
-// Until 1.89 (IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos() to extend the boundary of a parent (e.g. window or table cell)
-// This is causing issues and ambiguity and we need to retire that.
-// See https://github.com/ocornut/imgui/issues/5548 for more details.
-// [Scenario 1]
+// Until 1.89 (August 2022, IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos()/SetCursorScreenPos()
+// to extend contents size of our parent container (e.g. window contents size, which is used for auto-resizing
+// windows, table column contents size used for auto-resizing columns, group size).
+// This was causing issues and ambiguities and we needed to retire that.
+// From 1.89, extending contents size boundaries REQUIRES AN ITEM TO BE SUBMITTED.
+//
 //  Previously this would make the window content size ~200x200:
-//    Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End();  // NOT OK
+//    Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End();                      // NOT OK ANYMORE
 //  Instead, please submit an item:
 //    Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); // OK
 //  Alternative:
 //    Begin(...) + Dummy(ImVec2(200,200)) + End(); // OK
-// [Scenario 2]
-//  For reference this is one of the issue what we aim to fix with this change:
-//    BeginGroup() + SomeItem("foobar") + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup()
-//  The previous logic made SetCursorScreenPos(GetCursorScreenPos()) have a side-effect! It would erroneously incorporate ItemSpacing.y after the item into content size, making the group taller!
-//  While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect. Using vertical alignment patterns could trigger this issue.
+//
+// The assert below detects when the _last_ call in a window was a SetCursorPos() not followed by an Item,
+// and with a position that would grow the parent contents size.
+//
+// Advanced:
+// - For reference, old logic was causing issues because it meant that SetCursorScreenPos(GetCursorScreenPos())
+//   had a side-effect on layout! In particular this caused problem to compute group boundaries.
+//   e.g. BeginGroup() + SomeItem() + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup() would cause the
+//   group to be taller because auto-sizing generally adds padding on bottom and right side.
+// - While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect.
+//   Using vertical alignment patterns would frequently trigger this sorts of issue.
+// - See https://github.com/ocornut/imgui/issues/5548 for more details.
 void ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
     IM_ASSERT(window->DC.IsSetPos);
     window->DC.IsSetPos = false;
-#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     if (window->DC.CursorPos.x <= window->DC.CursorMaxPos.x && window->DC.CursorPos.y <= window->DC.CursorMaxPos.y)
         return;
     if (window->SkipItems)
         return;
-    IM_ASSERT(0 && "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries. Please submit an item e.g. Dummy() to validate extent.");
-#else
-    window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos);
-#endif
+    IM_ASSERT_USER_ERROR(0, "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries.\nPlease submit an item e.g. Dummy() afterwards in order to grow window/parent boundaries.");
+
+    // For reference, the old behavior was essentially:
+    //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos);
 }
 
 static void ImGui::ErrorCheckNewFrameSanityChecks()
@@ -10226,7 +10717,6 @@
     IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0)              && "Need a positive DeltaTime!");
     IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount)  && "Forgot to call Render() or EndFrame() at the end of the previous frame?");
     IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f  && "Invalid DisplaySize value!");
-    IM_ASSERT(g.IO.Fonts->IsBuilt()                                     && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()");
     IM_ASSERT(g.Style.CurveTessellationTol > 0.0f                       && "Invalid style setting!");
     IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f                 && "Invalid style setting!");
     IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f            && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations
@@ -10234,12 +10724,16 @@
     IM_ASSERT(g.Style.WindowBorderHoverPadding > 0.0f                   && "Invalid style setting!"); // Required otherwise cannot resize from borders.
     IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right);
     IM_ASSERT(g.Style.ColorButtonPosition == ImGuiDir_Left || g.Style.ColorButtonPosition == ImGuiDir_Right);
+    IM_ASSERT(g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesNone || g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesFull || g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesToNodes);
 
     // Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way.
     if (g.IO.ConfigErrorRecovery)
         IM_ASSERT(g.IO.ConfigErrorRecoveryEnableAssert || g.IO.ConfigErrorRecoveryEnableDebugLog || g.IO.ConfigErrorRecoveryEnableTooltip || g.ErrorCallback != NULL);
 
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    if (g.IO.FontGlobalScale > 1.0f)
+        IM_ASSERT(g.Style.FontScaleMain == 1.0f && "Since 1.92: use style.FontScaleMain instead of g.IO.FontGlobalScale!");
+
     // Remap legacy names
     if (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos)
     {
@@ -10470,9 +10964,9 @@
 {
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
     ImGuiContext& g = *GImGui;
-    if (g.DebugDrawIdConflicts != 0 && g.IO.KeyCtrl == false)
+    if (g.DebugDrawIdConflictsId != 0 && g.IO.KeyCtrl == false)
         g.DebugDrawIdConflictsCount = g.HoveredIdPreviousFrameItemCount;
-    if (g.DebugDrawIdConflicts != 0 && g.DebugItemPickerActive == false && BeginErrorTooltip())
+    if (g.DebugDrawIdConflictsId != 0 && g.DebugItemPickerActive == false && BeginErrorTooltip())
     {
         Text("Programmer error: %d visible items with conflicting ID!", g.DebugDrawIdConflictsCount);
         BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!");
@@ -10641,6 +11135,21 @@
         // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something".
         // READ THE FAQ: https://dearimgui.com/faq
         IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!");
+
+        // [DEBUG] Highlight all conflicts WITHOUT needing to hover. THIS WILL SLOW DOWN DEAR IMGUI. DON'T KEEP ACTIVATED.
+        // This will only work for items submitted with ItemAdd(). Some very rare/odd/unrecommended code patterns are calling ButtonBehavior() without ItemAdd().
+#ifdef IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
+        if ((g.LastItemData.ItemFlags & ImGuiItemFlags_AllowDuplicateId) == 0)
+        {
+            int* p_alive = g.DebugDrawIdConflictsAliveCount.GetIntRef(id, -1); // Could halve lookups if we knew ImGuiStorage can store 64-bit, or by storing FrameCount as 30-bits + highlight as 2-bits. But the point is that we should not pretend that this is fast.
+            int* p_highlight = g.DebugDrawIdConflictsHighlightSet.GetIntRef(id, -1);
+            if (*p_alive == g.FrameCount)
+                *p_highlight = g.FrameCount;
+            *p_alive = g.FrameCount;
+            if (*p_highlight >= g.FrameCount - 1)
+                window->DrawList->AddRect(bb.Min - ImVec2(1, 1), bb.Max + ImVec2(1, 1), IM_COL32(255, 0, 0, 255), 0.0f, ImDrawFlags_None, 2.0f);
+        }
+#endif
     }
     //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG]
     //if ((g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav) == 0)
@@ -11122,7 +11631,7 @@
             }
             scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]);
         }
-        scroll[axis] = IM_ROUND(ImMax(scroll[axis], 0.0f));
+        scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0f));
         if (!window->Collapsed && !window->SkipItems)
             scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]);
     }
@@ -11355,7 +11864,7 @@
         // - offset visibility to increase visibility around mouse.
         // - never clamp within outer viewport boundary.
         // We call SetNextWindowPos() to enforce position and disable clamping.
-        // See FindBestWindowPosForPopup() for positionning logic of other tooltips (not drag and drop ones).
+        // See FindBestWindowPosForPopup() for positioning logic of other tooltips (not drag and drop ones).
         //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding;
         const bool is_touchscreen = (g.IO.MouseSource == ImGuiMouseSource_TouchScreen);
         if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) == 0)
@@ -11818,8 +12327,11 @@
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
-    IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup);  // Mismatched BeginPopup()/EndPopup() calls
-    IM_ASSERT(g.BeginPopupStack.Size > 0);
+    if ((window->Flags & ImGuiWindowFlags_Popup) == 0 || g.BeginPopupStack.Size == 0)
+    {
+        IM_ASSERT_USER_ERROR(0, "Calling EndPopup() too many times or in wrong window!");
+        return;
+    }
 
     // Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests)
     if (g.NavWindow == window)
@@ -12106,10 +12618,10 @@
 
     IM_ASSERT(cur_window); // Not inside a Begin()/End()
     const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0;
-    if (flags & ImGuiHoveredFlags_RootWindow)
+    if (flags & ImGuiFocusedFlags_RootWindow)
         cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy);
 
-    if (flags & ImGuiHoveredFlags_ChildWindows)
+    if (flags & ImGuiFocusedFlags_ChildWindows)
         return IsWindowChildOf(ref_window, cur_window, popup_hierarchy);
     else
         return (ref_window == cur_window);
@@ -12431,7 +12943,7 @@
 }
 
 // Scoring function for keyboard/gamepad directional navigation. Based on https://gist.github.com/rygorous/6981057
-static bool ImGui::NavScoreItem(ImGuiNavItemData* result)
+static bool ImGui::NavScoreItem(ImGuiNavItemData* result, const ImRect& nav_bb)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
@@ -12439,7 +12951,7 @@
         return false;
 
     // FIXME: Those are not good variables names
-    ImRect cand = g.LastItemData.NavRect;   // Current item nav rectangle
+    ImRect cand = nav_bb;                   // Current item nav rectangle
     const ImRect curr = g.NavScoringRect;   // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width)
     g.NavScoringDebugCount++;
 
@@ -12606,13 +13118,13 @@
     const ImGuiID id = g.LastItemData.ID;
     const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags;
 
-    // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221)
+    // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816)
+    ImRect nav_bb = g.LastItemData.NavRect;
     if (window->DC.NavIsScrollPushableX == false)
     {
-        g.LastItemData.NavRect.Min.x = ImClamp(g.LastItemData.NavRect.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x);
-        g.LastItemData.NavRect.Max.x = ImClamp(g.LastItemData.NavRect.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x);
+        nav_bb.Min.x = ImClamp(nav_bb.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x);
+        nav_bb.Max.x = ImClamp(nav_bb.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x);
     }
-    const ImRect nav_bb = g.LastItemData.NavRect;
 
     // Process Init Request
     if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent && (item_flags & ImGuiItemFlags_Disabled) == 0)
@@ -12644,14 +13156,14 @@
             else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId))
             {
                 ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther;
-                if (NavScoreItem(result))
+                if (NavScoreItem(result, nav_bb))
                     NavApplyItemToResult(result);
 
                 // Features like PageUp/PageDown need to maintain a separate score for the visible set of items.
                 const float VISIBLE_RATIO = 0.70f;
                 if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb))
                     if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO)
-                        if (NavScoreItem(&g.NavMoveResultLocalVisible))
+                        if (NavScoreItem(&g.NavMoveResultLocalVisible, nav_bb))
                             NavApplyItemToResult(&g.NavMoveResultLocalVisible);
             }
         }
@@ -12781,8 +13293,8 @@
     NavUpdateAnyRequestFlag();
 }
 
-// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere
-void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data)
+// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsToParent
+void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, const ImGuiTreeNodeStackData* tree_node_data)
 {
     ImGuiContext& g = *GImGui;
     g.NavMoveScoringItems = false;
@@ -12931,6 +13443,9 @@
 
     const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID;
 
+    if (source != ImGuiInputSource_Mouse && !activated_shortcut && window == NULL)
+        source = ImGuiInputSource_Mouse;
+
     // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag.
     if (source == ImGuiInputSource_Mouse)
     {
@@ -12946,11 +13461,11 @@
         ImRect ref_rect;
         if (activated_shortcut)
             ref_rect = g.LastItemData.NavRect;
-        else
+        else if (window != NULL)
             ref_rect = WindowRectRelToAbs(window, window->NavRectRel[g.NavLayer]);
 
         // Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?)
-        if (window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX))
+        if (window != NULL && window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX))
         {
             ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window);
             ref_rect.Translate(window->Scroll - next_scroll);
@@ -13311,6 +13826,7 @@
         }
     }
 
+    // Prepare scoring rectangle.
     // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items)
     ImRect scoring_rect;
     if (window != NULL)
@@ -13325,7 +13841,7 @@
         //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG]
     }
     g.NavScoringRect = scoring_rect;
-    g.NavScoringNoClipRect.Add(scoring_rect);
+    //g.NavScoringNoClipRect.Add(scoring_rect);
 }
 
 void ImGui::NavUpdateCreateTabbingRequest()
@@ -13460,6 +13976,8 @@
     {
         g.NavNextActivateId = result->ID;
         g.NavNextActivateFlags = ImGuiActivateFlags_None;
+        if (g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi)
+            g.NavNextActivateFlags |= ImGuiActivateFlags_FromFocusApi;
         if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing)
             g.NavNextActivateFlags |= ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState | ImGuiActivateFlags_FromTabbing;
     }
@@ -13544,7 +14062,7 @@
     if (g.NavLayer != ImGuiNavLayer_Main)
         NavRestoreLayer(ImGuiNavLayer_Main);
 
-    if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY)
+    if ((window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Main)) == 0 && window->DC.NavWindowHasScrollY)
     {
         // Fallback manual-scroll when window has no navigable item
         if (IsKeyPressed(ImGuiKey_PageUp, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner))
@@ -13721,6 +14239,7 @@
         SetNavCursorVisibleAfterMove();
         ClosePopupsOverWindow(apply_focus_window, false);
         FocusWindow(apply_focus_window, ImGuiFocusRequestFlags_RestoreFocusedChild);
+        IM_ASSERT(g.NavWindow != NULL);
         apply_focus_window = g.NavWindow;
         if (apply_focus_window->NavLastIds[0] == 0)
             NavInitWindow(apply_focus_window, false);
@@ -13769,17 +14288,24 @@
     const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0;
     const bool keyboard_next_window = allow_windowing && g.ConfigNavWindowingKeyNext && Shortcut(g.ConfigNavWindowingKeyNext, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id);
     const bool keyboard_prev_window = allow_windowing && g.ConfigNavWindowingKeyPrev && Shortcut(g.ConfigNavWindowingKeyPrev, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id);
-    const bool start_windowing_with_gamepad = allow_windowing && nav_gamepad_active && !g.NavWindowingTarget && IsKeyPressed(ImGuiKey_NavGamepadMenu, ImGuiInputFlags_None);
+    const bool start_toggling_with_gamepad = nav_gamepad_active && !g.NavWindowingTarget && Shortcut(ImGuiKey_NavGamepadMenu, ImGuiInputFlags_RouteAlways, owner_id);
+    const bool start_windowing_with_gamepad = allow_windowing && start_toggling_with_gamepad;
     const bool start_windowing_with_keyboard = allow_windowing && !g.NavWindowingTarget && (keyboard_next_window || keyboard_prev_window); // Note: enabled even without NavEnableKeyboard!
     bool just_started_windowing_from_null_focus = false;
+    if (start_toggling_with_gamepad)
+    {
+        g.NavWindowingToggleLayer = true; // Gamepad starts toggling layer
+        g.NavWindowingToggleKey = ImGuiKey_NavGamepadMenu;
+        g.NavWindowingInputSource = g.NavInputSource = ImGuiInputSource_Gamepad;
+    }
     if (start_windowing_with_gamepad || start_windowing_with_keyboard)
-        if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1))
+        if (ImGuiWindow* window = (g.NavWindow && IsWindowNavFocusable(g.NavWindow)) ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1))
         {
-            g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; // Current location
+            if (start_windowing_with_keyboard || g.ConfigNavWindowingWithGamepad)
+                g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; // Current location
             g.NavWindowingTimer = g.NavWindowingHighlightAlpha = 0.0f;
             g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f);
-            g.NavWindowingToggleLayer = start_windowing_with_gamepad ? true : false; // Gamepad starts toggling layer
-            g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad;
+            g.NavWindowingInputSource = g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad;
             if (g.NavWindow == NULL)
                 just_started_windowing_from_null_focus = true;
 
@@ -13789,18 +14315,22 @@
         }
 
     // Gamepad update
-    g.NavWindowingTimer += io.DeltaTime;
-    if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Gamepad)
+    if ((g.NavWindowingTarget || g.NavWindowingToggleLayer) && g.NavWindowingInputSource == ImGuiInputSource_Gamepad)
     {
-        // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise
-        g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f));
-
-        // Select window to focus
-        const int focus_change_dir = (int)IsKeyPressed(ImGuiKey_GamepadL1) - (int)IsKeyPressed(ImGuiKey_GamepadR1);
-        if (focus_change_dir != 0 && !just_started_windowing_from_null_focus)
+        if (g.NavWindowingTarget != NULL)
         {
-            NavUpdateWindowingTarget(focus_change_dir);
-            g.NavWindowingHighlightAlpha = 1.0f;
+            // Highlight only appears after a brief time holding the button, so that a fast tap on ImGuiKey_NavGamepadMenu (to toggle NavLayer) doesn't add visual noise
+            // However inputs are accepted immediately, so you press ImGuiKey_NavGamepadMenu + L1/R1 fast.
+            g.NavWindowingTimer += io.DeltaTime;
+            g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f));
+
+            // Select window to focus
+            const int focus_change_dir = (int)IsKeyPressed(ImGuiKey_GamepadL1) - (int)IsKeyPressed(ImGuiKey_GamepadR1);
+            if (focus_change_dir != 0 && !just_started_windowing_from_null_focus)
+            {
+                NavUpdateWindowingTarget(focus_change_dir);
+                g.NavWindowingHighlightAlpha = 1.0f;
+            }
         }
 
         // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most)
@@ -13812,15 +14342,17 @@
             else if (!g.NavWindowingToggleLayer)
                 apply_focus_window = g.NavWindowingTarget;
             g.NavWindowingTarget = NULL;
+            g.NavWindowingToggleLayer = false;
         }
     }
 
     // Keyboard: Focus
-    if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Keyboard)
+    if (g.NavWindowingTarget && g.NavWindowingInputSource == ImGuiInputSource_Keyboard)
     {
         // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise
         ImGuiKeyChord shared_mods = ((g.ConfigNavWindowingKeyNext ? g.ConfigNavWindowingKeyNext : ImGuiMod_Mask_) & (g.ConfigNavWindowingKeyPrev ? g.ConfigNavWindowingKeyPrev : ImGuiMod_Mask_)) & ImGuiMod_Mask_;
         IM_ASSERT(shared_mods != 0); // Next/Prev shortcut currently needs a shared modifier to "hold", otherwise Prev actions would keep cycling between two windows.
+        g.NavWindowingTimer += io.DeltaTime;
         g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // 1.0f
         if ((keyboard_next_window || keyboard_prev_window) && !just_started_windowing_from_null_focus)
             NavUpdateWindowingTarget(keyboard_next_window ? -1 : +1);
@@ -13838,10 +14370,10 @@
                 windowing_toggle_layer_start = true;
                 g.NavWindowingToggleLayer = true;
                 g.NavWindowingToggleKey = windowing_toggle_key;
-                g.NavInputSource = ImGuiInputSource_Keyboard;
+                g.NavWindowingInputSource = g.NavInputSource = ImGuiInputSource_Keyboard;
                 break;
             }
-    if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard)
+    if (g.NavWindowingToggleLayer && g.NavWindowingInputSource == ImGuiInputSource_Keyboard)
     {
         // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370)
         // We cancel toggling nav layer when other modifiers are pressed. (See #4439)
@@ -14207,6 +14739,31 @@
     g.DragDropTargetRect = bb;
     g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case?
     g.DragDropTargetId = id;
+    g.DragDropTargetFullViewport = 0;
+    g.DragDropWithinTarget = true;
+    return true;
+}
+
+// Typical usage would be:
+//   if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
+//       if (ImGui::BeginDragDropTargetViewport(ImGui::GetMainViewport(), NULL))
+// But we are leaving the hover test to the caller for maximum flexibility.
+bool ImGui::BeginDragDropTargetViewport(ImGuiViewport* viewport, const ImRect* p_bb)
+{
+    ImGuiContext& g = *GImGui;
+    if (!g.DragDropActive)
+        return false;
+
+    ImRect bb = p_bb ? *p_bb : ((ImGuiViewportP*)viewport)->GetWorkRect();
+    ImGuiID id = viewport->ID;
+    if (!IsMouseHoveringRect(bb.Min, bb.Max, false) || (id == g.DragDropPayload.SourceId))
+        return false;
+
+    IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget()
+    g.DragDropTargetRect = bb;
+    g.DragDropTargetClipRect = bb;
+    g.DragDropTargetId = id;
+    g.DragDropTargetFullViewport = id;
     g.DragDropWithinTarget = true;
     return true;
 }
@@ -14277,8 +14834,17 @@
     // Render default drop visuals
     payload.Preview = was_accepted_previously;
     flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that live for 1 frame)
-    if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview)
-        RenderDragDropTargetRect(r, g.DragDropTargetClipRect);
+    const bool draw_target_rect = payload.Preview && !(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect);
+    if (draw_target_rect && g.DragDropTargetFullViewport != 0)
+    {
+        ImRect bb = g.DragDropTargetRect;
+        bb.Expand(-3.5f);
+        RenderDragDropTargetRectEx(GetForegroundDrawList(), bb);
+    }
+    else if (draw_target_rect)
+    {
+        RenderDragDropTargetRectForItem(r);
+    }
 
     g.DragDropAcceptFrameCount = g.FrameCount;
     if ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) && g.DragDropMouseButton == -1)
@@ -14294,21 +14860,26 @@
 }
 
 // FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target.
-void ImGui::RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect)
+void ImGui::RenderDragDropTargetRectForItem(const ImRect& bb)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
     ImRect bb_display = bb;
-    bb_display.ClipWith(item_clip_rect); // Clip THEN expand so we have a way to visualize that target is not entirely visible.
+    bb_display.ClipWith(g.DragDropTargetClipRect); // Clip THEN expand so we have a way to visualize that target is not entirely visible.
     bb_display.Expand(3.5f);
     bool push_clip_rect = !window->ClipRect.Contains(bb_display);
     if (push_clip_rect)
         window->DrawList->PushClipRectFullScreen();
-    window->DrawList->AddRect(bb_display.Min, bb_display.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f);
+    RenderDragDropTargetRectEx(window->DrawList, bb_display);
     if (push_clip_rect)
         window->DrawList->PopClipRect();
 }
 
+void ImGui::RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb)
+{
+    draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); // FIXME-DPI
+}
+
 const ImGuiPayload* ImGui::GetDragDropPayload()
 {
     ImGuiContext& g = *GImGui;
@@ -14788,13 +15359,9 @@
 {
     ImGuiContext& g = *GImGui;
 
+    // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier.
     if (g.IO.ConfigDebugIniSettings == false)
-    {
-        // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID()
-        // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier.
-        if (const char* p = strstr(name, "###"))
-            name = p;
-    }
+        name = ImHashSkipUncontributingPrefix(name);
     const size_t name_len = ImStrlen(name);
 
     // Allocate chunk
@@ -14955,6 +15522,23 @@
 // (this section is more complete in the 'docking' branch)
 //-----------------------------------------------------------------------------
 
+void ImGuiPlatformIO::ClearPlatformHandlers()
+{
+    Platform_GetClipboardTextFn = NULL;
+    Platform_SetClipboardTextFn = NULL;
+    Platform_ClipboardUserData = NULL;
+    Platform_OpenInShellFn = NULL;
+    Platform_OpenInShellUserData = NULL;
+    Platform_SetImeDataFn = NULL;
+    Platform_ImeUserData = NULL;
+}
+
+void ImGuiPlatformIO::ClearRendererHandlers()
+{
+    Renderer_TextureMaxWidth = Renderer_TextureMaxHeight = 0;
+    Renderer_RenderState = NULL;
+}
+
 ImGuiViewport* ImGui::GetMainViewport()
 {
     ImGuiContext& g = *GImGui;
@@ -14996,6 +15580,8 @@
     main_viewport->Flags = ImGuiViewportFlags_IsPlatformWindow | ImGuiViewportFlags_OwnedByApp;
     main_viewport->Pos = ImVec2(0.0f, 0.0f);
     main_viewport->Size = g.IO.DisplaySize;
+    main_viewport->FramebufferScale = g.IO.DisplayFramebufferScale;
+    IM_ASSERT(main_viewport->FramebufferScale.x > 0.0f && main_viewport->FramebufferScale.y > 0.0f);
 
     for (ImGuiViewportP* viewport : g.Viewports)
     {
@@ -15250,11 +15836,16 @@
 //-----------------------------------------------------------------------------
 // [SECTION] METRICS/DEBUGGER WINDOW
 //-----------------------------------------------------------------------------
+// - MetricsHelpMarker() [Internal]
 // - DebugRenderViewportThumbnail() [Internal]
 // - RenderViewportsThumbnails() [Internal]
+// - DebugRenderKeyboardPreview() [Internal]
 // - DebugTextEncoding()
-// - MetricsHelpMarker() [Internal]
-// - ShowFontAtlas() [Internal]
+// - DebugFlashStyleColorStop() [Internal]
+// - DebugFlashStyleColor()
+// - UpdateDebugToolFlashStyleColor() [Internal]
+// - ShowFontAtlas() [Internal but called by Demo!]
+// - DebugNodeTexture() [Internal]
 // - ShowMetricsWindow()
 // - DebugNodeColumns() [Internal]
 // - DebugNodeDrawList() [Internal]
@@ -15268,8 +15859,24 @@
 // - DebugNodeWindowSettings() [Internal]
 // - DebugNodeWindowsList() [Internal]
 // - DebugNodeWindowsListByBeginStackParent() [Internal]
+// - ShowFontSelector()
 //-----------------------------------------------------------------------------
 
+#if !defined(IMGUI_DISABLE_DEMO_WINDOWS) || !defined(IMGUI_DISABLE_DEBUG_TOOLS)
+// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds.
+static void MetricsHelpMarker(const char* desc)
+{
+    ImGui::TextDisabled("(?)");
+    if (ImGui::BeginItemTooltip())
+    {
+        ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
+        ImGui::TextUnformatted(desc);
+        ImGui::PopTextWrapPos();
+        ImGui::EndTooltip();
+    }
+}
+#endif
+
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
 
 void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb)
@@ -15397,10 +16004,12 @@
             Text("0x%02X", (int)(unsigned char)p[byte_index]);
         }
         TableNextColumn();
-        if (GetFont()->FindGlyphNoFallback((ImWchar)c))
-            TextUnformatted(p, p + c_utf8_len);
-        else
-            TextUnformatted((c == IM_UNICODE_CODEPOINT_INVALID) ? "[invalid]" : "[missing]");
+        TextUnformatted(p, p + c_utf8_len);
+        if (!GetFont()->IsGlyphInFont((ImWchar)c))
+        {
+            SameLine();
+            TextUnformatted("[missing]");
+        }
         TableNextColumn();
         Text("U+%04X", (int)c);
         p += c_utf8_len;
@@ -15448,36 +16057,203 @@
     return buf;
 }
 
-// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds.
-static void MetricsHelpMarker(const char* desc)
+static const char* FormatTextureRefForDebugDisplay(char* buf, int buf_size, ImTextureRef tex_ref)
 {
-    ImGui::TextDisabled("(?)");
-    if (ImGui::BeginItemTooltip())
-    {
-        ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
-        ImGui::TextUnformatted(desc);
-        ImGui::PopTextWrapPos();
-        ImGui::EndTooltip();
-    }
+    char* buf_end = buf + buf_size;
+    if (tex_ref._TexData != NULL)
+        buf += ImFormatString(buf, buf_end - buf, "#%03d: ", tex_ref._TexData->UniqueID);
+    return FormatTextureIDForDebugDisplay(buf, (int)(buf_end - buf), tex_ref.GetTexID()); // Calling TexRef::GetTexID() to avoid assert of cmd->GetTexID()
 }
 
+#ifdef IMGUI_ENABLE_FREETYPE
+namespace ImGuiFreeType { IMGUI_API const ImFontLoader* GetFontLoader(); IMGUI_API bool DebugEditFontLoaderFlags(unsigned int* p_font_builder_flags); }
+#endif
+
 // [DEBUG] List fonts in a font atlas and display its texture
 void ImGui::ShowFontAtlas(ImFontAtlas* atlas)
 {
+    ImGuiContext& g = *GImGui;
+    ImGuiIO& io = g.IO;
+    ImGuiStyle& style = g.Style;
+
+    BeginDisabled();
+    CheckboxFlags("io.BackendFlags: RendererHasTextures", &io.BackendFlags, ImGuiBackendFlags_RendererHasTextures);
+    EndDisabled();
+    ShowFontSelector("Font");
+    //BeginDisabled((io.BackendFlags & ImGuiBackendFlags_RendererHasTextures) == 0);
+    if (DragFloat("FontSizeBase", &style.FontSizeBase, 0.20f, 5.0f, 100.0f, "%.0f"))
+        style._NextFrameFontSizeBase = style.FontSizeBase; // FIXME: Temporary hack until we finish remaining work.
+    SameLine(0.0f, 0.0f); Text(" (out %.2f)", GetFontSize());
+    SameLine(); MetricsHelpMarker("- This is scaling font only. General scaling will come later.");
+    DragFloat("FontScaleMain", &style.FontScaleMain, 0.02f, 0.5f, 4.0f);
+    //BeginDisabled(io.ConfigDpiScaleFonts);
+    DragFloat("FontScaleDpi", &style.FontScaleDpi, 0.02f, 0.5f, 4.0f);
+    //SetItemTooltip("When io.ConfigDpiScaleFonts is set, this value is automatically overwritten.");
+    //EndDisabled();
+    if ((io.BackendFlags & ImGuiBackendFlags_RendererHasTextures) == 0)
+    {
+        BulletText("Warning: Font scaling will NOT be smooth, because\nImGuiBackendFlags_RendererHasTextures is not set!");
+        BulletText("For instructions, see:");
+        SameLine();
+        TextLinkOpenURL("docs/BACKENDS.md", "https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md");
+    }
+    BulletText("Load a nice font for better results!");
+    BulletText("Please submit feedback:");
+    SameLine(); TextLinkOpenURL("#8465", "https://github.com/ocornut/imgui/issues/8465");
+    BulletText("Read FAQ for more details:");
+    SameLine(); TextLinkOpenURL("dearimgui.com/faq", "https://www.dearimgui.com/faq/");
+    //EndDisabled();
+
+    SeparatorText("Font List");
+
+    ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig;
+    Checkbox("Show font preview", &cfg->ShowFontPreview);
+
+    // Font loaders
+    if (TreeNode("Loader", "Loader: \'%s\'", atlas->FontLoaderName ? atlas->FontLoaderName : "NULL"))
+    {
+        const ImFontLoader* loader_current = atlas->FontLoader;
+        BeginDisabled(!atlas->RendererHasTextures);
+#ifdef IMGUI_ENABLE_STB_TRUETYPE
+        const ImFontLoader* loader_stbtruetype = ImFontAtlasGetFontLoaderForStbTruetype();
+        if (RadioButton("stb_truetype", loader_current == loader_stbtruetype))
+            atlas->SetFontLoader(loader_stbtruetype);
+#else
+        BeginDisabled();
+        RadioButton("stb_truetype", false);
+        SetItemTooltip("Requires #define IMGUI_ENABLE_STB_TRUETYPE");
+        EndDisabled();
+#endif
+        SameLine();
+#ifdef IMGUI_ENABLE_FREETYPE
+        const ImFontLoader* loader_freetype = ImGuiFreeType::GetFontLoader();
+        if (RadioButton("FreeType", loader_current == loader_freetype))
+            atlas->SetFontLoader(loader_freetype);
+        if (loader_current == loader_freetype)
+        {
+            unsigned int loader_flags = atlas->FontLoaderFlags;
+            Text("Shared FreeType Loader Flags:  0x%08X", loader_flags);
+            if (ImGuiFreeType::DebugEditFontLoaderFlags(&loader_flags))
+            {
+                for (ImFont* font : atlas->Fonts)
+                    ImFontAtlasFontDestroyOutput(atlas, font);
+                atlas->FontLoaderFlags = loader_flags;
+                for (ImFont* font : atlas->Fonts)
+                    ImFontAtlasFontInitOutput(atlas, font);
+            }
+        }
+#else
+        BeginDisabled();
+        RadioButton("FreeType", false);
+        SetItemTooltip("Requires #define IMGUI_ENABLE_FREETYPE + imgui_freetype.cpp.");
+        EndDisabled();
+#endif
+        EndDisabled();
+        TreePop();
+    }
+
+    // Font list
     for (ImFont* font : atlas->Fonts)
     {
         PushID(font);
         DebugNodeFont(font);
         PopID();
     }
-    if (TreeNode("Font Atlas", "Font Atlas (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight))
+
+    SeparatorText("Font Atlas");
+    if (Button("Compact"))
+        atlas->CompactCache();
+    SameLine();
+    if (Button("Grow"))
+        ImFontAtlasTextureGrow(atlas);
+    SameLine();
+    if (Button("Clear All"))
+        ImFontAtlasBuildClear(atlas);
+    SetItemTooltip("Destroy cache and custom rectangles.");
+
+    for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++)
+    {
+        ImTextureData* tex = atlas->TexList[tex_n];
+        if (tex_n > 0)
+            SameLine();
+        Text("Tex: %dx%d", tex->Width, tex->Height);
+    }
+    const int packed_surface_sqrt = (int)sqrtf((float)atlas->Builder->RectsPackedSurface);
+    const int discarded_surface_sqrt = (int)sqrtf((float)atlas->Builder->RectsDiscardedSurface);
+    Text("Packed rects: %d, area: about %d px ~%dx%d px", atlas->Builder->RectsPackedCount, atlas->Builder->RectsPackedSurface, packed_surface_sqrt, packed_surface_sqrt);
+    Text("incl. Discarded rects: %d, area: about %d px ~%dx%d px", atlas->Builder->RectsDiscardedCount, atlas->Builder->RectsDiscardedSurface, discarded_surface_sqrt, discarded_surface_sqrt);
+
+    ImFontAtlasRectId highlight_r_id = ImFontAtlasRectId_Invalid;
+    if (TreeNode("Rects Index", "Rects Index (%d)", atlas->Builder->RectsPackedCount)) // <-- Use count of used rectangles
     {
-        ImGuiContext& g = *GImGui;
+        PushStyleVar(ImGuiStyleVar_ImageBorderSize, 1.0f);
+        if (BeginTable("##table", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY, ImVec2(0.0f, GetTextLineHeightWithSpacing() * 12)))
+        {
+            for (const ImFontAtlasRectEntry& entry : atlas->Builder->RectsIndex)
+                if (entry.IsUsed)
+                {
+                    ImFontAtlasRectId id = ImFontAtlasRectId_Make(atlas->Builder->RectsIndex.index_from_ptr(&entry), entry.Generation);
+                    ImFontAtlasRect r = {};
+                    atlas->GetCustomRect(id, &r);
+                    const char* buf;
+                    ImFormatStringToTempBuffer(&buf, NULL, "ID:%08X, used:%d, { w:%3d, h:%3d } { x:%4d, y:%4d }", id, entry.IsUsed, r.w, r.h, r.x, r.y);
+                    TableNextColumn();
+                    Selectable(buf);
+                    if (IsItemHovered())
+                        highlight_r_id = id;
+                    TableNextColumn();
+                    Image(atlas->TexRef, ImVec2(r.w, r.h), r.uv0, r.uv1);
+                }
+            EndTable();
+        }
+        PopStyleVar();
+        TreePop();
+    }
+
+    // Texture list
+    // (ensure the last texture always use the same ID, so we can keep it open neatly)
+    ImFontAtlasRect highlight_r;
+    if (highlight_r_id != ImFontAtlasRectId_Invalid)
+        atlas->GetCustomRect(highlight_r_id, &highlight_r);
+    for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++)
+    {
+        if (tex_n == atlas->TexList.Size - 1)
+            SetNextItemOpen(true, ImGuiCond_Once);
+        DebugNodeTexture(atlas->TexList[tex_n], atlas->TexList.Size - 1 - tex_n, (highlight_r_id != ImFontAtlasRectId_Invalid) ? &highlight_r : NULL);
+    }
+}
+
+void ImGui::DebugNodeTexture(ImTextureData* tex, int int_id, const ImFontAtlasRect* highlight_rect)
+{
+    ImGuiContext& g = *GImGui;
+    PushID(int_id);
+    if (TreeNode("", "Texture #%03d (%dx%d pixels)", tex->UniqueID, tex->Width, tex->Height))
+    {
+        ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig;
+        Checkbox("Show used rect", &cfg->ShowTextureUsedRect);
         PushStyleVar(ImGuiStyleVar_ImageBorderSize, ImMax(1.0f, g.Style.ImageBorderSize));
-        ImageWithBg(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
+        ImVec2 p = GetCursorScreenPos();
+        if (tex->WantDestroyNextFrame)
+            Dummy(ImVec2((float)tex->Width, (float)tex->Height));
+        else
+            ImageWithBg(tex->GetTexRef(), ImVec2((float)tex->Width, (float)tex->Height), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
+        if (cfg->ShowTextureUsedRect)
+            GetWindowDrawList()->AddRect(ImVec2(p.x + tex->UsedRect.x, p.y + tex->UsedRect.y), ImVec2(p.x + tex->UsedRect.x + tex->UsedRect.w, p.y + tex->UsedRect.y + tex->UsedRect.h), IM_COL32(255, 0, 255, 255));
+        if (highlight_rect != NULL)
+        {
+            ImRect r_outer(p.x, p.y, p.x + tex->Width, p.y + tex->Height);
+            ImRect r_inner(p.x + highlight_rect->x, p.y + highlight_rect->y, p.x + highlight_rect->x + highlight_rect->w, p.y + highlight_rect->y + highlight_rect->h);
+            RenderRectFilledWithHole(GetWindowDrawList(), r_outer, r_inner, IM_COL32(0, 0, 0, 100), 0.0f);
+            GetWindowDrawList()->AddRect(r_inner.Min - ImVec2(1, 1), r_inner.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255));
+        }
         PopStyleVar();
+
+        char texid_desc[30];
+        Text("Status = %s (%d), Format = %s (%d), UseColors = %d", ImTextureDataGetStatusName(tex->Status), tex->Status, ImTextureDataGetFormatName(tex->Format), tex->Format, tex->UseColors);
+        Text("TexID = %s, BackendUserData = %p", FormatTextureRefForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), tex->GetTexRef()), tex->BackendUserData);
         TreePop();
     }
+    PopID();
 }
 
 void ImGui::ShowMetricsWindow(bool* p_open)
@@ -15560,6 +16336,10 @@
         }
     };
 
+#ifdef IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
+    TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS is enabled.\nMust disable after use! Otherwise Dear ImGui will run slower.\n");
+#endif
+
     // Tools
     if (TreeNode("Tools"))
     {
@@ -15719,6 +16499,14 @@
         TreePop();
     }
 
+    // Details for Fonts
+    for (ImFontAtlas* atlas : g.FontAtlases)
+        if (TreeNode((void*)atlas, "Fonts (%d), Textures (%d)", atlas->Fonts.Size, atlas->TexList.Size))
+        {
+            ShowFontAtlas(atlas);
+            TreePop();
+        }
+
     // Details for Popups
     if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size))
     {
@@ -15755,14 +16543,6 @@
         TreePop();
     }
 
-    // Details for Fonts
-    ImFontAtlas* atlas = g.IO.Fonts;
-    if (TreeNode("Fonts", "Fonts (%d)", atlas->Fonts.Size))
-    {
-        ShowFontAtlas(atlas);
-        TreePop();
-    }
-
     // Details for InputText
     if (TreeNode("InputText"))
     {
@@ -16170,8 +16950,8 @@
             continue;
         }
 
-        char texid_desc[20];
-        FormatTextureIDForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TextureId);
+        char texid_desc[30];
+        FormatTextureRefForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TexRef);
         char buf[300];
         ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)",
             pcmd->ElemCount / 3, texid_desc, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w);
@@ -16260,19 +17040,39 @@
     out_draw_list->Flags = backup_flags;
 }
 
+// [DEBUG] Compute mask of inputs with the same codepoint.
+static int CalcFontGlyphSrcOverlapMask(ImFontAtlas* atlas, ImFont* font, unsigned int codepoint)
+{
+    int mask = 0, count = 0;
+    for (int src_n = 0; src_n < font->Sources.Size; src_n++)
+    {
+        ImFontConfig* src = font->Sources[src_n];
+        if (!(src->FontLoader ? src->FontLoader : atlas->FontLoader)->FontSrcContainsGlyph(atlas, src, (ImWchar)codepoint))
+            continue;
+        mask |= (1 << src_n);
+        count++;
+    }
+    return count > 1 ? mask : 0;
+}
+
 // [DEBUG] Display details for a single font, called by ShowStyleEditor().
 void ImGui::DebugNodeFont(ImFont* font)
 {
-    bool opened = TreeNode(font, "Font: \"%s\": %.2f px, %d glyphs, %d sources(s)",
-        font->Sources ? font->Sources[0].Name : "", font->FontSize, font->Glyphs.Size, font->SourcesCount);
+    ImGuiContext& g = *GImGui;
+    ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig;
+    ImFontAtlas* atlas = font->ContainerAtlas;
+    bool opened = TreeNode(font, "Font: \"%s\": %d sources(s)", font->GetDebugName(), font->Sources.Size);
 
     // Display preview text
     if (!opened)
         Indent();
     Indent();
-    PushFont(font);
-    Text("The quick brown fox jumps over the lazy dog");
-    PopFont();
+    if (cfg->ShowFontPreview)
+    {
+        PushFont(font, 0.0f);
+        Text("The quick brown fox jumps over the lazy dog");
+        PopFont();
+    }
     if (!opened)
     {
         Unindent();
@@ -16281,90 +17081,182 @@
     }
     if (SmallButton("Set as default"))
         GetIO().FontDefault = font;
+    SameLine();
+    BeginDisabled(atlas->Fonts.Size <= 1 || atlas->Locked);
+    if (SmallButton("Remove"))
+        atlas->RemoveFont(font);
+    EndDisabled();
+    SameLine();
+    if (SmallButton("Clear bakes"))
+        ImFontAtlasFontDiscardBakes(atlas, font, 0);
+    SameLine();
+    if (SmallButton("Clear unused"))
+        ImFontAtlasFontDiscardBakes(atlas, font, 2);
 
     // Display details
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     SetNextItemWidth(GetFontSize() * 8);
     DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f");
-    SameLine(); MetricsHelpMarker(
+    /*SameLine(); MetricsHelpMarker(
         "Note that the default embedded font is NOT meant to be scaled.\n\n"
         "Font are currently rendered into bitmaps at a given size at the time of building the atlas. "
         "You may oversample them to get some flexibility with scaling. "
         "You can also render at multiple sizes and select which one to use at runtime.\n\n"
-        "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)");
-    Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent);
-    char c_str[5];
-    Text("Fallback character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->FallbackChar), font->FallbackChar);
-    Text("Ellipsis character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->EllipsisChar), font->EllipsisChar);
-    const int surface_sqrt = (int)ImSqrt((float)font->MetricsTotalSurface);
-    Text("Texture Area: about %d px ~%dx%d px", font->MetricsTotalSurface, surface_sqrt, surface_sqrt);
-    for (int config_i = 0; config_i < font->SourcesCount; config_i++)
-        if (font->Sources)
-        {
-            const ImFontConfig* src = &font->Sources[config_i];
-            int oversample_h, oversample_v;
-            ImFontAtlasBuildGetOversampleFactors(src, &oversample_h, &oversample_v);
-            BulletText("Input %d: \'%s\', Oversample: (%d=>%d,%d=>%d), PixelSnapH: %d, Offset: (%.1f,%.1f)",
-                config_i, src->Name, src->OversampleH, oversample_h, src->OversampleV, oversample_v, src->PixelSnapH, src->GlyphOffset.x, src->GlyphOffset.y);
-        }
+        "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)");*/
+#endif
 
-    // Display all glyphs of the fonts in separate pages of 256 characters
-    {
-        if (TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size))
-        {
-            ImDrawList* draw_list = GetWindowDrawList();
-            const ImU32 glyph_col = GetColorU32(ImGuiCol_Text);
-            const float cell_size = font->FontSize * 1;
-            const float cell_spacing = GetStyle().ItemSpacing.y;
-            for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256)
+    char c_str[5];
+    ImTextCharToUtf8(c_str, font->FallbackChar);
+    Text("Fallback character: '%s' (U+%04X)", c_str, font->FallbackChar);
+    ImTextCharToUtf8(c_str, font->EllipsisChar);
+    Text("Ellipsis character: '%s' (U+%04X)", c_str, font->EllipsisChar);
+
+    for (int src_n = 0; src_n < font->Sources.Size; src_n++)
+    {
+        ImFontConfig* src = font->Sources[src_n];
+        if (TreeNode(src, "Input %d: \'%s\' [%d], Oversample: %d,%d, PixelSnapH: %d, Offset: (%.1f,%.1f)",
+            src_n, src->Name, src->FontNo, src->OversampleH, src->OversampleV, src->PixelSnapH, src->GlyphOffset.x, src->GlyphOffset.y))
+        {
+            const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+            Text("Loader: '%s'", loader->Name ? loader->Name : "N/A");
+#ifdef IMGUI_ENABLE_FREETYPE
+            if (loader->Name != NULL && strcmp(loader->Name, "FreeType") == 0)
             {
-                // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k)
-                // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT
-                // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here)
-                if (!(base & 8191) && font->IsGlyphRangeUnused(base, base + 8191))
+                unsigned int loader_flags = src->FontLoaderFlags;
+                Text("FreeType Loader Flags: 0x%08X", loader_flags);
+                if (ImGuiFreeType::DebugEditFontLoaderFlags(&loader_flags))
                 {
-                    base += 8192 - 256;
-                    continue;
+                    ImFontAtlasFontDestroyOutput(atlas, font);
+                    src->FontLoaderFlags = loader_flags;
+                    ImFontAtlasFontInitOutput(atlas, font);
                 }
-
-                int count = 0;
-                for (unsigned int n = 0; n < 256; n++)
-                    if (font->FindGlyphNoFallback((ImWchar)(base + n)))
-                        count++;
-                if (count <= 0)
-                    continue;
-                if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph"))
-                    continue;
-
-                // Draw a 16x16 grid of glyphs
-                ImVec2 base_pos = GetCursorScreenPos();
-                for (unsigned int n = 0; n < 256; n++)
+            }
+#endif
+            TreePop();
+        }
+    }
+    if (font->Sources.Size > 1 && TreeNode("Input Glyphs Overlap Detection Tool"))
+    {
+        TextWrapped("- First Input that contains the glyph is used.\n"
+            "- Use ImFontConfig::GlyphExcludeRanges[] to specify ranges to ignore glyph in given Input.\n- Prefer using a small number of ranges as the list is scanned every time a new glyph is loaded,\n  - e.g. GlyphExcludeRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };\n- This tool doesn't cache results and is slow, don't keep it open!");
+        if (BeginTable("table", 2))
+        {
+            for (unsigned int c = 0; c < 0x10000; c++)
+                if (int overlap_mask = CalcFontGlyphSrcOverlapMask(atlas, font, c))
                 {
-                    // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions
-                    // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string.
-                    ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing));
-                    ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size);
-                    const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n));
-                    draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50));
-                    if (!glyph)
-                        continue;
-                    font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n));
-                    if (IsMouseHoveringRect(cell_p1, cell_p2) && BeginTooltip())
+                    unsigned int c_end = c + 1;
+                    while (c_end < 0x10000 && CalcFontGlyphSrcOverlapMask(atlas, font, c_end) == overlap_mask)
+                        c_end++;
+                    if (TableNextColumn() && TreeNode((void*)(intptr_t)c, "U+%04X-U+%04X: %d codepoints in %d inputs", c, c_end - 1, c_end - c, ImCountSetBits(overlap_mask)))
                     {
-                        DebugNodeFontGlyph(font, glyph);
-                        EndTooltip();
+                        char utf8_buf[5];
+                        for (unsigned int n = c; n < c_end; n++)
+                        {
+                            ImTextCharToUtf8(utf8_buf, n);
+                            BulletText("Codepoint U+%04X (%s)", n, utf8_buf);
+                        }
+                        TreePop();
                     }
+                    TableNextColumn();
+                    for (int src_n = 0; src_n < font->Sources.Size; src_n++)
+                        if (overlap_mask & (1 << src_n))
+                        {
+                            Text("%d ", src_n);
+                            SameLine();
+                        }
+                    c = c_end - 1;
                 }
-                Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16));
-                TreePop();
+            EndTable();
+        }
+        TreePop();
+    }
+
+    // Display all glyphs of the fonts in separate pages of 256 characters
+    for (int baked_n = 0; baked_n < atlas->Builder->BakedPool.Size; baked_n++)
+    {
+        ImFontBaked* baked = &atlas->Builder->BakedPool[baked_n];
+        if (baked->ContainerFont != font)
+            continue;
+        PushID(baked_n);
+        if (TreeNode("Glyphs", "Baked at { %.2fpx, d.%.2f }: %d glyphs%s", baked->Size, baked->RasterizerDensity, baked->Glyphs.Size, (baked->LastUsedFrame < atlas->Builder->FrameCount - 1) ? " *Unused*" : ""))
+        {
+            if (SmallButton("Load all"))
+                for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base++)
+                    baked->FindGlyph((ImWchar)base);
+
+            const int surface_sqrt = (int)ImSqrt((float)baked->MetricsTotalSurface);
+            Text("Ascent: %f, Descent: %f, Ascent-Descent: %f", baked->Ascent, baked->Descent, baked->Ascent - baked->Descent);
+            Text("Texture Area: about %d px ~%dx%d px", baked->MetricsTotalSurface, surface_sqrt, surface_sqrt);
+            for (int src_n = 0; src_n < font->Sources.Size; src_n++)
+            {
+                ImFontConfig* src = font->Sources[src_n];
+                int oversample_h, oversample_v;
+                ImFontAtlasBuildGetOversampleFactors(src, baked, &oversample_h, &oversample_v);
+                BulletText("Input %d: \'%s\', Oversample: (%d=>%d,%d=>%d), PixelSnapH: %d, Offset: (%.1f,%.1f)",
+                    src_n, src->Name, src->OversampleH, oversample_h, src->OversampleV, oversample_v, src->PixelSnapH, src->GlyphOffset.x, src->GlyphOffset.y);
             }
+
+            DebugNodeFontGlyphesForSrcMask(font, baked, ~0);
             TreePop();
         }
+        PopID();
     }
     TreePop();
     Unindent();
 }
 
-void ImGui::DebugNodeFontGlyph(ImFont*, const ImFontGlyph* glyph)
+void ImGui::DebugNodeFontGlyphesForSrcMask(ImFont* font, ImFontBaked* baked, int src_mask)
+{
+    ImDrawList* draw_list = GetWindowDrawList();
+    const ImU32 glyph_col = GetColorU32(ImGuiCol_Text);
+    const float cell_size = baked->Size * 1;
+    const float cell_spacing = GetStyle().ItemSpacing.y;
+    for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256)
+    {
+        // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k)
+        // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT
+        // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here)
+        if (!(base & 8191) && font->IsGlyphRangeUnused(base, base + 8191))
+        {
+            base += 8192 - 256;
+            continue;
+        }
+
+        int count = 0;
+        for (unsigned int n = 0; n < 256; n++)
+            if (const ImFontGlyph* glyph = baked->IsGlyphLoaded((ImWchar)(base + n)) ? baked->FindGlyph((ImWchar)(base + n)) : NULL)
+                if (src_mask & (1 << glyph->SourceIdx))
+                    count++;
+        if (count <= 0)
+            continue;
+        if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph"))
+            continue;
+
+        // Draw a 16x16 grid of glyphs
+        ImVec2 base_pos = GetCursorScreenPos();
+        for (unsigned int n = 0; n < 256; n++)
+        {
+            // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions
+            // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string.
+            ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing));
+            ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size);
+            const ImFontGlyph* glyph = baked->IsGlyphLoaded((ImWchar)(base + n)) ? baked->FindGlyph((ImWchar)(base + n)) : NULL;
+            draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50));
+            if (!glyph || (src_mask & (1 << glyph->SourceIdx)) == 0)
+                continue;
+            font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n));
+            if (IsMouseHoveringRect(cell_p1, cell_p2) && BeginTooltip())
+            {
+                DebugNodeFontGlyph(font, glyph);
+                EndTooltip();
+            }
+        }
+        Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16));
+        TreePop();
+    }
+}
+
+void ImGui::DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph)
 {
     Text("Codepoint: U+%04X", glyph->Codepoint);
     Separator();
@@ -16372,6 +17264,12 @@
     Text("AdvanceX: %.1f", glyph->AdvanceX);
     Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1);
     Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1);
+    if (glyph->PackId >= 0)
+    {
+        ImTextureRect* r = ImFontAtlasPackGetRect(font->ContainerAtlas, glyph->PackId);
+        Text("PackId: 0x%X (%dx%d rect at %d,%d)", glyph->PackId, r->w, r->h, r->x, r->y);
+    }
+    Text("SourceIdx: %d", glyph->SourceIdx);
 }
 
 // [DEBUG] Display contents of ImGuiStorage
@@ -16503,7 +17401,7 @@
     }
     const ImVec2* pr = window->NavPreferredScoringPosRel;
     for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++)
-        BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater.
+        BulletText("NavPreferredScoringPosRel[%d] = (%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater.
     BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL");
     if (window->RootWindow != window)               { DebugNodeWindow(window->RootWindow, "RootWindow"); }
     if (window->ParentWindow != NULL)               { DebugNodeWindow(window->ParentWindow, "ParentWindow"); }
@@ -16554,9 +17452,9 @@
         ImFormatString(buf, IM_ARRAYSIZE(buf), "[%04d] Window", window->BeginOrderWithinContext);
         //BulletText("[%04d] Window '%s'", window->BeginOrderWithinContext, window->Name);
         DebugNodeWindow(window, buf);
-        Indent();
+        TreePush(buf);
         DebugNodeWindowsListByBeginStackParent(windows + i + 1, windows_size - i - 1, window);
-        Unindent();
+        TreePop();
     }
 }
 
@@ -16648,7 +17546,7 @@
     ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper);
     ShowDebugLogFlag("Focus", ImGuiDebugLogFlags_EventFocus);
     ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO);
-    //ShowDebugLogFlag("Font", ImGuiDebugLogFlags_EventFont);
+    ShowDebugLogFlag("Font", ImGuiDebugLogFlags_EventFont);
     ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav);
     ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup);
     ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection);
@@ -16849,20 +17747,22 @@
     ImGuiIDStackTool* tool = &g.DebugIDStackTool;
 
     // Clear hook when id stack tool is not visible
-    g.DebugHookIdInfo = 0;
+    g.DebugHookIdInfoId = 0;
+    tool->QueryHookActive = false;
     if (g.FrameCount != tool->LastActiveFrame + 1)
         return;
 
     // Update queries. The steps are: -1: query Stack, >= 0: query each stack item
     // We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time
-    const ImGuiID query_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId;
-    if (tool->QueryId != query_id)
+    const ImGuiID query_main_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId;
+    if (tool->QueryMainId != query_main_id)
     {
-        tool->QueryId = query_id;
+        tool->QueryMainId = query_main_id;
         tool->StackLevel = -1;
         tool->Results.resize(0);
+        tool->ResultPathsBuf.resize(0);
     }
-    if (query_id == 0)
+    if (query_main_id == 0)
         return;
 
     // Advance to next stack level when we got our result, or after 2 frames (in case we never get a result)
@@ -16874,11 +17774,15 @@
     // Update hook
     stack_level = tool->StackLevel;
     if (stack_level == -1)
-        g.DebugHookIdInfo = query_id;
-    if (stack_level >= 0 && stack_level < tool->Results.Size)
     {
-        g.DebugHookIdInfo = tool->Results[stack_level].ID;
+        g.DebugHookIdInfoId = query_main_id;
+        tool->QueryHookActive = true;
+    }
+    else if (stack_level >= 0 && stack_level < tool->Results.Size)
+    {
+        g.DebugHookIdInfoId = tool->Results[stack_level].ID;
         tool->Results[stack_level].QueryFrameCount++;
+        tool->QueryHookActive = true;
     }
 }
 
@@ -16888,11 +17792,17 @@
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
     ImGuiIDStackTool* tool = &g.DebugIDStackTool;
+    if (tool->QueryHookActive == false)
+    {
+        IM_ASSERT(id == 0);
+        return;
+    }
 
     // Step 0: stack query
     // This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget.
     if (tool->StackLevel == -1)
     {
+        IM_ASSERT(tool->Results.Size == 0);
         tool->StackLevel++;
         tool->Results.resize(window->IDStack.Size + 1, ImGuiStackLevelInfo());
         for (int n = 0; n < window->IDStack.Size + 1; n++)
@@ -16907,42 +17817,48 @@
     ImGuiStackLevelInfo* info = &tool->Results[tool->StackLevel];
     IM_ASSERT(info->ID == id && info->QueryFrameCount > 0);
 
-    switch (data_type)
+    if (info->DescOffset == -1)
     {
-    case ImGuiDataType_S32:
-        ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%d", (int)(intptr_t)data_id);
-        break;
-    case ImGuiDataType_String:
-        ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%.*s", data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)ImStrlen((const char*)data_id), (const char*)data_id);
-        break;
-    case ImGuiDataType_Pointer:
-        ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "(void*)0x%p", data_id);
-        break;
-    case ImGuiDataType_ID:
-        if (info->Desc[0] != 0) // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one.
-            return;
-        ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "0x%08X [override]", id);
-        break;
-    default:
-        IM_ASSERT(0);
+        const char* result = NULL;
+        const char* result_end = NULL;
+        switch (data_type)
+        {
+        case ImGuiDataType_S32:
+            ImFormatStringToTempBuffer(&result, &result_end, "%d", (int)(intptr_t)data_id);
+            break;
+        case ImGuiDataType_String:
+            ImFormatStringToTempBuffer(&result, &result_end, "%.*s", data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)ImStrlen((const char*)data_id), (const char*)data_id);
+            break;
+        case ImGuiDataType_Pointer:
+            ImFormatStringToTempBuffer(&result, &result_end, "(void*)0x%p", data_id);
+            break;
+        case ImGuiDataType_ID:
+            // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one.
+            ImFormatStringToTempBuffer(&result, &result_end, "0x%08X [override]", id);
+            break;
+        default:
+            IM_ASSERT(0);
+        }
+        info->DescOffset = tool->ResultPathsBuf.size();
+        tool->ResultPathsBuf.append(result, result_end + 1); // Include zero terminator
     }
     info->QuerySuccess = true;
-    info->DataType = data_type;
+    info->DataType = (ImS8)data_type;
 }
 
 static int StackToolFormatLevelInfo(ImGuiIDStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size)
 {
     ImGuiStackLevelInfo* info = &tool->Results[n];
-    ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL;
+    ImGuiWindow* window = (info->DescOffset == -1 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL;
     if (window)                                                                 // Source: window name (because the root ID don't call GetID() and so doesn't get hooked)
-        return ImFormatString(buf, buf_size, format_for_ui ? "\"%s\" [window]" : "%s", window->Name);
+        return ImFormatString(buf, buf_size, format_for_ui ? "\"%s\" [window]" : "%s", ImHashSkipUncontributingPrefix(window->Name));
     if (info->QuerySuccess)                                                     // Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id)
-        return ImFormatString(buf, buf_size, (format_for_ui && info->DataType == ImGuiDataType_String) ? "\"%s\"" : "%s", info->Desc);
+        return ImFormatString(buf, buf_size, (format_for_ui && info->DataType == ImGuiDataType_String) ? "\"%s\"" : "%s", ImHashSkipUncontributingPrefix(&tool->ResultPathsBuf.Buf[info->DescOffset]));
     if (tool->StackLevel < tool->Results.Size)                                  // Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers.
         return (*buf = 0);
 #ifdef IMGUI_ENABLE_TEST_ENGINE
     if (const char* label = ImGuiTestEngine_FindItemDebugLabel(GImGui, info->ID))   // Source: ImGuiTestEngine's ItemInfo()
-        return ImFormatString(buf, buf_size, format_for_ui ? "??? \"%s\"" : "%s", label);
+        return ImFormatString(buf, buf_size, format_for_ui ? "??? \"%s\"" : "%s", ImHashSkipUncontributingPrefix(label));
 #endif
     return ImFormatString(buf, buf_size, "???");
 }
@@ -16963,38 +17879,47 @@
     ImGuiIDStackTool* tool = &g.DebugIDStackTool;
 
     // Build and display path
-    tool->ResultPathBuf.resize(0);
+    tool->ResultTempBuf.resize(0);
     for (int stack_n = 0; stack_n < tool->Results.Size; stack_n++)
     {
         char level_desc[256];
         StackToolFormatLevelInfo(tool, stack_n, false, level_desc, IM_ARRAYSIZE(level_desc));
-        tool->ResultPathBuf.append(stack_n == 0 ? "//" : "/");
-        for (int n = 0; level_desc[n]; n++)
+        tool->ResultTempBuf.append(stack_n == 0 ? "//" : "/");
+        for (const char* p = level_desc; *p != 0; )
         {
-            if (level_desc[n] == '/')
-                tool->ResultPathBuf.append("\\");
-            tool->ResultPathBuf.append(level_desc + n, level_desc + n + 1);
+            unsigned int c;
+            const char* p_next = p + ImTextCharFromUtf8(&c, p, NULL);
+            if (c == '/')
+                tool->ResultTempBuf.append("\\");
+            if (c < 256 || !tool->OptHexEncodeNonAsciiChars)
+                tool->ResultTempBuf.append(p, p_next);
+            else for (; p < p_next; p++)
+                tool->ResultTempBuf.appendf("\\x%02x", (unsigned char)*p);
+            p = p_next;
         }
     }
-    Text("0x%08X", tool->QueryId);
+    Text("0x%08X", tool->QueryMainId);
     SameLine();
     MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details.");
 
     // CTRL+C to copy path
     const float time_since_copy = (float)g.Time - tool->CopyToClipboardLastTime;
+    PushStyleVarY(ImGuiStyleVar_FramePadding, 0.0f);
+    Checkbox("Hex-encode non-ASCII", &tool->OptHexEncodeNonAsciiChars);
     SameLine();
-    PushStyleVarY(ImGuiStyleVar_FramePadding, 0.0f); Checkbox("Ctrl+C: copy path", &tool->CopyToClipboardOnCtrlC); PopStyleVar();
+    Checkbox("Ctrl+C: copy path", &tool->OptCopyToClipboardOnCtrlC);
+    PopStyleVar();
     SameLine();
     TextColored((time_since_copy >= 0.0f && time_since_copy < 0.75f && ImFmod(time_since_copy, 0.25f) < 0.25f * 0.5f) ? ImVec4(1.f, 1.f, 0.3f, 1.f) : ImVec4(), "*COPIED*");
-    if (tool->CopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
+    if (tool->OptCopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
     {
         tool->CopyToClipboardLastTime = (float)g.Time;
-        SetClipboardText(tool->ResultPathBuf.c_str());
+        SetClipboardText(tool->ResultTempBuf.c_str());
     }
 
-    Text("- Path \"%s\"", tool->ResultPathBuf.c_str());
+    Text("- Path \"%s\"", tool->ResultTempBuf.c_str());
 #ifdef IMGUI_ENABLE_TEST_ENGINE
-    Text("- Label \"%s\"", tool->QueryId ? ImGuiTestEngine_FindItemDebugLabel(&g, tool->QueryId) : "");
+    Text("- Label \"%s\"", tool->QueryMainId ? ImGuiTestEngine_FindItemDebugLabel(&g, tool->QueryMainId) : "");
 #endif
 
     Separator();
@@ -17034,6 +17959,7 @@
 void ImGui::DebugNodeDrawList(ImGuiWindow*, ImGuiViewportP*, const ImDrawList*, const char*) {}
 void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {}
 void ImGui::DebugNodeFont(ImFont*) {}
+void ImGui::DebugNodeFontGlyphesForSrcMask(ImFont*, ImFontBaked*, int) {}
 void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {}
 void ImGui::DebugNodeTabBar(ImGuiTabBar*, const char*) {}
 void ImGui::DebugNodeWindow(ImGuiWindow*, const char*) {}
@@ -17048,6 +17974,40 @@
 
 #endif // #ifndef IMGUI_DISABLE_DEBUG_TOOLS
 
+#if !defined(IMGUI_DISABLE_DEMO_WINDOWS) || !defined(IMGUI_DISABLE_DEBUG_TOOLS)
+// Demo helper function to select among loaded fonts.
+// Here we use the regular BeginCombo()/EndCombo() api which is the more flexible one.
+void ImGui::ShowFontSelector(const char* label)
+{
+    ImGuiIO& io = GetIO();
+    ImFont* font_current = GetFont();
+    if (BeginCombo(label, font_current->GetDebugName()))
+    {
+        for (ImFont* font : io.Fonts->Fonts)
+        {
+            PushID((void*)font);
+            if (Selectable(font->GetDebugName(), font == font_current, ImGuiSelectableFlags_SelectOnNav))
+                io.FontDefault = font;
+            if (font == font_current)
+                SetItemDefaultFocus();
+            PopID();
+        }
+        EndCombo();
+    }
+    SameLine();
+    if (io.BackendFlags & ImGuiBackendFlags_RendererHasTextures)
+        MetricsHelpMarker(
+            "- Load additional fonts with io.Fonts->AddFontXXX() functions.\n"
+            "- Read FAQ and docs/FONTS.md for more details.");
+    else
+        MetricsHelpMarker(
+            "- Load additional fonts with io.Fonts->AddFontXXX() functions.\n"
+            "- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n"
+            "- Read FAQ and docs/FONTS.md for more details.\n"
+            "- If you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame().");
+}
+#endif // #if !defined(IMGUI_DISABLE_DEMO_WINDOWS) || !defined(IMGUI_DISABLE_DEBUG_TOOLS)
+
 //-----------------------------------------------------------------------------
 
 // Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed.
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui_demo.cpp f3d-3.4.1/external/imgui/imgui_demo.cpp
--- f3d-3.2.0+dfsg/external/imgui/imgui_demo.cpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui_demo.cpp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91.9b
+// dear imgui, v1.92.4
 // (demo code)
 
 // Help:
@@ -82,6 +82,7 @@
 // [SECTION] DemoWindowWidgetsDisableBlocks()
 // [SECTION] DemoWindowWidgetsDragAndDrop()
 // [SECTION] DemoWindowWidgetsDragsAndSliders()
+// [SECTION] DemoWindowWidgetsFonts()
 // [SECTION] DemoWindowWidgetsImages()
 // [SECTION] DemoWindowWidgetsListBoxes()
 // [SECTION] DemoWindowWidgetsMultiComponents()
@@ -409,9 +410,19 @@
         return;
     }
 
-    // Most "big" widgets share a common width settings by default. See 'Demo->Layout->Widgets Width' for details.
-    ImGui::PushItemWidth(ImGui::GetFontSize() * -12);           // e.g. Leave a fixed amount of width for labels (by passing a negative value), the rest goes to widgets.
-    //ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f);   // e.g. Use 2/3 of the space for widgets and 1/3 for labels (right align)
+    // Most framed widgets share a common width settings. Remaining width is used for the label.
+    // The width of the frame may be changed with PushItemWidth() or SetNextItemWidth().
+    // - Positive value for absolute size, negative value for right-alignment.
+    // - The default value is about GetWindowWidth() * 0.65f.
+    // - See 'Demo->Layout->Widgets Width' for details.
+    // Here we change the frame width based on how much width we want to give to the label.
+    const float label_width_base = ImGui::GetFontSize() * 12;               // Some amount of width for label, based on font size.
+    const float label_width_max = ImGui::GetContentRegionAvail().x * 0.40f; // ...but always leave some room for framed widgets.
+    const float label_width = IM_MIN(label_width_base, label_width_max);
+    ImGui::PushItemWidth(-label_width);                                     // Right-align: framed items will leave 'label_width' available for the label.
+    //ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.40f);       // e.g. Use 40% width for framed widgets, leaving 60% width for labels.
+    //ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.40f);      // e.g. Use 40% width for labels, leaving 60% width for framed widgets.
+    //ImGui::PushItemWidth(ImGui::GetFontSize() * -12);                     // e.g. Use XXX width for labels, leaving the rest for framed widgets.
 
     // Menu Bar
     DemoWindowMenuBar(&demo_data);
@@ -565,14 +576,15 @@
             ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors",      &io.BackendFlags, ImGuiBackendFlags_HasMouseCursors);
             ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos",       &io.BackendFlags, ImGuiBackendFlags_HasSetMousePos);
             ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", &io.BackendFlags, ImGuiBackendFlags_RendererHasVtxOffset);
+            ImGui::CheckboxFlags("io.BackendFlags: RendererHasTextures",  &io.BackendFlags, ImGuiBackendFlags_RendererHasTextures);
             ImGui::EndDisabled();
 
             ImGui::TreePop();
             ImGui::Spacing();
         }
 
-        IMGUI_DEMO_MARKER("Configuration/Style");
-        if (ImGui::TreeNode("Style"))
+        IMGUI_DEMO_MARKER("Configuration/Style, Fonts");
+        if (ImGui::TreeNode("Style, Fonts"))
         {
             ImGui::Checkbox("Style Editor", &demo_data.ShowStyleEditor);
             ImGui::SameLine();
@@ -830,6 +842,9 @@
         ImGui::RadioButton("radio b", &e, 1); ImGui::SameLine();
         ImGui::RadioButton("radio c", &e, 2);
 
+        ImGui::AlignTextToFramePadding();
+        ImGui::TextLinkOpenURL("Hyperlink", "https://github.com/ocornut/imgui/wiki/Error-Handling");
+
         // Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style.
         IMGUI_DEMO_MARKER("Widgets/Basic/Buttons (Colored)");
         for (int i = 0; i < 7; i++)
@@ -1720,6 +1735,25 @@
 }
 
 //-----------------------------------------------------------------------------
+// [SECTION] DemoWindowWidgetsFonts()
+//-----------------------------------------------------------------------------
+
+// Forward declare ShowFontAtlas() which isn't worth putting in public API yet
+namespace ImGui { IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); }
+
+static void DemoWindowWidgetsFonts()
+{
+    IMGUI_DEMO_MARKER("Widgets/Fonts");
+    if (ImGui::TreeNode("Fonts"))
+    {
+        ImFontAtlas* atlas = ImGui::GetIO().Fonts;
+        ImGui::ShowFontAtlas(atlas);
+        // FIXME-NEWATLAS: Provide a demo to add/create a procedural font?
+        ImGui::TreePop();
+    }
+}
+
+//-----------------------------------------------------------------------------
 // [SECTION] DemoWindowWidgetsImages()
 //-----------------------------------------------------------------------------
 
@@ -1735,13 +1769,12 @@
             "Hover the texture for a zoomed view!");
 
         // Below we are displaying the font texture because it is the only texture we have access to inside the demo!
-        // Remember that ImTextureID is just storage for whatever you want it to be. It is essentially a value that
-        // will be passed to the rendering backend via the ImDrawCmd structure.
+        // Read description about ImTextureID/ImTextureRef and FAQ for details about texture identifiers.
         // If you use one of the default imgui_impl_XXXX.cpp rendering backend, they all have comments at the top
-        // of their respective source file to specify what they expect to be stored in ImTextureID, for example:
-        // - The imgui_impl_dx11.cpp renderer expect a 'ID3D11ShaderResourceView*' pointer
+        // of their respective source file to specify what they are using as texture identifier, for example:
+        // - The imgui_impl_dx11.cpp renderer expect a 'ID3D11ShaderResourceView*' pointer.
         // - The imgui_impl_opengl3.cpp renderer expect a GLuint OpenGL texture identifier, etc.
-        // More:
+        // So with the DirectX11 backend, you call ImGui::Image() with a 'ID3D11ShaderResourceView*' cast to ImTextureID.
         // - If you decided that ImTextureID = MyEngineTexture*, then you can pass your MyEngineTexture* pointers
         //   to ImGui::Image(), and gather width/height through your own functions, etc.
         // - You can use ShowMetricsWindow() to inspect the draw data that are being passed to your renderer,
@@ -1749,14 +1782,19 @@
         // - Consider using the lower-level ImDrawList::AddImage() API, via ImGui::GetWindowDrawList()->AddImage().
         // - Read https://github.com/ocornut/imgui/blob/master/docs/FAQ.md
         // - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
-        ImTextureID my_tex_id = io.Fonts->TexID;
-        float my_tex_w = (float)io.Fonts->TexWidth;
-        float my_tex_h = (float)io.Fonts->TexHeight;
+
+        // Grab the current texture identifier used by the font atlas.
+        ImTextureRef my_tex_id = io.Fonts->TexRef;
+
+        // Regular user code should never have to care about TexData-> fields, but since we want to display the entire texture here, we pull Width/Height from it.
+        float my_tex_w = (float)io.Fonts->TexData->Width;
+        float my_tex_h = (float)io.Fonts->TexData->Height;
+
         {
             ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h);
             ImVec2 pos = ImGui::GetCursorScreenPos();
-            ImVec2 uv_min = ImVec2(0.0f, 0.0f);                 // Top-left
-            ImVec2 uv_max = ImVec2(1.0f, 1.0f);                 // Lower-right
+            ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left
+            ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right
             ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize));
             ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
             if (ImGui::BeginItemTooltip())
@@ -2132,7 +2170,7 @@
         );
         ImGui::BulletText(
             "with Hovering Delay or Stationary test:\n"
-            "IsItemHovered() = = %d\n"
+            "IsItemHovered() = %d\n"
             "IsItemHovered(_Stationary) = %d\n"
             "IsItemHovered(_DelayShort) = %d\n"
             "IsItemHovered(_DelayNormal) = %d\n"
@@ -2388,7 +2426,7 @@
 struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage
 {
     // Find which item should be Focused after deletion.
-    // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it.
+    // Call _before_ item submission. Return an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it.
     // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection.
     // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data.
     // - We don't actually manipulate the ImVector<> here, only in ApplyDeletionPostLoop(), but using similar API for consistency and flexibility.
@@ -2491,7 +2529,7 @@
     {
         const int* a = (const int*)lhs;
         const int* b = (const int*)rhs;
-        return (*a - *b) > 0 ? +1 : -1;
+        return (*a - *b);
     }
     void SortItems(int n)
     {
@@ -2499,7 +2537,7 @@
     }
     void Show()
     {
-        //ImGui::Checkbox("Sorted", &OptKeepSorted);
+        //if (ImGui::Checkbox("Sorted", &OptKeepSorted) && OptKeepSorted) { SortItems(0); SortItems(1); }
         if (ImGui::BeginTable("split", 3, ImGuiTableFlags_None))
         {
             ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);    // Left side
@@ -2971,7 +3009,7 @@
                 static void DrawNode(ExampleTreeNode* node, ImGuiSelectionBasicStorage* selection)
                 {
                     ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
-                    tree_node_flags |= ImGuiTreeNodeFlags_NavLeftJumpsBackHere; // Enable pressing left to jump to parent
+                    tree_node_flags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent; // Enable pressing left to jump to parent
                     if (node->Childs.Size == 0)
                         tree_node_flags |= ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_Leaf;
                     if (selection->Contains((ImGuiID)node->UID))
@@ -3191,7 +3229,7 @@
                     ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX);
                     ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f);
                     ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f);
-                    //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacingY, 0.0f);
+                    //ImGui::PushStyleVarY(ImGuiStyleVar_ItemSpacing, 0.0f);
                 }
 
                 ImGuiListClipper clipper;
@@ -3341,6 +3379,18 @@
 // [SECTION] DemoWindowWidgetsTabs()
 //-----------------------------------------------------------------------------
 
+static void EditTabBarFittingPolicyFlags(ImGuiTabBarFlags* p_flags)
+{
+    if ((*p_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
+        *p_flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
+    if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyMixed", p_flags, ImGuiTabBarFlags_FittingPolicyMixed))
+        *p_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyMixed);
+    if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyShrink", p_flags, ImGuiTabBarFlags_FittingPolicyShrink))
+        *p_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyShrink);
+    if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", p_flags, ImGuiTabBarFlags_FittingPolicyScroll))
+        *p_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll);
+}
+
 static void DemoWindowWidgetsTabs()
 {
     IMGUI_DEMO_MARKER("Widgets/Tabs");
@@ -3383,12 +3433,7 @@
             ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton);
             ImGui::CheckboxFlags("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton", &tab_bar_flags, ImGuiTabBarFlags_NoCloseWithMiddleMouseButton);
             ImGui::CheckboxFlags("ImGuiTabBarFlags_DrawSelectedOverline", &tab_bar_flags, ImGuiTabBarFlags_DrawSelectedOverline);
-            if ((tab_bar_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
-                tab_bar_flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
-            if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown))
-                tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown);
-            if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll))
-                tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll);
+            EditTabBarFittingPolicyFlags(&tab_bar_flags);
 
             // Tab Bar
             ImGui::AlignTextToFramePadding();
@@ -3437,12 +3482,8 @@
             ImGui::Checkbox("Show Trailing TabItemButton()", &show_trailing_button);
 
             // Expose some other flags which are useful to showcase how they interact with Leading/Trailing tabs
-            static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown;
-            ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton);
-            if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown))
-                tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown);
-            if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll))
-                tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll);
+            static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyShrink;
+            EditTabBarFittingPolicyFlags(&tab_bar_flags);
 
             if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags))
             {
@@ -3510,6 +3551,43 @@
             ImGui::TreePop();
         }
 
+        IMGUI_DEMO_MARKER("Widgets/Text/Font Size");
+        if (ImGui::TreeNode("Font Size"))
+        {
+            ImGuiStyle& style = ImGui::GetStyle();
+            const float global_scale = style.FontScaleMain * style.FontScaleDpi;
+            ImGui::Text("style.FontScaleMain = %0.2f", style.FontScaleMain);
+            ImGui::Text("style.FontScaleDpi = %0.2f", style.FontScaleDpi);
+            ImGui::Text("global_scale = ~%0.2f", global_scale); // This is not technically accurate as internal scales may apply, but conceptually let's pretend it is.
+            ImGui::Text("FontSize = %0.2f", ImGui::GetFontSize());
+
+            ImGui::SeparatorText("");
+            static float custom_size = 16.0f;
+            ImGui::SliderFloat("custom_size", &custom_size, 10.0f, 100.0f, "%.0f");
+            ImGui::Text("ImGui::PushFont(nullptr, custom_size);");
+            ImGui::PushFont(NULL, custom_size);
+            ImGui::Text("FontSize = %.2f (== %.2f * global_scale)", ImGui::GetFontSize(), custom_size);
+            ImGui::PopFont();
+
+            ImGui::SeparatorText("");
+            static float custom_scale = 1.0f;
+            ImGui::SliderFloat("custom_scale", &custom_scale, 0.5f, 4.0f, "%.2f");
+            ImGui::Text("ImGui::PushFont(nullptr, style.FontSizeBase * custom_scale);");
+            ImGui::PushFont(NULL, style.FontSizeBase * custom_scale);
+            ImGui::Text("FontSize = %.2f (== style.FontSizeBase * %.2f * global_scale)", ImGui::GetFontSize(), custom_scale);
+            ImGui::PopFont();
+
+            ImGui::SeparatorText("");
+            for (float scaling = 0.5f; scaling <= 4.0f; scaling += 0.5f)
+            {
+                ImGui::PushFont(NULL, style.FontSizeBase * scaling);
+                ImGui::Text("FontSize = %.2f (== style.FontSizeBase * %.2f * global_scale)", ImGui::GetFontSize(), scaling);
+                ImGui::PopFont();
+            }
+
+            ImGui::TreePop();
+        }
+
         IMGUI_DEMO_MARKER("Widgets/Text/Word Wrapping");
         if (ImGui::TreeNode("Word Wrapping"))
         {
@@ -3630,6 +3708,8 @@
             static ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput;
             HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp because we don't want to include <string> in here)");
             ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly);
+            ImGui::CheckboxFlags("ImGuiInputTextFlags_WordWrap", &flags, ImGuiInputTextFlags_WordWrap);
+            ImGui::SameLine(); HelpMarker("Feature is currently in Beta. Please read comments in imgui.h");
             ImGui::CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", &flags, ImGuiInputTextFlags_AllowTabInput);
             ImGui::SameLine(); HelpMarker("When _AllowTabInput is set, passing through the widget with Tabbing doesn't automatically activate it, in order to also cycling through subsequent widgets.");
             ImGui::CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", &flags, ImGuiInputTextFlags_CtrlEnterForNewLine);
@@ -3659,13 +3739,13 @@
                 }
             };
 
-            static char buf1[32] = ""; ImGui::InputText("default", buf1, 32);
-            static char buf2[32] = ""; ImGui::InputText("decimal", buf2, 32, ImGuiInputTextFlags_CharsDecimal);
-            static char buf3[32] = ""; ImGui::InputText("hexadecimal", buf3, 32, ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase);
-            static char buf4[32] = ""; ImGui::InputText("uppercase", buf4, 32, ImGuiInputTextFlags_CharsUppercase);
-            static char buf5[32] = ""; ImGui::InputText("no blank", buf5, 32, ImGuiInputTextFlags_CharsNoBlank);
-            static char buf6[32] = ""; ImGui::InputText("casing swap", buf6, 32, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterCasingSwap); // Use CharFilter callback to replace characters.
-            static char buf7[32] = ""; ImGui::InputText("\"imgui\"", buf7, 32, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterImGuiLetters); // Use CharFilter callback to disable some characters.
+            static char buf1[32] = ""; ImGui::InputText("default", buf1, IM_ARRAYSIZE(buf1));
+            static char buf2[32] = ""; ImGui::InputText("decimal", buf2, IM_ARRAYSIZE(buf2), ImGuiInputTextFlags_CharsDecimal);
+            static char buf3[32] = ""; ImGui::InputText("hexadecimal", buf3, IM_ARRAYSIZE(buf3), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase);
+            static char buf4[32] = ""; ImGui::InputText("uppercase", buf4, IM_ARRAYSIZE(buf4), ImGuiInputTextFlags_CharsUppercase);
+            static char buf5[32] = ""; ImGui::InputText("no blank", buf5, IM_ARRAYSIZE(buf5), ImGuiInputTextFlags_CharsNoBlank);
+            static char buf6[32] = ""; ImGui::InputText("casing swap", buf6, IM_ARRAYSIZE(buf6), ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterCasingSwap); // Use CharFilter callback to replace characters.
+            static char buf7[32] = ""; ImGui::InputText("\"imgui\"", buf7, IM_ARRAYSIZE(buf7), ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterImGuiLetters); // Use CharFilter callback to disable some characters.
             ImGui::TreePop();
         }
 
@@ -3721,20 +3801,20 @@
                 }
             };
             static char buf1[64];
-            ImGui::InputText("Completion", buf1, 64, ImGuiInputTextFlags_CallbackCompletion, Funcs::MyCallback);
+            ImGui::InputText("Completion", buf1, IM_ARRAYSIZE(buf1), ImGuiInputTextFlags_CallbackCompletion, Funcs::MyCallback);
             ImGui::SameLine(); HelpMarker(
                 "Here we append \"..\" each time Tab is pressed. "
                 "See 'Examples>Console' for a more meaningful demonstration of using this callback.");
 
             static char buf2[64];
-            ImGui::InputText("History", buf2, 64, ImGuiInputTextFlags_CallbackHistory, Funcs::MyCallback);
+            ImGui::InputText("History", buf2, IM_ARRAYSIZE(buf2), ImGuiInputTextFlags_CallbackHistory, Funcs::MyCallback);
             ImGui::SameLine(); HelpMarker(
                 "Here we replace and select text each time Up/Down are pressed. "
                 "See 'Examples>Console' for a more meaningful demonstration of using this callback.");
 
             static char buf3[64];
             static int edit_count = 0;
-            ImGui::InputText("Edit", buf3, 64, ImGuiInputTextFlags_CallbackEdit, Funcs::MyCallback, (void*)&edit_count);
+            ImGui::InputText("Edit", buf3, IM_ARRAYSIZE(buf3), ImGuiInputTextFlags_CallbackEdit, Funcs::MyCallback, (void*)&edit_count);
             ImGui::SameLine(); HelpMarker(
                 "Here we toggle the casing of the first character on every edit + count edits.");
             ImGui::SameLine(); ImGui::Text("(%d)", edit_count);
@@ -3777,10 +3857,13 @@
             // For this demo we are using ImVector as a string container.
             // Note that because we need to store a terminating zero character, our size/capacity are 1 more
             // than usually reported by a typical string class.
+            static ImGuiInputTextFlags flags = ImGuiInputTextFlags_None;
+            ImGui::CheckboxFlags("ImGuiInputTextFlags_WordWrap", &flags, ImGuiInputTextFlags_WordWrap);
+
             static ImVector<char> my_str;
             if (my_str.empty())
                 my_str.push_back(0);
-            Funcs::MyInputTextMultiline("##MyStr", &my_str, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16));
+            Funcs::MyInputTextMultiline("##MyStr", &my_str, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16), flags);
             ImGui::Text("Data: %p\nSize: %d\nCapacity: %d", (void*)my_str.begin(), my_str.size(), my_str.capacity());
             ImGui::TreePop();
         }
@@ -3920,6 +4003,7 @@
     IMGUI_DEMO_MARKER("Widgets/Tree Nodes");
     if (ImGui::TreeNode("Tree Nodes"))
     {
+        // See see "Examples -> Property Editor" (ShowExampleAppPropertyEditor() function) for a fancier, data-driven tree.
         IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Basic trees");
         if (ImGui::TreeNode("Basic trees"))
         {
@@ -3946,6 +4030,35 @@
             ImGui::TreePop();
         }
 
+        IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Hierarchy lines");
+        if (ImGui::TreeNode("Hierarchy lines"))
+        {
+            static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DefaultOpen;
+            HelpMarker("Default option for DrawLinesXXX is stored in style.TreeLinesFlags");
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesNone", &base_flags, ImGuiTreeNodeFlags_DrawLinesNone);
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesFull", &base_flags, ImGuiTreeNodeFlags_DrawLinesFull);
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesToNodes", &base_flags, ImGuiTreeNodeFlags_DrawLinesToNodes);
+
+            if (ImGui::TreeNodeEx("Parent", base_flags))
+            {
+                if (ImGui::TreeNodeEx("Child 1", base_flags))
+                {
+                    ImGui::Button("Button for Child 1");
+                    ImGui::TreePop();
+                }
+                if (ImGui::TreeNodeEx("Child 2", base_flags))
+                {
+                    ImGui::Button("Button for Child 2");
+                    ImGui::TreePop();
+                }
+                ImGui::Text("Remaining contents");
+                ImGui::Text("Remaining contents");
+                ImGui::TreePop();
+            }
+
+            ImGui::TreePop();
+        }
+
         IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Advanced, with Selectable nodes");
         if (ImGui::TreeNode("Advanced, with Selectable nodes"))
         {
@@ -3963,7 +4076,13 @@
             ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &base_flags, ImGuiTreeNodeFlags_SpanAllColumns); ImGui::SameLine(); HelpMarker("For use in Tables only.");
             ImGui::CheckboxFlags("ImGuiTreeNodeFlags_AllowOverlap", &base_flags, ImGuiTreeNodeFlags_AllowOverlap);
             ImGui::CheckboxFlags("ImGuiTreeNodeFlags_Framed", &base_flags, ImGuiTreeNodeFlags_Framed); ImGui::SameLine(); HelpMarker("Draw frame with background (e.g. for CollapsingHeader)");
-            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_NavLeftJumpsBackHere", &base_flags, ImGuiTreeNodeFlags_NavLeftJumpsBackHere);
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_NavLeftJumpsToParent", &base_flags, ImGuiTreeNodeFlags_NavLeftJumpsToParent);
+
+            HelpMarker("Default option for DrawLinesXXX is stored in style.TreeLinesFlags");
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesNone", &base_flags, ImGuiTreeNodeFlags_DrawLinesNone);
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesFull", &base_flags, ImGuiTreeNodeFlags_DrawLinesFull);
+            ImGui::CheckboxFlags("ImGuiTreeNodeFlags_DrawLinesToNodes", &base_flags, ImGuiTreeNodeFlags_DrawLinesToNodes);
+
             ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position);
             ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop);
             ImGui::Text("Hello!");
@@ -4146,6 +4265,7 @@
 
     DemoWindowWidgetsDragAndDrop();
     DemoWindowWidgetsDragsAndSliders();
+    DemoWindowWidgetsFonts();
     DemoWindowWidgetsImages();
     DemoWindowWidgetsListBoxes();
     DemoWindowWidgetsMultiComponents();
@@ -4370,16 +4490,27 @@
         }
         ImGui::PopItemWidth();
 
+        ImGui::Text("SetNextItemWidth/PushItemWidth(-Min(GetContentRegionAvail().x * 0.40f, GetFontSize() * 12))");
+        ImGui::PushItemWidth(-IM_MIN(ImGui::GetFontSize() * 12, ImGui::GetContentRegionAvail().x * 0.40f));
+        ImGui::DragFloat("float##5a", &f);
+        if (show_indented_items)
+        {
+            ImGui::Indent();
+            ImGui::DragFloat("float (indented)##5b", &f);
+            ImGui::Unindent();
+        }
+        ImGui::PopItemWidth();
+
         // Demonstrate using PushItemWidth to surround three items.
         // Calling SetNextItemWidth() before each of them would have the same effect.
         ImGui::Text("SetNextItemWidth/PushItemWidth(-FLT_MIN)");
         ImGui::SameLine(); HelpMarker("Align to right edge");
         ImGui::PushItemWidth(-FLT_MIN);
-        ImGui::DragFloat("##float5a", &f);
+        ImGui::DragFloat("##float6a", &f);
         if (show_indented_items)
         {
             ImGui::Indent();
-            ImGui::DragFloat("float (indented)##5b", &f);
+            ImGui::DragFloat("float (indented)##6b", &f);
             ImGui::Unindent();
         }
         ImGui::PopItemWidth();
@@ -4607,10 +4738,11 @@
             ImGui::SmallButton("SmallButton()");
 
             // Tree
+            // (here the node appears after a button and has odd intent, so we use ImGuiTreeNodeFlags_DrawLinesNone to disable hierarchy outline)
             const float spacing = ImGui::GetStyle().ItemInnerSpacing.x;
             ImGui::Button("Button##1");
             ImGui::SameLine(0.0f, spacing);
-            if (ImGui::TreeNode("Node##1"))
+            if (ImGui::TreeNodeEx("Node##1", ImGuiTreeNodeFlags_DrawLinesNone))
             {
                 // Placeholder tree data
                 for (int i = 0; i < 6; i++)
@@ -4663,15 +4795,18 @@
 
         ImGui::Checkbox("Decoration", &enable_extra_decorations);
 
+        ImGui::PushItemWidth(ImGui::GetFontSize() * 10);
+        enable_track |= ImGui::DragInt("##item", &track_item, 0.25f, 0, 99, "Item = %d");
+        ImGui::SameLine();
         ImGui::Checkbox("Track", &enable_track);
-        ImGui::PushItemWidth(100);
-        ImGui::SameLine(140); enable_track |= ImGui::DragInt("##item", &track_item, 0.25f, 0, 99, "Item = %d");
 
-        bool scroll_to_off = ImGui::Button("Scroll Offset");
-        ImGui::SameLine(140); scroll_to_off |= ImGui::DragFloat("##off", &scroll_to_off_px, 1.00f, 0, FLT_MAX, "+%.0f px");
+        bool scroll_to_off = ImGui::DragFloat("##off", &scroll_to_off_px, 1.00f, 0, FLT_MAX, "+%.0f px");
+        ImGui::SameLine();
+        scroll_to_off |= ImGui::Button("Scroll Offset");
 
-        bool scroll_to_pos = ImGui::Button("Scroll To Pos");
-        ImGui::SameLine(140); scroll_to_pos |= ImGui::DragFloat("##pos", &scroll_to_pos_px, 1.00f, -10, FLT_MAX, "X/Y = %.0f px");
+        bool scroll_to_pos = ImGui::DragFloat("##pos", &scroll_to_pos_px, 1.00f, -10, FLT_MAX, "X/Y = %.0f px");
+        ImGui::SameLine();
+        scroll_to_pos |= ImGui::Button("Scroll To Pos");
         ImGui::PopItemWidth();
 
         if (scroll_to_off || scroll_to_pos)
@@ -5055,7 +5190,7 @@
     // Typical use for regular windows:
     //   bool my_tool_is_active = false; if (ImGui::Button("Open")) my_tool_is_active = true; [...] if (my_tool_is_active) Begin("My Tool", &my_tool_is_active) { [...] } End();
     // Typical use for popups:
-    //   if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if (ImGui::BeginPopup("MyPopup") { [...] EndPopup(); }
+    //   if (ImGui::Button("Open")) ImGui::OpenPopup("MyPopup"); if (ImGui::BeginPopup("MyPopup")) { [...] EndPopup(); }
 
     // With popups we have to go through a library call (here OpenPopup) to manipulate the visibility state.
     // This may be a bit confusing at first but it should quickly make sense. Follow on the examples below.
@@ -5399,7 +5534,7 @@
                 return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1;
         }
 
-        // qsort() is instable so always return a way to differenciate items.
+        // qsort() is instable so always return a way to differentiate items.
         // Your own compare function may want to avoid fallback on implicit sort specs.
         // e.g. a Name compare if it wasn't already part of the sort specs.
         return (a->ID - b->ID);
@@ -5641,7 +5776,7 @@
         ImGui::SameLine(); ImGui::RadioButton("Text", &contents_type, CT_Text);
         ImGui::SameLine(); ImGui::RadioButton("FillButton", &contents_type, CT_FillButton);
         ImGui::Checkbox("Display headers", &display_headers);
-        ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers");
+        ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers)");
         PopStyleCompact();
 
         if (ImGui::BeginTable("table1", 3, flags))
@@ -6592,7 +6727,7 @@
     {
         static ImGuiTableFlags table_flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
 
-        static ImGuiTreeNodeFlags tree_node_flags_base = ImGuiTreeNodeFlags_SpanAllColumns;
+        static ImGuiTreeNodeFlags tree_node_flags_base = ImGuiTreeNodeFlags_SpanAllColumns | ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_DrawLinesFull;
         ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth",  &tree_node_flags_base, ImGuiTreeNodeFlags_SpanFullWidth);
         ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanLabelWidth",  &tree_node_flags_base, ImGuiTreeNodeFlags_SpanLabelWidth);
         ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags_base, ImGuiTreeNodeFlags_SpanAllColumns);
@@ -7134,7 +7269,7 @@
                 ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH);
                 ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH);
                 ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH);
-                ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers");
+                ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers)");
                 ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers)");
                 ImGui::TreePop();
             }
@@ -7762,7 +7897,7 @@
             ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "...");
 
             // 2: InputText also polling for CTRL+A: it always uses _RouteFocused internally (gets priority when active)
-            // (Commmented because the owner-aware version of Shortcut() is still in imgui_internal.h)
+            // (Commented because the owner-aware version of Shortcut() is still in imgui_internal.h)
             //char str[16] = "Press CTRL+A";
             //ImGui::Spacing();
             //ImGui::InputText("InputTextB", str, IM_ARRAYSIZE(str), ImGuiInputTextFlags_ReadOnly);
@@ -7789,7 +7924,7 @@
             {
                 ImGui::Text("(in PopupF)");
                 ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "...");
-                // (Commmented because the owner-aware version of Shortcut() is still in imgui_internal.h)
+                // (Commented because the owner-aware version of Shortcut() is still in imgui_internal.h)
                 //ImGui::InputText("InputTextG", str, IM_ARRAYSIZE(str), ImGuiInputTextFlags_ReadOnly);
                 //ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags, ImGui::GetItemID()) ? "PRESSED" : "...");
                 ImGui::EndPopup();
@@ -7940,6 +8075,8 @@
     ImGui::SameLine();
     ImGui::TextLinkOpenURL("Wiki", "https://github.com/ocornut/imgui/wiki");
     ImGui::SameLine();
+    ImGui::TextLinkOpenURL("Extensions", "https://github.com/ocornut/imgui/wiki/Useful-Extensions");
+    ImGui::SameLine();
     ImGui::TextLinkOpenURL("Releases", "https://github.com/ocornut/imgui/releases");
     ImGui::SameLine();
     ImGui::TextLinkOpenURL("Funding", "https://github.com/ocornut/imgui/wiki/Funding");
@@ -7963,13 +8100,16 @@
         if (copy_to_clipboard)
         {
             ImGui::LogToClipboard();
-            ImGui::LogText("```\n"); // Back quotes will make text appears without formatting when pasting on GitHub
+            ImGui::LogText("```cpp\n"); // Back quotes will make text appears without formatting when pasting on GitHub
         }
 
         ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM);
         ImGui::Separator();
         ImGui::Text("sizeof(size_t): %d, sizeof(ImDrawIdx): %d, sizeof(ImDrawVert): %d", (int)sizeof(size_t), (int)sizeof(ImDrawIdx), (int)sizeof(ImDrawVert));
         ImGui::Text("define: __cplusplus=%d", (int)__cplusplus);
+#ifdef IMGUI_ENABLE_TEST_ENGINE
+        ImGui::Text("define: IMGUI_ENABLE_TEST_ENGINE");
+#endif
 #ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
         ImGui::Text("define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS");
 #endif
@@ -8059,8 +8199,10 @@
         if (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors)        ImGui::Text(" HasMouseCursors");
         if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)         ImGui::Text(" HasSetMousePos");
         if (io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset)   ImGui::Text(" RendererHasVtxOffset");
+        if (io.BackendFlags & ImGuiBackendFlags_RendererHasTextures)    ImGui::Text(" RendererHasTextures");
         ImGui::Separator();
-        ImGui::Text("io.Fonts: %d fonts, Flags: 0x%08X, TexSize: %d,%d", io.Fonts->Fonts.Size, io.Fonts->Flags, io.Fonts->TexWidth, io.Fonts->TexHeight);
+        ImGui::Text("io.Fonts: %d fonts, Flags: 0x%08X, TexSize: %d,%d", io.Fonts->Fonts.Size, io.Fonts->Flags, io.Fonts->TexData->Width, io.Fonts->TexData->Height);
+        ImGui::Text("io.Fonts->FontLoaderName: %s", io.Fonts->FontLoaderName ? io.Fonts->FontLoaderName : "NULL");
         ImGui::Text("io.DisplaySize: %.2f,%.2f", io.DisplaySize.x, io.DisplaySize.y);
         ImGui::Text("io.DisplayFramebufferScale: %.2f,%.2f", io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y);
         ImGui::Separator();
@@ -8085,66 +8227,56 @@
 //-----------------------------------------------------------------------------
 // [SECTION] Style Editor / ShowStyleEditor()
 //-----------------------------------------------------------------------------
-// - ShowFontSelector()
 // - ShowStyleSelector()
 // - ShowStyleEditor()
 //-----------------------------------------------------------------------------
 
-// Forward declare ShowFontAtlas() which isn't worth putting in public API yet
-namespace ImGui { IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); }
-
-// Demo helper function to select among loaded fonts.
-// Here we use the regular BeginCombo()/EndCombo() api which is the more flexible one.
-void ImGui::ShowFontSelector(const char* label)
+// Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options.
+bool ImGui::ShowStyleSelector(const char* label)
 {
-    ImGuiIO& io = ImGui::GetIO();
-    ImFont* font_current = ImGui::GetFont();
-    if (ImGui::BeginCombo(label, font_current->GetDebugName()))
+    // FIXME: This is a bit tricky to get right as style are functions, they don't register a name nor the fact that one is active.
+    // So we keep track of last active one among our limited selection.
+    static int style_idx = -1;
+    const char* style_names[] = { "Dark", "Light", "Classic" };
+    bool ret = false;
+    if (ImGui::BeginCombo(label, (style_idx >= 0 && style_idx < IM_ARRAYSIZE(style_names)) ? style_names[style_idx] : ""))
     {
-        for (ImFont* font : io.Fonts->Fonts)
+        for (int n = 0; n < IM_ARRAYSIZE(style_names); n++)
         {
-            ImGui::PushID((void*)font);
-            if (ImGui::Selectable(font->GetDebugName(), font == font_current))
-                io.FontDefault = font;
-            if (font == font_current)
+            if (ImGui::Selectable(style_names[n], style_idx == n, ImGuiSelectableFlags_SelectOnNav))
+            {
+                style_idx = n;
+                ret = true;
+                switch (style_idx)
+                {
+                case 0: ImGui::StyleColorsDark(); break;
+                case 1: ImGui::StyleColorsLight(); break;
+                case 2: ImGui::StyleColorsClassic(); break;
+                }
+            }
+            else if (style_idx == n)
                 ImGui::SetItemDefaultFocus();
-            ImGui::PopID();
         }
         ImGui::EndCombo();
     }
-    ImGui::SameLine();
-    HelpMarker(
-        "- Load additional fonts with io.Fonts->AddFontFromFileTTF().\n"
-        "- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n"
-        "- Read FAQ and docs/FONTS.md for more details.\n"
-        "- If you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame().");
+    return ret;
 }
 
-// Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options.
-// Here we use the simplified Combo() api that packs items into a single literal string.
-// Useful for quick combo boxes where the choices are known locally.
-bool ImGui::ShowStyleSelector(const char* label)
+static const char* GetTreeLinesFlagsName(ImGuiTreeNodeFlags flags)
 {
-    static int style_idx = -1;
-    if (ImGui::Combo(label, &style_idx, "Dark\0Light\0Classic\0"))
-    {
-        switch (style_idx)
-        {
-        case 0: ImGui::StyleColorsDark(); break;
-        case 1: ImGui::StyleColorsLight(); break;
-        case 2: ImGui::StyleColorsClassic(); break;
-        }
-        return true;
-    }
-    return false;
+    if (flags == ImGuiTreeNodeFlags_DrawLinesNone) return "DrawLinesNone";
+    if (flags == ImGuiTreeNodeFlags_DrawLinesFull) return "DrawLinesFull";
+    if (flags == ImGuiTreeNodeFlags_DrawLinesToNodes) return "DrawLinesToNodes";
+    return "";
 }
 
+// We omit the ImGui:: prefix in this function, as we don't expect user to be copy and pasting this code.
 void ImGui::ShowStyleEditor(ImGuiStyle* ref)
 {
     IMGUI_DEMO_MARKER("Tools/Style Editor");
     // You can pass in a reference ImGuiStyle structure to compare to, revert to and save to
     // (without a reference style pointer, we will use one compared locally as a reference)
-    ImGuiStyle& style = ImGui::GetStyle();
+    ImGuiStyle& style = GetStyle();
     static ImGuiStyle ref_saved_style;
 
     // Default to using internal storage as reference
@@ -8155,194 +8287,234 @@
     if (ref == NULL)
         ref = &ref_saved_style;
 
-    ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.50f);
-
-    if (ImGui::ShowStyleSelector("Colors##Selector"))
-        ref_saved_style = style;
-    ImGui::ShowFontSelector("Fonts##Selector");
+    PushItemWidth(GetWindowWidth() * 0.50f);
 
-    // Simplified Settings (expose floating-pointer border sizes as boolean representing 0.0f or 1.0f)
-    if (ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"))
-        style.GrabRounding = style.FrameRounding; // Make GrabRounding always the same value as FrameRounding
-    { bool border = (style.WindowBorderSize > 0.0f); if (ImGui::Checkbox("WindowBorder", &border)) { style.WindowBorderSize = border ? 1.0f : 0.0f; } }
-    ImGui::SameLine();
-    { bool border = (style.FrameBorderSize > 0.0f);  if (ImGui::Checkbox("FrameBorder",  &border)) { style.FrameBorderSize  = border ? 1.0f : 0.0f; } }
-    ImGui::SameLine();
-    { bool border = (style.PopupBorderSize > 0.0f);  if (ImGui::Checkbox("PopupBorder",  &border)) { style.PopupBorderSize  = border ? 1.0f : 0.0f; } }
+    {
+        // General
+        SeparatorText("General");
+        if ((GetIO().BackendFlags & ImGuiBackendFlags_RendererHasTextures) == 0)
+        {
+            BulletText("Warning: Font scaling will NOT be smooth, because\nImGuiBackendFlags_RendererHasTextures is not set!");
+            BulletText("For instructions, see:");
+            SameLine();
+            TextLinkOpenURL("docs/BACKENDS.md", "https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md");
+        }
+
+        if (ShowStyleSelector("Colors##Selector"))
+            ref_saved_style = style;
+        ShowFontSelector("Fonts##Selector");
+        if (DragFloat("FontSizeBase", &style.FontSizeBase, 0.20f, 5.0f, 100.0f, "%.0f"))
+            style._NextFrameFontSizeBase = style.FontSizeBase; // FIXME: Temporary hack until we finish remaining work.
+        SameLine(0.0f, 0.0f); Text(" (out %.2f)", GetFontSize());
+        DragFloat("FontScaleMain", &style.FontScaleMain, 0.02f, 0.5f, 4.0f);
+        //BeginDisabled(GetIO().ConfigDpiScaleFonts);
+        DragFloat("FontScaleDpi", &style.FontScaleDpi, 0.02f, 0.5f, 4.0f);
+        //SetItemTooltip("When io.ConfigDpiScaleFonts is set, this value is automatically overwritten.");
+        //EndDisabled();
+
+        // Simplified Settings (expose floating-pointer border sizes as boolean representing 0.0f or 1.0f)
+        if (SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"))
+            style.GrabRounding = style.FrameRounding; // Make GrabRounding always the same value as FrameRounding
+        { bool border = (style.WindowBorderSize > 0.0f); if (Checkbox("WindowBorder", &border)) { style.WindowBorderSize = border ? 1.0f : 0.0f; } }
+        SameLine();
+        { bool border = (style.FrameBorderSize > 0.0f);  if (Checkbox("FrameBorder", &border)) { style.FrameBorderSize = border ? 1.0f : 0.0f; } }
+        SameLine();
+        { bool border = (style.PopupBorderSize > 0.0f);  if (Checkbox("PopupBorder", &border)) { style.PopupBorderSize = border ? 1.0f : 0.0f; } }
+    }
 
     // Save/Revert button
-    if (ImGui::Button("Save Ref"))
+    if (Button("Save Ref"))
         *ref = ref_saved_style = style;
-    ImGui::SameLine();
-    if (ImGui::Button("Revert Ref"))
+    SameLine();
+    if (Button("Revert Ref"))
         style = *ref;
-    ImGui::SameLine();
+    SameLine();
     HelpMarker(
         "Save/Revert in local non-persistent storage. Default Colors definition are not affected. "
         "Use \"Export\" below to save them somewhere.");
 
-    ImGui::Separator();
-
-    if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None))
+    SeparatorText("Details");
+    if (BeginTabBar("##tabs", ImGuiTabBarFlags_None))
     {
-        if (ImGui::BeginTabItem("Sizes"))
+        if (BeginTabItem("Sizes"))
         {
-            ImGui::SeparatorText("Main");
-            ImGui::SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f");
-            ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f");
-            ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f");
-            ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f");
-            ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f");
-            ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f");
-            ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f");
-            ImGui::SliderFloat("GrabMinSize", &style.GrabMinSize, 1.0f, 20.0f, "%.0f");
-
-            ImGui::SeparatorText("Borders");
-            ImGui::SliderFloat("WindowBorderSize", &style.WindowBorderSize, 0.0f, 1.0f, "%.0f");
-            ImGui::SliderFloat("ChildBorderSize", &style.ChildBorderSize, 0.0f, 1.0f, "%.0f");
-            ImGui::SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, "%.0f");
-            ImGui::SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, "%.0f");
-
-            ImGui::SeparatorText("Rounding");
-            ImGui::SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("ChildRounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("PopupRounding", &style.PopupRounding, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("ScrollbarRounding", &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f");
-
-            ImGui::SeparatorText("Tabs");
-            ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f");
-            ImGui::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, 2.0f, "%.0f");
-            ImGui::SliderFloat("TabBarOverlineSize", &style.TabBarOverlineSize, 0.0f, 3.0f, "%.0f");
-            ImGui::SameLine(); HelpMarker("Overline is only drawn over the selected tab when ImGuiTabBarFlags_DrawSelectedOverline is set.");
-            ImGui::DragFloat("TabCloseButtonMinWidthSelected", &style.TabCloseButtonMinWidthSelected, 0.1f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthSelected < 0.0f) ? "%.0f (Always)" : "%.0f");
-            ImGui::DragFloat("TabCloseButtonMinWidthUnselected", &style.TabCloseButtonMinWidthUnselected, 0.1f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthUnselected < 0.0f) ? "%.0f (Always)" : "%.0f");
-            ImGui::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, "%.0f");
-
-            ImGui::SeparatorText("Tables");
-            ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f");
-            ImGui::SliderAngle("TableAngledHeadersAngle", &style.TableAngledHeadersAngle, -50.0f, +50.0f);
-            ImGui::SliderFloat2("TableAngledHeadersTextAlign", (float*)&style.TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f");
-
-            ImGui::SeparatorText("Windows");
-            ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f");
-            ImGui::SliderFloat("WindowBorderHoverPadding", &style.WindowBorderHoverPadding, 1.0f, 20.0f, "%.0f");
+            SeparatorText("Main");
+            SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f");
+            SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f");
+            SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f");
+            SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f");
+            SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, 0.0f, 10.0f, "%.0f");
+            SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f");
+            SliderFloat("GrabMinSize", &style.GrabMinSize, 1.0f, 20.0f, "%.0f");
+
+            SeparatorText("Borders");
+            SliderFloat("WindowBorderSize", &style.WindowBorderSize, 0.0f, 1.0f, "%.0f");
+            SliderFloat("ChildBorderSize", &style.ChildBorderSize, 0.0f, 1.0f, "%.0f");
+            SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, "%.0f");
+            SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, "%.0f");
+
+            SeparatorText("Rounding");
+            SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f");
+            SliderFloat("ChildRounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f");
+            SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f");
+            SliderFloat("PopupRounding", &style.PopupRounding, 0.0f, 12.0f, "%.0f");
+            SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f");
+
+            SeparatorText("Scrollbar");
+            SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f");
+            SliderFloat("ScrollbarRounding", &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f");
+            SliderFloat("ScrollbarPadding", &style.ScrollbarPadding, 0.0f, 10.0f, "%.0f");
+
+            SeparatorText("Tabs");
+            SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f");
+            SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, 2.0f, "%.0f");
+            SliderFloat("TabBarOverlineSize", &style.TabBarOverlineSize, 0.0f, 3.0f, "%.0f");
+            SameLine(); HelpMarker("Overline is only drawn over the selected tab when ImGuiTabBarFlags_DrawSelectedOverline is set.");
+            DragFloat("TabMinWidthBase", &style.TabMinWidthBase, 0.5f, 1.0f, 500.0f, "%.0f");
+            DragFloat("TabMinWidthShrink", &style.TabMinWidthShrink, 0.5f, 1.0f, 500.0f, "%0.f");
+            DragFloat("TabCloseButtonMinWidthSelected", &style.TabCloseButtonMinWidthSelected, 0.5f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthSelected < 0.0f) ? "%.0f (Always)" : "%.0f");
+            DragFloat("TabCloseButtonMinWidthUnselected", &style.TabCloseButtonMinWidthUnselected, 0.5f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthUnselected < 0.0f) ? "%.0f (Always)" : "%.0f");
+            SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, "%.0f");
+
+            SeparatorText("Tables");
+            SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f");
+            SliderAngle("TableAngledHeadersAngle", &style.TableAngledHeadersAngle, -50.0f, +50.0f);
+            SliderFloat2("TableAngledHeadersTextAlign", (float*)&style.TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f");
+
+            SeparatorText("Trees");
+            bool combo_open = BeginCombo("TreeLinesFlags", GetTreeLinesFlagsName(style.TreeLinesFlags));
+            SameLine();
+            HelpMarker("[Experimental] Tree lines may not work in all situations (e.g. using a clipper) and may incurs slight traversal overhead.\n\nImGuiTreeNodeFlags_DrawLinesFull is faster than ImGuiTreeNodeFlags_DrawLinesToNode.");
+            if (combo_open)
+            {
+                const ImGuiTreeNodeFlags options[] = { ImGuiTreeNodeFlags_DrawLinesNone, ImGuiTreeNodeFlags_DrawLinesFull, ImGuiTreeNodeFlags_DrawLinesToNodes };
+                for (ImGuiTreeNodeFlags option : options)
+                    if (Selectable(GetTreeLinesFlagsName(option), style.TreeLinesFlags == option))
+                        style.TreeLinesFlags = option;
+                EndCombo();
+            }
+            SliderFloat("TreeLinesSize", &style.TreeLinesSize, 0.0f, 2.0f, "%.0f");
+            SliderFloat("TreeLinesRounding", &style.TreeLinesRounding, 0.0f, 12.0f, "%.0f");
+
+            SeparatorText("Windows");
+            SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f");
+            SliderFloat("WindowBorderHoverPadding", &style.WindowBorderHoverPadding, 1.0f, 20.0f, "%.0f");
             int window_menu_button_position = style.WindowMenuButtonPosition + 1;
-            if (ImGui::Combo("WindowMenuButtonPosition", (int*)&window_menu_button_position, "None\0Left\0Right\0"))
+            if (Combo("WindowMenuButtonPosition", (int*)&window_menu_button_position, "None\0Left\0Right\0"))
                 style.WindowMenuButtonPosition = (ImGuiDir)(window_menu_button_position - 1);
 
-            ImGui::SeparatorText("Widgets");
-            ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0");
-            ImGui::SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f");
-            ImGui::SameLine(); HelpMarker("Alignment applies when a button is larger than its text content.");
-            ImGui::SliderFloat2("SelectableTextAlign", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f");
-            ImGui::SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content.");
-            ImGui::SliderFloat("SeparatorTextBorderSize", &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f");
-            ImGui::SliderFloat2("SeparatorTextAlign", (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f");
-            ImGui::SliderFloat2("SeparatorTextPadding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f");
-            ImGui::SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f");
-            ImGui::SliderFloat("ImageBorderSize", &style.ImageBorderSize, 0.0f, 1.0f, "%.0f");
+            SeparatorText("Widgets");
+            Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0");
+            SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f");
+            SameLine(); HelpMarker("Alignment applies when a button is larger than its text content.");
+            SliderFloat2("SelectableTextAlign", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f");
+            SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content.");
+            SliderFloat("SeparatorTextBorderSize", &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f");
+            SliderFloat2("SeparatorTextAlign", (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f");
+            SliderFloat2("SeparatorTextPadding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f");
+            SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f");
+            SliderFloat("ImageBorderSize", &style.ImageBorderSize, 0.0f, 1.0f, "%.0f");
 
-            ImGui::SeparatorText("Tooltips");
+            SeparatorText("Tooltips");
             for (int n = 0; n < 2; n++)
-                if (ImGui::TreeNodeEx(n == 0 ? "HoverFlagsForTooltipMouse" : "HoverFlagsForTooltipNav"))
+                if (TreeNodeEx(n == 0 ? "HoverFlagsForTooltipMouse" : "HoverFlagsForTooltipNav"))
                 {
                     ImGuiHoveredFlags* p = (n == 0) ? &style.HoverFlagsForTooltipMouse : &style.HoverFlagsForTooltipNav;
-                    ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNone", p, ImGuiHoveredFlags_DelayNone);
-                    ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayShort", p, ImGuiHoveredFlags_DelayShort);
-                    ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNormal", p, ImGuiHoveredFlags_DelayNormal);
-                    ImGui::CheckboxFlags("ImGuiHoveredFlags_Stationary", p, ImGuiHoveredFlags_Stationary);
-                    ImGui::CheckboxFlags("ImGuiHoveredFlags_NoSharedDelay", p, ImGuiHoveredFlags_NoSharedDelay);
-                    ImGui::TreePop();
+                    CheckboxFlags("ImGuiHoveredFlags_DelayNone", p, ImGuiHoveredFlags_DelayNone);
+                    CheckboxFlags("ImGuiHoveredFlags_DelayShort", p, ImGuiHoveredFlags_DelayShort);
+                    CheckboxFlags("ImGuiHoveredFlags_DelayNormal", p, ImGuiHoveredFlags_DelayNormal);
+                    CheckboxFlags("ImGuiHoveredFlags_Stationary", p, ImGuiHoveredFlags_Stationary);
+                    CheckboxFlags("ImGuiHoveredFlags_NoSharedDelay", p, ImGuiHoveredFlags_NoSharedDelay);
+                    TreePop();
                 }
 
-            ImGui::SeparatorText("Misc");
-            ImGui::SliderFloat2("DisplayWindowPadding", (float*)&style.DisplayWindowPadding, 0.0f, 30.0f, "%.0f"); ImGui::SameLine(); HelpMarker("Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen.");
-            ImGui::SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); ImGui::SameLine(); HelpMarker("Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured).");
+            SeparatorText("Misc");
+            SliderFloat2("DisplayWindowPadding", (float*)&style.DisplayWindowPadding, 0.0f, 30.0f, "%.0f"); SameLine(); HelpMarker("Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen.");
+            SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); SameLine(); HelpMarker("Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured).");
 
-            ImGui::EndTabItem();
+            EndTabItem();
         }
 
-        if (ImGui::BeginTabItem("Colors"))
+        if (BeginTabItem("Colors"))
         {
             static int output_dest = 0;
             static bool output_only_modified = true;
-            if (ImGui::Button("Export"))
+            if (Button("Export"))
             {
                 if (output_dest == 0)
-                    ImGui::LogToClipboard();
+                    LogToClipboard();
                 else
-                    ImGui::LogToTTY();
-                ImGui::LogText("ImVec4* colors = ImGui::GetStyle().Colors;" IM_NEWLINE);
+                    LogToTTY();
+                LogText("ImVec4* colors = GetStyle().Colors;" IM_NEWLINE);
                 for (int i = 0; i < ImGuiCol_COUNT; i++)
                 {
                     const ImVec4& col = style.Colors[i];
-                    const char* name = ImGui::GetStyleColorName(i);
+                    const char* name = GetStyleColorName(i);
                     if (!output_only_modified || memcmp(&col, &ref->Colors[i], sizeof(ImVec4)) != 0)
-                        ImGui::LogText("colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, %.2ff);" IM_NEWLINE,
+                        LogText("colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, %.2ff);" IM_NEWLINE,
                             name, 23 - (int)strlen(name), "", col.x, col.y, col.z, col.w);
                 }
-                ImGui::LogFinish();
+                LogFinish();
             }
-            ImGui::SameLine(); ImGui::SetNextItemWidth(120); ImGui::Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0");
-            ImGui::SameLine(); ImGui::Checkbox("Only Modified Colors", &output_only_modified);
+            SameLine(); SetNextItemWidth(120); Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0");
+            SameLine(); Checkbox("Only Modified Colors", &output_only_modified);
 
             static ImGuiTextFilter filter;
-            filter.Draw("Filter colors", ImGui::GetFontSize() * 16);
+            filter.Draw("Filter colors", GetFontSize() * 16);
 
             static ImGuiColorEditFlags alpha_flags = 0;
-            if (ImGui::RadioButton("Opaque", alpha_flags == ImGuiColorEditFlags_AlphaOpaque))       { alpha_flags = ImGuiColorEditFlags_AlphaOpaque; } ImGui::SameLine();
-            if (ImGui::RadioButton("Alpha",  alpha_flags == ImGuiColorEditFlags_None))              { alpha_flags = ImGuiColorEditFlags_None; } ImGui::SameLine();
-            if (ImGui::RadioButton("Both",   alpha_flags == ImGuiColorEditFlags_AlphaPreviewHalf))  { alpha_flags = ImGuiColorEditFlags_AlphaPreviewHalf; } ImGui::SameLine();
+            if (RadioButton("Opaque", alpha_flags == ImGuiColorEditFlags_AlphaOpaque))       { alpha_flags = ImGuiColorEditFlags_AlphaOpaque; } SameLine();
+            if (RadioButton("Alpha",  alpha_flags == ImGuiColorEditFlags_None))              { alpha_flags = ImGuiColorEditFlags_None; } SameLine();
+            if (RadioButton("Both",   alpha_flags == ImGuiColorEditFlags_AlphaPreviewHalf))  { alpha_flags = ImGuiColorEditFlags_AlphaPreviewHalf; } SameLine();
             HelpMarker(
                 "In the color list:\n"
                 "Left-click on color square to open color picker,\n"
                 "Right-click to open edit options menu.");
 
-            ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 10), ImVec2(FLT_MAX, FLT_MAX));
-            ImGui::BeginChild("##colors", ImVec2(0, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar);
-            ImGui::PushItemWidth(ImGui::GetFontSize() * -12);
+            SetNextWindowSizeConstraints(ImVec2(0.0f, GetTextLineHeightWithSpacing() * 10), ImVec2(FLT_MAX, FLT_MAX));
+            BeginChild("##colors", ImVec2(0, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar);
+            PushItemWidth(GetFontSize() * -12);
             for (int i = 0; i < ImGuiCol_COUNT; i++)
             {
-                const char* name = ImGui::GetStyleColorName(i);
+                const char* name = GetStyleColorName(i);
                 if (!filter.PassFilter(name))
                     continue;
-                ImGui::PushID(i);
+                PushID(i);
 #ifndef IMGUI_DISABLE_DEBUG_TOOLS
-                if (ImGui::Button("?"))
-                    ImGui::DebugFlashStyleColor((ImGuiCol)i);
-                ImGui::SetItemTooltip("Flash given color to identify places where it is used.");
-                ImGui::SameLine();
+                if (Button("?"))
+                    DebugFlashStyleColor((ImGuiCol)i);
+                SetItemTooltip("Flash given color to identify places where it is used.");
+                SameLine();
 #endif
-                ImGui::ColorEdit4("##color", (float*)&style.Colors[i], ImGuiColorEditFlags_AlphaBar | alpha_flags);
+                ColorEdit4("##color", (float*)&style.Colors[i], ImGuiColorEditFlags_AlphaBar | alpha_flags);
                 if (memcmp(&style.Colors[i], &ref->Colors[i], sizeof(ImVec4)) != 0)
                 {
                     // Tips: in a real user application, you may want to merge and use an icon font into the main font,
                     // so instead of "Save"/"Revert" you'd use icons!
                     // Read the FAQ and docs/FONTS.md about using icon fonts. It's really easy and super convenient!
-                    ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Save")) { ref->Colors[i] = style.Colors[i]; }
-                    ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Revert")) { style.Colors[i] = ref->Colors[i]; }
+                    SameLine(0.0f, style.ItemInnerSpacing.x); if (Button("Save")) { ref->Colors[i] = style.Colors[i]; }
+                    SameLine(0.0f, style.ItemInnerSpacing.x); if (Button("Revert")) { style.Colors[i] = ref->Colors[i]; }
                 }
-                ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
-                ImGui::TextUnformatted(name);
-                ImGui::PopID();
+                SameLine(0.0f, style.ItemInnerSpacing.x);
+                TextUnformatted(name);
+                PopID();
             }
-            ImGui::PopItemWidth();
-            ImGui::EndChild();
+            PopItemWidth();
+            EndChild();
 
-            ImGui::EndTabItem();
+            EndTabItem();
         }
 
-        if (ImGui::BeginTabItem("Fonts"))
+        if (BeginTabItem("Fonts"))
         {
-            ImGuiIO& io = ImGui::GetIO();
+            ImGuiIO& io = GetIO();
             ImFontAtlas* atlas = io.Fonts;
-            HelpMarker("Read FAQ and docs/FONTS.md for details on font loading.");
-            ImGui::ShowFontAtlas(atlas);
+            ShowFontAtlas(atlas);
 
             // Post-baking font scaling. Note that this is NOT the nice way of scaling fonts, read below.
             // (we enforce hard clamping manually as by default DragFloat/SliderFloat allows CTRL+Click text to get out of bounds).
+            /*
+            SeparatorText("Legacy Scaling");
             const float MIN_SCALE = 0.3f;
             const float MAX_SCALE = 2.0f;
             HelpMarker(
@@ -8350,120 +8522,121 @@
                 "However, the _correct_ way of scaling your UI is currently to reload your font at the designed size, "
                 "rebuild the font atlas, and call style.ScaleAllSizes() on a reference ImGuiStyle structure.\n"
                 "Using those settings here will give you poor quality results.");
-            static float window_scale = 1.0f;
-            ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
-            if (ImGui::DragFloat("window scale", &window_scale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp)) // Scale only this window
-                ImGui::SetWindowFontScale(window_scale);
-            ImGui::DragFloat("global scale", &io.FontGlobalScale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp); // Scale everything
-            ImGui::PopItemWidth();
+            PushItemWidth(GetFontSize() * 8);
+            DragFloat("global scale", &io.FontGlobalScale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp); // Scale everything
+            //static float window_scale = 1.0f;
+            //if (DragFloat("window scale", &window_scale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp)) // Scale only this window
+            //    SetWindowFontScale(window_scale);
+            PopItemWidth();
+            */
 
-            ImGui::EndTabItem();
+            EndTabItem();
         }
 
-        if (ImGui::BeginTabItem("Rendering"))
+        if (BeginTabItem("Rendering"))
         {
-            ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines);
-            ImGui::SameLine();
+            Checkbox("Anti-aliased lines", &style.AntiAliasedLines);
+            SameLine();
             HelpMarker("When disabling anti-aliasing lines, you'll probably want to disable borders in your style as well.");
 
-            ImGui::Checkbox("Anti-aliased lines use texture", &style.AntiAliasedLinesUseTex);
-            ImGui::SameLine();
+            Checkbox("Anti-aliased lines use texture", &style.AntiAliasedLinesUseTex);
+            SameLine();
             HelpMarker("Faster lines using texture data. Require backend to render with bilinear filtering (not point/nearest filtering).");
 
-            ImGui::Checkbox("Anti-aliased fill", &style.AntiAliasedFill);
-            ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
-            ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, "%.2f");
+            Checkbox("Anti-aliased fill", &style.AntiAliasedFill);
+            PushItemWidth(GetFontSize() * 8);
+            DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, "%.2f");
             if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f;
 
             // When editing the "Circle Segment Max Error" value, draw a preview of its effect on auto-tessellated circles.
-            ImGui::DragFloat("Circle Tessellation Max Error", &style.CircleTessellationMaxError , 0.005f, 0.10f, 5.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp);
-            const bool show_samples = ImGui::IsItemActive();
+            DragFloat("Circle Tessellation Max Error", &style.CircleTessellationMaxError , 0.005f, 0.10f, 5.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp);
+            const bool show_samples = IsItemActive();
             if (show_samples)
-                ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos());
-            if (show_samples && ImGui::BeginTooltip())
+                SetNextWindowPos(GetCursorScreenPos());
+            if (show_samples && BeginTooltip())
             {
-                ImGui::TextUnformatted("(R = radius, N = approx number of segments)");
-                ImGui::Spacing();
-                ImDrawList* draw_list = ImGui::GetWindowDrawList();
-                const float min_widget_width = ImGui::CalcTextSize("R: MMM\nN: MMM").x;
+                TextUnformatted("(R = radius, N = approx number of segments)");
+                Spacing();
+                ImDrawList* draw_list = GetWindowDrawList();
+                const float min_widget_width = CalcTextSize("R: MMM\nN: MMM").x;
                 for (int n = 0; n < 8; n++)
                 {
                     const float RAD_MIN = 5.0f;
                     const float RAD_MAX = 70.0f;
                     const float rad = RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (8.0f - 1.0f);
 
-                    ImGui::BeginGroup();
+                    BeginGroup();
 
                     // N is not always exact here due to how PathArcTo() function work internally
-                    ImGui::Text("R: %.f\nN: %d", rad, draw_list->_CalcCircleAutoSegmentCount(rad));
+                    Text("R: %.f\nN: %d", rad, draw_list->_CalcCircleAutoSegmentCount(rad));
 
                     const float canvas_width = IM_MAX(min_widget_width, rad * 2.0f);
                     const float offset_x     = floorf(canvas_width * 0.5f);
                     const float offset_y     = floorf(RAD_MAX);
 
-                    const ImVec2 p1 = ImGui::GetCursorScreenPos();
-                    draw_list->AddCircle(ImVec2(p1.x + offset_x, p1.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text));
-                    ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2));
+                    const ImVec2 p1 = GetCursorScreenPos();
+                    draw_list->AddCircle(ImVec2(p1.x + offset_x, p1.y + offset_y), rad, GetColorU32(ImGuiCol_Text));
+                    Dummy(ImVec2(canvas_width, RAD_MAX * 2));
 
                     /*
-                    const ImVec2 p2 = ImGui::GetCursorScreenPos();
-                    draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text));
-                    ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2));
+                    const ImVec2 p2 = GetCursorScreenPos();
+                    draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), rad, GetColorU32(ImGuiCol_Text));
+                    Dummy(ImVec2(canvas_width, RAD_MAX * 2));
                     */
 
-                    ImGui::EndGroup();
-                    ImGui::SameLine();
+                    EndGroup();
+                    SameLine();
                 }
-                ImGui::EndTooltip();
+                EndTooltip();
             }
-            ImGui::SameLine();
+            SameLine();
             HelpMarker("When drawing circle primitives with \"num_segments == 0\" tessellation will be calculated automatically.");
 
-            ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero.
-            ImGui::DragFloat("Disabled Alpha", &style.DisabledAlpha, 0.005f, 0.0f, 1.0f, "%.2f"); ImGui::SameLine(); HelpMarker("Additional alpha multiplier for disabled items (multiply over current value of Alpha).");
-            ImGui::PopItemWidth();
+            DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero.
+            DragFloat("Disabled Alpha", &style.DisabledAlpha, 0.005f, 0.0f, 1.0f, "%.2f"); SameLine(); HelpMarker("Additional alpha multiplier for disabled items (multiply over current value of Alpha).");
+            PopItemWidth();
 
-            ImGui::EndTabItem();
+            EndTabItem();
         }
 
-        ImGui::EndTabBar();
+        EndTabBar();
     }
-
-    ImGui::PopItemWidth();
+    PopItemWidth();
 }
 
 //-----------------------------------------------------------------------------
 // [SECTION] User Guide / ShowUserGuide()
 //-----------------------------------------------------------------------------
 
+// We omit the ImGui:: prefix in this function, as we don't expect user to be copy and pasting this code.
 void ImGui::ShowUserGuide()
 {
-    ImGuiIO& io = ImGui::GetIO();
-    ImGui::BulletText("Double-click on title bar to collapse window.");
-    ImGui::BulletText(
+    ImGuiIO& io = GetIO();
+    BulletText("Double-click on title bar to collapse window.");
+    BulletText(
         "Click and drag on lower corner to resize window\n"
         "(double-click to auto fit window to its contents).");
-    ImGui::BulletText("CTRL+Click on a slider or drag box to input value as text.");
-    ImGui::BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields.");
-    ImGui::BulletText("CTRL+Tab to select a window.");
+    BulletText("CTRL+Click on a slider or drag box to input value as text.");
+    BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields.");
+    BulletText("CTRL+Tab to select a window.");
     if (io.FontAllowUserScaling)
-        ImGui::BulletText("CTRL+Mouse Wheel to zoom window contents.");
-    ImGui::BulletText("While inputting text:\n");
-    ImGui::Indent();
-    ImGui::BulletText("CTRL+Left/Right to word jump.");
-    ImGui::BulletText("CTRL+A or double-click to select all.");
-    ImGui::BulletText("CTRL+X/C/V to use clipboard cut/copy/paste.");
-    ImGui::BulletText("CTRL+Z to undo, CTRL+Y/CTRL+SHIFT+Z to redo.");
-    ImGui::BulletText("ESCAPE to revert.");
-    ImGui::Unindent();
-    ImGui::BulletText("With keyboard navigation enabled:");
-    ImGui::Indent();
-    ImGui::BulletText("Arrow keys to navigate.");
-    ImGui::BulletText("Space to activate a widget.");
-    ImGui::BulletText("Return to input text into a widget.");
-    ImGui::BulletText("Escape to deactivate a widget, close popup, exit child window.");
-    ImGui::BulletText("Alt to jump to the menu layer of a window.");
-    ImGui::Unindent();
+        BulletText("CTRL+Mouse Wheel to zoom window contents.");
+    BulletText("While inputting text:\n");
+    Indent();
+    BulletText("CTRL+Left/Right to word jump.");
+    BulletText("CTRL+A or double-click to select all.");
+    BulletText("CTRL+X/C/V to use clipboard cut/copy/paste.");
+    BulletText("CTRL+Z to undo, CTRL+Y/CTRL+SHIFT+Z to redo.");
+    BulletText("ESCAPE to revert.");
+    Unindent();
+    BulletText("With keyboard navigation enabled:");
+    Indent();
+    BulletText("Arrow keys to navigate.");
+    BulletText("Space to activate a widget.");
+    BulletText("Return to input text into a widget.");
+    BulletText("Escape to deactivate a widget, close popup, exit child window.");
+    BulletText("Alt to jump to the menu layer of a window.");
+    Unindent();
 }
 
 //-----------------------------------------------------------------------------
@@ -9135,10 +9308,9 @@
             ImGui::BeginChild("left pane", ImVec2(150, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX);
             for (int i = 0; i < 100; i++)
             {
-                // FIXME: Good candidate to use ImGuiSelectableFlags_SelectOnNav
                 char label[128];
                 sprintf(label, "MyObject %d", i);
-                if (ImGui::Selectable(label, selected == i))
+                if (ImGui::Selectable(label, selected == i, ImGuiSelectableFlags_SelectOnNav))
                     selected = i;
             }
             ImGui::EndChild();
@@ -9285,8 +9457,10 @@
         ImGui::TableNextColumn();
         ImGui::PushID(node->UID);
         ImGuiTreeNodeFlags tree_flags = ImGuiTreeNodeFlags_None;
-        tree_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;    // Standard opening mode as we are likely to want to add selection afterwards
-        tree_flags |= ImGuiTreeNodeFlags_NavLeftJumpsBackHere;                                  // Left arrow support
+        tree_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;// Standard opening mode as we are likely to want to add selection afterwards
+        tree_flags |= ImGuiTreeNodeFlags_NavLeftJumpsToParent;  // Left arrow support
+        tree_flags |= ImGuiTreeNodeFlags_SpanFullWidth;         // Span full width for easier mouse reach
+        tree_flags |= ImGuiTreeNodeFlags_DrawLinesToNodes;      // Always draw hierarchy outlines
         if (node == VisibleNode)
             tree_flags |= ImGuiTreeNodeFlags_Selected;
         if (node->Childs.Size == 0)
@@ -9488,7 +9662,7 @@
         IMGUI_DEMO_MARKER("Examples/Constrained Resizing window");
         if (ImGui::GetIO().KeyShift)
         {
-            // Display a dummy viewport (in your real app you would likely use ImageButton() to display a texture.
+            // Display a dummy viewport (in your real app you would likely use ImageButton() to display a texture)
             ImVec2 avail_size = ImGui::GetContentRegionAvail();
             ImVec2 pos = ImGui::GetCursorScreenPos();
             ImGui::ColorButton("viewport", ImVec4(0.5f, 0.2f, 0.5f, 1.0f), ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, avail_size);
@@ -10368,7 +10542,7 @@
         Selection.Clear();
     }
 
-    // Logic would be written in the main code BeginChild() and outputing to local variables.
+    // Logic would be written in the main code BeginChild() and outputting to local variables.
     // We extracted it into a function so we can call it easily from multiple places.
     void UpdateLayoutSizes(float avail_width)
     {
@@ -10678,9 +10852,8 @@
 void ImGui::ShowDemoWindow(bool*) {}
 void ImGui::ShowUserGuide() {}
 void ImGui::ShowStyleEditor(ImGuiStyle*) {}
-bool ImGui::ShowStyleSelector(const char* label) { return false; }
-void ImGui::ShowFontSelector(const char* label) {}
+bool ImGui::ShowStyleSelector(const char*) { return false; }
 
-#endif
+#endif // #ifndef IMGUI_DISABLE_DEMO_WINDOWS
 
 #endif // #ifndef IMGUI_DISABLE
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui_draw.cpp f3d-3.4.1/external/imgui/imgui_draw.cpp
--- f3d-3.2.0+dfsg/external/imgui/imgui_draw.cpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui_draw.cpp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91.9b
+// dear imgui, v1.92.4
 // (drawing and font code)
 
 /*
@@ -13,7 +13,8 @@
 // [SECTION] ImDrawData
 // [SECTION] Helpers ShadeVertsXXX functions
 // [SECTION] ImFontConfig
-// [SECTION] ImFontAtlas
+// [SECTION] ImFontAtlas, ImFontAtlasBuilder
+// [SECTION] ImFontAtlas: backend for stb_truetype
 // [SECTION] ImFontAtlas: glyph ranges helpers
 // [SECTION] ImFontGlyphRangesBuilder
 // [SECTION] ImFont
@@ -39,6 +40,7 @@
 #endif
 
 #include <stdio.h>      // vsnprintf, sscanf, printf
+#include <stdint.h>     // intptr_t
 
 // Visual Studio warnings
 #ifdef _MSC_VER
@@ -46,7 +48,7 @@
 #pragma warning (disable: 4505)     // unreferenced local function has been removed (stb stuff)
 #pragma warning (disable: 4996)     // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen
 #pragma warning (disable: 26451)    // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2).
-#pragma warning (disable: 26812)    // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer)
+#pragma warning (disable: 26812)    // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
 #endif
 
 // Clang/GCC warnings with -Weverything
@@ -217,6 +219,7 @@
     colors[ImGuiCol_ResizeGrip]             = ImVec4(0.26f, 0.59f, 0.98f, 0.20f);
     colors[ImGuiCol_ResizeGripHovered]      = ImVec4(0.26f, 0.59f, 0.98f, 0.67f);
     colors[ImGuiCol_ResizeGripActive]       = ImVec4(0.26f, 0.59f, 0.98f, 0.95f);
+    colors[ImGuiCol_InputTextCursor]        = colors[ImGuiCol_Text];
     colors[ImGuiCol_TabHovered]             = colors[ImGuiCol_HeaderHovered];
     colors[ImGuiCol_Tab]                    = ImLerp(colors[ImGuiCol_Header],       colors[ImGuiCol_TitleBgActive], 0.80f);
     colors[ImGuiCol_TabSelected]            = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f);
@@ -235,7 +238,9 @@
     colors[ImGuiCol_TableRowBgAlt]          = ImVec4(1.00f, 1.00f, 1.00f, 0.06f);
     colors[ImGuiCol_TextLink]               = colors[ImGuiCol_HeaderActive];
     colors[ImGuiCol_TextSelectedBg]         = ImVec4(0.26f, 0.59f, 0.98f, 0.35f);
+    colors[ImGuiCol_TreeLines]              = colors[ImGuiCol_Border];
     colors[ImGuiCol_DragDropTarget]         = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
+    colors[ImGuiCol_UnsavedMarker]          = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
     colors[ImGuiCol_NavCursor]              = ImVec4(0.26f, 0.59f, 0.98f, 1.00f);
     colors[ImGuiCol_NavWindowingHighlight]  = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
     colors[ImGuiCol_NavWindowingDimBg]      = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
@@ -280,6 +285,7 @@
     colors[ImGuiCol_ResizeGrip]             = ImVec4(1.00f, 1.00f, 1.00f, 0.10f);
     colors[ImGuiCol_ResizeGripHovered]      = ImVec4(0.78f, 0.82f, 1.00f, 0.60f);
     colors[ImGuiCol_ResizeGripActive]       = ImVec4(0.78f, 0.82f, 1.00f, 0.90f);
+    colors[ImGuiCol_InputTextCursor]        = colors[ImGuiCol_Text];
     colors[ImGuiCol_TabHovered]             = colors[ImGuiCol_HeaderHovered];
     colors[ImGuiCol_Tab]                    = ImLerp(colors[ImGuiCol_Header],       colors[ImGuiCol_TitleBgActive], 0.80f);
     colors[ImGuiCol_TabSelected]            = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f);
@@ -298,7 +304,9 @@
     colors[ImGuiCol_TableRowBgAlt]          = ImVec4(1.00f, 1.00f, 1.00f, 0.07f);
     colors[ImGuiCol_TextLink]               = colors[ImGuiCol_HeaderActive];
     colors[ImGuiCol_TextSelectedBg]         = ImVec4(0.00f, 0.00f, 1.00f, 0.35f);
+    colors[ImGuiCol_TreeLines]              = colors[ImGuiCol_Border];
     colors[ImGuiCol_DragDropTarget]         = ImVec4(1.00f, 1.00f, 0.00f, 0.90f);
+    colors[ImGuiCol_UnsavedMarker]          = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
     colors[ImGuiCol_NavCursor]              = colors[ImGuiCol_HeaderHovered];
     colors[ImGuiCol_NavWindowingHighlight]  = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
     colors[ImGuiCol_NavWindowingDimBg]      = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
@@ -344,6 +352,7 @@
     colors[ImGuiCol_ResizeGrip]             = ImVec4(0.35f, 0.35f, 0.35f, 0.17f);
     colors[ImGuiCol_ResizeGripHovered]      = ImVec4(0.26f, 0.59f, 0.98f, 0.67f);
     colors[ImGuiCol_ResizeGripActive]       = ImVec4(0.26f, 0.59f, 0.98f, 0.95f);
+    colors[ImGuiCol_InputTextCursor]        = colors[ImGuiCol_Text];
     colors[ImGuiCol_TabHovered]             = colors[ImGuiCol_HeaderHovered];
     colors[ImGuiCol_Tab]                    = ImLerp(colors[ImGuiCol_Header],       colors[ImGuiCol_TitleBgActive], 0.90f);
     colors[ImGuiCol_TabSelected]            = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f);
@@ -362,7 +371,9 @@
     colors[ImGuiCol_TableRowBgAlt]          = ImVec4(0.30f, 0.30f, 0.30f, 0.09f);
     colors[ImGuiCol_TextLink]               = colors[ImGuiCol_HeaderActive];
     colors[ImGuiCol_TextSelectedBg]         = ImVec4(0.26f, 0.59f, 0.98f, 0.35f);
+    colors[ImGuiCol_TreeLines]              = colors[ImGuiCol_Border];
     colors[ImGuiCol_DragDropTarget]         = ImVec4(0.26f, 0.59f, 0.98f, 0.95f);
+    colors[ImGuiCol_UnsavedMarker]          = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
     colors[ImGuiCol_NavCursor]              = colors[ImGuiCol_HeaderHovered];
     colors[ImGuiCol_NavWindowingHighlight]  = ImVec4(0.70f, 0.70f, 0.70f, 0.70f);
     colors[ImGuiCol_NavWindowingDimBg]      = ImVec4(0.20f, 0.20f, 0.20f, 0.20f);
@@ -385,6 +396,11 @@
     ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError);
 }
 
+ImDrawListSharedData::~ImDrawListSharedData()
+{
+    IM_ASSERT(DrawLists.Size == 0);
+}
+
 void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error)
 {
     if (CircleSegmentMaxError == max_error)
@@ -403,22 +419,32 @@
 ImDrawList::ImDrawList(ImDrawListSharedData* shared_data)
 {
     memset(this, 0, sizeof(*this));
-    _Data = shared_data;
+    _SetDrawListSharedData(shared_data);
 }
 
 ImDrawList::~ImDrawList()
 {
     _ClearFreeMemory();
+    _SetDrawListSharedData(NULL);
+}
+
+void ImDrawList::_SetDrawListSharedData(ImDrawListSharedData* data)
+{
+    if (_Data != NULL)
+        _Data->DrawLists.find_erase_unsorted(this);
+    _Data = data;
+    if (_Data != NULL)
+        _Data->DrawLists.push_back(this);
 }
 
 // Initialize before use in a new frame. We always have a command ready in the buffer.
-// In the majority of cases, you would want to call PushClipRect() and PushTextureID() after this.
+// In the majority of cases, you would want to call PushClipRect() and PushTexture() after this.
 void ImDrawList::_ResetForNewFrame()
 {
     // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory.
     IM_STATIC_ASSERT(offsetof(ImDrawCmd, ClipRect) == 0);
-    IM_STATIC_ASSERT(offsetof(ImDrawCmd, TextureId) == sizeof(ImVec4));
-    IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID));
+    IM_STATIC_ASSERT(offsetof(ImDrawCmd, TexRef) == sizeof(ImVec4));
+    IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureRef));
     if (_Splitter._Count > 1)
         _Splitter.Merge(this);
 
@@ -431,7 +457,7 @@
     _VtxWritePtr = NULL;
     _IdxWritePtr = NULL;
     _ClipRectStack.resize(0);
-    _TextureIdStack.resize(0);
+    _TextureStack.resize(0);
     _CallbacksDataBuf.resize(0);
     _Path.resize(0);
     _Splitter.Clear();
@@ -449,15 +475,16 @@
     _VtxWritePtr = NULL;
     _IdxWritePtr = NULL;
     _ClipRectStack.clear();
-    _TextureIdStack.clear();
+    _TextureStack.clear();
     _CallbacksDataBuf.clear();
     _Path.clear();
     _Splitter.ClearFreeMemory();
 }
 
+// Note: For multi-threaded rendering, consider using `imgui_threaded_rendering` from https://github.com/ocornut/imgui_club
 ImDrawList* ImDrawList::CloneOutput() const
 {
-    ImDrawList* dst = IM_NEW(ImDrawList(_Data));
+    ImDrawList* dst = IM_NEW(ImDrawList(NULL));
     dst->CmdBuffer = CmdBuffer;
     dst->IdxBuffer = IdxBuffer;
     dst->VtxBuffer = VtxBuffer;
@@ -469,7 +496,7 @@
 {
     ImDrawCmd draw_cmd;
     draw_cmd.ClipRect = _CmdHeader.ClipRect;    // Same as calling ImDrawCmd_HeaderCopy()
-    draw_cmd.TextureId = _CmdHeader.TextureId;
+    draw_cmd.TexRef = _CmdHeader.TexRef;
     draw_cmd.VtxOffset = _CmdHeader.VtxOffset;
     draw_cmd.IdxOffset = IdxBuffer.Size;
 
@@ -494,6 +521,7 @@
 {
     IM_ASSERT_PARANOID(CmdBuffer.Size > 0);
     ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1];
+    IM_ASSERT(callback != NULL);
     IM_ASSERT(curr_cmd->UserCallback == NULL);
     if (curr_cmd->ElemCount != 0)
     {
@@ -524,10 +552,10 @@
     AddDrawCmd(); // Force a new command after us (see comment below)
 }
 
-// Compare ClipRect, TextureId and VtxOffset with a single memcmp()
+// Compare ClipRect, TexRef and VtxOffset with a single memcmp()
 #define ImDrawCmd_HeaderSize                            (offsetof(ImDrawCmd, VtxOffset) + sizeof(unsigned int))
-#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS)       (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize))    // Compare ClipRect, TextureId, VtxOffset
-#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC)          (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize))    // Copy ClipRect, TextureId, VtxOffset
+#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS)       (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize))    // Compare ClipRect, TexRef, VtxOffset
+#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC)          (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize))    // Copy ClipRect, TexRef, VtxOffset
 #define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1)  (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset)
 
 // Try to merge two last draw commands
@@ -567,17 +595,20 @@
     curr_cmd->ClipRect = _CmdHeader.ClipRect;
 }
 
-void ImDrawList::_OnChangedTextureID()
+void ImDrawList::_OnChangedTexture()
 {
     // If current command is used with different settings we need to add a new command
     IM_ASSERT_PARANOID(CmdBuffer.Size > 0);
     ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1];
-    if (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != _CmdHeader.TextureId)
+    if (curr_cmd->ElemCount != 0 && curr_cmd->TexRef != _CmdHeader.TexRef)
     {
         AddDrawCmd();
         return;
     }
-    IM_ASSERT(curr_cmd->UserCallback == NULL);
+
+    // Unlike other _OnChangedXXX functions this may be called by ImFontAtlasUpdateDrawListsTextures() in more locations so we need to handle this case.
+    if (curr_cmd->UserCallback != NULL)
+        return;
 
     // Try to merge with previous command if it matches, else use current command
     ImDrawCmd* prev_cmd = curr_cmd - 1;
@@ -586,7 +617,7 @@
         CmdBuffer.pop_back();
         return;
     }
-    curr_cmd->TextureId = _CmdHeader.TextureId;
+    curr_cmd->TexRef = _CmdHeader.TexRef;
 }
 
 void ImDrawList::_OnChangedVtxOffset()
@@ -647,27 +678,30 @@
     _OnChangedClipRect();
 }
 
-void ImDrawList::PushTextureID(ImTextureID texture_id)
+void ImDrawList::PushTexture(ImTextureRef tex_ref)
 {
-    _TextureIdStack.push_back(texture_id);
-    _CmdHeader.TextureId = texture_id;
-    _OnChangedTextureID();
+    _TextureStack.push_back(tex_ref);
+    _CmdHeader.TexRef = tex_ref;
+    if (tex_ref._TexData != NULL)
+        IM_ASSERT(tex_ref._TexData->WantDestroyNextFrame == false);
+    _OnChangedTexture();
 }
 
-void ImDrawList::PopTextureID()
+void ImDrawList::PopTexture()
 {
-    _TextureIdStack.pop_back();
-    _CmdHeader.TextureId = (_TextureIdStack.Size == 0) ? (ImTextureID)NULL : _TextureIdStack.Data[_TextureIdStack.Size - 1];
-    _OnChangedTextureID();
+    _TextureStack.pop_back();
+    _CmdHeader.TexRef = (_TextureStack.Size == 0) ? ImTextureRef() : _TextureStack.Data[_TextureStack.Size - 1];
+    _OnChangedTexture();
 }
 
-// This is used by ImGui::PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTextureID()/PopTextureID().
-void ImDrawList::_SetTextureID(ImTextureID texture_id)
+// This is used by ImGui::PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTexture()/PopTexture().
+void ImDrawList::_SetTexture(ImTextureRef tex_ref)
 {
-    if (_CmdHeader.TextureId == texture_id)
+    if (_CmdHeader.TexRef == tex_ref)
         return;
-    _CmdHeader.TextureId = texture_id;
-    _OnChangedTextureID();
+    _CmdHeader.TexRef = tex_ref;
+    _TextureStack.back() = tex_ref;
+    _OnChangedTexture();
 }
 
 // Reserve space for a number of vertices and indices.
@@ -853,7 +887,7 @@
                 dm_x *= half_draw_size; // dm_x, dm_y are offset to the outer edge of the AA area
                 dm_y *= half_draw_size;
 
-                // Add temporary vertexes for the outer edges
+                // Add temporary vertices for the outer edges
                 ImVec2* out_vtx = &temp_points[i2 * 2];
                 out_vtx[0].x = points[i2].x + dm_x;
                 out_vtx[0].y = points[i2].y + dm_y;
@@ -880,7 +914,7 @@
                 idx1 = idx2;
             }
 
-            // Add vertexes for each point on the line
+            // Add vertices for each point on the line
             if (use_texture)
             {
                 // If we're using textures we only need to emit the left/right edge vertices
@@ -1680,8 +1714,6 @@
     if (font_size == 0.0f)
         font_size = _Data->FontSize;
 
-    IM_ASSERT(font->ContainerAtlas->TexID == _CmdHeader.TextureId);  // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font.
-
     ImVec4 clip_rect = _CmdHeader.ClipRect;
     if (cpu_fine_clip_rect)
     {
@@ -1690,7 +1722,7 @@
         clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z);
         clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w);
     }
-    font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL);
+    font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, (cpu_fine_clip_rect != NULL) ? ImDrawTextFlags_CpuFineClip : ImDrawTextFlags_None);
 }
 
 void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end)
@@ -1698,39 +1730,39 @@
     AddText(_Data->Font, _Data->FontSize, pos, col, text_begin, text_end);
 }
 
-void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col)
+void ImDrawList::AddImage(ImTextureRef tex_ref, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col)
 {
     if ((col & IM_COL32_A_MASK) == 0)
         return;
 
-    const bool push_texture_id = user_texture_id != _CmdHeader.TextureId;
+    const bool push_texture_id = tex_ref != _CmdHeader.TexRef;
     if (push_texture_id)
-        PushTextureID(user_texture_id);
+        PushTexture(tex_ref);
 
     PrimReserve(6, 4);
     PrimRectUV(p_min, p_max, uv_min, uv_max, col);
 
     if (push_texture_id)
-        PopTextureID();
+        PopTexture();
 }
 
-void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col)
+void ImDrawList::AddImageQuad(ImTextureRef tex_ref, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col)
 {
     if ((col & IM_COL32_A_MASK) == 0)
         return;
 
-    const bool push_texture_id = user_texture_id != _CmdHeader.TextureId;
+    const bool push_texture_id = tex_ref != _CmdHeader.TexRef;
     if (push_texture_id)
-        PushTextureID(user_texture_id);
+        PushTexture(tex_ref);
 
     PrimReserve(6, 4);
     PrimQuadUV(p1, p2, p3, p4, uv1, uv2, uv3, uv4, col);
 
     if (push_texture_id)
-        PopTextureID();
+        PopTexture();
 }
 
-void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags)
+void ImDrawList::AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags)
 {
     if ((col & IM_COL32_A_MASK) == 0)
         return;
@@ -1738,13 +1770,13 @@
     flags = FixRectCornerFlags(flags);
     if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone)
     {
-        AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col);
+        AddImage(tex_ref, p_min, p_max, uv_min, uv_max, col);
         return;
     }
 
-    const bool push_texture_id = user_texture_id != _CmdHeader.TextureId;
+    const bool push_texture_id = tex_ref != _CmdHeader.TexRef;
     if (push_texture_id)
-        PushTextureID(user_texture_id);
+        PushTexture(tex_ref);
 
     int vert_start_idx = VtxBuffer.Size;
     PathRect(p_min, p_max, rounding, flags);
@@ -1753,7 +1785,7 @@
     ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true);
 
     if (push_texture_id)
-        PopTextureID();
+        PopTexture();
 }
 
 //-----------------------------------------------------------------------------
@@ -2181,7 +2213,7 @@
     // If current command is used with different settings we need to add a new command
     ImDrawCmd* curr_cmd = &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1];
     if (curr_cmd->ElemCount == 0)
-        ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset
+        ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TexRef, VtxOffset
     else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0)
         draw_list->AddDrawCmd();
 
@@ -2207,7 +2239,7 @@
     if (curr_cmd == NULL)
         draw_list->AddDrawCmd();
     else if (curr_cmd->ElemCount == 0)
-        ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset
+        ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TexRef, VtxOffset
     else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0)
         draw_list->AddDrawCmd();
 }
@@ -2223,6 +2255,7 @@
     CmdLists.resize(0); // The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them.
     DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.0f, 0.0f);
     OwnerViewport = NULL;
+    Textures = NULL;
 }
 
 // Important: 'out_list' is generally going to be draw_data->CmdLists, but may be another temporary list
@@ -2284,17 +2317,16 @@
 {
     ImVector<ImDrawVert> new_vtx_buffer;
     TotalVtxCount = TotalIdxCount = 0;
-    for (int i = 0; i < CmdListsCount; i++)
+    for (ImDrawList* draw_list : CmdLists)
     {
-        ImDrawList* cmd_list = CmdLists[i];
-        if (cmd_list->IdxBuffer.empty())
+        if (draw_list->IdxBuffer.empty())
             continue;
-        new_vtx_buffer.resize(cmd_list->IdxBuffer.Size);
-        for (int j = 0; j < cmd_list->IdxBuffer.Size; j++)
-            new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]];
-        cmd_list->VtxBuffer.swap(new_vtx_buffer);
-        cmd_list->IdxBuffer.resize(0);
-        TotalVtxCount += cmd_list->VtxBuffer.Size;
+        new_vtx_buffer.resize(draw_list->IdxBuffer.Size);
+        for (int j = 0; j < draw_list->IdxBuffer.Size; j++)
+            new_vtx_buffer[j] = draw_list->VtxBuffer[draw_list->IdxBuffer[j]];
+        draw_list->VtxBuffer.swap(new_vtx_buffer);
+        draw_list->IdxBuffer.resize(0);
+        TotalVtxCount += draw_list->VtxBuffer.Size;
     }
 }
 
@@ -2373,6 +2405,7 @@
 // [SECTION] ImFontConfig
 //-----------------------------------------------------------------------------
 
+// FIXME-NEWATLAS: Oversample specification could be more dynamic. For now, favoring automatic selection.
 ImFontConfig::ImFontConfig()
 {
     memset(this, 0, sizeof(*this));
@@ -2386,39 +2419,157 @@
 }
 
 //-----------------------------------------------------------------------------
-// [SECTION] ImFontAtlas
+// [SECTION] ImTextureData
+//-----------------------------------------------------------------------------
+// - ImTextureData::Create()
+// - ImTextureData::DestroyPixels()
+//-----------------------------------------------------------------------------
+
+int ImTextureDataGetFormatBytesPerPixel(ImTextureFormat format)
+{
+    switch (format)
+    {
+    case ImTextureFormat_Alpha8: return 1;
+    case ImTextureFormat_RGBA32: return 4;
+    }
+    IM_ASSERT(0);
+    return 0;
+}
+
+const char* ImTextureDataGetStatusName(ImTextureStatus status)
+{
+    switch (status)
+    {
+    case ImTextureStatus_OK: return "OK";
+    case ImTextureStatus_Destroyed: return "Destroyed";
+    case ImTextureStatus_WantCreate: return "WantCreate";
+    case ImTextureStatus_WantUpdates: return "WantUpdates";
+    case ImTextureStatus_WantDestroy: return "WantDestroy";
+    }
+    return "N/A";
+}
+
+const char* ImTextureDataGetFormatName(ImTextureFormat format)
+{
+    switch (format)
+    {
+    case ImTextureFormat_Alpha8: return "Alpha8";
+    case ImTextureFormat_RGBA32: return "RGBA32";
+    }
+    return "N/A";
+}
+
+void ImTextureData::Create(ImTextureFormat format, int w, int h)
+{
+    IM_ASSERT(Status == ImTextureStatus_Destroyed);
+    DestroyPixels();
+    Format = format;
+    Status = ImTextureStatus_WantCreate;
+    Width = w;
+    Height = h;
+    BytesPerPixel = ImTextureDataGetFormatBytesPerPixel(format);
+    UseColors = false;
+    Pixels = (unsigned char*)IM_ALLOC(Width * Height * BytesPerPixel);
+    IM_ASSERT(Pixels != NULL);
+    memset(Pixels, 0, Width * Height * BytesPerPixel);
+    UsedRect.x = UsedRect.y = UsedRect.w = UsedRect.h = 0;
+    UpdateRect.x = UpdateRect.y = (unsigned short)~0;
+    UpdateRect.w = UpdateRect.h = 0;
+}
+
+void ImTextureData::DestroyPixels()
+{
+    if (Pixels)
+        IM_FREE(Pixels);
+    Pixels = NULL;
+    UseColors = false;
+}
+
+//-----------------------------------------------------------------------------
+// [SECTION] ImFontAtlas, ImFontAtlasBuilder
 //-----------------------------------------------------------------------------
 // - Default texture data encoded in ASCII
+// - ImFontAtlas()
+// - ImFontAtlas::Clear()
+// - ImFontAtlas::CompactCache()
 // - ImFontAtlas::ClearInputData()
 // - ImFontAtlas::ClearTexData()
 // - ImFontAtlas::ClearFonts()
-// - ImFontAtlas::Clear()
-// - ImFontAtlas::GetTexDataAsAlpha8()
-// - ImFontAtlas::GetTexDataAsRGBA32()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasUpdateNewFrame()
+// - ImFontAtlasTextureBlockConvert()
+// - ImFontAtlasTextureBlockPostProcess()
+// - ImFontAtlasTextureBlockPostProcessMultiply()
+// - ImFontAtlasTextureBlockFill()
+// - ImFontAtlasTextureBlockCopy()
+// - ImFontAtlasTextureBlockQueueUpload()
+//-----------------------------------------------------------------------------
+// - ImFontAtlas::GetTexDataAsAlpha8() [legacy]
+// - ImFontAtlas::GetTexDataAsRGBA32() [legacy]
+// - ImFontAtlas::Build() [legacy]
+//-----------------------------------------------------------------------------
 // - ImFontAtlas::AddFont()
 // - ImFontAtlas::AddFontDefault()
 // - ImFontAtlas::AddFontFromFileTTF()
 // - ImFontAtlas::AddFontFromMemoryTTF()
 // - ImFontAtlas::AddFontFromMemoryCompressedTTF()
 // - ImFontAtlas::AddFontFromMemoryCompressedBase85TTF()
-// - ImFontAtlas::AddCustomRectRegular()
-// - ImFontAtlas::AddCustomRectFontGlyph()
-// - ImFontAtlas::CalcCustomRectUV()
+// - ImFontAtlas::RemoveFont()
+// - ImFontAtlasBuildNotifySetFont()
+//-----------------------------------------------------------------------------
+// - ImFontAtlas::AddCustomRect()
+// - ImFontAtlas::RemoveCustomRect()
+// - ImFontAtlas::GetCustomRect()
+// - ImFontAtlas::AddCustomRectFontGlyph() [legacy]
+// - ImFontAtlas::AddCustomRectFontGlyphForSize() [legacy]
 // - ImFontAtlasGetMouseCursorTexData()
-// - ImFontAtlas::Build()
-// - ImFontAtlasBuildMultiplyCalcLookupTable()
-// - ImFontAtlasBuildMultiplyRectAlpha8()
-// - ImFontAtlasBuildWithStbTruetype()
-// - ImFontAtlasGetBuilderForStbTruetype()
-// - ImFontAtlasUpdateSourcesPointers()
-// - ImFontAtlasBuildSetupFont()
-// - ImFontAtlasBuildPackCustomRects()
-// - ImFontAtlasBuildRender8bppRectFromString()
-// - ImFontAtlasBuildRender32bppRectFromString()
-// - ImFontAtlasBuildRenderDefaultTexData()
-// - ImFontAtlasBuildRenderLinesTexData()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasBuildMain()
+// - ImFontAtlasBuildSetupFontLoader()
+// - ImFontAtlasBuildPreloadAllGlyphRanges()
+// - ImFontAtlasBuildUpdatePointers()
+// - ImFontAtlasBuildRenderBitmapFromString()
+// - ImFontAtlasBuildUpdateBasicTexData()
+// - ImFontAtlasBuildUpdateLinesTexData()
+// - ImFontAtlasBuildAddFont()
+// - ImFontAtlasBuildSetupFontBakedEllipsis()
+// - ImFontAtlasBuildSetupFontBakedBlanks()
+// - ImFontAtlasBuildSetupFontBakedFallback()
+// - ImFontAtlasBuildSetupFontSpecialGlyphs()
+// - ImFontAtlasBuildDiscardBakes()
+// - ImFontAtlasBuildDiscardFontBakedGlyph()
+// - ImFontAtlasBuildDiscardFontBaked()
+// - ImFontAtlasBuildDiscardFontBakes()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasAddDrawListSharedData()
+// - ImFontAtlasRemoveDrawListSharedData()
+// - ImFontAtlasUpdateDrawListsTextures()
+// - ImFontAtlasUpdateDrawListsSharedData()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasBuildSetTexture()
+// - ImFontAtlasBuildAddTexture()
+// - ImFontAtlasBuildMakeSpace()
+// - ImFontAtlasBuildRepackTexture()
+// - ImFontAtlasBuildGrowTexture()
+// - ImFontAtlasBuildRepackOrGrowTexture()
+// - ImFontAtlasBuildGetTextureSizeEstimate()
+// - ImFontAtlasBuildCompactTexture()
 // - ImFontAtlasBuildInit()
-// - ImFontAtlasBuildFinish()
+// - ImFontAtlasBuildDestroy()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasPackInit()
+// - ImFontAtlasPackAllocRectEntry()
+// - ImFontAtlasPackReuseRectEntry()
+// - ImFontAtlasPackDiscardRect()
+// - ImFontAtlasPackAddRect()
+// - ImFontAtlasPackGetRect()
+//-----------------------------------------------------------------------------
+// - ImFontBaked_BuildGrowIndex()
+// - ImFontBaked_BuildLoadGlyph()
+// - ImFontBaked_BuildLoadGlyphAdvanceX()
+// - ImFontAtlasDebugLogTextureRequests()
+//-----------------------------------------------------------------------------
+// - ImFontAtlasGetFontLoaderForStbTruetype()
 //-----------------------------------------------------------------------------
 
 // A work of art lies ahead! (. = white layer, X = black layer, others are blank)
@@ -2473,143 +2624,467 @@
     { ImVec2(109,0),ImVec2(13,15), ImVec2( 6, 7) }, // ImGuiMouseCursor_NotAllowed
 };
 
+#define IM_FONTGLYPH_INDEX_UNUSED           ((ImU16)-1) // 0xFFFF
+#define IM_FONTGLYPH_INDEX_NOT_FOUND        ((ImU16)-2) // 0xFFFE
+
 ImFontAtlas::ImFontAtlas()
 {
     memset(this, 0, sizeof(*this));
+    TexDesiredFormat = ImTextureFormat_RGBA32;
     TexGlyphPadding = 1;
-    PackIdMouseCursors = PackIdLines = -1;
+    TexMinWidth = 512;
+    TexMinHeight = 128;
+    TexMaxWidth = 8192;
+    TexMaxHeight = 8192;
+    TexRef._TexID = ImTextureID_Invalid;
+    RendererHasTextures = false; // Assumed false by default, as apps can call e.g Atlas::Build() after backend init and before ImGui can update.
+    TexNextUniqueID = 1;
+    FontNextUniqueID = 1;
+    Builder = NULL;
 }
 
 ImFontAtlas::~ImFontAtlas()
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
-    Clear();
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
+    RendererHasTextures = false; // Full Clear() is supported, but ClearTexData() only isn't.
+    ClearFonts();
+    ClearTexData();
+    TexList.clear_delete();
+    TexData = NULL;
 }
 
-void    ImFontAtlas::ClearInputData()
+void ImFontAtlas::Clear()
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
-    for (ImFontConfig& font_cfg : Sources)
-        if (font_cfg.FontData && font_cfg.FontDataOwnedByAtlas)
-        {
-            IM_FREE(font_cfg.FontData);
-            font_cfg.FontData = NULL;
-        }
+    bool backup_renderer_has_textures = RendererHasTextures;
+    RendererHasTextures = false; // Full Clear() is supported, but ClearTexData() only isn't.
+    ClearFonts();
+    ClearTexData();
+    RendererHasTextures = backup_renderer_has_textures;
+}
+
+void ImFontAtlas::CompactCache()
+{
+    ImFontAtlasTextureCompact(this);
+}
+
+void ImFontAtlas::SetFontLoader(const ImFontLoader* font_loader)
+{
+    ImFontAtlasBuildSetupFontLoader(this, font_loader);
+}
+
+void ImFontAtlas::ClearInputData()
+{
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
 
-    // When clearing this we lose access to the font name and other information used to build the font.
     for (ImFont* font : Fonts)
-        if (font->Sources >= Sources.Data && font->Sources < Sources.Data + Sources.Size)
-        {
-            font->Sources = NULL;
-            font->SourcesCount = 0;
-        }
+        ImFontAtlasFontDestroyOutput(this, font);
+    for (ImFontConfig& font_cfg : Sources)
+        ImFontAtlasFontDestroySourceData(this, &font_cfg);
+    for (ImFont* font : Fonts)
+    {
+        // When clearing this we lose access to the font name and other information used to build the font.
+        font->Sources.clear();
+        font->Flags |= ImFontFlags_NoLoadGlyphs;
+    }
     Sources.clear();
-    CustomRects.clear();
-    PackIdMouseCursors = PackIdLines = -1;
-    // Important: we leave TexReady untouched
 }
 
-void    ImFontAtlas::ClearTexData()
+// Clear CPU-side copy of the texture data.
+void ImFontAtlas::ClearTexData()
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
-    if (TexPixelsAlpha8)
-        IM_FREE(TexPixelsAlpha8);
-    if (TexPixelsRGBA32)
-        IM_FREE(TexPixelsRGBA32);
-    TexPixelsAlpha8 = NULL;
-    TexPixelsRGBA32 = NULL;
-    TexPixelsUseColors = false;
-    // Important: we leave TexReady untouched
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
+    IM_ASSERT(RendererHasTextures == false && "Not supported for dynamic atlases, but you may call Clear().");
+    for (ImTextureData* tex : TexList)
+        tex->DestroyPixels();
+    //Locked = true; // Hoped to be able to lock this down but some reload patterns may not be happy with it.
 }
 
-void    ImFontAtlas::ClearFonts()
+void ImFontAtlas::ClearFonts()
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
+    // FIXME-NEWATLAS: Illegal to remove currently bound font.
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
+    ImFontAtlasBuildDestroy(this);
     ClearInputData();
     Fonts.clear_delete();
-    TexReady = false;
+    TexIsBuilt = false;
+    for (ImDrawListSharedData* shared_data : DrawListSharedDatas)
+        if (shared_data->FontAtlas == this)
+        {
+            shared_data->Font = NULL;
+            shared_data->FontScale = shared_data->FontSize = 0.0f;
+        }
 }
 
-void    ImFontAtlas::Clear()
+static void ImFontAtlasBuildUpdateRendererHasTexturesFromContext(ImFontAtlas* atlas)
 {
-    ClearInputData();
-    ClearTexData();
-    ClearFonts();
+    // [LEGACY] Copy back the ImGuiBackendFlags_RendererHasTextures flag from ImGui context.
+    // - This is the 1% exceptional case where that dependency if useful, to bypass an issue where otherwise at the
+    //   time of an early call to Build(), it would be impossible for us to tell if the backend supports texture update.
+    // - Without this hack, we would have quite a pitfall as many legacy codebases have an early call to Build().
+    //   Whereas conversely, the portion of people using ImDrawList without ImGui is expected to be pathologically rare.
+    for (ImDrawListSharedData* shared_data : atlas->DrawListSharedDatas)
+        if (ImGuiContext* imgui_ctx = shared_data->Context)
+        {
+            atlas->RendererHasTextures = (imgui_ctx->IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures) != 0;
+            break;
+        }
 }
 
-void    ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel)
+// Called by NewFrame() for atlases owned by a context.
+// If you manually manage font atlases, you'll need to call this yourself.
+// - 'frame_count' needs to be provided because we can gc/prioritize baked fonts based on their age.
+// - 'frame_count' may not match those of all imgui contexts using this atlas, as contexts may be updated as different frequencies. But generally you can use ImGui::GetFrameCount() on one of your context.
+void ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, int frame_count, bool renderer_has_textures)
+{
+    IM_ASSERT(atlas->Builder == NULL || atlas->Builder->FrameCount < frame_count); // Protection against being called twice.
+    atlas->RendererHasTextures = renderer_has_textures;
+
+    // Check that font atlas was built or backend support texture reload in which case we can build now
+    if (atlas->RendererHasTextures)
+    {
+        atlas->TexIsBuilt = true;
+        if (atlas->Builder == NULL) // This will only happen if fonts were not already loaded.
+            ImFontAtlasBuildMain(atlas);
+    }
+    // Legacy backend
+    if (!atlas->RendererHasTextures)
+        IM_ASSERT_USER_ERROR(atlas->TexIsBuilt, "Backend does not support ImGuiBackendFlags_RendererHasTextures, and font atlas is not built! Update backend OR make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8().");
+    if (atlas->TexIsBuilt && atlas->Builder->PreloadedAllGlyphsRanges)
+        IM_ASSERT_USER_ERROR(atlas->RendererHasTextures == false, "Called ImFontAtlas::Build() before ImGuiBackendFlags_RendererHasTextures got set! With new backends: you don't need to call Build().");
+
+    // Clear BakedCurrent cache, this is important because it ensure the uncached path gets taken once.
+    // We also rely on ImFontBaked* pointers never crossing frames.
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    builder->FrameCount = frame_count;
+    for (ImFont* font : atlas->Fonts)
+        font->LastBaked = NULL;
+
+    // Garbage collect BakedPool
+    if (builder->BakedDiscardedCount > 0)
+    {
+        int dst_n = 0, src_n = 0;
+        for (; src_n < builder->BakedPool.Size; src_n++)
+        {
+            ImFontBaked* p_src = &builder->BakedPool[src_n];
+            if (p_src->WantDestroy)
+                continue;
+            ImFontBaked* p_dst = &builder->BakedPool[dst_n++];
+            if (p_dst == p_src)
+                continue;
+            memcpy(p_dst, p_src, sizeof(ImFontBaked));
+            builder->BakedMap.SetVoidPtr(p_dst->BakedId, p_dst);
+        }
+        IM_ASSERT(dst_n + builder->BakedDiscardedCount == src_n);
+        builder->BakedPool.Size -= builder->BakedDiscardedCount;
+        builder->BakedDiscardedCount = 0;
+    }
+
+    // Update texture status
+    for (int tex_n = 0; tex_n < atlas->TexList.Size; tex_n++)
+    {
+        ImTextureData* tex = atlas->TexList[tex_n];
+        bool remove_from_list = false;
+        if (tex->Status == ImTextureStatus_OK)
+        {
+            tex->Updates.resize(0);
+            tex->UpdateRect.x = tex->UpdateRect.y = (unsigned short)~0;
+            tex->UpdateRect.w = tex->UpdateRect.h = 0;
+        }
+        if (tex->Status == ImTextureStatus_WantCreate && atlas->RendererHasTextures)
+            IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture's TexID/BackendUserData but did not update Status to OK.");
+
+        // Request destroy
+        // - Keep bool to true in order to differentiate a planned destroy vs a destroy decided by the backend.
+        // - We don't destroy pixels right away, as backend may have an in-flight copy from RAM.
+        if (tex->WantDestroyNextFrame && tex->Status != ImTextureStatus_Destroyed && tex->Status != ImTextureStatus_WantDestroy)
+        {
+            IM_ASSERT(tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantCreate || tex->Status == ImTextureStatus_WantUpdates);
+            tex->Status = ImTextureStatus_WantDestroy;
+        }
+
+        // If a texture has never reached the backend, they don't need to know about it.
+        // (note: backends between 1.92.0 and 1.92.4 could set an already destroyed texture to ImTextureStatus_WantDestroy
+        //  when invalidating graphics objects twice, which would previously remove it from the list and crash.)
+        if (tex->Status == ImTextureStatus_WantDestroy && tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL)
+            tex->Status = ImTextureStatus_Destroyed;
+
+        // Process texture being destroyed
+        if (tex->Status == ImTextureStatus_Destroyed)
+        {
+            IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == NULL && "Backend set texture Status to Destroyed but did not clear TexID/BackendUserData!");
+            if (tex->WantDestroyNextFrame)
+                remove_from_list = true; // Destroy was scheduled by us
+            else
+                tex->Status = ImTextureStatus_WantCreate; // Destroy was done was backend: recreate it (e.g. freed resources mid-run)
+        }
+
+        // The backend may need defer destroying by a few frames, to handle texture used by previous in-flight rendering.
+        // We allow the texture staying in _WantDestroy state and increment a counter which the backend can use to take its decision.
+        if (tex->Status == ImTextureStatus_WantDestroy)
+            tex->UnusedFrames++;
+
+        // Destroy and remove
+        if (remove_from_list)
+        {
+            IM_ASSERT(atlas->TexData != tex);
+            tex->DestroyPixels();
+            IM_DELETE(tex);
+            atlas->TexList.erase(atlas->TexList.begin() + tex_n);
+            tex_n--;
+        }
+    }
+}
+
+void ImFontAtlasTextureBlockConvert(const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch, unsigned char* dst_pixels, ImTextureFormat dst_fmt, int dst_pitch, int w, int h)
+{
+    IM_ASSERT(src_pixels != NULL && dst_pixels != NULL);
+    if (src_fmt == dst_fmt)
+    {
+        int line_sz = w * ImTextureDataGetFormatBytesPerPixel(src_fmt);
+        for (int ny = h; ny > 0; ny--, src_pixels += src_pitch, dst_pixels += dst_pitch)
+            memcpy(dst_pixels, src_pixels, line_sz);
+    }
+    else if (src_fmt == ImTextureFormat_Alpha8 && dst_fmt == ImTextureFormat_RGBA32)
+    {
+        for (int ny = h; ny > 0; ny--, src_pixels += src_pitch, dst_pixels += dst_pitch)
+        {
+            const ImU8* src_p = (const ImU8*)src_pixels;
+            ImU32* dst_p = (ImU32*)(void*)dst_pixels;
+            for (int nx = w; nx > 0; nx--)
+                *dst_p++ = IM_COL32(255, 255, 255, (unsigned int)(*src_p++));
+        }
+    }
+    else if (src_fmt == ImTextureFormat_RGBA32 && dst_fmt == ImTextureFormat_Alpha8)
+    {
+        for (int ny = h; ny > 0; ny--, src_pixels += src_pitch, dst_pixels += dst_pitch)
+        {
+            const ImU32* src_p = (const ImU32*)(void*)src_pixels;
+            ImU8* dst_p = (ImU8*)dst_pixels;
+            for (int nx = w; nx > 0; nx--)
+                *dst_p++ = ((*src_p++) >> IM_COL32_A_SHIFT) & 0xFF;
+        }
+    }
+    else
+    {
+        IM_ASSERT(0);
+    }
+}
+
+// Source buffer may be written to (used for in-place mods).
+// Post-process hooks may eventually be added here.
+void ImFontAtlasTextureBlockPostProcess(ImFontAtlasPostProcessData* data)
 {
-    // Build atlas on demand
-    if (TexPixelsAlpha8 == NULL)
-        Build();
+    // Multiply operator (legacy)
+    if (data->FontSrc->RasterizerMultiply != 1.0f)
+        ImFontAtlasTextureBlockPostProcessMultiply(data, data->FontSrc->RasterizerMultiply);
+}
 
-    *out_pixels = TexPixelsAlpha8;
-    if (out_width) *out_width = TexWidth;
-    if (out_height) *out_height = TexHeight;
-    if (out_bytes_per_pixel) *out_bytes_per_pixel = 1;
+void ImFontAtlasTextureBlockPostProcessMultiply(ImFontAtlasPostProcessData* data, float multiply_factor)
+{
+    unsigned char* pixels = (unsigned char*)data->Pixels;
+    int pitch = data->Pitch;
+    if (data->Format == ImTextureFormat_Alpha8)
+    {
+        for (int ny = data->Height; ny > 0; ny--, pixels += pitch)
+        {
+            ImU8* p = (ImU8*)pixels;
+            for (int nx = data->Width; nx > 0; nx--, p++)
+            {
+                unsigned int v = ImMin((unsigned int)(*p * multiply_factor), (unsigned int)255);
+                *p = (unsigned char)v;
+            }
+        }
+    }
+    else if (data->Format == ImTextureFormat_RGBA32) //-V547
+    {
+        for (int ny = data->Height; ny > 0; ny--, pixels += pitch)
+        {
+            ImU32* p = (ImU32*)(void*)pixels;
+            for (int nx = data->Width; nx > 0; nx--, p++)
+            {
+                unsigned int a = ImMin((unsigned int)(((*p >> IM_COL32_A_SHIFT) & 0xFF) * multiply_factor), (unsigned int)255);
+                *p = IM_COL32((*p >> IM_COL32_R_SHIFT) & 0xFF, (*p >> IM_COL32_G_SHIFT) & 0xFF, (*p >> IM_COL32_B_SHIFT) & 0xFF, a);
+            }
+        }
+    }
+    else
+    {
+        IM_ASSERT(0);
+    }
 }
 
-void    ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel)
+// Fill with single color. We don't use this directly but it is convenient for anyone working on uploading custom rects.
+void ImFontAtlasTextureBlockFill(ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h, ImU32 col)
 {
-    // Convert to RGBA32 format on demand
-    // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp
-    if (!TexPixelsRGBA32)
+    if (dst_tex->Format == ImTextureFormat_Alpha8)
+    {
+        ImU8 col_a = (col >> IM_COL32_A_SHIFT) & 0xFF;
+        for (int y = 0; y < h; y++)
+            memset((ImU8*)dst_tex->GetPixelsAt(dst_x, dst_y + y), col_a, w);
+    }
+    else
     {
-        unsigned char* pixels = NULL;
-        GetTexDataAsAlpha8(&pixels, NULL, NULL);
-        if (pixels)
+        for (int y = 0; y < h; y++)
         {
-            TexPixelsRGBA32 = (unsigned int*)IM_ALLOC((size_t)TexWidth * (size_t)TexHeight * 4);
-            const unsigned char* src = pixels;
-            unsigned int* dst = TexPixelsRGBA32;
-            for (int n = TexWidth * TexHeight; n > 0; n--)
-                *dst++ = IM_COL32(255, 255, 255, (unsigned int)(*src++));
+            ImU32* p = (ImU32*)(void*)dst_tex->GetPixelsAt(dst_x, dst_y + y);
+            for (int x = w; x > 0; x--, p++)
+                *p = col;
         }
     }
+}
+
+// Copy block from one texture to another
+void ImFontAtlasTextureBlockCopy(ImTextureData* src_tex, int src_x, int src_y, ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h)
+{
+    IM_ASSERT(src_tex->Pixels != NULL && dst_tex->Pixels != NULL);
+    IM_ASSERT(src_tex->Format == dst_tex->Format);
+    IM_ASSERT(src_x >= 0 && src_x + w <= src_tex->Width);
+    IM_ASSERT(src_y >= 0 && src_y + h <= src_tex->Height);
+    IM_ASSERT(dst_x >= 0 && dst_x + w <= dst_tex->Width);
+    IM_ASSERT(dst_y >= 0 && dst_y + h <= dst_tex->Height);
+    for (int y = 0; y < h; y++)
+        memcpy(dst_tex->GetPixelsAt(dst_x, dst_y + y), src_tex->GetPixelsAt(src_x, src_y + y), w * dst_tex->BytesPerPixel);
+}
+
+// Queue texture block update for renderer backend
+void ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h)
+{
+    IM_ASSERT(tex->Status != ImTextureStatus_WantDestroy && tex->Status != ImTextureStatus_Destroyed);
+    IM_ASSERT(x >= 0 && x <= 0xFFFF && y >= 0 && y <= 0xFFFF && w >= 0 && x + w <= 0x10000 && h >= 0 && y + h <= 0x10000);
+    IM_UNUSED(atlas);
+
+    ImTextureRect req = { (unsigned short)x, (unsigned short)y, (unsigned short)w, (unsigned short)h };
+    int new_x1 = ImMax(tex->UpdateRect.w == 0 ? 0 : tex->UpdateRect.x + tex->UpdateRect.w, req.x + req.w);
+    int new_y1 = ImMax(tex->UpdateRect.h == 0 ? 0 : tex->UpdateRect.y + tex->UpdateRect.h, req.y + req.h);
+    tex->UpdateRect.x = ImMin(tex->UpdateRect.x, req.x);
+    tex->UpdateRect.y = ImMin(tex->UpdateRect.y, req.y);
+    tex->UpdateRect.w = (unsigned short)(new_x1 - tex->UpdateRect.x);
+    tex->UpdateRect.h = (unsigned short)(new_y1 - tex->UpdateRect.y);
+    tex->UsedRect.x = ImMin(tex->UsedRect.x, req.x);
+    tex->UsedRect.y = ImMin(tex->UsedRect.y, req.y);
+    tex->UsedRect.w = (unsigned short)(ImMax(tex->UsedRect.x + tex->UsedRect.w, req.x + req.w) - tex->UsedRect.x);
+    tex->UsedRect.h = (unsigned short)(ImMax(tex->UsedRect.y + tex->UsedRect.h, req.y + req.h) - tex->UsedRect.y);
+    atlas->TexIsBuilt = false;
+
+    // No need to queue if status is == ImTextureStatus_WantCreate
+    if (tex->Status == ImTextureStatus_OK || tex->Status == ImTextureStatus_WantUpdates)
+    {
+        tex->Status = ImTextureStatus_WantUpdates;
+        tex->Updates.push_back(req);
+    }
+}
+
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+static void GetTexDataAsFormat(ImFontAtlas* atlas, ImTextureFormat format, unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel)
+{
+    ImTextureData* tex = atlas->TexData;
+    if (!atlas->TexIsBuilt || tex == NULL || tex->Pixels == NULL || atlas->TexDesiredFormat != format)
+    {
+        atlas->TexDesiredFormat = format;
+        atlas->Build();
+        tex = atlas->TexData;
+    }
+    if (out_pixels) { *out_pixels = (unsigned char*)tex->Pixels; };
+    if (out_width) { *out_width = tex->Width; };
+    if (out_height) { *out_height = tex->Height; };
+    if (out_bytes_per_pixel) { *out_bytes_per_pixel = tex->BytesPerPixel; }
+}
+
+void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel)
+{
+    GetTexDataAsFormat(this, ImTextureFormat_Alpha8, out_pixels, out_width, out_height, out_bytes_per_pixel);
+}
+
+void ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel)
+{
+    GetTexDataAsFormat(this, ImTextureFormat_RGBA32, out_pixels, out_width, out_height, out_bytes_per_pixel);
+}
 
-    *out_pixels = (unsigned char*)TexPixelsRGBA32;
-    if (out_width) *out_width = TexWidth;
-    if (out_height) *out_height = TexHeight;
-    if (out_bytes_per_pixel) *out_bytes_per_pixel = 4;
+bool ImFontAtlas::Build()
+{
+    ImFontAtlasBuildMain(this);
+    return true;
 }
+#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
 
-ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg)
+ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg_in)
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
-    IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0);
-    IM_ASSERT(font_cfg->SizePixels > 0.0f && "Is ImFontConfig struct correctly initialized?");
-    IM_ASSERT(font_cfg->RasterizerDensity > 0.0f && "Is ImFontConfig struct correctly initialized?");
+    // Sanity Checks
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
+    IM_ASSERT((font_cfg_in->FontData != NULL && font_cfg_in->FontDataSize > 0) || (font_cfg_in->FontLoader != NULL));
+    //IM_ASSERT(font_cfg_in->SizePixels > 0.0f && "Is ImFontConfig struct correctly initialized?");
+    IM_ASSERT(font_cfg_in->RasterizerDensity > 0.0f && "Is ImFontConfig struct correctly initialized?");
+    if (font_cfg_in->GlyphOffset.x != 0.0f || font_cfg_in->GlyphOffset.y != 0.0f || font_cfg_in->GlyphMinAdvanceX != 0.0f || font_cfg_in->GlyphMaxAdvanceX != FLT_MAX)
+        IM_ASSERT(font_cfg_in->SizePixels != 0.0f && "Specifying glyph offset/advances requires a reference size to base it on.");
+
+    // Lazily create builder on the first call to AddFont
+    if (Builder == NULL)
+        ImFontAtlasBuildInit(this);
 
     // Create new font
-    if (!font_cfg->MergeMode)
-        Fonts.push_back(IM_NEW(ImFont));
+    ImFont* font;
+    if (!font_cfg_in->MergeMode)
+    {
+        font = IM_NEW(ImFont)();
+        font->FontId = FontNextUniqueID++;
+        font->Flags = font_cfg_in->Flags;
+        font->LegacySize = font_cfg_in->SizePixels;
+        font->CurrentRasterizerDensity = font_cfg_in->RasterizerDensity;
+        Fonts.push_back(font);
+    }
     else
+    {
         IM_ASSERT(Fonts.Size > 0 && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font.
+        font = font_cfg_in->DstFont ? font_cfg_in->DstFont : Fonts.back();
+    }
+
+    // Add to list
+    Sources.push_back(*font_cfg_in);
+    ImFontConfig* font_cfg = &Sources.back();
+    if (font_cfg->DstFont == NULL)
+        font_cfg->DstFont = font;
+    font->Sources.push_back(font_cfg);
+    ImFontAtlasBuildUpdatePointers(this); // Pointers to Sources are otherwise dangling after we called Sources.push_back().
 
-    Sources.push_back(*font_cfg);
-    ImFontConfig& new_font_cfg = Sources.back();
-    if (new_font_cfg.DstFont == NULL)
-        new_font_cfg.DstFont = Fonts.back();
-    if (!new_font_cfg.FontDataOwnedByAtlas)
-    {
-        new_font_cfg.FontData = IM_ALLOC(new_font_cfg.FontDataSize);
-        new_font_cfg.FontDataOwnedByAtlas = true;
-        memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize);
+    if (font_cfg->FontDataOwnedByAtlas == false)
+    {
+        font_cfg->FontDataOwnedByAtlas = true;
+        font_cfg->FontData = ImMemdup(font_cfg->FontData, (size_t)font_cfg->FontDataSize);
     }
 
-    // Round font size
-    // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet.
-    // - Note that using io.FontGlobalScale or SetWindowFontScale(), with are legacy-ish, partially supported features, can still lead to unrounded sizes.
-    // - We may support it better later and remove this rounding.
-    new_font_cfg.SizePixels = ImTrunc(new_font_cfg.SizePixels);
+    // Sanity check
+    // We don't round cfg.SizePixels yet as relative size of merged fonts are used afterwards.
+    if (font_cfg->GlyphExcludeRanges != NULL)
+    {
+        int size = 0;
+        for (const ImWchar* p = font_cfg->GlyphExcludeRanges; p[0] != 0; p++, size++) {}
+        IM_ASSERT((size & 1) == 0 && "GlyphExcludeRanges[] size must be multiple of two!");
+        IM_ASSERT((size <= 64) && "GlyphExcludeRanges[] size must be small!");
+        font_cfg->GlyphExcludeRanges = (ImWchar*)ImMemdup(font_cfg->GlyphExcludeRanges, sizeof(font_cfg->GlyphExcludeRanges[0]) * (size + 1));
+    }
+    if (font_cfg->FontLoader != NULL)
+    {
+        IM_ASSERT(font_cfg->FontLoader->FontBakedLoadGlyph != NULL);
+        IM_ASSERT(font_cfg->FontLoader->LoaderInit == NULL && font_cfg->FontLoader->LoaderShutdown == NULL); // FIXME-NEWATLAS: Unsupported yet.
+    }
+    IM_ASSERT(font_cfg->FontLoaderData == NULL);
 
-    // Pointers to Sources data are otherwise dangling
-    ImFontAtlasUpdateSourcesPointers(this);
+    if (!ImFontAtlasFontSourceInit(this, font_cfg))
+    {
+        // Rollback (this is a fragile/rarely exercised code-path. TestSuite's "misc_atlas_add_invalid_font" aim to test this)
+        ImFontAtlasFontDestroySourceData(this, font_cfg);
+        Sources.pop_back();
+        font->Sources.pop_back();
+        if (!font_cfg->MergeMode)
+        {
+            IM_DELETE(font);
+            Fonts.pop_back();
+        }
+        return NULL;
+    }
+    ImFontAtlasFontSourceAddToFont(this, font, font_cfg);
 
-    // Invalidate texture
-    TexReady = false;
-    ClearTexData();
-    return new_font_cfg.DstFont;
+    return font;
 }
 
 // Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder)
@@ -2631,6 +3106,7 @@
 #endif
 
 // Load embedded ProggyClean.ttf at size 13, disable oversampling
+// If you want a similar font which may be better scaled, consider using ProggyVector from the same author!
 ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template)
 {
 #ifndef IMGUI_DISABLE_DEFAULT_FONT
@@ -2643,15 +3119,13 @@
     if (font_cfg.SizePixels <= 0.0f)
         font_cfg.SizePixels = 13.0f * 1.0f;
     if (font_cfg.Name[0] == '\0')
-        ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels);
+        ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf");
     font_cfg.EllipsisChar = (ImWchar)0x0085;
-    font_cfg.GlyphOffset.y = 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f);  // Add +1 offset per 13 units
+    font_cfg.GlyphOffset.y += 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f);  // Add +1 offset per 13 units
 
     int ttf_compressed_size = 0;
     const char* ttf_compressed = GetDefaultCompressedFontDataTTF(&ttf_compressed_size);
-    const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault();
-    ImFont* font = AddFontFromMemoryCompressedTTF(ttf_compressed, ttf_compressed_size, font_cfg.SizePixels, &font_cfg, glyph_ranges);
-    return font;
+    return AddFontFromMemoryCompressedTTF(ttf_compressed, ttf_compressed_size, font_cfg.SizePixels, &font_cfg);
 #else
     IM_ASSERT(0 && "AddFontDefault() disabled in this build.");
     IM_UNUSED(font_cfg_template);
@@ -2661,12 +3135,16 @@
 
 ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges)
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
     size_t data_size = 0;
     void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0);
     if (!data)
     {
-        IM_ASSERT_USER_ERROR(0, "Could not load font file!");
+        if (font_cfg_template == NULL || (font_cfg_template->Flags & ImFontFlags_NoLoadError) == 0)
+        {
+            IMGUI_DEBUG_LOG("While loading '%s'\n", filename);
+            IM_ASSERT_USER_ERROR(0, "Could not load font file!");
+        }
         return NULL;
     }
     ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig();
@@ -2675,7 +3153,7 @@
         // Store a short copy of filename into into the font name for convenience
         const char* p;
         for (p = filename + ImStrlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {}
-        ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels);
+        ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s", p);
     }
     return AddFontFromMemoryTTF(data, (int)data_size, size_pixels, &font_cfg, glyph_ranges);
 }
@@ -2683,7 +3161,7 @@
 // NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build().
 ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges)
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
     ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig();
     IM_ASSERT(font_cfg.FontData == NULL);
     IM_ASSERT(font_data_size > 100 && "Incorrect value for font_data_size!"); // Heuristic to prevent accidentally passing a wrong value to font_data_size.
@@ -2717,43 +3195,156 @@
     return font;
 }
 
-int ImFontAtlas::AddCustomRectRegular(int width, int height)
+// On font removal we need to remove references (otherwise we could queue removal?)
+// We allow old_font == new_font which forces updating all values (e.g. sizes)
+static void ImFontAtlasBuildNotifySetFont(ImFontAtlas* atlas, ImFont* old_font, ImFont* new_font)
+{
+    for (ImDrawListSharedData* shared_data : atlas->DrawListSharedDatas)
+    {
+        if (shared_data->Font == old_font)
+            shared_data->Font = new_font;
+        if (ImGuiContext* ctx = shared_data->Context)
+        {
+            if (ctx->IO.FontDefault == old_font)
+                ctx->IO.FontDefault = new_font;
+            if (ctx->Font == old_font)
+            {
+                ImGuiContext* curr_ctx = ImGui::GetCurrentContext();
+                bool need_bind_ctx = ctx != curr_ctx;
+                if (need_bind_ctx)
+                    ImGui::SetCurrentContext(ctx);
+                ImGui::SetCurrentFont(new_font, ctx->FontSizeBase, ctx->FontSize);
+                if (need_bind_ctx)
+                    ImGui::SetCurrentContext(curr_ctx);
+            }
+            for (ImFontStackData& font_stack_data : ctx->FontStack)
+                if (font_stack_data.Font == old_font)
+                    font_stack_data.Font = new_font;
+        }
+    }
+}
+
+void ImFontAtlas::RemoveFont(ImFont* font)
+{
+    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas!");
+    font->ClearOutputData();
+
+    ImFontAtlasFontDestroyOutput(this, font);
+    for (ImFontConfig* src : font->Sources)
+        ImFontAtlasFontDestroySourceData(this, src);
+    for (int src_n = 0; src_n < Sources.Size; src_n++)
+        if (Sources[src_n].DstFont == font)
+            Sources.erase(&Sources[src_n--]);
+
+    bool removed = Fonts.find_erase(font);
+    IM_ASSERT(removed);
+    IM_UNUSED(removed);
+
+    ImFontAtlasBuildUpdatePointers(this);
+
+    font->ContainerAtlas = NULL;
+    IM_DELETE(font);
+
+    // Notify external systems
+    ImFont* new_current_font = Fonts.empty() ? NULL : Fonts[0];
+    ImFontAtlasBuildNotifySetFont(this, font, new_current_font);
+}
+
+// At it is common to do an AddCustomRect() followed by a GetCustomRect(), we provide an optional 'ImFontAtlasRect* out_r = NULL' argument to retrieve the info straight away.
+ImFontAtlasRectId ImFontAtlas::AddCustomRect(int width, int height, ImFontAtlasRect* out_r)
 {
     IM_ASSERT(width > 0 && width <= 0xFFFF);
     IM_ASSERT(height > 0 && height <= 0xFFFF);
-    ImFontAtlasCustomRect r;
-    r.Width = (unsigned short)width;
-    r.Height = (unsigned short)height;
-    CustomRects.push_back(r);
-    return CustomRects.Size - 1; // Return index
+
+    if (Builder == NULL)
+        ImFontAtlasBuildInit(this);
+
+    ImFontAtlasRectId r_id = ImFontAtlasPackAddRect(this, width, height);
+    if (r_id == ImFontAtlasRectId_Invalid)
+        return ImFontAtlasRectId_Invalid;
+    if (out_r != NULL)
+        GetCustomRect(r_id, out_r);
+
+    if (RendererHasTextures)
+    {
+        ImTextureRect* r = ImFontAtlasPackGetRect(this, r_id);
+        ImFontAtlasTextureBlockQueueUpload(this, TexData, r->x, r->y, r->w, r->h);
+    }
+    return r_id;
+}
+
+void ImFontAtlas::RemoveCustomRect(ImFontAtlasRectId id)
+{
+    if (ImFontAtlasPackGetRectSafe(this, id) == NULL)
+        return;
+    ImFontAtlasPackDiscardRect(this, id);
 }
 
-int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset)
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+// This API does not make sense anymore with scalable fonts.
+// - Prefer adding a font source (ImFontConfig) using a custom/procedural loader.
+// - You may use ImFontFlags_LockBakedSizes to limit an existing font to known baked sizes:
+//     ImFont* myfont = io.Fonts->AddFontFromFileTTF(....);
+//     myfont->GetFontBaked(16.0f);
+//     myfont->Flags |= ImFontFlags_LockBakedSizes;
+ImFontAtlasRectId ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar codepoint, int width, int height, float advance_x, const ImVec2& offset)
+{
+    float font_size = font->LegacySize;
+    return AddCustomRectFontGlyphForSize(font, font_size, codepoint, width, height, advance_x, offset);
+}
+// FIXME: we automatically set glyph.Colored=true by default.
+// If you need to alter this, you can write 'font->Glyphs.back()->Colored' after calling AddCustomRectFontGlyph().
+ImFontAtlasRectId ImFontAtlas::AddCustomRectFontGlyphForSize(ImFont* font, float font_size, ImWchar codepoint, int width, int height, float advance_x, const ImVec2& offset)
 {
 #ifdef IMGUI_USE_WCHAR32
-    IM_ASSERT(id <= IM_UNICODE_CODEPOINT_MAX);
+    IM_ASSERT(codepoint <= IM_UNICODE_CODEPOINT_MAX);
 #endif
     IM_ASSERT(font != NULL);
     IM_ASSERT(width > 0 && width <= 0xFFFF);
     IM_ASSERT(height > 0 && height <= 0xFFFF);
-    ImFontAtlasCustomRect r;
-    r.Width = (unsigned short)width;
-    r.Height = (unsigned short)height;
-    r.GlyphID = id;
-    r.GlyphColored = 0; // Set to 1 manually to mark glyph as colored // FIXME: No official API for that (#8133)
-    r.GlyphAdvanceX = advance_x;
-    r.GlyphOffset = offset;
-    r.Font = font;
-    CustomRects.push_back(r);
-    return CustomRects.Size - 1; // Return index
-}
-
-void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const
-{
-    IM_ASSERT(TexWidth > 0 && TexHeight > 0);   // Font atlas needs to be built before we can calculate UV coordinates
-    IM_ASSERT(rect->IsPacked());                // Make sure the rectangle has been packed
-    *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y);
-    *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y);
+
+    ImFontBaked* baked = font->GetFontBaked(font_size);
+
+    ImFontAtlasRectId r_id = ImFontAtlasPackAddRect(this, width, height);
+    if (r_id == ImFontAtlasRectId_Invalid)
+        return ImFontAtlasRectId_Invalid;
+    ImTextureRect* r = ImFontAtlasPackGetRect(this, r_id);
+    if (RendererHasTextures)
+        ImFontAtlasTextureBlockQueueUpload(this, TexData, r->x, r->y, r->w, r->h);
+
+    if (baked->IsGlyphLoaded(codepoint))
+        ImFontAtlasBakedDiscardFontGlyph(this, font, baked, baked->FindGlyph(codepoint));
+
+    ImFontGlyph glyph;
+    glyph.Codepoint = codepoint;
+    glyph.AdvanceX = advance_x;
+    glyph.X0 = offset.x;
+    glyph.Y0 = offset.y;
+    glyph.X1 = offset.x + r->w;
+    glyph.Y1 = offset.y + r->h;
+    glyph.Visible = true;
+    glyph.Colored = true; // FIXME: Arbitrary
+    glyph.PackId = r_id;
+    ImFontAtlasBakedAddFontGlyph(this, baked, font->Sources[0], &glyph);
+    return r_id;
+}
+#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+
+bool ImFontAtlas::GetCustomRect(ImFontAtlasRectId id, ImFontAtlasRect* out_r) const
+{
+    ImTextureRect* r = ImFontAtlasPackGetRectSafe((ImFontAtlas*)this, id);
+    if (r == NULL)
+        return false;
+    IM_ASSERT(TexData->Width > 0 && TexData->Height > 0);   // Font atlas needs to be built before we can calculate UV coordinates
+    if (out_r == NULL)
+        return true;
+    out_r->x = r->x;
+    out_r->y = r->y;
+    out_r->w = r->w;
+    out_r->h = r->h;
+    out_r->uv0 = ImVec2((float)(r->x), (float)(r->y)) * TexUvScale;
+    out_r->uv1 = ImVec2((float)(r->x + r->w), (float)(r->y + r->h)) * TexUvScale;
+    return true;
 }
 
 bool ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2])
@@ -2763,9 +3354,8 @@
     if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors)
         return false;
 
-    IM_ASSERT(atlas->PackIdMouseCursors != -1);
-    ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors);
-    ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->X, (float)r->Y);
+    ImTextureRect* r = ImFontAtlasPackGetRect(atlas, atlas->Builder->PackIdMouseCursors);
+    ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->x, (float)r->y);
     ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1];
     *out_size = size;
     *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2];
@@ -2777,604 +3367,1383 @@
     return true;
 }
 
-bool    ImFontAtlas::Build()
+// When atlas->RendererHasTextures = true, this is only called if no font were loaded.
+void ImFontAtlasBuildMain(ImFontAtlas* atlas)
 {
-    IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!");
+    IM_ASSERT(!atlas->Locked && "Cannot modify a locked ImFontAtlas!");
+    if (atlas->TexData && atlas->TexData->Format != atlas->TexDesiredFormat)
+        ImFontAtlasBuildClear(atlas);
+
+    if (atlas->Builder == NULL)
+        ImFontAtlasBuildInit(atlas);
 
     // Default font is none are specified
-    if (Sources.Size == 0)
-        AddFontDefault();
+    if (atlas->Sources.Size == 0)
+        atlas->AddFontDefault();
 
-    // Select builder
-    // - Note that we do not reassign to atlas->FontBuilderIO, since it is likely to point to static data which
-    //   may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are
-    //   using a hot-reloading scheme that messes up static data, store your own instance of ImFontBuilderIO somewhere
-    //   and point to it instead of pointing directly to return value of the GetBuilderXXX functions.
-    const ImFontBuilderIO* builder_io = FontBuilderIO;
-    if (builder_io == NULL)
-    {
-#ifdef IMGUI_ENABLE_FREETYPE
-        builder_io = ImGuiFreeType::GetBuilderForFreeType();
-#elif defined(IMGUI_ENABLE_STB_TRUETYPE)
-        builder_io = ImFontAtlasGetBuilderForStbTruetype();
-#else
-        IM_ASSERT(0); // Invalid Build function
-#endif
-    }
+    // [LEGACY] For backends not supporting RendererHasTextures: preload all glyphs
+    ImFontAtlasBuildUpdateRendererHasTexturesFromContext(atlas);
+    if (atlas->RendererHasTextures == false) // ~ImGuiBackendFlags_RendererHasTextures
+        ImFontAtlasBuildLegacyPreloadAllGlyphRanges(atlas);
+    atlas->TexIsBuilt = true;
+}
 
-    // Build
-    return builder_io->FontBuilder_Build(this);
+void ImFontAtlasBuildGetOversampleFactors(ImFontConfig* src, ImFontBaked* baked, int* out_oversample_h, int* out_oversample_v)
+{
+    // Automatically disable horizontal oversampling over size 36
+    const float raster_size = baked->Size * baked->RasterizerDensity * src->RasterizerDensity;
+    *out_oversample_h = (src->OversampleH != 0) ? src->OversampleH : (raster_size > 36.0f || src->PixelSnapH) ? 1 : 2;
+    *out_oversample_v = (src->OversampleV != 0) ? src->OversampleV : 1;
 }
 
-void    ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor)
+// Setup main font loader for the atlas
+// Every font source (ImFontConfig) will use this unless ImFontConfig::FontLoader specify a custom loader.
+void ImFontAtlasBuildSetupFontLoader(ImFontAtlas* atlas, const ImFontLoader* font_loader)
 {
-    for (unsigned int i = 0; i < 256; i++)
+    if (atlas->FontLoader == font_loader)
+        return;
+    IM_ASSERT(!atlas->Locked && "Cannot modify a locked ImFontAtlas!");
+
+    for (ImFont* font : atlas->Fonts)
+        ImFontAtlasFontDestroyOutput(atlas, font);
+    if (atlas->Builder && atlas->FontLoader && atlas->FontLoader->LoaderShutdown)
+        atlas->FontLoader->LoaderShutdown(atlas);
+
+    atlas->FontLoader = font_loader;
+    atlas->FontLoaderName = font_loader ? font_loader->Name : "NULL";
+    IM_ASSERT(atlas->FontLoaderData == NULL);
+
+    if (atlas->Builder && atlas->FontLoader && atlas->FontLoader->LoaderInit)
+        atlas->FontLoader->LoaderInit(atlas);
+    for (ImFont* font : atlas->Fonts)
+        ImFontAtlasFontInitOutput(atlas, font);
+    for (ImFont* font : atlas->Fonts)
+        for (ImFontConfig* src : font->Sources)
+            ImFontAtlasFontSourceAddToFont(atlas, font, src);
+}
+
+// Preload all glyph ranges for legacy backends.
+// This may lead to multiple texture creation which might be a little slower than before.
+void ImFontAtlasBuildLegacyPreloadAllGlyphRanges(ImFontAtlas* atlas)
+{
+    atlas->Builder->PreloadedAllGlyphsRanges = true;
+    for (ImFont* font : atlas->Fonts)
     {
-        unsigned int value = (unsigned int)(i * in_brighten_factor);
-        out_table[i] = value > 255 ? 255 : (value & 0xFF);
+        ImFontBaked* baked = font->GetFontBaked(font->LegacySize);
+        if (font->FallbackChar != 0)
+            baked->FindGlyph(font->FallbackChar);
+        if (font->EllipsisChar != 0)
+            baked->FindGlyph(font->EllipsisChar);
+        for (ImFontConfig* src : font->Sources)
+        {
+            const ImWchar* ranges = src->GlyphRanges ? src->GlyphRanges : atlas->GetGlyphRangesDefault();
+            for (; ranges[0]; ranges += 2)
+                for (unsigned int c = ranges[0]; c <= ranges[1] && c <= IM_UNICODE_CODEPOINT_MAX; c++) //-V560
+                    baked->FindGlyph((ImWchar)c);
+        }
     }
 }
 
-void    ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride)
+// FIXME: May make ImFont::Sources a ImSpan<> and move ownership to ImFontAtlas
+void ImFontAtlasBuildUpdatePointers(ImFontAtlas* atlas)
 {
-    IM_ASSERT_PARANOID(w <= stride);
-    unsigned char* data = pixels + x + y * stride;
-    for (int j = h; j > 0; j--, data += stride - w)
-        for (int i = w; i > 0; i--, data++)
-            *data = table[*data];
+    for (ImFont* font : atlas->Fonts)
+        font->Sources.resize(0);
+    for (ImFontConfig& src : atlas->Sources)
+        src.DstFont->Sources.push_back(&src);
 }
 
-void ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v)
+// Render a white-colored bitmap encoded in a string
+void ImFontAtlasBuildRenderBitmapFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char)
 {
-    // Automatically disable horizontal oversampling over size 36
-    *out_oversample_h = (src->OversampleH != 0) ? src->OversampleH : (src->SizePixels * src->RasterizerDensity > 36.0f || src->PixelSnapH) ? 1 : 2;
-    *out_oversample_v = (src->OversampleV != 0) ? src->OversampleV : 1;
-}
+    ImTextureData* tex = atlas->TexData;
+    IM_ASSERT(x >= 0 && x + w <= tex->Width);
+    IM_ASSERT(y >= 0 && y + h <= tex->Height);
 
-#ifdef IMGUI_ENABLE_STB_TRUETYPE
-// Temporary data for one source font (multiple source fonts can be merged into one destination ImFont)
-// (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.)
-struct ImFontBuildSrcData
-{
-    stbtt_fontinfo      FontInfo;
-    stbtt_pack_range    PackRange;          // Hold the list of codepoints to pack (essentially points to Codepoints.Data)
-    stbrp_rect*         Rects;              // Rectangle to pack. We first fill in their size and the packer will give us their position.
-    stbtt_packedchar*   PackedChars;        // Output glyphs
-    const ImWchar*      SrcRanges;          // Ranges as requested by user (user is allowed to request too much, e.g. 0x0020..0xFFFF)
-    int                 DstIndex;           // Index into atlas->Fonts[] and dst_tmp_array[]
-    int                 GlyphsHighest;      // Highest requested codepoint
-    int                 GlyphsCount;        // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font)
-    ImBitVector         GlyphsSet;          // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB)
-    ImVector<int>       GlyphsList;         // Glyph codepoints list (flattened version of GlyphsSet)
-};
+    switch (tex->Format)
+    {
+    case ImTextureFormat_Alpha8:
+    {
+        ImU8* out_p = (ImU8*)tex->GetPixelsAt(x, y);
+        for (int off_y = 0; off_y < h; off_y++, out_p += tex->Width, in_str += w)
+            for (int off_x = 0; off_x < w; off_x++)
+                out_p[off_x] = (in_str[off_x] == in_marker_char) ? 0xFF : 0x00;
+        break;
+    }
+    case ImTextureFormat_RGBA32:
+    {
+        ImU32* out_p = (ImU32*)tex->GetPixelsAt(x, y);
+        for (int off_y = 0; off_y < h; off_y++, out_p += tex->Width, in_str += w)
+            for (int off_x = 0; off_x < w; off_x++)
+                out_p[off_x] = (in_str[off_x] == in_marker_char) ? IM_COL32_WHITE : IM_COL32_BLACK_TRANS;
+        break;
+    }
+    }
+}
 
-// Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont)
-struct ImFontBuildDstData
+static void ImFontAtlasBuildUpdateBasicTexData(ImFontAtlas* atlas)
 {
-    int                 SrcCount;           // Number of source fonts targeting this destination font.
-    int                 GlyphsHighest;
-    int                 GlyphsCount;
-    ImBitVector         GlyphsSet;          // This is used to resolve collision when multiple sources are merged into a same destination font.
-};
+    // Pack and store identifier so we can refresh UV coordinates on texture resize.
+    // FIXME-NEWATLAS: User/custom rects where user code wants to store UV coordinates will need to do the same thing.
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    ImVec2i pack_size = (atlas->Flags & ImFontAtlasFlags_NoMouseCursors) ? ImVec2i(2, 2) : ImVec2i(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H);
 
-static void UnpackBitVectorToFlatIndexList(const ImBitVector* in, ImVector<int>* out)
-{
-    IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int));
-    const ImU32* it_begin = in->Storage.begin();
-    const ImU32* it_end = in->Storage.end();
-    for (const ImU32* it = it_begin; it < it_end; it++)
-        if (ImU32 entries_32 = *it)
-            for (ImU32 bit_n = 0; bit_n < 32; bit_n++)
-                if (entries_32 & ((ImU32)1 << bit_n))
-                    out->push_back((int)(((it - it_begin) << 5) + bit_n));
+    ImFontAtlasRect r;
+    bool add_and_draw = (atlas->GetCustomRect(builder->PackIdMouseCursors, &r) == false);
+    if (add_and_draw)
+    {
+        builder->PackIdMouseCursors = atlas->AddCustomRect(pack_size.x, pack_size.y, &r);
+        IM_ASSERT(builder->PackIdMouseCursors != ImFontAtlasRectId_Invalid);
+
+        // Draw to texture
+        if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors)
+        {
+            // 2x2 white pixels
+            ImFontAtlasBuildRenderBitmapFromString(atlas, r.x, r.y, 2, 2, "XX" "XX", 'X');
+        }
+        else
+        {
+            // 2x2 white pixels + mouse cursors
+            const int x_for_white = r.x;
+            const int x_for_black = r.x + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1;
+            ImFontAtlasBuildRenderBitmapFromString(atlas, x_for_white, r.y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.');
+            ImFontAtlasBuildRenderBitmapFromString(atlas, x_for_black, r.y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X');
+        }
+    }
+
+    // Refresh UV coordinates
+    atlas->TexUvWhitePixel = ImVec2((r.x + 0.5f) * atlas->TexUvScale.x, (r.y + 0.5f) * atlas->TexUvScale.y);
 }
 
-static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
+static void ImFontAtlasBuildUpdateLinesTexData(ImFontAtlas* atlas)
 {
-    IM_ASSERT(atlas->Sources.Size > 0);
+    if (atlas->Flags & ImFontAtlasFlags_NoBakedLines)
+        return;
 
-    ImFontAtlasBuildInit(atlas);
+    // Pack and store identifier so we can refresh UV coordinates on texture resize.
+    ImTextureData* tex = atlas->TexData;
+    ImFontAtlasBuilder* builder = atlas->Builder;
+
+    ImFontAtlasRect r;
+    bool add_and_draw = atlas->GetCustomRect(builder->PackIdLinesTexData, &r) == false;
+    if (add_and_draw)
+    {
+        ImVec2i pack_size = ImVec2i(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1);
+        builder->PackIdLinesTexData = atlas->AddCustomRect(pack_size.x, pack_size.y, &r);
+        IM_ASSERT(builder->PackIdLinesTexData != ImFontAtlasRectId_Invalid);
+    }
+
+    // Register texture region for thick lines
+    // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row
+    // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them
+    for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row
+    {
+        // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle
+        const int y = n;
+        const int line_width = n;
+        const int pad_left = (r.w - line_width) / 2;
+        const int pad_right = r.w - (pad_left + line_width);
+        IM_ASSERT(pad_left + line_width + pad_right == r.w && y < r.h); // Make sure we're inside the texture bounds before we start writing pixels
 
-    // Clear atlas
-    atlas->TexID = (ImTextureID)NULL;
-    atlas->TexWidth = atlas->TexHeight = 0;
-    atlas->TexUvScale = ImVec2(0.0f, 0.0f);
-    atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f);
-    atlas->ClearTexData();
-
-    // Temporary storage for building
-    ImVector<ImFontBuildSrcData> src_tmp_array;
-    ImVector<ImFontBuildDstData> dst_tmp_array;
-    src_tmp_array.resize(atlas->Sources.Size);
-    dst_tmp_array.resize(atlas->Fonts.Size);
-    memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes());
-    memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes());
-
-    // 1. Initialize font loading structure, check font data validity
-    for (int src_i = 0; src_i < atlas->Sources.Size; src_i++)
-    {
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        ImFontConfig& src = atlas->Sources[src_i];
-        IM_ASSERT(src.DstFont && (!src.DstFont->IsLoaded() || src.DstFont->ContainerAtlas == atlas));
-
-        // Find index from src.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices)
-        src_tmp.DstIndex = -1;
-        for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++)
-            if (src.DstFont == atlas->Fonts[output_i])
-                src_tmp.DstIndex = output_i;
-        if (src_tmp.DstIndex == -1)
+        // Write each slice
+        if (add_and_draw && tex->Format == ImTextureFormat_Alpha8)
         {
-            IM_ASSERT(src_tmp.DstIndex != -1); // src.DstFont not pointing within atlas->Fonts[] array?
-            return false;
+            ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(r.x, r.y + y);
+            for (int i = 0; i < pad_left; i++)
+                *(write_ptr + i) = 0x00;
+
+            for (int i = 0; i < line_width; i++)
+                *(write_ptr + pad_left + i) = 0xFF;
+
+            for (int i = 0; i < pad_right; i++)
+                *(write_ptr + pad_left + line_width + i) = 0x00;
         }
-        // Initialize helper structure for font loading and verify that the TTF/OTF data is correct
-        const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)src.FontData, src.FontNo);
-        IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found.");
-        if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)src.FontData, font_offset))
+        else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32)
         {
-            IM_ASSERT(0 && "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize.");
-            return false;
+            ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(r.x, r.y + y);
+            for (int i = 0; i < pad_left; i++)
+                *(write_ptr + i) = IM_COL32(255, 255, 255, 0);
+
+            for (int i = 0; i < line_width; i++)
+                *(write_ptr + pad_left + i) = IM_COL32_WHITE;
+
+            for (int i = 0; i < pad_right; i++)
+                *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0);
         }
 
-        // Measure highest codepoints
-        ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex];
-        src_tmp.SrcRanges = src.GlyphRanges ? src.GlyphRanges : atlas->GetGlyphRangesDefault();
-        for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2)
-        {
-            // Check for valid range. This may also help detect *some* dangling pointers, because a common
-            // user error is to setup ImFontConfig::GlyphRanges with a pointer to data that isn't persistent,
-            // or to forget to zero-terminate the glyph range array.
-            IM_ASSERT(src_range[0] <= src_range[1] && "Invalid range: is your glyph range array persistent? it is zero-terminated?");
-            src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]);
-        }
-        dst_tmp.SrcCount++;
-        dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest);
-    }
-
-    // 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs.
-    int total_glyphs_count = 0;
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
-    {
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex];
-        src_tmp.GlyphsSet.Create(src_tmp.GlyphsHighest + 1);
-        if (dst_tmp.GlyphsSet.Storage.empty())
-            dst_tmp.GlyphsSet.Create(dst_tmp.GlyphsHighest + 1);
+        // Refresh UV coordinates
+        ImVec2 uv0 = ImVec2((float)(r.x + pad_left - 1), (float)(r.y + y)) * atlas->TexUvScale;
+        ImVec2 uv1 = ImVec2((float)(r.x + pad_left + line_width + 1), (float)(r.y + y + 1)) * atlas->TexUvScale;
+        float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts
+        atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v);
+    }
+}
+
+//-----------------------------------------------------------------------------------------------------------------------------
+
+// Was tempted to lazily init FontSrc but wouldn't save much + makes it more complicated to detect invalid data at AddFont()
+bool ImFontAtlasFontInitOutput(ImFontAtlas* atlas, ImFont* font)
+{
+    bool ret = true;
+    for (ImFontConfig* src : font->Sources)
+        if (!ImFontAtlasFontSourceInit(atlas, src))
+            ret = false;
+    IM_ASSERT(ret); // Unclear how to react to this meaningfully. Assume that result will be same as initial AddFont() call.
+    return ret;
+}
+
+// Keep source/input FontData
+void ImFontAtlasFontDestroyOutput(ImFontAtlas* atlas, ImFont* font)
+{
+    font->ClearOutputData();
+    for (ImFontConfig* src : font->Sources)
+    {
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        if (loader && loader->FontSrcDestroy != NULL)
+            loader->FontSrcDestroy(atlas, src);
+    }
+}
+
+//-----------------------------------------------------------------------------------------------------------------------------
+
+bool ImFontAtlasFontSourceInit(ImFontAtlas* atlas, ImFontConfig* src)
+{
+    const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+    if (loader->FontSrcInit != NULL && !loader->FontSrcInit(atlas, src))
+        return false;
+    return true;
+}
+
+void ImFontAtlasFontSourceAddToFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src)
+{
+    if (src->MergeMode == false)
+    {
+        font->ClearOutputData();
+        //font->FontSize = src->SizePixels;
+        font->ContainerAtlas = atlas;
+        IM_ASSERT(font->Sources[0] == src);
+    }
+    atlas->TexIsBuilt = false; // For legacy backends
+    ImFontAtlasBuildSetupFontSpecialGlyphs(atlas, font, src);
+}
+
+void ImFontAtlasFontDestroySourceData(ImFontAtlas* atlas, ImFontConfig* src)
+{
+    IM_UNUSED(atlas);
+    if (src->FontDataOwnedByAtlas)
+        IM_FREE(src->FontData);
+    src->FontData = NULL;
+    if (src->GlyphExcludeRanges)
+        IM_FREE((void*)src->GlyphExcludeRanges);
+    src->GlyphExcludeRanges = NULL;
+}
+
+// Create a compact, baked "..." if it doesn't exist, by using the ".".
+// This may seem overly complicated right now but the point is to exercise and improve a technique which should be increasingly used.
+// FIXME-NEWATLAS: This borrows too much from FontLoader's FontLoadGlyph() handlers and suggest that we should add further helpers.
+static ImFontGlyph* ImFontAtlasBuildSetupFontBakedEllipsis(ImFontAtlas* atlas, ImFontBaked* baked)
+{
+    ImFont* font = baked->ContainerFont;
+    IM_ASSERT(font->EllipsisChar != 0);
+
+    const ImFontGlyph* dot_glyph = baked->FindGlyphNoFallback((ImWchar)'.');
+    if (dot_glyph == NULL)
+        dot_glyph = baked->FindGlyphNoFallback((ImWchar)0xFF0E);
+    if (dot_glyph == NULL)
+        return NULL;
+    ImFontAtlasRectId dot_r_id = dot_glyph->PackId; // Deep copy to avoid invalidation of glyphs and rect pointers
+    ImTextureRect* dot_r = ImFontAtlasPackGetRect(atlas, dot_r_id);
+    const int dot_spacing = 1;
+    const float dot_step = (dot_glyph->X1 - dot_glyph->X0) + dot_spacing;
+
+    ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, (dot_r->w * 3 + dot_spacing * 2), dot_r->h);
+    ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
+
+    ImFontGlyph glyph_in = {};
+    ImFontGlyph* glyph = &glyph_in;
+    glyph->Codepoint = font->EllipsisChar;
+    glyph->AdvanceX = ImMax(dot_glyph->AdvanceX, dot_glyph->X0 + dot_step * 3.0f - dot_spacing); // FIXME: Slightly odd for normally mono-space fonts but since this is used for trailing contents.
+    glyph->X0 = dot_glyph->X0;
+    glyph->Y0 = dot_glyph->Y0;
+    glyph->X1 = dot_glyph->X0 + dot_step * 3 - dot_spacing;
+    glyph->Y1 = dot_glyph->Y1;
+    glyph->Visible = true;
+    glyph->PackId = pack_id;
+    glyph = ImFontAtlasBakedAddFontGlyph(atlas, baked, NULL, glyph);
+    dot_glyph = NULL; // Invalidated
+
+    // Copy to texture, post-process and queue update for backend
+    // FIXME-NEWATLAS-V2: Dot glyph is already post-processed as this point, so this would damage it.
+    dot_r = ImFontAtlasPackGetRect(atlas, dot_r_id);
+    ImTextureData* tex = atlas->TexData;
+    for (int n = 0; n < 3; n++)
+        ImFontAtlasTextureBlockCopy(tex, dot_r->x, dot_r->y, tex, r->x + (dot_r->w + dot_spacing) * n, r->y, dot_r->w, dot_r->h);
+    ImFontAtlasTextureBlockQueueUpload(atlas, tex, r->x, r->y, r->w, r->h);
+
+    return glyph;
+}
+
+// Load fallback in order to obtain its index
+// (this is called from in hot-path so we avoid extraneous parameters to minimize impact on code size)
+static void ImFontAtlasBuildSetupFontBakedFallback(ImFontBaked* baked)
+{
+    IM_ASSERT(baked->FallbackGlyphIndex == -1);
+    IM_ASSERT(baked->FallbackAdvanceX == 0.0f);
+    ImFont* font = baked->ContainerFont;
+    ImFontGlyph* fallback_glyph = NULL;
+    if (font->FallbackChar != 0)
+        fallback_glyph = baked->FindGlyphNoFallback(font->FallbackChar);
+    if (fallback_glyph == NULL)
+    {
+        ImFontGlyph* space_glyph = baked->FindGlyphNoFallback((ImWchar)' ');
+        ImFontGlyph glyph;
+        glyph.Codepoint = 0;
+        glyph.AdvanceX = space_glyph ? space_glyph->AdvanceX : IM_ROUND(baked->Size * 0.40f);
+        fallback_glyph = ImFontAtlasBakedAddFontGlyph(font->ContainerAtlas, baked, NULL, &glyph);
+    }
+    baked->FallbackGlyphIndex = baked->Glyphs.index_from_ptr(fallback_glyph); // Storing index avoid need to update pointer on growth and simplify inner loop code
+    baked->FallbackAdvanceX = fallback_glyph->AdvanceX;
+}
+
+static void ImFontAtlasBuildSetupFontBakedBlanks(ImFontAtlas* atlas, ImFontBaked* baked)
+{
+    // Mark space as always hidden (not strictly correct/necessary. but some e.g. icons fonts don't have a space. it tends to look neater in previews)
+    ImFontGlyph* space_glyph = baked->FindGlyphNoFallback((ImWchar)' ');
+    if (space_glyph != NULL)
+        space_glyph->Visible = false;
+
+    // Setup Tab character.
+    // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?)
+    if (baked->FindGlyphNoFallback('\t') == NULL && space_glyph != NULL)
+    {
+        ImFontGlyph tab_glyph;
+        tab_glyph.Codepoint = '\t';
+        tab_glyph.AdvanceX = space_glyph->AdvanceX * IM_TABSIZE;
+        ImFontAtlasBakedAddFontGlyph(atlas, baked, NULL, &tab_glyph);
+    }
+}
+
+// Load/identify special glyphs
+// (note that this is called again for fonts with MergeMode)
+void ImFontAtlasBuildSetupFontSpecialGlyphs(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src)
+{
+    IM_UNUSED(atlas);
+    IM_ASSERT(font->Sources.contains(src));
 
-        for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2)
-            for (unsigned int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++)
+    // Find Fallback character. Actual glyph loaded in GetFontBaked().
+    const ImWchar fallback_chars[] = { font->FallbackChar, (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' };
+    if (font->FallbackChar == 0)
+        for (ImWchar candidate_char : fallback_chars)
+            if (candidate_char != 0 && font->IsGlyphInFont(candidate_char))
             {
-                if (dst_tmp.GlyphsSet.TestBit(codepoint))    // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true)
-                    continue;
-                if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint))    // It is actually in the font?
-                    continue;
+                font->FallbackChar = (ImWchar)candidate_char;
+                break;
+            }
 
-                // Add to avail set/counters
-                src_tmp.GlyphsCount++;
-                dst_tmp.GlyphsCount++;
-                src_tmp.GlyphsSet.SetBit(codepoint);
-                dst_tmp.GlyphsSet.SetBit(codepoint);
-                total_glyphs_count++;
+    // Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis).
+    // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character.
+    // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots.
+    const ImWchar ellipsis_chars[] = { src->EllipsisChar, (ImWchar)0x2026, (ImWchar)0x0085 };
+    if (font->EllipsisChar == 0)
+        for (ImWchar candidate_char : ellipsis_chars)
+            if (candidate_char != 0 && font->IsGlyphInFont(candidate_char))
+            {
+                font->EllipsisChar = candidate_char;
+                break;
             }
+    if (font->EllipsisChar == 0)
+    {
+        font->EllipsisChar = 0x0085;
+        font->EllipsisAutoBake = true;
     }
+}
 
-    // 3. Unpack our bit map into a flat list (we now have all the Unicode points that we know are requested _and_ available _and_ not overlapping another)
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
+void ImFontAtlasBakedDiscardFontGlyph(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked, ImFontGlyph* glyph)
+{
+    if (glyph->PackId != ImFontAtlasRectId_Invalid)
     {
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount);
-        UnpackBitVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList);
-        src_tmp.GlyphsSet.Clear();
-        IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount);
-    }
-    for (int dst_i = 0; dst_i < dst_tmp_array.Size; dst_i++)
-        dst_tmp_array[dst_i].GlyphsSet.Clear();
-    dst_tmp_array.clear();
-
-    // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0)
-    // (We technically don't need to zero-clear buf_rects, but let's do it for the sake of sanity)
-    ImVector<stbrp_rect> buf_rects;
-    ImVector<stbtt_packedchar> buf_packedchars;
-    buf_rects.resize(total_glyphs_count);
-    buf_packedchars.resize(total_glyphs_count);
-    memset(buf_rects.Data, 0, (size_t)buf_rects.size_in_bytes());
-    memset(buf_packedchars.Data, 0, (size_t)buf_packedchars.size_in_bytes());
-
-    // 4. Gather glyphs sizes so we can pack them in our virtual canvas.
-    int total_surface = 0;
-    int buf_rects_out_n = 0;
-    int buf_packedchars_out_n = 0;
-    const int pack_padding = atlas->TexGlyphPadding;
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
+        ImFontAtlasPackDiscardRect(atlas, glyph->PackId);
+        glyph->PackId = ImFontAtlasRectId_Invalid;
+    }
+    ImWchar c = (ImWchar)glyph->Codepoint;
+    IM_ASSERT(font->FallbackChar != c && font->EllipsisChar != c); // Unsupported for simplicity
+    IM_ASSERT(glyph >= baked->Glyphs.Data && glyph < baked->Glyphs.Data + baked->Glyphs.Size);
+    IM_UNUSED(font);
+    baked->IndexLookup[c] = IM_FONTGLYPH_INDEX_UNUSED;
+    baked->IndexAdvanceX[c] = baked->FallbackAdvanceX;
+}
+
+ImFontBaked* ImFontAtlasBakedAdd(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density, ImGuiID baked_id)
+{
+    IMGUI_DEBUG_LOG_FONT("[font] Created baked %.2fpx\n", font_size);
+    ImFontBaked* baked = atlas->Builder->BakedPool.push_back(ImFontBaked());
+    baked->Size = font_size;
+    baked->RasterizerDensity = font_rasterizer_density;
+    baked->BakedId = baked_id;
+    baked->ContainerFont = font;
+    baked->LastUsedFrame = atlas->Builder->FrameCount;
+
+    // Initialize backend data
+    size_t loader_data_size = 0;
+    for (ImFontConfig* src : font->Sources) // Cannot easily be cached as we allow changing backend
     {
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        if (src_tmp.GlyphsCount == 0)
-            continue;
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        loader_data_size += loader->FontBakedSrcLoaderDataSize;
+    }
+    baked->FontLoaderDatas = (loader_data_size > 0) ? IM_ALLOC(loader_data_size) : NULL;
+    char* loader_data_p = (char*)baked->FontLoaderDatas;
+    for (ImFontConfig* src : font->Sources)
+    {
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        if (loader->FontBakedInit)
+            loader->FontBakedInit(atlas, src, baked, loader_data_p);
+        loader_data_p += loader->FontBakedSrcLoaderDataSize;
+    }
 
-        src_tmp.Rects = &buf_rects[buf_rects_out_n];
-        src_tmp.PackedChars = &buf_packedchars[buf_packedchars_out_n];
-        buf_rects_out_n += src_tmp.GlyphsCount;
-        buf_packedchars_out_n += src_tmp.GlyphsCount;
-
-        // Automatic selection of oversampling parameters
-        ImFontConfig& src = atlas->Sources[src_i];
-        int oversample_h, oversample_v;
-        ImFontAtlasBuildGetOversampleFactors(&src, &oversample_h, &oversample_v);
-
-        // Convert our ranges in the format stb_truetype wants
-        src_tmp.PackRange.font_size = src.SizePixels * src.RasterizerDensity;
-        src_tmp.PackRange.first_unicode_codepoint_in_range = 0;
-        src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data;
-        src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size;
-        src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars;
-        src_tmp.PackRange.h_oversample = (unsigned char)oversample_h;
-        src_tmp.PackRange.v_oversample = (unsigned char)oversample_v;
-
-        // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects)
-        const float scale = (src.SizePixels > 0.0f) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels * src.RasterizerDensity) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -src.SizePixels * src.RasterizerDensity);
-        for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++)
-        {
-            int x0, y0, x1, y1;
-            const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]);
-            IM_ASSERT(glyph_index_in_font != 0);
-            stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * oversample_h, scale * oversample_v, 0, 0, &x0, &y0, &x1, &y1);
-            src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + pack_padding + oversample_h - 1);
-            src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + pack_padding + oversample_v - 1);
-            total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h;
-        }
-    }
-    for (int i = 0; i < atlas->CustomRects.Size; i++)
-        total_surface += (atlas->CustomRects[i].Width + pack_padding) * (atlas->CustomRects[i].Height + pack_padding);
-
-    // We need a width for the skyline algorithm, any width!
-    // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height.
-    // User can override TexDesiredWidth and TexGlyphPadding if they wish, otherwise we use a simple heuristic to select the width based on expected surface.
-    const int surface_sqrt = (int)ImSqrt((float)total_surface) + 1;
-    atlas->TexHeight = 0;
-    if (atlas->TexDesiredWidth > 0)
-        atlas->TexWidth = atlas->TexDesiredWidth;
-    else
-        atlas->TexWidth = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512;
+    ImFontAtlasBuildSetupFontBakedBlanks(atlas, baked);
+    return baked;
+}
+
+// FIXME-OPT: This is not a fast query. Adding a BakedCount field in Font might allow to take a shortcut for the most common case.
+ImFontBaked* ImFontAtlasBakedGetClosestMatch(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density)
+{
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    for (int step_n = 0; step_n < 2; step_n++)
+    {
+        ImFontBaked* closest_larger_match = NULL;
+        ImFontBaked* closest_smaller_match = NULL;
+        for (int baked_n = 0; baked_n < builder->BakedPool.Size; baked_n++)
+        {
+            ImFontBaked* baked = &builder->BakedPool[baked_n];
+            if (baked->ContainerFont != font || baked->WantDestroy)
+                continue;
+            if (step_n == 0 && baked->RasterizerDensity != font_rasterizer_density) // First try with same density
+                continue;
+            if (baked->Size > font_size && (closest_larger_match == NULL || baked->Size < closest_larger_match->Size))
+                closest_larger_match = baked;
+            if (baked->Size < font_size && (closest_smaller_match == NULL || baked->Size > closest_smaller_match->Size))
+                closest_smaller_match = baked;
+        }
+        if (closest_larger_match)
+            if (closest_smaller_match == NULL || (closest_larger_match->Size >= font_size * 2.0f && closest_smaller_match->Size > font_size * 0.5f))
+                return closest_larger_match;
+        if (closest_smaller_match)
+            return closest_smaller_match;
+    }
+    return NULL;
+}
+
+void ImFontAtlasBakedDiscard(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked)
+{
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    IMGUI_DEBUG_LOG_FONT("[font] Discard baked %.2f for \"%s\"\n", baked->Size, font->GetDebugName());
 
-    // 5. Start packing
-    // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values).
-    const int TEX_HEIGHT_MAX = 1024 * 32;
-    stbtt_pack_context spc = {};
-    stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, 0, NULL);
-    spc.padding = atlas->TexGlyphPadding; // Because we mixup stbtt_PackXXX and stbrp_PackXXX there's a bit of a hack here, not passing the value to stbtt_PackBegin() allows us to still pack a TexWidth-1 wide item. (#8107)
-    ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info);
+    for (ImFontGlyph& glyph : baked->Glyphs)
+        if (glyph.PackId != ImFontAtlasRectId_Invalid)
+            ImFontAtlasPackDiscardRect(atlas, glyph.PackId);
 
-    // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point.
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
+    char* loader_data_p = (char*)baked->FontLoaderDatas;
+    for (ImFontConfig* src : font->Sources)
     {
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        if (src_tmp.GlyphsCount == 0)
-            continue;
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        if (loader->FontBakedDestroy)
+            loader->FontBakedDestroy(atlas, src, baked, loader_data_p);
+        loader_data_p += loader->FontBakedSrcLoaderDataSize;
+    }
+    if (baked->FontLoaderDatas)
+    {
+        IM_FREE(baked->FontLoaderDatas);
+        baked->FontLoaderDatas = NULL;
+    }
+    builder->BakedMap.SetVoidPtr(baked->BakedId, NULL);
+    builder->BakedDiscardedCount++;
+    baked->ClearOutputData();
+    baked->WantDestroy = true;
+    font->LastBaked = NULL;
+}
 
-        stbrp_pack_rects((stbrp_context*)spc.pack_info, src_tmp.Rects, src_tmp.GlyphsCount);
+// use unused_frames==0 to discard everything.
+void ImFontAtlasFontDiscardBakes(ImFontAtlas* atlas, ImFont* font, int unused_frames)
+{
+    if (ImFontAtlasBuilder* builder = atlas->Builder) // This can be called from font destructor
+        for (int baked_n = 0; baked_n < builder->BakedPool.Size; baked_n++)
+        {
+            ImFontBaked* baked = &builder->BakedPool[baked_n];
+            if (baked->LastUsedFrame + unused_frames > atlas->Builder->FrameCount)
+                continue;
+            if (baked->ContainerFont != font || baked->WantDestroy)
+                continue;
+            ImFontAtlasBakedDiscard(atlas, font, baked);
+        }
+}
 
-        // Extend texture height and mark missing glyphs as non-packed so we won't render them.
-        // FIXME: We are not handling packing failure here (would happen if we got off TEX_HEIGHT_MAX or if a single if larger than TexWidth?)
-        for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++)
-            if (src_tmp.Rects[glyph_i].was_packed)
-                atlas->TexHeight = ImMax(atlas->TexHeight, src_tmp.Rects[glyph_i].y + src_tmp.Rects[glyph_i].h);
-    }
-
-    // 7. Allocate texture
-    atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight);
-    atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight);
-    atlas->TexPixelsAlpha8 = (unsigned char*)IM_ALLOC(atlas->TexWidth * atlas->TexHeight);
-    memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight);
-    spc.pixels = atlas->TexPixelsAlpha8;
-    spc.height = atlas->TexHeight;
-
-    // 8. Render/rasterize font characters into the texture
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
-    {
-        ImFontConfig& src = atlas->Sources[src_i];
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        if (src_tmp.GlyphsCount == 0)
+// use unused_frames==0 to discard everything.
+void ImFontAtlasBuildDiscardBakes(ImFontAtlas* atlas, int unused_frames)
+{
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    for (int baked_n = 0; baked_n < builder->BakedPool.Size; baked_n++)
+    {
+        ImFontBaked* baked = &builder->BakedPool[baked_n];
+        if (baked->LastUsedFrame + unused_frames > atlas->Builder->FrameCount)
+            continue;
+        if (baked->WantDestroy || (baked->ContainerFont->Flags & ImFontFlags_LockBakedSizes))
             continue;
+        ImFontAtlasBakedDiscard(atlas, baked->ContainerFont, baked);
+    }
+}
 
-        stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects);
+// Those functions are designed to facilitate changing the underlying structures for ImFontAtlas to store an array of ImDrawListSharedData*
+void ImFontAtlasAddDrawListSharedData(ImFontAtlas* atlas, ImDrawListSharedData* data)
+{
+    IM_ASSERT(!atlas->DrawListSharedDatas.contains(data));
+    atlas->DrawListSharedDatas.push_back(data);
+}
 
-        // Apply multiply operator
-        if (src.RasterizerMultiply != 1.0f)
+void ImFontAtlasRemoveDrawListSharedData(ImFontAtlas* atlas, ImDrawListSharedData* data)
+{
+    IM_ASSERT(atlas->DrawListSharedDatas.contains(data));
+    atlas->DrawListSharedDatas.find_erase(data);
+}
+
+// Update texture identifier in all active draw lists
+void ImFontAtlasUpdateDrawListsTextures(ImFontAtlas* atlas, ImTextureRef old_tex, ImTextureRef new_tex)
+{
+    for (ImDrawListSharedData* shared_data : atlas->DrawListSharedDatas)
+        for (ImDrawList* draw_list : shared_data->DrawLists)
         {
-            unsigned char multiply_table[256];
-            ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, src.RasterizerMultiply);
-            stbrp_rect* r = &src_tmp.Rects[0];
-            for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++)
-                if (r->was_packed)
-                    ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, atlas->TexPixelsAlpha8, r->x, r->y, r->w, r->h, atlas->TexWidth * 1);
+            // Replace in command-buffer
+            // (there is not need to replace in ImDrawListSplitter: current channel is in ImDrawList's CmdBuffer[],
+            //  other channels will be on SetCurrentChannel() which already needs to compare CmdHeader anyhow)
+            if (draw_list->CmdBuffer.Size > 0 && draw_list->_CmdHeader.TexRef == old_tex)
+                draw_list->_SetTexture(new_tex);
+
+            // Replace in stack
+            for (ImTextureRef& stacked_tex : draw_list->_TextureStack)
+                if (stacked_tex == old_tex)
+                    stacked_tex = new_tex;
         }
-        src_tmp.Rects = NULL;
-    }
+}
 
-    // End packing
-    stbtt_PackEnd(&spc);
-    buf_rects.clear();
+// Update texture coordinates in all draw list shared context
+// FIXME-NEWATLAS FIXME-OPT: Doesn't seem necessary to update for all, only one bound to current context?
+void ImFontAtlasUpdateDrawListsSharedData(ImFontAtlas* atlas)
+{
+    for (ImDrawListSharedData* shared_data : atlas->DrawListSharedDatas)
+        if (shared_data->FontAtlas == atlas)
+        {
+            shared_data->TexUvWhitePixel = atlas->TexUvWhitePixel;
+            shared_data->TexUvLines = atlas->TexUvLines;
+        }
+}
 
-    // 9. Setup ImFont and glyphs for runtime
-    for (int src_i = 0; src_i < src_tmp_array.Size; src_i++)
+// Set current texture. This is mostly called from AddTexture() + to handle a failed resize.
+static void ImFontAtlasBuildSetTexture(ImFontAtlas* atlas, ImTextureData* tex)
+{
+    ImTextureRef old_tex_ref = atlas->TexRef;
+    atlas->TexData = tex;
+    atlas->TexUvScale = ImVec2(1.0f / tex->Width, 1.0f / tex->Height);
+    atlas->TexRef._TexData = tex;
+    //atlas->TexRef._TexID = tex->TexID; // <-- We intentionally don't do that. It would be misleading and betray promise that both fields aren't set.
+    ImFontAtlasUpdateDrawListsTextures(atlas, old_tex_ref, atlas->TexRef);
+}
+
+// Create a new texture, discard previous one
+ImTextureData* ImFontAtlasTextureAdd(ImFontAtlas* atlas, int w, int h)
+{
+    ImTextureData* old_tex = atlas->TexData;
+    ImTextureData* new_tex;
+
+    // FIXME: Cannot reuse texture because old UV may have been used already (unless we remap UV).
+    /*if (old_tex != NULL && old_tex->Status == ImTextureStatus_WantCreate)
+    {
+        // Reuse texture not yet used by backend.
+        IM_ASSERT(old_tex->TexID == ImTextureID_Invalid && old_tex->BackendUserData == NULL);
+        old_tex->DestroyPixels();
+        old_tex->Updates.clear();
+        new_tex = old_tex;
+        old_tex = NULL;
+    }
+    else*/
     {
-        // When merging fonts with MergeMode=true:
-        // - We can have multiple input fonts writing into a same destination font.
-        // - dst_font->Sources is != from src which is our source configuration.
-        ImFontBuildSrcData& src_tmp = src_tmp_array[src_i];
-        ImFontConfig& src = atlas->Sources[src_i];
-        ImFont* dst_font = src.DstFont;
+        // Add new
+        new_tex = IM_NEW(ImTextureData)();
+        new_tex->UniqueID = atlas->TexNextUniqueID++;
+        atlas->TexList.push_back(new_tex);
+    }
+    if (old_tex != NULL)
+    {
+        // Queue old as to destroy next frame
+        old_tex->WantDestroyNextFrame = true;
+        IM_ASSERT(old_tex->Status == ImTextureStatus_OK || old_tex->Status == ImTextureStatus_WantCreate || old_tex->Status == ImTextureStatus_WantUpdates);
+    }
 
-        const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels);
-        int unscaled_ascent, unscaled_descent, unscaled_line_gap;
-        stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap);
+    new_tex->Create(atlas->TexDesiredFormat, w, h);
+    atlas->TexIsBuilt = false;
+
+    ImFontAtlasBuildSetTexture(atlas, new_tex);
+
+    return new_tex;
+}
+
+#if 0
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include "../stb/stb_image_write.h"
+static void ImFontAtlasDebugWriteTexToDisk(ImTextureData* tex, const char* description)
+{
+    ImGuiContext& g = *GImGui;
+    char buf[128];
+    ImFormatString(buf, IM_ARRAYSIZE(buf), "[%05d] Texture #%03d - %s.png", g.FrameCount, tex->UniqueID, description);
+    stbi_write_png(buf, tex->Width, tex->Height, tex->BytesPerPixel, tex->Pixels, tex->GetPitch()); // tex->BytesPerPixel is technically not component, but ok for the formats we support.
+}
+#endif
 
-        const float ascent = ImCeil(unscaled_ascent * font_scale);
-        const float descent = ImFloor(unscaled_descent * font_scale);
-        ImFontAtlasBuildSetupFont(atlas, dst_font, &src, ascent, descent);
-        const float font_off_x = src.GlyphOffset.x;
-        const float font_off_y = src.GlyphOffset.y + IM_ROUND(dst_font->Ascent);
+void ImFontAtlasTextureRepack(ImFontAtlas* atlas, int w, int h)
+{
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    builder->LockDisableResize = true;
 
-        const float inv_rasterization_scale = 1.0f / src.RasterizerDensity;
+    ImTextureData* old_tex = atlas->TexData;
+    ImTextureData* new_tex = ImFontAtlasTextureAdd(atlas, w, h);
+    new_tex->UseColors = old_tex->UseColors;
+    IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: resize+repack %dx%d => Texture #%03d: %dx%d\n", old_tex->UniqueID, old_tex->Width, old_tex->Height, new_tex->UniqueID, new_tex->Width, new_tex->Height);
+    //for (int baked_n = 0; baked_n < builder->BakedPool.Size; baked_n++)
+    //    IMGUI_DEBUG_LOG_FONT("[font] - Baked %.2fpx, %d glyphs, want_destroy=%d\n", builder->BakedPool[baked_n].FontSize, builder->BakedPool[baked_n].Glyphs.Size, builder->BakedPool[baked_n].WantDestroy);
+    //IMGUI_DEBUG_LOG_FONT("[font] - Old packed rects: %d, area %d px\n", builder->RectsPackedCount, builder->RectsPackedSurface);
+    //ImFontAtlasDebugWriteTexToDisk(old_tex, "Before Pack");
+
+    // Repack, lose discarded rectangle, copy pixels
+    // FIXME-NEWATLAS: This is unstable because packing order is based on RectsIndex
+    // FIXME-NEWATLAS-V2: Repacking in batch would be beneficial to packing heuristic, and fix stability.
+    // FIXME-NEWATLAS-TESTS: Test calling RepackTexture with size too small to fits existing rects.
+    ImFontAtlasPackInit(atlas);
+    ImVector<ImTextureRect> old_rects;
+    ImVector<ImFontAtlasRectEntry> old_index = builder->RectsIndex;
+    old_rects.swap(builder->Rects);
 
-        for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++)
+    for (ImFontAtlasRectEntry& index_entry : builder->RectsIndex)
+    {
+        if (index_entry.IsUsed == false)
+            continue;
+        ImTextureRect& old_r = old_rects[index_entry.TargetIndex];
+        if (old_r.w == 0 && old_r.h == 0)
+            continue;
+        ImFontAtlasRectId new_r_id = ImFontAtlasPackAddRect(atlas, old_r.w, old_r.h, &index_entry);
+        if (new_r_id == ImFontAtlasRectId_Invalid)
         {
-            // Register glyph
-            const int codepoint = src_tmp.GlyphsList[glyph_i];
-            const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i];
-            stbtt_aligned_quad q;
-            float unused_x = 0.0f, unused_y = 0.0f;
-            stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0);
-            float x0 = q.x0 * inv_rasterization_scale + font_off_x;
-            float y0 = q.y0 * inv_rasterization_scale + font_off_y;
-            float x1 = q.x1 * inv_rasterization_scale + font_off_x;
-            float y1 = q.y1 * inv_rasterization_scale + font_off_y;
-            dst_font->AddGlyph(&src, (ImWchar)codepoint, x0, y0, x1, y1, q.s0, q.t0, q.s1, q.t1, pc.xadvance * inv_rasterization_scale);
-        }
-    }
+            // Undo, grow texture and try repacking again.
+            // FIXME-NEWATLAS-TESTS: This is a very rarely exercised path! It needs to be automatically tested properly.
+            IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: resize failed. Will grow.\n", new_tex->UniqueID);
+            new_tex->WantDestroyNextFrame = true;
+            builder->Rects.swap(old_rects);
+            builder->RectsIndex = old_index;
+            ImFontAtlasBuildSetTexture(atlas, old_tex);
+            ImFontAtlasTextureGrow(atlas, w, h); // Recurse
+            return;
+        }
+        IM_ASSERT(ImFontAtlasRectId_GetIndex(new_r_id) == builder->RectsIndex.index_from_ptr(&index_entry));
+        ImTextureRect* new_r = ImFontAtlasPackGetRect(atlas, new_r_id);
+        ImFontAtlasTextureBlockCopy(old_tex, old_r.x, old_r.y, new_tex, new_r->x, new_r->y, new_r->w, new_r->h);
+    }
+    IM_ASSERT(old_rects.Size == builder->Rects.Size + builder->RectsDiscardedCount);
+    builder->RectsDiscardedCount = 0;
+    builder->RectsDiscardedSurface = 0;
+
+    // Patch glyphs UV
+    for (int baked_n = 0; baked_n < builder->BakedPool.Size; baked_n++)
+        for (ImFontGlyph& glyph : builder->BakedPool[baked_n].Glyphs)
+            if (glyph.PackId != ImFontAtlasRectId_Invalid)
+            {
+                ImTextureRect* r = ImFontAtlasPackGetRect(atlas, glyph.PackId);
+                glyph.U0 = (r->x) * atlas->TexUvScale.x;
+                glyph.V0 = (r->y) * atlas->TexUvScale.y;
+                glyph.U1 = (r->x + r->w) * atlas->TexUvScale.x;
+                glyph.V1 = (r->y + r->h) * atlas->TexUvScale.y;
+            }
 
-    // Cleanup
-    src_tmp_array.clear_destruct();
+    // Update other cached UV
+    ImFontAtlasBuildUpdateLinesTexData(atlas);
+    ImFontAtlasBuildUpdateBasicTexData(atlas);
+
+    builder->LockDisableResize = false;
+    ImFontAtlasUpdateDrawListsSharedData(atlas);
+    //ImFontAtlasDebugWriteTexToDisk(new_tex, "After Pack");
+}
+
+void ImFontAtlasTextureGrow(ImFontAtlas* atlas, int old_tex_w, int old_tex_h)
+{
+    //ImFontAtlasDebugWriteTexToDisk(atlas->TexData, "Before Grow");
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    if (old_tex_w == -1)
+        old_tex_w = atlas->TexData->Width;
+    if (old_tex_h == -1)
+        old_tex_h = atlas->TexData->Height;
+
+    // FIXME-NEWATLAS-V2: What to do when reaching limits exposed by backend?
+    // FIXME-NEWATLAS-V2: Does ImFontAtlasFlags_NoPowerOfTwoHeight makes sense now? Allow 'lock' and 'compact' operations?
+    IM_ASSERT(ImIsPowerOfTwo(old_tex_w) && ImIsPowerOfTwo(old_tex_h));
+    IM_ASSERT(ImIsPowerOfTwo(atlas->TexMinWidth) && ImIsPowerOfTwo(atlas->TexMaxWidth) && ImIsPowerOfTwo(atlas->TexMinHeight) && ImIsPowerOfTwo(atlas->TexMaxHeight));
+
+    // Grow texture so it follows roughly a square.
+    // - Grow height before width, as width imply more packing nodes.
+    // - Caller should be taking account of RectsDiscardedSurface and may not need to grow.
+    int new_tex_w = (old_tex_h <= old_tex_w) ? old_tex_w : old_tex_w * 2;
+    int new_tex_h = (old_tex_h <= old_tex_w) ? old_tex_h * 2 : old_tex_h;
 
-    ImFontAtlasBuildFinish(atlas);
-    return true;
+    // Handle minimum size first (for pathologically large packed rects)
+    const int pack_padding = atlas->TexGlyphPadding;
+    new_tex_w = ImMax(new_tex_w, ImUpperPowerOfTwo(builder->MaxRectSize.x + pack_padding));
+    new_tex_h = ImMax(new_tex_h, ImUpperPowerOfTwo(builder->MaxRectSize.y + pack_padding));
+    new_tex_w = ImClamp(new_tex_w, atlas->TexMinWidth, atlas->TexMaxWidth);
+    new_tex_h = ImClamp(new_tex_h, atlas->TexMinHeight, atlas->TexMaxHeight);
+    if (new_tex_w == old_tex_w && new_tex_h == old_tex_h)
+        return;
+
+    ImFontAtlasTextureRepack(atlas, new_tex_w, new_tex_h);
 }
 
-const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype()
+void ImFontAtlasTextureMakeSpace(ImFontAtlas* atlas)
 {
-    static ImFontBuilderIO io;
-    io.FontBuilder_Build = ImFontAtlasBuildWithStbTruetype;
-    return &io;
+    // Can some baked contents be ditched?
+    //IMGUI_DEBUG_LOG_FONT("[font] ImFontAtlasBuildMakeSpace()\n");
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    ImFontAtlasBuildDiscardBakes(atlas, 2);
+
+    // Currently using a heuristic for repack without growing.
+    if (builder->RectsDiscardedSurface < builder->RectsPackedSurface * 0.20f)
+        ImFontAtlasTextureGrow(atlas);
+    else
+        ImFontAtlasTextureRepack(atlas, atlas->TexData->Width, atlas->TexData->Height);
 }
 
-#endif // IMGUI_ENABLE_STB_TRUETYPE
+ImVec2i ImFontAtlasTextureGetSizeEstimate(ImFontAtlas* atlas)
+{
+    int min_w = ImUpperPowerOfTwo(atlas->TexMinWidth);
+    int min_h = ImUpperPowerOfTwo(atlas->TexMinHeight);
+    if (atlas->Builder == NULL || atlas->TexData == NULL || atlas->TexData->Status == ImTextureStatus_WantDestroy)
+        return ImVec2i(min_w, min_h);
+
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    min_w = ImMax(ImUpperPowerOfTwo(builder->MaxRectSize.x), min_w);
+    min_h = ImMax(ImUpperPowerOfTwo(builder->MaxRectSize.y), min_h);
+    const int surface_approx = builder->RectsPackedSurface - builder->RectsDiscardedSurface; // Expected surface after repack
+    const int surface_sqrt = (int)sqrtf((float)surface_approx);
+
+    int new_tex_w;
+    int new_tex_h;
+    if (min_w >= min_h)
+    {
+        new_tex_w = ImMax(min_w, ImUpperPowerOfTwo(surface_sqrt));
+        new_tex_h = ImMax(min_h, (int)((surface_approx + new_tex_w - 1) / new_tex_w));
+        if ((atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) == 0)
+            new_tex_h = ImUpperPowerOfTwo(new_tex_h);
+    }
+    else
+    {
+        new_tex_h = ImMax(min_h, ImUpperPowerOfTwo(surface_sqrt));
+        if ((atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) == 0)
+            new_tex_h = ImUpperPowerOfTwo(new_tex_h);
+        new_tex_w = ImMax(min_w, (int)((surface_approx + new_tex_h - 1) / new_tex_h));
+    }
+
+    IM_ASSERT(ImIsPowerOfTwo(new_tex_w) && ImIsPowerOfTwo(new_tex_h));
+    return ImVec2i(new_tex_w, new_tex_h);
+}
 
-void ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas)
+// Clear all output. Invalidates all AddCustomRect() return values!
+void ImFontAtlasBuildClear(ImFontAtlas* atlas)
 {
+    ImVec2i new_tex_size = ImFontAtlasTextureGetSizeEstimate(atlas);
+    ImFontAtlasBuildDestroy(atlas);
+    ImFontAtlasTextureAdd(atlas, new_tex_size.x, new_tex_size.y);
+    ImFontAtlasBuildInit(atlas);
     for (ImFontConfig& src : atlas->Sources)
+        ImFontAtlasFontSourceInit(atlas, &src);
+    for (ImFont* font : atlas->Fonts)
+        for (ImFontConfig* src : font->Sources)
+            ImFontAtlasFontSourceAddToFont(atlas, font, src);
+}
+
+// You should not need to call this manually!
+// If you think you do, let us know and we can advise about policies auto-compact.
+void ImFontAtlasTextureCompact(ImFontAtlas* atlas)
+{
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    ImFontAtlasBuildDiscardBakes(atlas, 1);
+
+    ImTextureData* old_tex = atlas->TexData;
+    ImVec2i old_tex_size = ImVec2i(old_tex->Width, old_tex->Height);
+    ImVec2i new_tex_size = ImFontAtlasTextureGetSizeEstimate(atlas);
+    if (builder->RectsDiscardedCount == 0 && new_tex_size.x == old_tex_size.x && new_tex_size.y == old_tex_size.y)
+        return;
+
+    ImFontAtlasTextureRepack(atlas, new_tex_size.x, new_tex_size.y);
+}
+
+// Start packing over current empty texture
+void ImFontAtlasBuildInit(ImFontAtlas* atlas)
+{
+    // Select Backend
+    // - Note that we do not reassign to atlas->FontLoader, since it is likely to point to static data which
+    //   may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are
+    //   using a hot-reloading scheme that messes up static data, store your own instance of FontLoader somewhere
+    //   and point to it instead of pointing directly to return value of the GetFontLoaderXXX functions.
+    if (atlas->FontLoader == NULL)
     {
-        ImFont* font = src.DstFont;
-        if (!src.MergeMode)
-        {
-            font->Sources = &src;
-            font->SourcesCount = 0;
-        }
-        font->SourcesCount++;
+#ifdef IMGUI_ENABLE_FREETYPE
+        atlas->SetFontLoader(ImGuiFreeType::GetFontLoader());
+#elif defined(IMGUI_ENABLE_STB_TRUETYPE)
+        atlas->SetFontLoader(ImFontAtlasGetFontLoaderForStbTruetype());
+#else
+        IM_ASSERT(0); // Invalid Build function
+#endif
     }
+
+    // Create initial texture size
+    if (atlas->TexData == NULL || atlas->TexData->Pixels == NULL)
+        ImFontAtlasTextureAdd(atlas, ImUpperPowerOfTwo(atlas->TexMinWidth), ImUpperPowerOfTwo(atlas->TexMinHeight));
+
+    atlas->Builder = IM_NEW(ImFontAtlasBuilder)();
+    if (atlas->FontLoader->LoaderInit)
+        atlas->FontLoader->LoaderInit(atlas);
+
+    ImFontAtlasBuildUpdateRendererHasTexturesFromContext(atlas);
+
+    ImFontAtlasPackInit(atlas);
+
+    // Add required texture data
+    ImFontAtlasBuildUpdateLinesTexData(atlas);
+    ImFontAtlasBuildUpdateBasicTexData(atlas);
+
+    // Register fonts
+    ImFontAtlasBuildUpdatePointers(atlas);
+
+    // Update UV coordinates etc. stored in bound ImDrawListSharedData instance
+    ImFontAtlasUpdateDrawListsSharedData(atlas);
+
+    //atlas->TexIsBuilt = true;
 }
 
-void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent)
+// Destroy builder and all cached glyphs. Do not destroy actual fonts.
+void ImFontAtlasBuildDestroy(ImFontAtlas* atlas)
 {
-    if (!font_config->MergeMode)
+    for (ImFont* font : atlas->Fonts)
+        ImFontAtlasFontDestroyOutput(atlas, font);
+    if (atlas->Builder && atlas->FontLoader && atlas->FontLoader->LoaderShutdown)
     {
-        font->ClearOutputData();
-        font->FontSize = font_config->SizePixels;
-        IM_ASSERT(font->Sources == font_config);
-        font->ContainerAtlas = atlas;
-        font->Ascent = ascent;
-        font->Descent = descent;
+        atlas->FontLoader->LoaderShutdown(atlas);
+        IM_ASSERT(atlas->FontLoaderData == NULL);
     }
+    IM_DELETE(atlas->Builder);
+    atlas->Builder = NULL;
 }
 
-void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque)
+void ImFontAtlasPackInit(ImFontAtlas * atlas)
 {
-    stbrp_context* pack_context = (stbrp_context*)stbrp_context_opaque;
-    IM_ASSERT(pack_context != NULL);
+    ImTextureData* tex = atlas->TexData;
+    ImFontAtlasBuilder* builder = atlas->Builder;
 
-    ImVector<ImFontAtlasCustomRect>& user_rects = atlas->CustomRects;
-    IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong.
-#ifdef __GNUC__
-    if (user_rects.Size < 1) { __builtin_unreachable(); } // Workaround for GCC bug if IM_ASSERT() is defined to conditionally throw (see #5343)
-#endif
+    // In theory we could decide to reduce the number of nodes, e.g. halve them, and waste a little texture space, but it doesn't seem worth it.
+    const int pack_node_count = tex->Width / 2;
+    builder->PackNodes.resize(pack_node_count);
+    IM_STATIC_ASSERT(sizeof(stbrp_context) <= sizeof(stbrp_context_opaque));
+    stbrp_init_target((stbrp_context*)(void*)&builder->PackContext, tex->Width, tex->Height, builder->PackNodes.Data, builder->PackNodes.Size);
+    builder->RectsPackedSurface = builder->RectsPackedCount = 0;
+    builder->MaxRectSize = ImVec2i(0, 0);
+    builder->MaxRectBounds = ImVec2i(0, 0);
+}
 
-    const int pack_padding = atlas->TexGlyphPadding;
-    ImVector<stbrp_rect> pack_rects;
-    pack_rects.resize(user_rects.Size);
-    memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes());
-    for (int i = 0; i < user_rects.Size; i++)
+// This is essentially a free-list pattern, it may be nice to wrap it into a dedicated type.
+static ImFontAtlasRectId ImFontAtlasPackAllocRectEntry(ImFontAtlas* atlas, int rect_idx)
+{
+    ImFontAtlasBuilder* builder = (ImFontAtlasBuilder*)atlas->Builder;
+    int index_idx;
+    ImFontAtlasRectEntry* index_entry;
+    if (builder->RectsIndexFreeListStart < 0)
+    {
+        builder->RectsIndex.resize(builder->RectsIndex.Size + 1);
+        index_idx = builder->RectsIndex.Size - 1;
+        index_entry = &builder->RectsIndex[index_idx];
+        memset(index_entry, 0, sizeof(*index_entry));
+    }
+    else
     {
-        pack_rects[i].w = user_rects[i].Width + pack_padding;
-        pack_rects[i].h = user_rects[i].Height + pack_padding;
+        index_idx = builder->RectsIndexFreeListStart;
+        index_entry = &builder->RectsIndex[index_idx];
+        IM_ASSERT(index_entry->IsUsed == false && index_entry->Generation > 0); // Generation is incremented during DiscardRect
+        builder->RectsIndexFreeListStart = index_entry->TargetIndex;
     }
-    stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size);
-    for (int i = 0; i < pack_rects.Size; i++)
-        if (pack_rects[i].was_packed)
+    index_entry->TargetIndex = rect_idx;
+    index_entry->IsUsed = 1;
+    return ImFontAtlasRectId_Make(index_idx, index_entry->Generation);
+}
+
+// Overwrite existing entry
+static ImFontAtlasRectId ImFontAtlasPackReuseRectEntry(ImFontAtlas* atlas, ImFontAtlasRectEntry* index_entry)
+{
+    IM_ASSERT(index_entry->IsUsed);
+    index_entry->TargetIndex = atlas->Builder->Rects.Size - 1;
+    int index_idx = atlas->Builder->RectsIndex.index_from_ptr(index_entry);
+    return ImFontAtlasRectId_Make(index_idx, index_entry->Generation);
+}
+
+// This is expected to be called in batches and followed by a repack
+void ImFontAtlasPackDiscardRect(ImFontAtlas* atlas, ImFontAtlasRectId id)
+{
+    IM_ASSERT(id != ImFontAtlasRectId_Invalid);
+
+    ImTextureRect* rect = ImFontAtlasPackGetRect(atlas, id);
+    if (rect == NULL)
+        return;
+
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    int index_idx = ImFontAtlasRectId_GetIndex(id);
+    ImFontAtlasRectEntry* index_entry = &builder->RectsIndex[index_idx];
+    IM_ASSERT(index_entry->IsUsed && index_entry->TargetIndex >= 0);
+    index_entry->IsUsed = false;
+    index_entry->TargetIndex = builder->RectsIndexFreeListStart;
+    index_entry->Generation++;
+    if (index_entry->Generation == 0)
+        index_entry->Generation++; // Keep non-zero on overflow
+
+    const int pack_padding = atlas->TexGlyphPadding;
+    builder->RectsIndexFreeListStart = index_idx;
+    builder->RectsDiscardedCount++;
+    builder->RectsDiscardedSurface += (rect->w + pack_padding) * (rect->h + pack_padding);
+    rect->w = rect->h = 0; // Clear rectangle so it won't be packed again
+}
+
+// Important: Calling this may recreate a new texture and therefore change atlas->TexData
+// FIXME-NEWFONTS: Expose other glyph padding settings for custom alteration (e.g. drop shadows). See #7962
+ImFontAtlasRectId ImFontAtlasPackAddRect(ImFontAtlas* atlas, int w, int h, ImFontAtlasRectEntry* overwrite_entry)
+{
+    IM_ASSERT(w > 0 && w <= 0xFFFF);
+    IM_ASSERT(h > 0 && h <= 0xFFFF);
+
+    ImFontAtlasBuilder* builder = (ImFontAtlasBuilder*)atlas->Builder;
+    const int pack_padding = atlas->TexGlyphPadding;
+    builder->MaxRectSize.x = ImMax(builder->MaxRectSize.x, w);
+    builder->MaxRectSize.y = ImMax(builder->MaxRectSize.y, h);
+
+    // Pack
+    ImTextureRect r = { 0, 0, (unsigned short)w, (unsigned short)h };
+    for (int attempts_remaining = 3; attempts_remaining >= 0; attempts_remaining--)
+    {
+        // Try packing
+        stbrp_rect pack_r = {};
+        pack_r.w = w + pack_padding;
+        pack_r.h = h + pack_padding;
+        stbrp_pack_rects((stbrp_context*)(void*)&builder->PackContext, &pack_r, 1);
+        r.x = (unsigned short)pack_r.x;
+        r.y = (unsigned short)pack_r.y;
+        if (pack_r.was_packed)
+            break;
+
+        // If we ran out of attempts, return fallback
+        if (attempts_remaining == 0 || builder->LockDisableResize)
         {
-            user_rects[i].X = (unsigned short)pack_rects[i].x;
-            user_rects[i].Y = (unsigned short)pack_rects[i].y;
-            IM_ASSERT(pack_rects[i].w == user_rects[i].Width + pack_padding && pack_rects[i].h == user_rects[i].Height + pack_padding);
-            atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h);
+            IMGUI_DEBUG_LOG_FONT("[font] Failed packing %dx%d rectangle. Returning fallback.\n", w, h);
+            return ImFontAtlasRectId_Invalid;
         }
+
+        // Resize or repack atlas! (this should be a rare event)
+        ImFontAtlasTextureMakeSpace(atlas);
+    }
+
+    builder->MaxRectBounds.x = ImMax(builder->MaxRectBounds.x, r.x + r.w + pack_padding);
+    builder->MaxRectBounds.y = ImMax(builder->MaxRectBounds.y, r.y + r.h + pack_padding);
+    builder->RectsPackedCount++;
+    builder->RectsPackedSurface += (w + pack_padding) * (h + pack_padding);
+
+    builder->Rects.push_back(r);
+    if (overwrite_entry != NULL)
+        return ImFontAtlasPackReuseRectEntry(atlas, overwrite_entry); // Write into an existing entry instead of adding one (used during repack)
+    else
+        return ImFontAtlasPackAllocRectEntry(atlas, builder->Rects.Size - 1);
 }
 
-void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value)
+// Generally for non-user facing functions: assert on invalid ID.
+ImTextureRect* ImFontAtlasPackGetRect(ImFontAtlas* atlas, ImFontAtlasRectId id)
 {
-    IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth);
-    IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight);
-    unsigned char* out_pixel = atlas->TexPixelsAlpha8 + x + (y * atlas->TexWidth);
-    for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w)
-        for (int off_x = 0; off_x < w; off_x++)
-            out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : 0x00;
+    IM_ASSERT(id != ImFontAtlasRectId_Invalid);
+    int index_idx = ImFontAtlasRectId_GetIndex(id);
+    ImFontAtlasBuilder* builder = (ImFontAtlasBuilder*)atlas->Builder;
+    ImFontAtlasRectEntry* index_entry = &builder->RectsIndex[index_idx];
+    IM_ASSERT(index_entry->Generation == ImFontAtlasRectId_GetGeneration(id));
+    IM_ASSERT(index_entry->IsUsed);
+    return &builder->Rects[index_entry->TargetIndex];
 }
 
-void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value)
+// For user-facing functions: return NULL on invalid ID.
+// Important: return pointer is valid until next call to AddRect(), e.g. FindGlyph(), CalcTextSize() can all potentially invalidate previous pointers.
+ImTextureRect* ImFontAtlasPackGetRectSafe(ImFontAtlas* atlas, ImFontAtlasRectId id)
 {
-    IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth);
-    IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight);
-    unsigned int* out_pixel = atlas->TexPixelsRGBA32 + x + (y * atlas->TexWidth);
-    for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w)
-        for (int off_x = 0; off_x < w; off_x++)
-            out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : IM_COL32_BLACK_TRANS;
+    if (id == ImFontAtlasRectId_Invalid)
+        return NULL;
+    int index_idx = ImFontAtlasRectId_GetIndex(id);
+    if (atlas->Builder == NULL)
+        ImFontAtlasBuildInit(atlas);
+    ImFontAtlasBuilder* builder = (ImFontAtlasBuilder*)atlas->Builder;
+    IM_MSVC_WARNING_SUPPRESS(28182); // Static Analysis false positive "warning C28182: Dereferencing NULL pointer 'builder'"
+    if (index_idx >= builder->RectsIndex.Size)
+        return NULL;
+    ImFontAtlasRectEntry* index_entry = &builder->RectsIndex[index_idx];
+    if (index_entry->Generation != ImFontAtlasRectId_GetGeneration(id) || !index_entry->IsUsed)
+        return NULL;
+    return &builder->Rects[index_entry->TargetIndex];
 }
 
-static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas)
+// Important! This assume by ImFontConfig::GlyphExcludeRanges[] is a SMALL ARRAY (e.g. <10 entries)
+// Use "Input Glyphs Overlap Detection Tool" to display a list of glyphs provided by multiple sources in order to set this array up.
+static bool ImFontAtlasBuildAcceptCodepointForSource(ImFontConfig* src, ImWchar codepoint)
 {
-    ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors);
-    IM_ASSERT(r->IsPacked());
+    if (const ImWchar* exclude_list = src->GlyphExcludeRanges)
+        for (; exclude_list[0] != 0; exclude_list += 2)
+            if (codepoint >= exclude_list[0] && codepoint <= exclude_list[1])
+                return false;
+    return true;
+}
 
-    const int w = atlas->TexWidth;
-    if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors)
+static void ImFontBaked_BuildGrowIndex(ImFontBaked* baked, int new_size)
+{
+    IM_ASSERT(baked->IndexAdvanceX.Size == baked->IndexLookup.Size);
+    if (new_size <= baked->IndexLookup.Size)
+        return;
+    baked->IndexAdvanceX.resize(new_size, -1.0f);
+    baked->IndexLookup.resize(new_size, IM_FONTGLYPH_INDEX_UNUSED);
+}
+
+static void ImFontAtlas_FontHookRemapCodepoint(ImFontAtlas* atlas, ImFont* font, ImWchar* c)
+{
+    IM_UNUSED(atlas);
+    if (font->RemapPairs.Data.Size != 0)
+        *c = (ImWchar)font->RemapPairs.GetInt((ImGuiID)*c, (int)*c);
+}
+
+static ImFontGlyph* ImFontBaked_BuildLoadGlyph(ImFontBaked* baked, ImWchar codepoint, float* only_load_advance_x)
+{
+    ImFont* font = baked->ContainerFont;
+    ImFontAtlas* atlas = font->ContainerAtlas;
+    if (atlas->Locked || (font->Flags & ImFontFlags_NoLoadGlyphs))
     {
-        // White pixels only
-        IM_ASSERT(r->Width == 2 && r->Height == 2);
-        const int offset = (int)r->X + (int)r->Y * w;
-        if (atlas->TexPixelsAlpha8 != NULL)
-        {
-            atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF;
-        }
-        else
+        // Lazily load fallback glyph
+        if (baked->FallbackGlyphIndex == -1 && baked->LoadNoFallback == 0)
+            ImFontAtlasBuildSetupFontBakedFallback(baked);
+        return NULL;
+    }
+
+    // User remapping hooks
+    ImWchar src_codepoint = codepoint;
+    ImFontAtlas_FontHookRemapCodepoint(atlas, font, &codepoint);
+
+    //char utf8_buf[5];
+    //IMGUI_DEBUG_LOG("[font] BuildLoadGlyph U+%04X (%s)\n", (unsigned int)codepoint, ImTextCharToUtf8(utf8_buf, (unsigned int)codepoint));
+
+    // Special hook
+    // FIXME-NEWATLAS: it would be nicer if this used a more standardized way of hooking
+    if (codepoint == font->EllipsisChar && font->EllipsisAutoBake)
+        if (ImFontGlyph* glyph = ImFontAtlasBuildSetupFontBakedEllipsis(atlas, baked))
+            return glyph;
+
+    // Call backend
+    char* loader_user_data_p = (char*)baked->FontLoaderDatas;
+    int src_n = 0;
+    for (ImFontConfig* src : font->Sources)
+    {
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        if (!src->GlyphExcludeRanges || ImFontAtlasBuildAcceptCodepointForSource(src, codepoint))
         {
-            atlas->TexPixelsRGBA32[offset] = atlas->TexPixelsRGBA32[offset + 1] = atlas->TexPixelsRGBA32[offset + w] = atlas->TexPixelsRGBA32[offset + w + 1] = IM_COL32_WHITE;
+            if (only_load_advance_x == NULL)
+            {
+                ImFontGlyph glyph_buf;
+                if (loader->FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, &glyph_buf, NULL))
+                {
+                    // FIXME: Add hooks for e.g. #7962
+                    glyph_buf.Codepoint = src_codepoint;
+                    glyph_buf.SourceIdx = src_n;
+                    return ImFontAtlasBakedAddFontGlyph(atlas, baked, src, &glyph_buf);
+                }
+            }
+            else
+            {
+                // Special mode but only loading glyphs metrics. Will rasterize and pack later.
+                if (loader->FontBakedLoadGlyph(atlas, src, baked, loader_user_data_p, codepoint, NULL, only_load_advance_x))
+                {
+                    ImFontAtlasBakedAddFontGlyphAdvancedX(atlas, baked, src, codepoint, *only_load_advance_x);
+                    return NULL;
+                }
+            }
         }
+        loader_user_data_p += loader->FontBakedSrcLoaderDataSize;
+        src_n++;
+    }
+
+    // Lazily load fallback glyph
+    if (baked->LoadNoFallback)
+        return NULL;
+    if (baked->FallbackGlyphIndex == -1)
+        ImFontAtlasBuildSetupFontBakedFallback(baked);
+
+    // Mark index as not found, so we don't attempt the search twice
+    ImFontBaked_BuildGrowIndex(baked, codepoint + 1);
+    baked->IndexAdvanceX[codepoint] = baked->FallbackAdvanceX;
+    baked->IndexLookup[codepoint] = IM_FONTGLYPH_INDEX_NOT_FOUND;
+    return NULL;
+}
+
+static float ImFontBaked_BuildLoadGlyphAdvanceX(ImFontBaked* baked, ImWchar codepoint)
+{
+    if (baked->Size >= IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE || baked->LoadNoRenderOnLayout)
+    {
+        // First load AdvanceX value used by CalcTextSize() API then load the rest when loaded by drawing API.
+        float only_advance_x = 0.0f;
+        ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(baked, (ImWchar)codepoint, &only_advance_x);
+        return glyph ? glyph->AdvanceX : only_advance_x;
     }
     else
     {
-        // White pixels and mouse cursor
-        IM_ASSERT(r->Width == FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1 && r->Height == FONT_ATLAS_DEFAULT_TEX_DATA_H);
-        const int x_for_white = r->X;
-        const int x_for_black = r->X + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1;
-        if (atlas->TexPixelsAlpha8 != NULL)
-        {
-            ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', 0xFF);
-            ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', 0xFF);
-        }
-        else
+        ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(baked, (ImWchar)codepoint, NULL);
+        return glyph ? glyph->AdvanceX : baked->FallbackAdvanceX;
+    }
+}
+
+// The point of this indirection is to not be inlined in debug mode in order to not bloat inner loop.b
+IM_MSVC_RUNTIME_CHECKS_OFF
+static float BuildLoadGlyphGetAdvanceOrFallback(ImFontBaked* baked, unsigned int codepoint)
+{
+    return ImFontBaked_BuildLoadGlyphAdvanceX(baked, (ImWchar)codepoint);
+}
+IM_MSVC_RUNTIME_CHECKS_RESTORE
+
+#ifndef IMGUI_DISABLE_DEBUG_TOOLS
+void ImFontAtlasDebugLogTextureRequests(ImFontAtlas* atlas)
+{
+    // [DEBUG] Log texture update requests
+    ImGuiContext& g = *GImGui;
+    IM_UNUSED(g);
+    for (ImTextureData* tex : atlas->TexList)
+    {
+        if ((g.IO.BackendFlags & ImGuiBackendFlags_RendererHasTextures) == 0)
+            IM_ASSERT(tex->Updates.Size == 0);
+        if (tex->Status == ImTextureStatus_WantCreate)
+            IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: create %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
+        else if (tex->Status == ImTextureStatus_WantDestroy)
+            IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: destroy %dx%d, texid=0x%" IM_PRIX64 ", backend_data=%p\n", tex->UniqueID, tex->Width, tex->Height, IM_TEXTUREID_TO_U64(tex->TexID), tex->BackendUserData);
+        else if (tex->Status == ImTextureStatus_WantUpdates)
         {
-            ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', IM_COL32_WHITE);
-            ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', IM_COL32_WHITE);
+            IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: update %d regions, texid=0x%" IM_PRIX64 ", backend_data=0x%" IM_PRIX64 "\n", tex->UniqueID, tex->Updates.Size, IM_TEXTUREID_TO_U64(tex->TexID), (ImU64)(intptr_t)tex->BackendUserData);
+            for (const ImTextureRect& r : tex->Updates)
+            {
+                IM_UNUSED(r);
+                IM_ASSERT(r.x >= 0 && r.y >= 0);
+                IM_ASSERT(r.x + r.w <= tex->Width && r.y + r.h <= tex->Height); // In theory should subtract PackPadding but it's currently part of atlas and mid-frame change would wreck assert.
+                //IMGUI_DEBUG_LOG_FONT("[font] Texture #%03d: update (% 4d..%-4d)->(% 4d..%-4d), texid=0x%" IM_PRIX64 ", backend_data=0x%" IM_PRIX64 "\n", tex->UniqueID, r.x, r.y, r.x + r.w, r.y + r.h, IM_TEXTUREID_TO_U64(tex->TexID), (ImU64)(intptr_t)tex->BackendUserData);
+            }
         }
     }
-    atlas->TexUvWhitePixel = ImVec2((r->X + 0.5f) * atlas->TexUvScale.x, (r->Y + 0.5f) * atlas->TexUvScale.y);
 }
+#endif
+
+//-------------------------------------------------------------------------
+// [SECTION] ImFontAtlas: backend for stb_truetype
+//-------------------------------------------------------------------------
+// (imstb_truetype.h in included near the top of this file, when IMGUI_ENABLE_STB_TRUETYPE is set)
+//-------------------------------------------------------------------------
 
-static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas)
+#ifdef IMGUI_ENABLE_STB_TRUETYPE
+
+// One for each ConfigData
+struct ImGui_ImplStbTrueType_FontSrcData
 {
-    if (atlas->Flags & ImFontAtlasFlags_NoBakedLines)
-        return;
+    stbtt_fontinfo  FontInfo;
+    float           ScaleFactor;
+};
 
-    // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them
-    ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdLines);
-    IM_ASSERT(r->IsPacked());
-    for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row
+static bool ImGui_ImplStbTrueType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
+{
+    IM_UNUSED(atlas);
+
+    ImGui_ImplStbTrueType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplStbTrueType_FontSrcData);
+    IM_ASSERT(src->FontLoaderData == NULL);
+
+    // Initialize helper structure for font loading and verify that the TTF/OTF data is correct
+    const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)src->FontData, src->FontNo);
+    if (font_offset < 0)
     {
-        // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle
-        int y = n;
-        int line_width = n;
-        int pad_left = (r->Width - line_width) / 2;
-        int pad_right = r->Width - (pad_left + line_width);
+        IM_DELETE(bd_font_data);
+        IM_ASSERT_USER_ERROR(0, "stbtt_GetFontOffsetForIndex(): FontData is incorrect, or FontNo cannot be found.");
+        return false;
+    }
+    if (!stbtt_InitFont(&bd_font_data->FontInfo, (unsigned char*)src->FontData, font_offset))
+    {
+        IM_DELETE(bd_font_data);
+        IM_ASSERT_USER_ERROR(0, "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize.");
+        return false;
+    }
+    src->FontLoaderData = bd_font_data;
 
-        // Write each slice
-        IM_ASSERT(pad_left + line_width + pad_right == r->Width && y < r->Height); // Make sure we're inside the texture bounds before we start writing pixels
-        if (atlas->TexPixelsAlpha8 != NULL)
-        {
-            unsigned char* write_ptr = &atlas->TexPixelsAlpha8[r->X + ((r->Y + y) * atlas->TexWidth)];
-            for (int i = 0; i < pad_left; i++)
-                *(write_ptr + i) = 0x00;
+    const float ref_size = src->DstFont->Sources[0]->SizePixels;
+    if (src->MergeMode && src->SizePixels == 0.0f)
+        src->SizePixels = ref_size;
 
-            for (int i = 0; i < line_width; i++)
-                *(write_ptr + pad_left + i) = 0xFF;
+    if (src->SizePixels >= 0.0f)
+        bd_font_data->ScaleFactor = stbtt_ScaleForPixelHeight(&bd_font_data->FontInfo, 1.0f);
+    else
+        bd_font_data->ScaleFactor = stbtt_ScaleForMappingEmToPixels(&bd_font_data->FontInfo, 1.0f);
+    if (src->MergeMode && src->SizePixels != 0.0f && ref_size != 0.0f)
+        bd_font_data->ScaleFactor *= src->SizePixels / ref_size; // FIXME-NEWATLAS: Should tidy up that a bit
 
-            for (int i = 0; i < pad_right; i++)
-                *(write_ptr + pad_left + line_width + i) = 0x00;
-        }
-        else
-        {
-            unsigned int* write_ptr = &atlas->TexPixelsRGBA32[r->X + ((r->Y + y) * atlas->TexWidth)];
-            for (int i = 0; i < pad_left; i++)
-                *(write_ptr + i) = IM_COL32(255, 255, 255, 0);
+    return true;
+}
 
-            for (int i = 0; i < line_width; i++)
-                *(write_ptr + pad_left + i) = IM_COL32_WHITE;
+static void ImGui_ImplStbTrueType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
+{
+    IM_UNUSED(atlas);
+    ImGui_ImplStbTrueType_FontSrcData* bd_font_data = (ImGui_ImplStbTrueType_FontSrcData*)src->FontLoaderData;
+    IM_DELETE(bd_font_data);
+    src->FontLoaderData = NULL;
+}
 
-            for (int i = 0; i < pad_right; i++)
-                *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0);
-        }
+static bool ImGui_ImplStbTrueType_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
+{
+    IM_UNUSED(atlas);
 
-        // Calculate UVs for this line
-        ImVec2 uv0 = ImVec2((float)(r->X + pad_left - 1), (float)(r->Y + y)) * atlas->TexUvScale;
-        ImVec2 uv1 = ImVec2((float)(r->X + pad_left + line_width + 1), (float)(r->Y + y + 1)) * atlas->TexUvScale;
-        float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts
-        atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v);
-    }
+    ImGui_ImplStbTrueType_FontSrcData* bd_font_data = (ImGui_ImplStbTrueType_FontSrcData*)src->FontLoaderData;
+    IM_ASSERT(bd_font_data != NULL);
+
+    int glyph_index = stbtt_FindGlyphIndex(&bd_font_data->FontInfo, (int)codepoint);
+    return glyph_index != 0;
 }
 
-// Note: this is called / shared by both the stb_truetype and the FreeType builder
-void ImFontAtlasBuildInit(ImFontAtlas* atlas)
+static bool ImGui_ImplStbTrueType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void*)
 {
-    // Register texture region for mouse cursors or standard white pixels
-    if (atlas->PackIdMouseCursors < 0)
-    {
-        if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors))
-            atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H);
-        else
-            atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(2, 2);
-    }
+    IM_UNUSED(atlas);
 
-    // Register texture region for thick lines
-    // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row
-    if (atlas->PackIdLines < 0)
+    ImGui_ImplStbTrueType_FontSrcData* bd_font_data = (ImGui_ImplStbTrueType_FontSrcData*)src->FontLoaderData;
+    if (src->MergeMode == false)
     {
-        if (!(atlas->Flags & ImFontAtlasFlags_NoBakedLines))
-            atlas->PackIdLines = atlas->AddCustomRectRegular(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1);
+        // FIXME-NEWFONTS: reevaluate how to use sizing metrics
+        // FIXME-NEWFONTS: make use of line gap value
+        float scale_for_layout = bd_font_data->ScaleFactor * baked->Size;
+        int unscaled_ascent, unscaled_descent, unscaled_line_gap;
+        stbtt_GetFontVMetrics(&bd_font_data->FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap);
+        baked->Ascent = ImCeil(unscaled_ascent * scale_for_layout);
+        baked->Descent = ImFloor(unscaled_descent * scale_for_layout);
     }
+    return true;
 }
 
-// This is called/shared by both the stb_truetype and the FreeType builder.
-void ImFontAtlasBuildFinish(ImFontAtlas* atlas)
+static bool ImGui_ImplStbTrueType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void*, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
 {
-    // Render into our custom data blocks
-    IM_ASSERT(atlas->TexPixelsAlpha8 != NULL || atlas->TexPixelsRGBA32 != NULL);
-    ImFontAtlasBuildRenderDefaultTexData(atlas);
-    ImFontAtlasBuildRenderLinesTexData(atlas);
+    // Search for first font which has the glyph
+    ImGui_ImplStbTrueType_FontSrcData* bd_font_data = (ImGui_ImplStbTrueType_FontSrcData*)src->FontLoaderData;
+    IM_ASSERT(bd_font_data);
+    int glyph_index = stbtt_FindGlyphIndex(&bd_font_data->FontInfo, (int)codepoint);
+    if (glyph_index == 0)
+        return false;
 
-    // Register custom rectangle glyphs
-    for (int i = 0; i < atlas->CustomRects.Size; i++)
-    {
-        const ImFontAtlasCustomRect* r = &atlas->CustomRects[i];
-        if (r->Font == NULL || r->GlyphID == 0)
-            continue;
+    // Fonts unit to pixels
+    int oversample_h, oversample_v;
+    ImFontAtlasBuildGetOversampleFactors(src, baked, &oversample_h, &oversample_v);
+    const float scale_for_layout = bd_font_data->ScaleFactor * baked->Size;
+    const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
+    const float scale_for_raster_x = bd_font_data->ScaleFactor * baked->Size * rasterizer_density * oversample_h;
+    const float scale_for_raster_y = bd_font_data->ScaleFactor * baked->Size * rasterizer_density * oversample_v;
+
+    // Obtain size and advance
+    int x0, y0, x1, y1;
+    int advance, lsb;
+    stbtt_GetGlyphBitmapBoxSubpixel(&bd_font_data->FontInfo, glyph_index, scale_for_raster_x, scale_for_raster_y, 0, 0, &x0, &y0, &x1, &y1);
+    stbtt_GetGlyphHMetrics(&bd_font_data->FontInfo, glyph_index, &advance, &lsb);
+
+    // Load metrics only mode
+    if (out_advance_x != NULL)
+    {
+        IM_ASSERT(out_glyph == NULL);
+        *out_advance_x = advance * scale_for_layout;
+        return true;
+    }
+
+    // Prepare glyph
+    out_glyph->Codepoint = codepoint;
+    out_glyph->AdvanceX = advance * scale_for_layout;
+
+    // Pack and retrieve position inside texture atlas
+    // (generally based on stbtt_PackFontRangesRenderIntoRects)
+    const bool is_visible = (x0 != x1 && y0 != y1);
+    if (is_visible)
+    {
+        const int w = (x1 - x0 + oversample_h - 1);
+        const int h = (y1 - y0 + oversample_v - 1);
+        ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
+        if (pack_id == ImFontAtlasRectId_Invalid)
+        {
+            // Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
+            IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
+            return false;
+        }
+        ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
 
-        // Will ignore ImFontConfig settings: GlyphMinAdvanceX, GlyphMinAdvanceY, PixelSnapH
-        IM_ASSERT(r->Font->ContainerAtlas == atlas);
-        ImVec2 uv0, uv1;
-        atlas->CalcCustomRectUV(r, &uv0, &uv1);
-        r->Font->AddGlyph(NULL, (ImWchar)r->GlyphID, r->GlyphOffset.x, r->GlyphOffset.y, r->GlyphOffset.x + r->Width, r->GlyphOffset.y + r->Height, uv0.x, uv0.y, uv1.x, uv1.y, r->GlyphAdvanceX);
-        if (r->GlyphColored)
-            r->Font->Glyphs.back().Colored = 1;
+        // Render
+        stbtt_GetGlyphBitmapBox(&bd_font_data->FontInfo, glyph_index, scale_for_raster_x, scale_for_raster_y, &x0, &y0, &x1, &y1);
+        ImFontAtlasBuilder* builder = atlas->Builder;
+        builder->TempBuffer.resize(w * h * 1);
+        unsigned char* bitmap_pixels = builder->TempBuffer.Data;
+        memset(bitmap_pixels, 0, w * h * 1);
+
+        // Render with oversampling
+        // (those functions conveniently assert if pixels are not cleared, which is another safety layer)
+        float sub_x, sub_y;
+        stbtt_MakeGlyphBitmapSubpixelPrefilter(&bd_font_data->FontInfo, bitmap_pixels, w, h, w,
+            scale_for_raster_x, scale_for_raster_y, 0, 0, oversample_h, oversample_v, &sub_x, &sub_y, glyph_index);
+
+        const float ref_size = baked->ContainerFont->Sources[0]->SizePixels;
+        const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
+        float font_off_x = (src->GlyphOffset.x * offsets_scale);
+        float font_off_y = (src->GlyphOffset.y * offsets_scale);
+        if (src->PixelSnapH) // Snap scaled offset. This is to mitigate backward compatibility issues for GlyphOffset, but a better design would be welcome.
+            font_off_x = IM_ROUND(font_off_x);
+        if (src->PixelSnapV)
+            font_off_y = IM_ROUND(font_off_y);
+        font_off_x += sub_x;
+        font_off_y += sub_y + IM_ROUND(baked->Ascent);
+        float recip_h = 1.0f / (oversample_h * rasterizer_density);
+        float recip_v = 1.0f / (oversample_v * rasterizer_density);
+
+        // Register glyph
+        // r->x r->y are coordinates inside texture (in pixels)
+        // glyph.X0, glyph.Y0 are drawing coordinates from base text position, and accounting for oversampling.
+        out_glyph->X0 = x0 * recip_h + font_off_x;
+        out_glyph->Y0 = y0 * recip_v + font_off_y;
+        out_glyph->X1 = (x0 + (int)r->w) * recip_h + font_off_x;
+        out_glyph->Y1 = (y0 + (int)r->h) * recip_v + font_off_y;
+        out_glyph->Visible = true;
+        out_glyph->PackId = pack_id;
+        ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, bitmap_pixels, ImTextureFormat_Alpha8, w);
     }
 
-    // Build all fonts lookup tables
-    for (ImFont* font : atlas->Fonts)
-        if (font->DirtyLookupTables)
-            font->BuildLookupTable();
+    return true;
+}
 
-    atlas->TexReady = true;
+const ImFontLoader* ImFontAtlasGetFontLoaderForStbTruetype()
+{
+    static ImFontLoader loader;
+    loader.Name = "stb_truetype";
+    loader.FontSrcInit = ImGui_ImplStbTrueType_FontSrcInit;
+    loader.FontSrcDestroy = ImGui_ImplStbTrueType_FontSrcDestroy;
+    loader.FontSrcContainsGlyph = ImGui_ImplStbTrueType_FontSrcContainsGlyph;
+    loader.FontBakedInit = ImGui_ImplStbTrueType_FontBakedInit;
+    loader.FontBakedDestroy = NULL;
+    loader.FontBakedLoadGlyph = ImGui_ImplStbTrueType_FontBakedLoadGlyph;
+    return &loader;
 }
 
+#endif // IMGUI_ENABLE_STB_TRUETYPE
+
 //-------------------------------------------------------------------------
 // [SECTION] ImFontAtlas: glyph ranges helpers
 //-------------------------------------------------------------------------
 // - GetGlyphRangesDefault()
+// Obsolete functions since 1.92:
 // - GetGlyphRangesGreek()
 // - GetGlyphRangesKorean()
 // - GetGlyphRangesChineseFull()
@@ -3396,6 +4765,7 @@
     return &ranges[0];
 }
 
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
 const ImWchar*   ImFontAtlas::GetGlyphRangesGreek()
 {
     static const ImWchar ranges[] =
@@ -3645,6 +5015,7 @@
     };
     return &ranges[0];
 }
+#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
 
 //-----------------------------------------------------------------------------
 // [SECTION] ImFontGlyphRangesBuilder
@@ -3688,124 +5059,43 @@
 // [SECTION] ImFont
 //-----------------------------------------------------------------------------
 
-ImFont::ImFont()
+ImFontBaked::ImFontBaked()
 {
     memset(this, 0, sizeof(*this));
-    Scale = 1.0f;
-}
-
-ImFont::~ImFont()
-{
-    ClearOutputData();
+    FallbackGlyphIndex = -1;
 }
 
-void    ImFont::ClearOutputData()
+void ImFontBaked::ClearOutputData()
 {
-    FontSize = 0.0f;
     FallbackAdvanceX = 0.0f;
     Glyphs.clear();
     IndexAdvanceX.clear();
     IndexLookup.clear();
-    FallbackGlyph = NULL;
-    ContainerAtlas = NULL;
-    DirtyLookupTables = true;
+    FallbackGlyphIndex = -1;
     Ascent = Descent = 0.0f;
     MetricsTotalSurface = 0;
-    memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap));
 }
 
-static ImWchar FindFirstExistingGlyph(ImFont* font, const ImWchar* candidate_chars, int candidate_chars_count)
+ImFont::ImFont()
 {
-    for (int n = 0; n < candidate_chars_count; n++)
-        if (font->FindGlyphNoFallback(candidate_chars[n]) != NULL)
-            return candidate_chars[n];
-    return 0;
+    memset(this, 0, sizeof(*this));
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    Scale = 1.0f;
+#endif
 }
 
-void ImFont::BuildLookupTable()
+ImFont::~ImFont()
 {
-    int max_codepoint = 0;
-    for (int i = 0; i != Glyphs.Size; i++)
-        max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint);
-
-    // Build lookup table
-    IM_ASSERT(Glyphs.Size > 0 && "Font has not loaded glyph!");
-    IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved
-    IndexAdvanceX.clear();
-    IndexLookup.clear();
-    DirtyLookupTables = false;
-    memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap));
-    GrowIndex(max_codepoint + 1);
-    for (int i = 0; i < Glyphs.Size; i++)
-    {
-        int codepoint = (int)Glyphs[i].Codepoint;
-        IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX;
-        IndexLookup[codepoint] = (ImU16)i;
-
-        // Mark 4K page as used
-        const int page_n = codepoint / 8192;
-        Used8kPagesMap[page_n >> 3] |= 1 << (page_n & 7);
-    }
-
-    // Create a glyph to handle TAB
-    // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?)
-    if (FindGlyph((ImWchar)' '))
-    {
-        if (Glyphs.back().Codepoint != '\t')   // So we can call this function multiple times (FIXME: Flaky)
-            Glyphs.resize(Glyphs.Size + 1);
-        ImFontGlyph& tab_glyph = Glyphs.back();
-        tab_glyph = *FindGlyph((ImWchar)' ');
-        tab_glyph.Codepoint = '\t';
-        tab_glyph.AdvanceX *= IM_TABSIZE;
-        IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX;
-        IndexLookup[(int)tab_glyph.Codepoint] = (ImU16)(Glyphs.Size - 1);
-    }
-
-    // Mark special glyphs as not visible (note that AddGlyph already mark as non-visible glyphs with zero-size polygons)
-    if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)' '))
-        glyph->Visible = false;
-    if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)'\t'))
-        glyph->Visible = false;
-
-    // Setup Fallback character
-    const ImWchar fallback_chars[] = { (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' };
-    FallbackGlyph = FindGlyphNoFallback(FallbackChar);
-    if (FallbackGlyph == NULL)
-    {
-        FallbackChar = FindFirstExistingGlyph(this, fallback_chars, IM_ARRAYSIZE(fallback_chars));
-        FallbackGlyph = FindGlyphNoFallback(FallbackChar);
-        if (FallbackGlyph == NULL)
-        {
-            FallbackGlyph = &Glyphs.back();
-            FallbackChar = (ImWchar)FallbackGlyph->Codepoint;
-        }
-    }
-    FallbackAdvanceX = FallbackGlyph->AdvanceX;
-    for (int i = 0; i < max_codepoint + 1; i++)
-        if (IndexAdvanceX[i] < 0.0f)
-            IndexAdvanceX[i] = FallbackAdvanceX;
+    ClearOutputData();
+}
 
-    // Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis).
-    // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character.
-    // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots.
-    const ImWchar ellipsis_chars[] = { Sources->EllipsisChar, (ImWchar)0x2026, (ImWchar)0x0085 };
-    const ImWchar dots_chars[] = { (ImWchar)'.', (ImWchar)0xFF0E };
-    if (EllipsisChar == 0)
-        EllipsisChar = FindFirstExistingGlyph(this, ellipsis_chars, IM_ARRAYSIZE(ellipsis_chars));
-    const ImWchar dot_char = FindFirstExistingGlyph(this, dots_chars, IM_ARRAYSIZE(dots_chars));
-    if (EllipsisChar != 0)
-    {
-        EllipsisCharCount = 1;
-        EllipsisWidth = EllipsisCharStep = FindGlyph(EllipsisChar)->X1;
-    }
-    else if (dot_char != 0)
-    {
-        const ImFontGlyph* dot_glyph = FindGlyph(dot_char);
-        EllipsisChar = dot_char;
-        EllipsisCharCount = 3;
-        EllipsisCharStep = (float)(int)(dot_glyph->X1 - dot_glyph->X0) + 1.0f;
-        EllipsisWidth = ImMax(dot_glyph->AdvanceX, dot_glyph->X0 + EllipsisCharStep * 3.0f - 1.0f); // FIXME: Slightly odd for normally mono-space fonts but since this is used for trailing contents.
-    }
+void ImFont::ClearOutputData()
+{
+    if (ImFontAtlas* atlas = ContainerAtlas)
+        ImFontAtlasFontDiscardBakes(atlas, this, 0);
+    FallbackChar = EllipsisChar = 0;
+    memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap));
+    LastBaked = NULL;
 }
 
 // API is designed this way to avoid exposing the 8K page size
@@ -3821,116 +5111,258 @@
     return true;
 }
 
-void ImFont::GrowIndex(int new_size)
-{
-    IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size);
-    if (new_size <= IndexLookup.Size)
-        return;
-    IndexAdvanceX.resize(new_size, -1.0f);
-    IndexLookup.resize(new_size, (ImU16)-1);
-}
-
 // x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero.
 // Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis).
-// 'src' is not necessarily == 'this->Sources' because multiple source fonts+configs can be used to build one target font.
-void ImFont::AddGlyph(const ImFontConfig* src, ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x)
+// - 'src' is not necessarily == 'this->Sources' because multiple source fonts+configs can be used to build one target font.
+ImFontGlyph* ImFontAtlasBakedAddFontGlyph(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, const ImFontGlyph* in_glyph)
 {
+    int glyph_idx = baked->Glyphs.Size;
+    baked->Glyphs.push_back(*in_glyph);
+    ImFontGlyph* glyph = &baked->Glyphs[glyph_idx];
+    IM_ASSERT(baked->Glyphs.Size < 0xFFFE); // IndexLookup[] hold 16-bit values and -1/-2 are reserved.
+
+    // Set UV from packed rectangle
+    if (glyph->PackId != ImFontAtlasRectId_Invalid)
+    {
+        ImTextureRect* r = ImFontAtlasPackGetRect(atlas, glyph->PackId);
+        IM_ASSERT(glyph->U0 == 0.0f && glyph->V0 == 0.0f && glyph->U1 == 0.0f && glyph->V1 == 0.0f);
+        glyph->U0 = (r->x) * atlas->TexUvScale.x;
+        glyph->V0 = (r->y) * atlas->TexUvScale.y;
+        glyph->U1 = (r->x + r->w) * atlas->TexUvScale.x;
+        glyph->V1 = (r->y + r->h) * atlas->TexUvScale.y;
+        baked->MetricsTotalSurface += r->w * r->h;
+    }
+
     if (src != NULL)
     {
         // Clamp & recenter if needed
-        const float advance_x_original = advance_x;
-        advance_x = ImClamp(advance_x, src->GlyphMinAdvanceX, src->GlyphMaxAdvanceX);
-        if (advance_x != advance_x_original)
-        {
-            float char_off_x = src->PixelSnapH ? ImTrunc((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f;
-            x0 += char_off_x;
-            x1 += char_off_x;
+        const float ref_size = baked->ContainerFont->Sources[0]->SizePixels;
+        const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
+        float advance_x = ImClamp(glyph->AdvanceX, src->GlyphMinAdvanceX * offsets_scale, src->GlyphMaxAdvanceX * offsets_scale);
+        if (advance_x != glyph->AdvanceX)
+        {
+            float char_off_x = src->PixelSnapH ? ImTrunc((advance_x - glyph->AdvanceX) * 0.5f) : (advance_x - glyph->AdvanceX) * 0.5f;
+            glyph->X0 += char_off_x;
+            glyph->X1 += char_off_x;
         }
 
         // Snap to pixel
         if (src->PixelSnapH)
             advance_x = IM_ROUND(advance_x);
 
-        // Bake extra spacing
+        // Bake spacing
+        glyph->AdvanceX = advance_x + src->GlyphExtraAdvanceX;
+    }
+    if (glyph->Colored)
+        atlas->TexPixelsUseColors = atlas->TexData->UseColors = true;
+
+    // Update lookup tables
+    const int codepoint = glyph->Codepoint;
+    ImFontBaked_BuildGrowIndex(baked, codepoint + 1);
+    baked->IndexAdvanceX[codepoint] = glyph->AdvanceX;
+    baked->IndexLookup[codepoint] = (ImU16)glyph_idx;
+    const int page_n = codepoint / 8192;
+    baked->ContainerFont->Used8kPagesMap[page_n >> 3] |= 1 << (page_n & 7);
+
+    return glyph;
+}
+
+// FIXME: Code is duplicated with code above.
+void ImFontAtlasBakedAddFontGlyphAdvancedX(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImWchar codepoint, float advance_x)
+{
+    IM_UNUSED(atlas);
+    if (src != NULL)
+    {
+        // Clamp & recenter if needed
+        const float ref_size = baked->ContainerFont->Sources[0]->SizePixels;
+        const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
+        advance_x = ImClamp(advance_x, src->GlyphMinAdvanceX * offsets_scale, src->GlyphMaxAdvanceX * offsets_scale);
+
+        // Snap to pixel
+        if (src->PixelSnapH)
+            advance_x = IM_ROUND(advance_x);
+
+        // Bake spacing
         advance_x += src->GlyphExtraAdvanceX;
     }
 
-    int glyph_idx = Glyphs.Size;
-    Glyphs.resize(Glyphs.Size + 1);
-    ImFontGlyph& glyph = Glyphs[glyph_idx];
-    glyph.Codepoint = (unsigned int)codepoint;
-    glyph.Visible = (x0 != x1) && (y0 != y1);
-    glyph.Colored = false;
-    glyph.X0 = x0;
-    glyph.Y0 = y0;
-    glyph.X1 = x1;
-    glyph.Y1 = y1;
-    glyph.U0 = u0;
-    glyph.V0 = v0;
-    glyph.U1 = u1;
-    glyph.V1 = v1;
-    glyph.AdvanceX = advance_x;
-    IM_ASSERT(Glyphs.Size < 0xFFFF); // IndexLookup[] hold 16-bit values and -1 is reserved.
+    ImFontBaked_BuildGrowIndex(baked, codepoint + 1);
+    baked->IndexAdvanceX[codepoint] = advance_x;
+}
 
-    // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round)
-    // We use (U1-U0)*TexWidth instead of X1-X0 to account for oversampling.
-    float pad = ContainerAtlas->TexGlyphPadding + 0.99f;
-    DirtyLookupTables = true;
-    MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + pad) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + pad);
+// Copy to texture, post-process and queue update for backend
+void ImFontAtlasBakedSetFontGlyphBitmap(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImFontGlyph* glyph, ImTextureRect* r, const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch)
+{
+    ImTextureData* tex = atlas->TexData;
+    IM_ASSERT(r->x + r->w <= tex->Width && r->y + r->h <= tex->Height);
+    ImFontAtlasTextureBlockConvert(src_pixels, src_fmt, src_pitch, (unsigned char*)tex->GetPixelsAt(r->x, r->y), tex->Format, tex->GetPitch(), r->w, r->h);
+    ImFontAtlasPostProcessData pp_data = { atlas, baked->ContainerFont, src, baked, glyph, tex->GetPixelsAt(r->x, r->y), tex->Format, tex->GetPitch(), r->w, r->h };
+    ImFontAtlasTextureBlockPostProcess(&pp_data);
+    ImFontAtlasTextureBlockQueueUpload(atlas, tex, r->x, r->y, r->w, r->h);
 }
 
-void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst)
+void ImFont::AddRemapChar(ImWchar from_codepoint, ImWchar to_codepoint)
 {
-    IM_ASSERT(IndexLookup.Size > 0);    // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function.
-    unsigned int index_size = (unsigned int)IndexLookup.Size;
+    RemapPairs.SetInt((ImGuiID)from_codepoint, (int)to_codepoint);
+}
 
-    if (dst < index_size && IndexLookup.Data[dst] == (ImU16)-1 && !overwrite_dst) // 'dst' already exists
-        return;
-    if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op
-        return;
+// Find glyph, load if necessary, return fallback if missing
+ImFontGlyph* ImFontBaked::FindGlyph(ImWchar c)
+{
+    if (c < (size_t)IndexLookup.Size) IM_LIKELY
+    {
+        const int i = (int)IndexLookup.Data[c];
+        if (i == IM_FONTGLYPH_INDEX_NOT_FOUND)
+            return &Glyphs.Data[FallbackGlyphIndex];
+        if (i != IM_FONTGLYPH_INDEX_UNUSED)
+            return &Glyphs.Data[i];
+    }
+    ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c, NULL);
+    return glyph ? glyph : &Glyphs.Data[FallbackGlyphIndex];
+}
 
-    GrowIndex(dst + 1);
-    IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImU16)-1;
-    IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f;
+// Attempt to load but when missing, return NULL instead of FallbackGlyph
+ImFontGlyph* ImFontBaked::FindGlyphNoFallback(ImWchar c)
+{
+    if (c < (size_t)IndexLookup.Size) IM_LIKELY
+    {
+        const int i = (int)IndexLookup.Data[c];
+        if (i == IM_FONTGLYPH_INDEX_NOT_FOUND)
+            return NULL;
+        if (i != IM_FONTGLYPH_INDEX_UNUSED)
+            return &Glyphs.Data[i];
+    }
+    LoadNoFallback = true; // This is actually a rare call, not done in hot-loop, so we prioritize not adding extra cruft to ImFontBaked_BuildLoadGlyph() call sites.
+    ImFontGlyph* glyph = ImFontBaked_BuildLoadGlyph(this, c, NULL);
+    LoadNoFallback = false;
+    return glyph;
+}
+
+bool ImFontBaked::IsGlyphLoaded(ImWchar c)
+{
+    if (c < (size_t)IndexLookup.Size) IM_LIKELY
+    {
+        const int i = (int)IndexLookup.Data[c];
+        if (i == IM_FONTGLYPH_INDEX_NOT_FOUND)
+            return false;
+        if (i != IM_FONTGLYPH_INDEX_UNUSED)
+            return true;
+    }
+    return false;
 }
 
-// Find glyph, return fallback if missing
-ImFontGlyph* ImFont::FindGlyph(ImWchar c)
+// This is not fast query
+bool ImFont::IsGlyphInFont(ImWchar c)
 {
-    if (c >= (size_t)IndexLookup.Size)
-        return FallbackGlyph;
-    const ImU16 i = IndexLookup.Data[c];
-    if (i == (ImU16)-1)
-        return FallbackGlyph;
-    return &Glyphs.Data[i];
+    ImFontAtlas* atlas = ContainerAtlas;
+    ImFontAtlas_FontHookRemapCodepoint(atlas, this, &c);
+    for (ImFontConfig* src : Sources)
+    {
+        const ImFontLoader* loader = src->FontLoader ? src->FontLoader : atlas->FontLoader;
+        if (loader->FontSrcContainsGlyph != NULL && loader->FontSrcContainsGlyph(atlas, src, c))
+            return true;
+    }
+    return false;
 }
 
-ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c)
+// This is manually inlined in CalcTextSizeA() and CalcWordWrapPosition(), with a non-inline call to BuildLoadGlyphGetAdvanceOrFallback().
+IM_MSVC_RUNTIME_CHECKS_OFF
+float ImFontBaked::GetCharAdvance(ImWchar c)
 {
-    if (c >= (size_t)IndexLookup.Size)
-        return NULL;
-    const ImU16 i = IndexLookup.Data[c];
-    if (i == (ImU16)-1)
+    if ((int)c < IndexAdvanceX.Size)
+    {
+        // Missing glyphs fitting inside index will have stored FallbackAdvanceX already.
+        const float x = IndexAdvanceX.Data[c];
+        if (x >= 0.0f)
+            return x;
+    }
+    return ImFontBaked_BuildLoadGlyphAdvanceX(this, c);
+}
+IM_MSVC_RUNTIME_CHECKS_RESTORE
+
+ImGuiID ImFontAtlasBakedGetId(ImGuiID font_id, float baked_size, float rasterizer_density)
+{
+    struct { ImGuiID FontId; float BakedSize; float RasterizerDensity; } hashed_data;
+    hashed_data.FontId = font_id;
+    hashed_data.BakedSize = baked_size;
+    hashed_data.RasterizerDensity = rasterizer_density;
+    return ImHashData(&hashed_data, sizeof(hashed_data));
+}
+
+// ImFontBaked pointers are valid for the entire frame but shall never be kept between frames.
+ImFontBaked* ImFont::GetFontBaked(float size, float density)
+{
+    ImFontBaked* baked = LastBaked;
+
+    // Round font size
+    // - ImGui::PushFont() will already round, but other paths calling GetFontBaked() directly also needs it (e.g. ImFontAtlasBuildPreloadAllGlyphRanges)
+    size = ImGui::GetRoundedFontSize(size);
+
+    if (density < 0.0f)
+        density = CurrentRasterizerDensity;
+    if (baked && baked->Size == size && baked->RasterizerDensity == density)
+        return baked;
+
+    ImFontAtlas* atlas = ContainerAtlas;
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    baked = ImFontAtlasBakedGetOrAdd(atlas, this, size, density);
+    if (baked == NULL)
         return NULL;
-    return &Glyphs.Data[i];
+    baked->LastUsedFrame = builder->FrameCount;
+    LastBaked = baked;
+    return baked;
+}
+
+ImFontBaked* ImFontAtlasBakedGetOrAdd(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density)
+{
+    // FIXME-NEWATLAS: Design for picking a nearest size based on some criteria?
+    // FIXME-NEWATLAS: Altering font density won't work right away.
+    IM_ASSERT(font_size > 0.0f && font_rasterizer_density > 0.0f);
+    ImGuiID baked_id = ImFontAtlasBakedGetId(font->FontId, font_size, font_rasterizer_density);
+    ImFontAtlasBuilder* builder = atlas->Builder;
+    ImFontBaked** p_baked_in_map = (ImFontBaked**)builder->BakedMap.GetVoidPtrRef(baked_id);
+    ImFontBaked* baked = *p_baked_in_map;
+    if (baked != NULL)
+    {
+        IM_ASSERT(baked->Size == font_size && baked->ContainerFont == font && baked->BakedId == baked_id);
+        return baked;
+    }
+
+    // If atlas is locked, find closest match
+    // FIXME-OPT: This is not an optimal query.
+    if ((font->Flags & ImFontFlags_LockBakedSizes) || atlas->Locked)
+    {
+        baked = ImFontAtlasBakedGetClosestMatch(atlas, font, font_size, font_rasterizer_density);
+        if (baked != NULL)
+            return baked;
+        if (atlas->Locked)
+        {
+            IM_ASSERT(!atlas->Locked && "Cannot use dynamic font size with a locked ImFontAtlas!"); // Locked because rendering backend does not support ImGuiBackendFlags_RendererHasTextures!
+            return NULL;
+        }
+    }
+
+    // Create new
+    baked = ImFontAtlasBakedAdd(atlas, font, font_size, font_rasterizer_density, baked_id);
+    *p_baked_in_map = baked; // To avoid 'builder->BakedMap.SetVoidPtr(baked_id, baked);' while we can.
+    return baked;
 }
 
 // Trim trailing space and find beginning of next line
-static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end)
+const char* ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end, ImDrawTextFlags flags)
 {
-    while (text < text_end && ImCharIsBlankA(*text))
-        text++;
+    if ((flags & ImDrawTextFlags_WrapKeepBlanks) == 0)
+        while (text < text_end && ImCharIsBlankA(*text))
+            text++;
     if (*text == '\n')
         text++;
     return text;
 }
 
-#define ImFontGetCharAdvanceX(_FONT, _CH)  ((int)(_CH) < (_FONT)->IndexAdvanceX.Size ? (_FONT)->IndexAdvanceX.Data[_CH] : (_FONT)->FallbackAdvanceX)
-
 // Simple word-wrapping for English, not full-featured. Please submit failing cases!
 // This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end.
 // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.)
-const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width)
+const char* ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width, ImDrawTextFlags flags)
 {
     // For references, possible wrap point marked with ^
     //  "aaa bbb, ccc,ddd. eee   fff. ggg!"
@@ -3943,6 +5375,10 @@
 
     // Cut words that cannot possibly fit within one line.
     // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish"
+
+    ImFontBaked* baked = font->GetFontBaked(size);
+    const float scale = size / baked->Size;
+
     float line_width = 0.0f;
     float word_width = 0.0f;
     float blank_width = 0.0f;
@@ -3966,12 +5402,7 @@
         if (c < 32)
         {
             if (c == '\n')
-            {
-                line_width = word_width = blank_width = 0.0f;
-                inside_word = true;
-                s = next_s;
-                continue;
-            }
+                return s; // Direct return, skip "Wrap_width is too small to fit anything" path.
             if (c == '\r')
             {
                 s = next_s;
@@ -3979,7 +5410,11 @@
             }
         }
 
-        const float char_width = ImFontGetCharAdvanceX(this, c);
+        // Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);'
+        float char_width = (c < (unsigned int)baked->IndexAdvanceX.Size) ? baked->IndexAdvanceX.Data[c] : -1.0f;
+        if (char_width < 0.0f)
+            char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c);
+
         if (ImCharIsBlankW(c))
         {
             if (inside_word)
@@ -4002,11 +5437,13 @@
             {
                 prev_word_end = word_end;
                 line_width += word_width + blank_width;
+                if ((flags & ImDrawTextFlags_WrapKeepBlanks) && line_width <= wrap_width)
+                    prev_word_end = s;
                 word_width = blank_width = 0.0f;
             }
 
             // Allow wrapping after punctuation.
-            inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"');
+            inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"' && c != 0x3001 && c != 0x3002);
         }
 
         // We ignore blank width at the end of the line (they can be skipped)
@@ -4024,17 +5461,25 @@
     // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity.
     // +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol).
     if (s == text && text < text_end)
-        return s + 1;
+        return s + ImTextCountUtf8BytesFromChar(s, text_end);
     return s;
 }
 
-ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining)
+const char* ImFont::CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width)
+{
+    return ImFontCalcWordWrapPositionEx(this, size, text, text_end, wrap_width, ImDrawTextFlags_None);
+}
+
+ImVec2 ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
 {
     if (!text_end)
         text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this.
+    if (!text_end_display)
+        text_end_display = text_end;
 
+    ImFontBaked* baked = font->GetFontBaked(size);
     const float line_height = size;
-    const float scale = size / FontSize;
+    const float scale = line_height / baked->Size;
 
     ImVec2 text_size = ImVec2(0, 0);
     float line_width = 0.0f;
@@ -4043,13 +5488,14 @@
     const char* word_wrap_eol = NULL;
 
     const char* s = text_begin;
-    while (s < text_end)
+    while (s < text_end_display)
     {
+        // Word-wrapping
         if (word_wrap_enabled)
         {
             // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature.
             if (!word_wrap_eol)
-                word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width);
+                word_wrap_eol = ImFontCalcWordWrapPositionEx(font, size, s, text_end, wrap_width - line_width, flags);
 
             if (s >= word_wrap_eol)
             {
@@ -4057,8 +5503,10 @@
                     text_size.x = line_width;
                 text_size.y += line_height;
                 line_width = 0.0f;
+                s = ImTextCalcWordWrapNextLineStart(s, text_end, flags); // Wrapping skips upcoming blanks
+                if (flags & ImDrawTextFlags_StopOnNewLine)
+                    break;
                 word_wrap_eol = NULL;
-                s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks
                 continue;
             }
         }
@@ -4071,20 +5519,24 @@
         else
             s += ImTextCharFromUtf8(&c, s, text_end);
 
-        if (c < 32)
+        if (c == '\n')
         {
-            if (c == '\n')
-            {
-                text_size.x = ImMax(text_size.x, line_width);
-                text_size.y += line_height;
-                line_width = 0.0f;
-                continue;
-            }
-            if (c == '\r')
-                continue;
+            text_size.x = ImMax(text_size.x, line_width);
+            text_size.y += line_height;
+            line_width = 0.0f;
+            if (flags & ImDrawTextFlags_StopOnNewLine)
+                break;
+            continue;
         }
+        if (c == '\r')
+            continue;
+
+        // Optimized inline version of 'float char_width = GetCharAdvance((ImWchar)c);'
+        float char_width = (c < (unsigned int)baked->IndexAdvanceX.Size) ? baked->IndexAdvanceX.Data[c] : -1.0f;
+        if (char_width < 0.0f)
+            char_width = BuildLoadGlyphGetAdvanceOrFallback(baked, c);
+        char_width *= scale;
 
-        const float char_width = ImFontGetCharAdvanceX(this, c) * scale;
         if (line_width + char_width >= max_width)
         {
             s = prev_s;
@@ -4097,34 +5549,68 @@
     if (text_size.x < line_width)
         text_size.x = line_width;
 
-    if (line_width > 0 || text_size.y == 0.0f)
+    if (out_offset != NULL)
+        *out_offset = ImVec2(line_width, text_size.y + line_height);  // offset allow for the possibility of sitting after a trailing \n
+
+    if (line_width > 0 || text_size.y == 0.0f)                        // whereas size.y will ignore the trailing \n
         text_size.y += line_height;
 
-    if (remaining)
-        *remaining = s;
+    if (out_remaining != NULL)
+        *out_remaining = s;
 
     return text_size;
 }
 
+ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** out_remaining)
+{
+    return ImFontCalcTextSizeEx(this, size, max_width, wrap_width, text_begin, text_end, text_end, out_remaining, NULL, ImDrawTextFlags_None);
+}
+
 // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound.
-void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c)
+void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip)
 {
-    const ImFontGlyph* glyph = FindGlyph(c);
+    ImFontBaked* baked = GetFontBaked(size);
+    const ImFontGlyph* glyph = baked->FindGlyph(c);
     if (!glyph || !glyph->Visible)
         return;
     if (glyph->Colored)
         col |= ~IM_COL32_A_MASK;
-    float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f;
+    float scale = (size >= 0.0f) ? (size / baked->Size) : 1.0f;
     float x = IM_TRUNC(pos.x);
     float y = IM_TRUNC(pos.y);
+
+    float x1 = x + glyph->X0 * scale;
+    float x2 = x + glyph->X1 * scale;
+    if (cpu_fine_clip && (x1 > cpu_fine_clip->z || x2 < cpu_fine_clip->x))
+        return;
+    float y1 = y + glyph->Y0 * scale;
+    float y2 = y + glyph->Y1 * scale;
+    float u1 = glyph->U0;
+    float v1 = glyph->V0;
+    float u2 = glyph->U1;
+    float v2 = glyph->V1;
+
+    // Always CPU fine clip. Code extracted from RenderText().
+    // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads.
+    if (cpu_fine_clip != NULL)
+    {
+        if (x1 < cpu_fine_clip->x) { u1 = u1 + (1.0f - (x2 - cpu_fine_clip->x) / (x2 - x1)) * (u2 - u1); x1 = cpu_fine_clip->x; }
+        if (y1 < cpu_fine_clip->y) { v1 = v1 + (1.0f - (y2 - cpu_fine_clip->y) / (y2 - y1)) * (v2 - v1); y1 = cpu_fine_clip->y; }
+        if (x2 > cpu_fine_clip->z) { u2 = u1 + ((cpu_fine_clip->z - x1) / (x2 - x1)) * (u2 - u1); x2 = cpu_fine_clip->z; }
+        if (y2 > cpu_fine_clip->w) { v2 = v1 + ((cpu_fine_clip->w - y1) / (y2 - y1)) * (v2 - v1); y2 = cpu_fine_clip->w; }
+        if (y1 >= y2)
+            return;
+    }
     draw_list->PrimReserve(6, 4);
-    draw_list->PrimRectUV(ImVec2(x + glyph->X0 * scale, y + glyph->Y0 * scale), ImVec2(x + glyph->X1 * scale, y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col);
+    draw_list->PrimRectUV(ImVec2(x1, y1), ImVec2(x2, y2), ImVec2(u1, v1), ImVec2(u2, v2), col);
 }
 
 // Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound.
-void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip)
+// DO NOT CALL DIRECTLY THIS WILL CHANGE WILDLY IN 2025-2025. Use ImDrawList::AddText().
+void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, ImDrawTextFlags flags)
 {
     // Align to be pixel perfect
+begin:
     float x = IM_TRUNC(pos.x);
     float y = IM_TRUNC(pos.y);
     if (y > clip_rect.w)
@@ -4133,8 +5619,10 @@
     if (!text_end)
         text_end = text_begin + ImStrlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls.
 
-    const float scale = size / FontSize;
-    const float line_height = FontSize * scale;
+    const float line_height = size;
+    ImFontBaked* baked = GetFontBaked(size);
+
+    const float scale = size / baked->Size;
     const float origin_x = x;
     const bool word_wrap_enabled = (wrap_width > 0.0f);
 
@@ -4146,11 +5634,11 @@
             const char* line_end = (const char*)ImMemchr(s, '\n', text_end - s);
             if (word_wrap_enabled)
             {
-                // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPositionA().
-                // If the specs for CalcWordWrapPositionA() were reworked to optionally return on \n we could combine both.
+                // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPosition().
+                // If the specs for CalcWordWrapPosition() were reworked to optionally return on \n we could combine both.
                 // However it is still better than nothing performing the fast-forward!
-                s = CalcWordWrapPositionA(scale, s, line_end ? line_end : text_end, wrap_width);
-                s = CalcWordWrapNextLineStartA(s, text_end);
+                s = ImFontCalcWordWrapPositionEx(this, size, s, line_end ? line_end : text_end, wrap_width, flags);
+                s = ImTextCalcWordWrapNextLineStart(s, text_end, flags);
             }
             else
             {
@@ -4184,6 +5672,8 @@
     ImDrawVert*  vtx_write = draw_list->_VtxWritePtr;
     ImDrawIdx*   idx_write = draw_list->_IdxWritePtr;
     unsigned int vtx_index = draw_list->_VtxCurrentIdx;
+    const int cmd_count = draw_list->CmdBuffer.Size;
+    const bool cpu_fine_clip = (flags & ImDrawTextFlags_CpuFineClip) != 0;
 
     const ImU32 col_untinted = col | ~IM_COL32_A_MASK;
     const char* word_wrap_eol = NULL;
@@ -4194,7 +5684,7 @@
         {
             // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature.
             if (!word_wrap_eol)
-                word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - origin_x));
+                word_wrap_eol = ImFontCalcWordWrapPositionEx(this, size, s, text_end, wrap_width - (x - origin_x), flags);
 
             if (s >= word_wrap_eol)
             {
@@ -4203,7 +5693,7 @@
                 if (y > clip_rect.w)
                     break; // break out of main loop
                 word_wrap_eol = NULL;
-                s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks
+                s = ImTextCalcWordWrapNextLineStart(s, text_end, flags); // Wrapping skips upcoming blanks
                 continue;
             }
         }
@@ -4229,9 +5719,9 @@
                 continue;
         }
 
-        const ImFontGlyph* glyph = FindGlyph((ImWchar)c);
-        if (glyph == NULL)
-            continue;
+        const ImFontGlyph* glyph = baked->FindGlyph((ImWchar)c);
+        //if (glyph == NULL)
+        //    continue;
 
         float char_width = glyph->AdvanceX * scale;
         if (glyph->Visible)
@@ -4299,6 +5789,20 @@
         x += char_width;
     }
 
+    // Edge case: calling RenderText() with unloaded glyphs triggering texture change. It doesn't happen via ImGui:: calls because CalcTextSize() is always used.
+    if (cmd_count != draw_list->CmdBuffer.Size) //-V547
+    {
+        IM_ASSERT(draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount == 0);
+        draw_list->CmdBuffer.pop_back();
+        draw_list->PrimUnreserve(idx_count_max, vtx_count_max);
+        draw_list->AddDrawCmd();
+        //IMGUI_DEBUG_LOG("RenderText: cancel and retry to missing glyphs.\n"); // [DEBUG]
+        //draw_list->AddRectFilled(pos, pos + ImVec2(10, 10), IM_COL32(255, 0, 0, 255)); // [DEBUG]
+        goto begin;
+        //RenderText(draw_list, size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip); // FIXME-OPT: Would a 'goto begin' be better for code-gen?
+        //return;
+    }
+
     // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action.
     draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data); // Same as calling shrink()
     draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data);
@@ -4357,8 +5861,9 @@
 
 void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col)
 {
-    // FIXME-OPT: This should be baked in font.
-    draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8);
+    // FIXME-OPT: This should be baked in font now that it's easier.
+    float font_size = draw_list->_Data->FontSize;
+    draw_list->AddCircleFilled(pos, font_size * 0.20f, col, (font_size < 22) ? 8 : (font_size < 40) ? 12 : 0); // Hardcode optimal/nice tessellation threshold
 }
 
 void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz)
@@ -4641,6 +6146,7 @@
 // Copyright (c) 2004, 2005 Tristan Grimmer
 // MIT license (see License.txt in http://www.proggyfonts.net/index.php?menu=download)
 // Download and more information at http://www.proggyfonts.net or http://upperboundsinteractive.com/fonts.php
+// If you want a similar font which may be better scaled, consider using ProggyVector from the same author!
 //-----------------------------------------------------------------------------
 
 #ifndef IMGUI_DISABLE_DEFAULT_FONT
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui.h f3d-3.4.1/external/imgui/imgui.h
--- f3d-3.2.0+dfsg/external/imgui/imgui.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91.9b
+// dear imgui, v1.92.4
 // (headers)
 
 // Help:
@@ -28,16 +28,17 @@
 
 // Library Version
 // (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345')
-#define IMGUI_VERSION       "1.91.9b"
-#define IMGUI_VERSION_NUM   19191
-#define IMGUI_HAS_TABLE
+#define IMGUI_VERSION       "1.92.4"
+#define IMGUI_VERSION_NUM   19240
+#define IMGUI_HAS_TABLE             // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000
+#define IMGUI_HAS_TEXTURES          // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198
 
 /*
 
 Index of this file:
 // [SECTION] Header mess
 // [SECTION] Forward declarations and basic types
-// [SECTION] Texture identifier (ImTextureID)
+// [SECTION] Texture identifiers (ImTextureID, ImTextureRef)
 // [SECTION] Dear ImGui end-user API functions
 // [SECTION] Flags & Enumerations
 // [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs)
@@ -48,7 +49,8 @@
 // [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor)
 // [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage, ImGuiSelectionExternalStorage)
 // [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData)
-// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont)
+// [SECTION] Texture API (ImTextureFormat, ImTextureStatus, ImTextureRect, ImTextureData)
+// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFontBaked, ImFont)
 // [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport)
 // [SECTION] ImGuiPlatformIO + other Platform Dependent Interfaces (ImGuiPlatformImeData)
 // [SECTION] Obsolete functions and types
@@ -98,8 +100,10 @@
 #define IMGUI_CHECKVERSION()        ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx))
 
 // Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions.
-// (MSVC provides an equivalent mechanism via SAL Annotations but it would require the macros in a different
-//  location. e.g. #include <sal.h> + void myprintf(_Printf_format_string_ const char* format, ...))
+// (MSVC provides an equivalent mechanism via SAL Annotations but it requires the macros in a different
+//  location. e.g. #include <sal.h> + void myprintf(_Printf_format_string_ const char* format, ...),
+//  and only works when using Code Analysis, rather than just normal compiling).
+// (see https://github.com/ocornut/imgui/issues/8871 for a patch to enable this for MSVC's Code Analysis)
 #if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__)
 #define IM_FMTARGS(FMT)             __attribute__((format(gnu_printf, FMT, FMT+1)))
 #define IM_FMTLIST(FMT)             __attribute__((format(gnu_printf, FMT, 0)))
@@ -169,10 +173,15 @@
 struct ImDrawVert;                  // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT)
 struct ImFont;                      // Runtime data for a single font within a parent ImFontAtlas
 struct ImFontAtlas;                 // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader
-struct ImFontBuilderIO;             // Opaque interface to a font builder (stb_truetype or FreeType).
+struct ImFontAtlasBuilder;          // Opaque storage for building a ImFontAtlas
+struct ImFontAtlasRect;             // Output of ImFontAtlas::GetCustomRect() when using custom rectangles.
+struct ImFontBaked;                 // Baked data for a ImFont at a given size.
 struct ImFontConfig;                // Configuration data when adding a font or merging fonts
 struct ImFontGlyph;                 // A single font glyph (code point + coordinates within in ImFontAtlas + offset)
 struct ImFontGlyphRangesBuilder;    // Helper to build glyph ranges from text/string data
+struct ImFontLoader;                // Opaque interface to a font loading backend (stb_truetype, FreeType etc.).
+struct ImTextureData;               // Specs and pixel storage for a texture used by Dear ImGui.
+struct ImTextureRect;               // Coordinates of a rectangle within a texture.
 struct ImColor;                     // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using)
 
 // Forward declarations: ImGui layer
@@ -224,7 +233,9 @@
 //   - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments.
 typedef int ImDrawFlags;            // -> enum ImDrawFlags_          // Flags: for ImDrawList functions
 typedef int ImDrawListFlags;        // -> enum ImDrawListFlags_      // Flags: for ImDrawList instance
-typedef int ImFontAtlasFlags;       // -> enum ImFontAtlasFlags_     // Flags: for ImFontAtlas build
+typedef int ImDrawTextFlags;        // -> enum ImDrawTextFlags_      // Internal, do not use!
+typedef int ImFontFlags;            // -> enum ImFontFlags_          // Flags: for ImFont
+typedef int ImFontAtlasFlags;       // -> enum ImFontAtlasFlags_     // Flags: for ImFontAtlas
 typedef int ImGuiBackendFlags;      // -> enum ImGuiBackendFlags_    // Flags: for io.BackendFlags
 typedef int ImGuiButtonFlags;       // -> enum ImGuiButtonFlags_     // Flags: for InvisibleButton()
 typedef int ImGuiChildFlags;        // -> enum ImGuiChildFlags_      // Flags: for BeginChild()
@@ -238,6 +249,7 @@
 typedef int ImGuiInputTextFlags;    // -> enum ImGuiInputTextFlags_  // Flags: for InputText(), InputTextMultiline()
 typedef int ImGuiItemFlags;         // -> enum ImGuiItemFlags_       // Flags: for PushItemFlag(), shared by all items
 typedef int ImGuiKeyChord;          // -> ImGuiKey | ImGuiMod_XXX    // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values.
+typedef int ImGuiListClipperFlags;  // -> enum ImGuiListClipperFlags_// Flags: for ImGuiListClipper
 typedef int ImGuiPopupFlags;        // -> enum ImGuiPopupFlags_      // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen()
 typedef int ImGuiMultiSelectFlags;  // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect()
 typedef int ImGuiSelectableFlags;   // -> enum ImGuiSelectableFlags_ // Flags: for Selectable()
@@ -301,18 +313,67 @@
 IM_MSVC_RUNTIME_CHECKS_RESTORE
 
 //-----------------------------------------------------------------------------
-// [SECTION] Texture identifier (ImTextureID)
+// [SECTION] Texture identifiers (ImTextureID, ImTextureRef)
 //-----------------------------------------------------------------------------
 
-// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type]
-// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file.
-// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details.
-// - You can make this a structure with various constructors if you need. You will have to implement ==/!= operators.
-// - (note: before v1.91.4 (2024/10/08) the default type for ImTextureID was void*. Use intermediary intptr_t cast and read FAQ if you have casting warnings)
+// ImTextureID = backend specific, low-level identifier for a texture uploaded in GPU/graphics system.
+// [Compile-time configurable type]
+// - When a Rendered Backend creates a texture, it store its native identifier into a ImTextureID value.
+//   (e.g. Used by DX11 backend to a `ID3D11ShaderResourceView*`; Used by OpenGL backends to store `GLuint`;
+//         Used by SDLGPU backend to store a `SDL_GPUTextureSamplerBinding*`, etc.).
+// - User may submit their own textures to e.g. ImGui::Image() function by passing this value.
+// - During the rendering loop, the Renderer Backend retrieve the ImTextureID, which stored inside a
+//   ImTextureRef, which is stored inside a ImDrawCmd.
+// - Compile-time type configuration:
+//   - To use something other than a 64-bit value: add '#define ImTextureID MyTextureType*' in your imconfig.h file.
+//   - This can be whatever to you want it to be! read the FAQ entry about textures for details.
+//   - You may decide to store a higher-level structure containing texture, sampler, shader etc. with various
+//     constructors if you like. You will need to implement ==/!= operators.
+// History:
+// - In v1.91.4 (2024/10/08): the default type for ImTextureID was changed from 'void*' to 'ImU64'. This allowed backends requirig 64-bit worth of data to build on 32-bit architectures. Use intermediary intptr_t cast and read FAQ if you have casting warnings.
+// - In v1.92.0 (2025/06/11): added ImTextureRef which carry either a ImTextureID either a pointer to internal texture atlas. All user facing functions taking ImTextureID changed to ImTextureRef
 #ifndef ImTextureID
-typedef ImU64 ImTextureID;          // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that)
+typedef ImU64 ImTextureID;      // Default: store up to 64-bits (any pointer or integer). A majority of backends are ok with that.
 #endif
 
+// Define this if you need 0 to be a valid ImTextureID for your backend.
+#ifndef ImTextureID_Invalid
+#define ImTextureID_Invalid     ((ImTextureID)0)
+#endif
+
+// ImTextureRef = higher-level identifier for a texture. Store a ImTextureID _or_ a ImTextureData*.
+// The identifier is valid even before the texture has been uploaded to the GPU/graphics system.
+// This is what gets passed to functions such as `ImGui::Image()`, `ImDrawList::AddImage()`.
+// This is what gets stored in draw commands (`ImDrawCmd`) to identify a texture during rendering.
+// - When a texture is created by user code (e.g. custom images), we directly stores the low-level ImTextureID.
+//   Because of this, when displaying your own texture you are likely to ever only manage ImTextureID values on your side.
+// - When a texture is created by the backend, we stores a ImTextureData* which becomes an indirection
+//   to extract the ImTextureID value during rendering, after texture upload has happened.
+// - To create a ImTextureRef from a ImTextureData you can use ImTextureData::GetTexRef().
+//   We intentionally do not provide an ImTextureRef constructor for this: we don't expect this
+//   to be frequently useful to the end-user, and it would be erroneously called by many legacy code.
+// - If you want to bind the current atlas when using custom rectangle, you can use io.Fonts->TexRef.
+// - Binding generators for languages such as C (which don't have constructors), should provide a helper, e.g.
+//      inline ImTextureRef ImTextureRefFromID(ImTextureID tex_id) { ImTextureRef tex_ref = { ._TexData = NULL, .TexID = tex_id }; return tex_ref; }
+// In 1.92 we changed most drawing functions using ImTextureID to use ImTextureRef.
+// We intentionally do not provide an implicit ImTextureRef -> ImTextureID cast operator because it is technically lossy to convert ImTextureRef to ImTextureID before rendering.
+IM_MSVC_RUNTIME_CHECKS_OFF
+struct ImTextureRef
+{
+    ImTextureRef()                          { _TexData = NULL; _TexID = ImTextureID_Invalid; }
+    ImTextureRef(ImTextureID tex_id)        { _TexData = NULL; _TexID = tex_id; }
+#if !defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && !defined(ImTextureID)
+    ImTextureRef(void* tex_id)              { _TexData = NULL; _TexID = (ImTextureID)(size_t)tex_id; }  // For legacy backends casting to ImTextureID
+#endif
+
+    inline ImTextureID  GetTexID() const;   // == (_TexData ? _TexData->TexID : _TexID) // Implemented below in the file.
+
+    // Members (either are set, never both!)
+    ImTextureData*      _TexData;           //      A texture, generally owned by a ImFontAtlas. Will convert to ImTextureID during render loop, after texture has been uploaded.
+    ImTextureID         _TexID;             // _OR_ Low-level backend texture identifier, if already uploaded or created by user/app. Generally provided to e.g. ImGui::Image() calls.
+};
+IM_MSVC_RUNTIME_CHECKS_RESTORE
+
 //-----------------------------------------------------------------------------
 // [SECTION] Dear ImGui end-user API functions
 // (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!)
@@ -336,7 +397,7 @@
     IMGUI_API void          NewFrame();                                 // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame().
     IMGUI_API void          EndFrame();                                 // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all!
     IMGUI_API void          Render();                                   // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData().
-    IMGUI_API ImDrawData*   GetDrawData();                              // valid after Render() and until the next call to NewFrame(). this is what you have to render.
+    IMGUI_API ImDrawData*   GetDrawData();                              // valid after Render() and until the next call to NewFrame(). Call ImGui_ImplXXXX_RenderDrawData() function in your Renderer Backend to render.
 
     // Demo, Debug, Information
     IMGUI_API void          ShowDemoWindow(bool* p_open = NULL);        // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application!
@@ -418,7 +479,6 @@
     IMGUI_API void          SetWindowSize(const ImVec2& size, ImGuiCond cond = 0);                      // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects.
     IMGUI_API void          SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0);                     // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed().
     IMGUI_API void          SetWindowFocus();                                                           // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus().
-    IMGUI_API void          SetWindowFontScale(float scale);                                            // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes().
     IMGUI_API void          SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0);      // set named window position.
     IMGUI_API void          SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0);    // set named window size. set axis to 0.0f to force an auto-fit on this axis.
     IMGUI_API void          SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0);   // set named window collapsed state
@@ -438,9 +498,29 @@
     IMGUI_API void          SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f);  // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position.
     IMGUI_API void          SetScrollFromPosY(float local_y, float center_y_ratio = 0.5f);  // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position.
 
-    // Parameters stacks (shared)
-    IMGUI_API void          PushFont(ImFont* font);                                         // use NULL as a shortcut to push default font
+    // Parameters stacks (font)
+    //  - PushFont(font, 0.0f)                       // Change font and keep current size
+    //  - PushFont(NULL, 20.0f)                      // Keep font and change current size
+    //  - PushFont(font, 20.0f)                      // Change font and set size to 20.0f
+    //  - PushFont(font, style.FontSizeBase * 2.0f)  // Change font and set size to be twice bigger than current size.
+    //  - PushFont(font, font->LegacySize)           // Change font and set size to size passed to AddFontXXX() function. Same as pre-1.92 behavior.
+    // *IMPORTANT* before 1.92, fonts had a single size. They can now be dynamically be adjusted.
+    //  - In 1.92 we have REMOVED the single parameter version of PushFont() because it seems like the easiest way to provide an error-proof transition.
+    //  - PushFont(font) before 1.92 = PushFont(font, font->LegacySize) after 1.92          // Use default font size as passed to AddFontXXX() function.
+    // *IMPORTANT* global scale factors are applied over the provided size.
+    //  - Global scale factors are: 'style.FontScaleMain', 'style.FontScaleDpi' and maybe more.
+    // -  If you want to apply a factor to the _current_ font size:
+    //  - CORRECT:   PushFont(NULL, style.FontSizeBase)         // use current unscaled size    == does nothing
+    //  - CORRECT:   PushFont(NULL, style.FontSizeBase * 2.0f)  // use current unscaled size x2 == make text twice bigger
+    //  - INCORRECT: PushFont(NULL, GetFontSize())              // INCORRECT! using size after global factors already applied == GLOBAL SCALING FACTORS WILL APPLY TWICE!
+    //  - INCORRECT: PushFont(NULL, GetFontSize() * 2.0f)       // INCORRECT! using size after global factors already applied == GLOBAL SCALING FACTORS WILL APPLY TWICE!
+    IMGUI_API void          PushFont(ImFont* font, float font_size_base_unscaled);          // Use NULL as a shortcut to keep current font. Use 0.0f to keep current size.
     IMGUI_API void          PopFont();
+    IMGUI_API ImFont*       GetFont();                                                      // get current font
+    IMGUI_API float         GetFontSize();                                                  // get current scaled font size (= height in pixels). AFTER global scale factors applied. *IMPORTANT* DO NOT PASS THIS VALUE TO PushFont()! Use ImGui::GetStyle().FontSizeBase to get value before global scale factors.
+    IMGUI_API ImFontBaked*  GetFontBaked();                                                 // get current font bound at current size // == GetFont()->GetFontBaked(GetFontSize())
+
+    // Parameters stacks (shared)
     IMGUI_API void          PushStyleColor(ImGuiCol idx, ImU32 col);                        // modify a style color. always use this if you modify the style after NewFrame().
     IMGUI_API void          PushStyleColor(ImGuiCol idx, const ImVec4& col);
     IMGUI_API void          PopStyleColor(int count = 1);
@@ -462,8 +542,6 @@
 
     // Style read access
     // - Use the ShowStyleEditor() function to interactively see/edit the colors.
-    IMGUI_API ImFont*       GetFont();                                                      // get current font
-    IMGUI_API float         GetFontSize();                                                  // get current font size (= height in pixels) of current font with current scale applied
     IMGUI_API ImVec2        GetFontTexUvWhitePixel();                                       // get UV coordinate for a white pixel, useful to draw custom shapes via the ImDrawList API
     IMGUI_API ImU32         GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f);              // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList
     IMGUI_API ImU32         GetColorU32(const ImVec4& col);                                 // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList
@@ -559,16 +637,17 @@
     IMGUI_API void          ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL);
     IMGUI_API void          Bullet();                                                       // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses
     IMGUI_API bool          TextLink(const char* label);                                    // hyperlink text button, return true when clicked
-    IMGUI_API void          TextLinkOpenURL(const char* label, const char* url = NULL);     // hyperlink text button, automatically open file/url when clicked
+    IMGUI_API bool          TextLinkOpenURL(const char* label, const char* url = NULL);     // hyperlink text button, automatically open file/url when clicked
 
     // Widgets: Images
-    // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
+    // - Read about ImTextureID/ImTextureRef  here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
     // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
     // - Image() pads adds style.ImageBorderSize on each side, ImageButton() adds style.FramePadding on each side.
     // - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified.
-    IMGUI_API void          Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1));
-    IMGUI_API void          ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1));
-    IMGUI_API bool          ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1));
+    // - An obsolete version of Image(), before 1.91.9 (March 2025), had a 'tint_col' parameter which is now supported by the ImageWithBg() function.
+    IMGUI_API void          Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1));
+    IMGUI_API void          ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1));
+    IMGUI_API bool          ImageButton(const char* str_id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1));
 
     // Widgets: Combo Box (Dropdown)
     // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items.
@@ -696,7 +775,7 @@
     // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
     // - If you don't need a label you can probably simply use BeginChild() with the ImGuiChildFlags_FrameStyle flag for the same result.
     // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items.
-    // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created.
+    // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analogous to how Combos are created.
     // - Choose frame width:   size.x > 0.0f: custom  /  size.x < 0.0f or -FLT_MIN: right-align   /  size.x = 0.0f (default): use current ItemWidth
     // - Choose frame height:  size.y > 0.0f: custom  /  size.y < 0.0f or -FLT_MIN: bottom-align  /  size.y = 0.0f (default): arbitrary default height which can fit ~7 items
     IMGUI_API bool          BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region
@@ -841,7 +920,7 @@
     IMGUI_API ImGuiTableSortSpecs*  TableGetSortSpecs();                        // get latest sort specs for the table (NULL if not sorting).  Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable().
     IMGUI_API int                   TableGetColumnCount();                      // return number of columns (value passed to BeginTable)
     IMGUI_API int                   TableGetColumnIndex();                      // return current column index.
-    IMGUI_API int                   TableGetRowIndex();                         // return current row index.
+    IMGUI_API int                   TableGetRowIndex();                         // return current row index (header rows are accounted for)
     IMGUI_API const char*           TableGetColumnName(int column_n = -1);      // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column.
     IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1);     // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column.
     IMGUI_API void                  TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody)
@@ -978,8 +1057,8 @@
 
     // Inputs Utilities: Shortcut Testing & Routing [BETA]
     // - ImGuiKeyChord = a ImGuiKey + optional ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super.
-    //       ImGuiKey_C                          // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments)
-    //       ImGuiMod_Ctrl | ImGuiKey_C          // Accepted by functions taking ImGuiKeyChord arguments)
+    //       ImGuiKey_C                          // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments
+    //       ImGuiMod_Ctrl | ImGuiKey_C          // Accepted by functions taking ImGuiKeyChord arguments
     //   only ImGuiMod_XXX values are legal to combine with an ImGuiKey. You CANNOT combine two ImGuiKey values.
     // - The general idea is that several callers may register interest in a shortcut, and only one owner gets it.
     //      Parent   -> call Shortcut(Ctrl+S)    // When Parent is focused, Parent gets the shortcut.
@@ -1022,7 +1101,7 @@
     IMGUI_API void          ResetMouseDragDelta(ImGuiMouseButton button = 0);                   //
     IMGUI_API ImGuiMouseCursor GetMouseCursor();                                                // get desired mouse cursor shape. Important: reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you
     IMGUI_API void          SetMouseCursor(ImGuiMouseCursor cursor_type);                       // set desired mouse cursor shape
-    IMGUI_API void          SetNextFrameWantCaptureMouse(bool want_capture_mouse);              // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instucts your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call.
+    IMGUI_API void          SetNextFrameWantCaptureMouse(bool want_capture_mouse);              // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instructs your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call.
 
     // Clipboard Utilities
     // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard.
@@ -1107,7 +1186,7 @@
 };
 
 // Flags for ImGui::BeginChild()
-// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'.
+// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'.)
 // About using AutoResizeX/AutoResizeY flags:
 // - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints").
 // - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing.
@@ -1186,6 +1265,15 @@
     ImGuiInputTextFlags_CallbackResize      = 1 << 22,  // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this)
     ImGuiInputTextFlags_CallbackEdit        = 1 << 23,  // Callback on any edit. Note that InputText() already returns true on edit + you can always use IsItemEdited(). The callback is useful to manipulate the underlying buffer while focus is active.
 
+    // Multi-line Word-Wrapping [BETA]
+    // - Not well tested yet. Please report any incorrect cursor movement, selection behavior etc. bug to https://github.com/ocornut/imgui/issues/3237.
+    // - Wrapping style is not ideal. Wrapping of long words/sections (e.g. words larger than total available width) may be particularly unpleasing.
+    // - Wrapping width needs to always account for the possibility of a vertical scrollbar.
+    // - It is much slower than regular text fields.
+    //   Ballpark estimate of cost on my 2019 desktop PC: for a 100 KB text buffer: +~0.3 ms (Optimized) / +~1.0 ms (Debug build).
+    //   The CPU cost is very roughly proportional to text length, so a 10 KB buffer should cost about ten times less.
+    ImGuiInputTextFlags_WordWrap            = 1 << 24,  // InputTextMultine(): word-wrap lines that are too long.
+
     // Obsolete names
     //ImGuiInputTextFlags_AlwaysInsertMode  = ImGuiInputTextFlags_AlwaysOverwrite   // [renamed in 1.82] name was not matching behavior
 };
@@ -1211,12 +1299,19 @@
     ImGuiTreeNodeFlags_SpanAllColumns       = 1 << 14,  // Frame will span all columns of its container table (label will still fit in current column)
     ImGuiTreeNodeFlags_LabelSpanAllColumns  = 1 << 15,  // Label will span all columns of its container table
     //ImGuiTreeNodeFlags_NoScrollOnOpen     = 1 << 16,  // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible
-    ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 17,  // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop)
+    ImGuiTreeNodeFlags_NavLeftJumpsToParent = 1 << 17,  // Nav: left arrow moves back to parent. This is processed in TreePop() when there's an unfullfilled Left nav request remaining.
     ImGuiTreeNodeFlags_CollapsingHeader     = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog,
 
+    // [EXPERIMENTAL] Draw lines connecting TreeNode hierarchy. Discuss in GitHub issue #2920.
+    // Default value is pulled from style.TreeLinesFlags. May be overridden in TreeNode calls.
+    ImGuiTreeNodeFlags_DrawLinesNone        = 1 << 18,  // No lines drawn
+    ImGuiTreeNodeFlags_DrawLinesFull        = 1 << 19,  // Horizontal lines to child nodes. Vertical line drawn down to TreePop() position: cover full contents. Faster (for large trees).
+    ImGuiTreeNodeFlags_DrawLinesToNodes     = 1 << 20,  // Horizontal lines to child nodes. Vertical line drawn down to bottom-most child node. Slower (for large trees).
+
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
-    ImGuiTreeNodeFlags_AllowItemOverlap     = ImGuiTreeNodeFlags_AllowOverlap,  // Renamed in 1.89.7
-    ImGuiTreeNodeFlags_SpanTextWidth        = ImGuiTreeNodeFlags_SpanLabelWidth,// Renamed in 1.90.7
+    ImGuiTreeNodeFlags_NavLeftJumpsBackHere = ImGuiTreeNodeFlags_NavLeftJumpsToParent,  // Renamed in 1.92.0
+    ImGuiTreeNodeFlags_SpanTextWidth        = ImGuiTreeNodeFlags_SpanLabelWidth,        // Renamed in 1.90.7
+    //ImGuiTreeNodeFlags_AllowItemOverlap   = ImGuiTreeNodeFlags_AllowOverlap,          // Renamed in 1.89.7
 #endif
 };
 
@@ -1255,10 +1350,11 @@
     ImGuiSelectableFlags_Disabled           = 1 << 3,   // Cannot be selected, display grayed out text
     ImGuiSelectableFlags_AllowOverlap       = 1 << 4,   // (WIP) Hit testing to allow subsequent widgets to overlap this one
     ImGuiSelectableFlags_Highlight          = 1 << 5,   // Make the item be displayed as if it is hovered
+    ImGuiSelectableFlags_SelectOnNav        = 1 << 6,   // Auto-select when moved into, unless Ctrl is held. Automatic when in a BeginMultiSelect() block.
 
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     ImGuiSelectableFlags_DontClosePopups    = ImGuiSelectableFlags_NoAutoClosePopups,   // Renamed in 1.91.0
-    ImGuiSelectableFlags_AllowItemOverlap   = ImGuiSelectableFlags_AllowOverlap,        // Renamed in 1.89.7
+    //ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap,        // Renamed in 1.89.7
 #endif
 };
 
@@ -1288,10 +1384,17 @@
     ImGuiTabBarFlags_NoTabListScrollingButtons      = 1 << 4,   // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll)
     ImGuiTabBarFlags_NoTooltip                      = 1 << 5,   // Disable tooltips when hovering a tab
     ImGuiTabBarFlags_DrawSelectedOverline           = 1 << 6,   // Draw selected overline markers over selected tab
-    ImGuiTabBarFlags_FittingPolicyResizeDown        = 1 << 7,   // Resize tabs when they don't fit
-    ImGuiTabBarFlags_FittingPolicyScroll            = 1 << 8,   // Add scroll buttons when tabs don't fit
-    ImGuiTabBarFlags_FittingPolicyMask_             = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll,
-    ImGuiTabBarFlags_FittingPolicyDefault_          = ImGuiTabBarFlags_FittingPolicyResizeDown,
+
+    // Fitting/Resize policy
+    ImGuiTabBarFlags_FittingPolicyMixed             = 1 << 7,   // Shrink down tabs when they don't fit, until width is style.TabMinWidthShrink, then enable scrolling buttons.
+    ImGuiTabBarFlags_FittingPolicyShrink            = 1 << 8,   // Shrink down tabs when they don't fit
+    ImGuiTabBarFlags_FittingPolicyScroll            = 1 << 9,   // Enable scrolling buttons when tabs don't fit
+    ImGuiTabBarFlags_FittingPolicyMask_             = ImGuiTabBarFlags_FittingPolicyMixed | ImGuiTabBarFlags_FittingPolicyShrink | ImGuiTabBarFlags_FittingPolicyScroll,
+    ImGuiTabBarFlags_FittingPolicyDefault_          = ImGuiTabBarFlags_FittingPolicyMixed,
+
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    ImGuiTabBarFlags_FittingPolicyResizeDown        = ImGuiTabBarFlags_FittingPolicyShrink, // Renamed in 1.92.2
+#endif
 };
 
 // Flags for ImGui::BeginTabItem()
@@ -1453,7 +1556,7 @@
     ImGuiKey_Space,
     ImGuiKey_Enter,
     ImGuiKey_Escape,
-    ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper,
+    ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper,     // Also see ImGuiMod_Ctrl, ImGuiMod_Shift, ImGuiMod_Alt, ImGuiMod_Super below!
     ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper,
     ImGuiKey_Menu,
     ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9,
@@ -1493,32 +1596,34 @@
     ImGuiKey_AppForward,
     ImGuiKey_Oem102,                // Non-US backslash.
 
-    // Gamepad (some of those are analog values, 0.0f to 1.0f)                          // NAVIGATION ACTION
+    // Gamepad
+    // (analog values are 0.0f to 1.0f)
     // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets)
-    ImGuiKey_GamepadStart,          // Menu (Xbox)      + (Switch)   Start/Options (PS)
-    ImGuiKey_GamepadBack,           // View (Xbox)      - (Switch)   Share (PS)
-    ImGuiKey_GamepadFaceLeft,       // X (Xbox)         Y (Switch)   Square (PS)        // Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows)
-    ImGuiKey_GamepadFaceRight,      // B (Xbox)         A (Switch)   Circle (PS)        // Cancel / Close / Exit
-    ImGuiKey_GamepadFaceUp,         // Y (Xbox)         X (Switch)   Triangle (PS)      // Text Input / On-screen Keyboard
-    ImGuiKey_GamepadFaceDown,       // A (Xbox)         B (Switch)   Cross (PS)         // Activate / Open / Toggle / Tweak
-    ImGuiKey_GamepadDpadLeft,       // D-pad Left                                       // Move / Tweak / Resize Window (in Windowing mode)
-    ImGuiKey_GamepadDpadRight,      // D-pad Right                                      // Move / Tweak / Resize Window (in Windowing mode)
-    ImGuiKey_GamepadDpadUp,         // D-pad Up                                         // Move / Tweak / Resize Window (in Windowing mode)
-    ImGuiKey_GamepadDpadDown,       // D-pad Down                                       // Move / Tweak / Resize Window (in Windowing mode)
-    ImGuiKey_GamepadL1,             // L Bumper (Xbox)  L (Switch)   L1 (PS)            // Tweak Slower / Focus Previous (in Windowing mode)
-    ImGuiKey_GamepadR1,             // R Bumper (Xbox)  R (Switch)   R1 (PS)            // Tweak Faster / Focus Next (in Windowing mode)
-    ImGuiKey_GamepadL2,             // L Trig. (Xbox)   ZL (Switch)  L2 (PS) [Analog]
-    ImGuiKey_GamepadR2,             // R Trig. (Xbox)   ZR (Switch)  R2 (PS) [Analog]
-    ImGuiKey_GamepadL3,             // L Stick (Xbox)   L3 (Switch)  L3 (PS)
-    ImGuiKey_GamepadR3,             // R Stick (Xbox)   R3 (Switch)  R3 (PS)
-    ImGuiKey_GamepadLStickLeft,     // [Analog]                                         // Move Window (in Windowing mode)
-    ImGuiKey_GamepadLStickRight,    // [Analog]                                         // Move Window (in Windowing mode)
-    ImGuiKey_GamepadLStickUp,       // [Analog]                                         // Move Window (in Windowing mode)
-    ImGuiKey_GamepadLStickDown,     // [Analog]                                         // Move Window (in Windowing mode)
-    ImGuiKey_GamepadRStickLeft,     // [Analog]
-    ImGuiKey_GamepadRStickRight,    // [Analog]
-    ImGuiKey_GamepadRStickUp,       // [Analog]
-    ImGuiKey_GamepadRStickDown,     // [Analog]
+    //                              // XBOX        | SWITCH  | PLAYSTA. | -> ACTION
+    ImGuiKey_GamepadStart,          // Menu        | +       | Options  |
+    ImGuiKey_GamepadBack,           // View        | -       | Share    |
+    ImGuiKey_GamepadFaceLeft,       // X           | Y       | Square   | Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows)
+    ImGuiKey_GamepadFaceRight,      // B           | A       | Circle   | Cancel / Close / Exit
+    ImGuiKey_GamepadFaceUp,         // Y           | X       | Triangle | Text Input / On-screen Keyboard
+    ImGuiKey_GamepadFaceDown,       // A           | B       | Cross    | Activate / Open / Toggle / Tweak
+    ImGuiKey_GamepadDpadLeft,       // D-pad Left  | "       | "        | Move / Tweak / Resize Window (in Windowing mode)
+    ImGuiKey_GamepadDpadRight,      // D-pad Right | "       | "        | Move / Tweak / Resize Window (in Windowing mode)
+    ImGuiKey_GamepadDpadUp,         // D-pad Up    | "       | "        | Move / Tweak / Resize Window (in Windowing mode)
+    ImGuiKey_GamepadDpadDown,       // D-pad Down  | "       | "        | Move / Tweak / Resize Window (in Windowing mode)
+    ImGuiKey_GamepadL1,             // L Bumper    | L       | L1       | Tweak Slower / Focus Previous (in Windowing mode)
+    ImGuiKey_GamepadR1,             // R Bumper    | R       | R1       | Tweak Faster / Focus Next (in Windowing mode)
+    ImGuiKey_GamepadL2,             // L Trigger   | ZL      | L2       | [Analog]
+    ImGuiKey_GamepadR2,             // R Trigger   | ZR      | R2       | [Analog]
+    ImGuiKey_GamepadL3,             // L Stick     | L3      | L3       |
+    ImGuiKey_GamepadR3,             // R Stick     | R3      | R3       |
+    ImGuiKey_GamepadLStickLeft,     //             |         |          | [Analog] Move Window (in Windowing mode)
+    ImGuiKey_GamepadLStickRight,    //             |         |          | [Analog] Move Window (in Windowing mode)
+    ImGuiKey_GamepadLStickUp,       //             |         |          | [Analog] Move Window (in Windowing mode)
+    ImGuiKey_GamepadLStickDown,     //             |         |          | [Analog] Move Window (in Windowing mode)
+    ImGuiKey_GamepadRStickLeft,     //             |         |          | [Analog]
+    ImGuiKey_GamepadRStickRight,    //             |         |          | [Analog]
+    ImGuiKey_GamepadRStickUp,       //             |         |          | [Analog]
+    ImGuiKey_GamepadRStickDown,     //             |         |          | [Analog]
 
     // Aliases: Mouse Buttons (auto-submitted from AddMouseButtonEvent() calls)
     // - This is mirroring the data also written to io.MouseDown[], io.MouseWheel, in a format allowing them to be accessed via standard key API.
@@ -1526,11 +1631,15 @@
 
     // [Internal] Reserved for mod storage
     ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt, ImGuiKey_ReservedForModSuper,
+
+    // [Internal] If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END.
     ImGuiKey_NamedKey_END,
+    ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN,
 
     // Keyboard Modifiers (explicitly submitted by backend via AddKeyEvent() calls)
-    // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing
-    //   them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc.
+    // - Any functions taking a ImGuiKeyChord parameter can binary-or those with regular keys, e.g. Shortcut(ImGuiMod_Ctrl | ImGuiKey_S).
+    // - Those are written back into io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper for convenience,
+    //   but may be accessed via standard key API such as IsKeyPressed(), IsKeyReleased(), querying duration etc.
     // - Code polling every key (e.g. an interface to detect a key press for input mapping) might want to ignore those
     //   and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiMod_Ctrl).
     // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys.
@@ -1544,11 +1653,6 @@
     ImGuiMod_Super                  = 1 << 15, // Windows/Super (non-macOS), Ctrl (macOS)
     ImGuiMod_Mask_                  = 0xF000,  // 4-bits
 
-    // [Internal] If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END.
-    ImGuiKey_NamedKey_COUNT         = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN,
-    //ImGuiKey_KeysData_SIZE        = ImGuiKey_NamedKey_COUNT,  // Size of KeysData[]: only hold named keys
-    //ImGuiKey_KeysData_OFFSET      = ImGuiKey_NamedKey_BEGIN,  // Accesses to io.KeysData[] must use (key - ImGuiKey_NamedKey_BEGIN) index.
-
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     ImGuiKey_COUNT                  = ImGuiKey_NamedKey_END,    // Obsoleted in 1.91.5 because it was extremely misleading (since named keys don't start at 0 anymore)
     ImGuiMod_Shortcut               = ImGuiMod_Ctrl,            // Removed in 1.90.7, you can now simply use ImGuiMod_Ctrl
@@ -1610,6 +1714,7 @@
     ImGuiBackendFlags_HasMouseCursors       = 1 << 1,   // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape.
     ImGuiBackendFlags_HasSetMousePos        = 1 << 2,   // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if io.ConfigNavMoveSetMousePos is set).
     ImGuiBackendFlags_RendererHasVtxOffset  = 1 << 3,   // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices.
+    ImGuiBackendFlags_RendererHasTextures   = 1 << 4,   // Backend Renderer supports ImTextureData requests to create/update/destroy textures. This enables incremental texture updates and texture reloads. See https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md for instructions on how to upgrade your custom backend.
 };
 
 // Enumeration for PushStyleColor() / PopStyleColor()
@@ -1648,6 +1753,7 @@
     ImGuiCol_ResizeGrip,            // Resize grip in lower-right and lower-left corners of windows.
     ImGuiCol_ResizeGripHovered,
     ImGuiCol_ResizeGripActive,
+    ImGuiCol_InputTextCursor,       // InputText cursor/caret
     ImGuiCol_TabHovered,            // Tab background, when hovered
     ImGuiCol_Tab,                   // Tab background, when tab-bar is focused & tab is unselected
     ImGuiCol_TabSelected,           // Tab background, when tab-bar is focused & tab is selected
@@ -1665,8 +1771,10 @@
     ImGuiCol_TableRowBg,            // Table row background (even rows)
     ImGuiCol_TableRowBgAlt,         // Table row background (odd rows)
     ImGuiCol_TextLink,              // Hyperlink color
-    ImGuiCol_TextSelectedBg,
+    ImGuiCol_TextSelectedBg,        // Selected text inside an InputText
+    ImGuiCol_TreeLines,             // Tree node hierarchy outlines when using ImGuiTreeNodeFlags_DrawLines
     ImGuiCol_DragDropTarget,        // Rectangle highlighting a drop target
+    ImGuiCol_UnsavedMarker,         // Unsaved Document marker (in window title and tabs)
     ImGuiCol_NavCursor,             // Color of keyboard/gamepad navigation cursor/rectangle, when visible
     ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB
     ImGuiCol_NavWindowingDimBg,     // Darken/colorize entire screen behind the CTRL+TAB window list, when active
@@ -1712,15 +1820,20 @@
     ImGuiStyleVar_CellPadding,              // ImVec2    CellPadding
     ImGuiStyleVar_ScrollbarSize,            // float     ScrollbarSize
     ImGuiStyleVar_ScrollbarRounding,        // float     ScrollbarRounding
+    ImGuiStyleVar_ScrollbarPadding,         // float     ScrollbarPadding
     ImGuiStyleVar_GrabMinSize,              // float     GrabMinSize
     ImGuiStyleVar_GrabRounding,             // float     GrabRounding
     ImGuiStyleVar_ImageBorderSize,          // float     ImageBorderSize
     ImGuiStyleVar_TabRounding,              // float     TabRounding
     ImGuiStyleVar_TabBorderSize,            // float     TabBorderSize
+    ImGuiStyleVar_TabMinWidthBase,          // float     TabMinWidthBase
+    ImGuiStyleVar_TabMinWidthShrink,        // float     TabMinWidthShrink
     ImGuiStyleVar_TabBarBorderSize,         // float     TabBarBorderSize
     ImGuiStyleVar_TabBarOverlineSize,       // float     TabBarOverlineSize
     ImGuiStyleVar_TableAngledHeadersAngle,  // float     TableAngledHeadersAngle
     ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2  TableAngledHeadersTextAlign
+    ImGuiStyleVar_TreeLinesSize,            // float     TreeLinesSize
+    ImGuiStyleVar_TreeLinesRounding,        // float     TreeLinesRounding
     ImGuiStyleVar_ButtonTextAlign,          // ImVec2    ButtonTextAlign
     ImGuiStyleVar_SelectableTextAlign,      // ImVec2    SelectableTextAlign
     ImGuiStyleVar_SeparatorTextBorderSize,  // float     SeparatorTextBorderSize
@@ -2084,7 +2197,7 @@
     // Constructors, destructor
     inline ImVector()                                       { Size = Capacity = 0; Data = NULL; }
     inline ImVector(const ImVector<T>& src)                 { Size = Capacity = 0; Data = NULL; operator=(src); }
-    inline ImVector<T>& operator=(const ImVector<T>& src)   { clear(); resize(src.Size); if (src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; }
+    inline ImVector<T>& operator=(const ImVector<T>& src)   { clear(); resize(src.Size); if (Data && src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; }
     inline ~ImVector()                                      { if (Data) IM_FREE(Data); } // Important: does not destruct anything
 
     inline void         clear()                             { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } }  // Important: does not destruct anything
@@ -2144,6 +2257,12 @@
 
 struct ImGuiStyle
 {
+    // Font scaling
+    // - recap: ImGui::GetFontSize() == FontSizeBase * (FontScaleMain * FontScaleDpi * other_scaling_factors)
+    float       FontSizeBase;               // Current base font size before external global factors are applied. Use PushFont(NULL, size) to modify. Use ImGui::GetFontSize() to obtain scaled value.
+    float       FontScaleMain;              // Main global scale factor. May be set by application once, or exposed to end-user.
+    float       FontScaleDpi;               // Additional global scale factor from viewport/monitor contents scale. When io.ConfigDpiScaleFonts is enabled, this is automatically overwritten when changing monitor DPI.
+
     float       Alpha;                      // Global alpha applies to everything in Dear ImGui.
     float       DisabledAlpha;              // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha.
     ImVec2      WindowPadding;              // Padding within a window.
@@ -2168,18 +2287,24 @@
     float       ColumnsMinSpacing;          // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1).
     float       ScrollbarSize;              // Width of the vertical scrollbar, Height of the horizontal scrollbar.
     float       ScrollbarRounding;          // Radius of grab corners for scrollbar.
+    float       ScrollbarPadding;           // Padding of scrollbar grab within its frame (same for both axises).
     float       GrabMinSize;                // Minimum width/height of a grab box for slider/scrollbar.
     float       GrabRounding;               // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs.
     float       LogSliderDeadzone;          // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero.
     float       ImageBorderSize;            // Thickness of border around Image() calls.
     float       TabRounding;                // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs.
     float       TabBorderSize;              // Thickness of border around tabs.
+    float       TabMinWidthBase;            // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected.
+    float       TabMinWidthShrink;          // Minimum tab width after shrinking, when using ImGuiTabBarFlags_FittingPolicyMixed policy.
     float       TabCloseButtonMinWidthSelected;     // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width.
     float       TabCloseButtonMinWidthUnselected;   // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected.
     float       TabBarBorderSize;           // Thickness of tab-bar separator, which takes on the tab active color to denote focus.
     float       TabBarOverlineSize;         // Thickness of tab-bar overline, which highlights the selected tab-bar.
     float       TableAngledHeadersAngle;    // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees).
     ImVec2      TableAngledHeadersTextAlign;// Alignment of angled headers within the cell
+    ImGuiTreeNodeFlags TreeLinesFlags;      // Default way to draw lines connecting TreeNode hierarchy. ImGuiTreeNodeFlags_DrawLinesNone or ImGuiTreeNodeFlags_DrawLinesFull or ImGuiTreeNodeFlags_DrawLinesToNodes.
+    float       TreeLinesSize;              // Thickness of outlines when using ImGuiTreeNodeFlags_DrawLines.
+    float       TreeLinesRounding;          // Radius of lines connecting child nodes to the vertical line.
     ImGuiDir    ColorButtonPosition;        // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right.
     ImVec2      ButtonTextAlign;            // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered).
     ImVec2      SelectableTextAlign;        // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line.
@@ -2206,8 +2331,13 @@
     ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse.
     ImGuiHoveredFlags HoverFlagsForTooltipNav;  // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad.
 
-    IMGUI_API ImGuiStyle();
-    IMGUI_API void ScaleAllSizes(float scale_factor);
+    // [Internal]
+    float       _MainScale;                 // FIXME-WIP: Reference scale, as applied by ScaleAllSizes().
+    float       _NextFrameFontSizeBase;     // FIXME: Temporary hack until we finish remaining work.
+
+    // Functions
+    IMGUI_API   ImGuiStyle();
+    IMGUI_API   void ScaleAllSizes(float scale_factor); // Scale all spacing/padding/thickness values. Do not scale fonts.
 
     // Obsolete names
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
@@ -2245,7 +2375,8 @@
 
     ImGuiConfigFlags   ConfigFlags;             // = 0              // See ImGuiConfigFlags_ enum. Set by user/application. Keyboard/Gamepad navigation options, etc.
     ImGuiBackendFlags  BackendFlags;            // = 0              // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend.
-    ImVec2      DisplaySize;                    // <unset>          // Main display size, in pixels (generally == GetMainViewport()->Size). May change every frame.
+    ImVec2      DisplaySize;                    // <unset>          // Main display size, in pixels (== GetMainViewport()->Size). May change every frame.
+    ImVec2      DisplayFramebufferScale;        // = (1, 1)         // Main display density. For retina display where window coordinates are different from framebuffer coordinates. This will affect font density + will end up in ImDrawData::FramebufferScale.
     float       DeltaTime;                      // = 1.0f/60.0f     // Time elapsed since last frame, in seconds. May change every frame.
     float       IniSavingRate;                  // = 5.0f           // Minimum time between saving positions/sizes to .ini file, in seconds.
     const char* IniFilename;                    // = "imgui.ini"    // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions.
@@ -2254,10 +2385,8 @@
 
     // Font system
     ImFontAtlas*Fonts;                          // <auto>           // Font atlas: load, rasterize and pack one or more fonts into a single texture.
-    float       FontGlobalScale;                // = 1.0f           // Global scale all fonts
-    bool        FontAllowUserScaling;           // = false          // [OBSOLETE] Allow user scaling text of individual window with CTRL+Wheel.
     ImFont*     FontDefault;                    // = NULL           // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0].
-    ImVec2      DisplayFramebufferScale;        // = (1, 1)         // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale.
+    bool        FontAllowUserScaling;           // = false          // [OBSOLETE] Allow user scaling text of individual window with CTRL+Wheel.
 
     // Keyboard/Gamepad Navigation options
     bool        ConfigNavSwapGamepadButtons;    // = false          // Swap Activate<>Cancel (A<>B) buttons, matching typical "Nintendo/Japanese style" gamepad layout.
@@ -2316,7 +2445,7 @@
     // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro.
     // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability.
     // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application.
-    //   e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version).
+    //   e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version.
     bool        ConfigDebugIsDebuggerPresent;   // = false          // Enable various tools calling IM_DEBUG_BREAK().
 
     // Tools to detect code submitting items with conflicting/duplicate IDs
@@ -2413,14 +2542,14 @@
     float       MouseWheel;                         // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll.
     float       MouseWheelH;                        // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends.
     ImGuiMouseSource MouseSource;                   // Mouse actual input peripheral (Mouse/TouchScreen/Pen).
-    bool        KeyCtrl;                            // Keyboard modifier down: Control
+    bool        KeyCtrl;                            // Keyboard modifier down: Ctrl (non-macOS), Cmd (macOS)
     bool        KeyShift;                           // Keyboard modifier down: Shift
     bool        KeyAlt;                             // Keyboard modifier down: Alt
-    bool        KeySuper;                           // Keyboard modifier down: Cmd/Super/Windows
+    bool        KeySuper;                           // Keyboard modifier down: Windows/Super (non-macOS), Ctrl (macOS)
 
     // Other state maintained from data above + IO function calls
-    ImGuiKeyChord KeyMods;                          // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. Read-only, updated by NewFrame()
-    ImGuiKeyData  KeysData[ImGuiKey_NamedKey_COUNT];// Key state for all known keys. Use IsKeyXXX() functions to access this.
+    ImGuiKeyChord KeyMods;                          // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags). Read-only, updated by NewFrame()
+    ImGuiKeyData  KeysData[ImGuiKey_NamedKey_COUNT];// Key state for all known keys. MUST use 'key - ImGuiKey_NamedKey_BEGIN' as index. Use IsKeyXXX() functions to access this.
     bool        WantCaptureMouseUnlessPopupClose;   // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup.
     ImVec2      MousePosPrev;                       // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid)
     ImVec2      MouseClickedPos[5];                 // Position at time of clicking
@@ -2454,9 +2583,11 @@
     //float     NavInputs[ImGuiNavInput_COUNT];     // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums.
     //void*     ImeWindowHandle;                    // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning.
 
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    float       FontGlobalScale;                    // Moved io.FontGlobalScale to style.FontScaleMain in 1.92 (June 2025)
+
     // Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO.
     // As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete).
-#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     const char* (*GetClipboardTextFn)(void* user_data);
     void        (*SetClipboardTextFn)(void* user_data, const char* text);
     void*       ClipboardUserData;
@@ -2494,10 +2625,10 @@
     ImGuiKey            EventKey;       // Key pressed (Up/Down/TAB)            // Read-only    // [Completion,History]
     char*               Buf;            // Text buffer                          // Read-write   // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer!
     int                 BufTextLen;     // Text length (in bytes)               // Read-write   // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length()
-    int                 BufSize;        // Buffer size (in bytes) = capacity+1  // Read-only    // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1
+    int                 BufSize;        // Buffer size (in bytes) = capacity+1  // Read-only    // [Resize,Completion,History,Always] Include zero-terminator storage. In C land: == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1
     bool                BufDirty;       // Set if you modify Buf/BufTextLen!    // Write        // [Completion,History,Always]
     int                 CursorPos;      //                                      // Read-write   // [Completion,History,Always]
-    int                 SelectionStart; //                                      // Read-write   // [Completion,History,Always] == to SelectionEnd when no selection)
+    int                 SelectionStart; //                                      // Read-write   // [Completion,History,Always] == to SelectionEnd when no selection
     int                 SelectionEnd;   //                                      // Read-write   // [Completion,History,Always]
 
     // Helper functions for text manipulation.
@@ -2666,6 +2797,13 @@
 #endif
 };
 
+// Flags for ImGuiListClipper (currently not fully exposed in function calls: a future refactor will likely add this to ImGuiListClipper::Begin function equivalent)
+enum ImGuiListClipperFlags_
+{
+    ImGuiListClipperFlags_None                  = 0,
+    ImGuiListClipperFlags_NoSetTableRowCounters = 1 << 0,   // [Internal] Disabled modifying table row counters. Avoid assumption that 1 clipper item == 1 table row.
+};
+
 // Helper: Manually clip large list of items.
 // If you have lots evenly spaced items and you have random access to the list, you can perform coarse
 // clipping based on visibility to only submit items that are in view.
@@ -2693,9 +2831,10 @@
     int             DisplayEnd;         // End of items to display (exclusive)
     int             ItemsCount;         // [Internal] Number of items
     float           ItemsHeight;        // [Internal] Height of item after a first step and item submission can calculate it
-    float           StartPosY;          // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed
+    double          StartPosY;          // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed
     double          StartSeekOffsetY;   // [Internal] Account for frozen rows in a table and initial loss of precision in very large windows.
     void*           TempData;           // [Internal] Internal data
+    ImGuiListClipperFlags Flags;        // [Internal] Flags, currently not yet well exposed.
 
     // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step, and you can call SeekCursorForItem() manually if you need)
     // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing().
@@ -2716,8 +2855,8 @@
     IMGUI_API void  SeekCursorForItem(int item_index);
 
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
-    inline void IncludeRangeByIndices(int item_begin, int item_end)      { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9]
-    inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6]
+    //inline void IncludeRangeByIndices(int item_begin, int item_end)      { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9]
+    //inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6]
     //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79]
 #endif
 };
@@ -2730,26 +2869,32 @@
 #ifdef IMGUI_DEFINE_MATH_OPERATORS
 #define IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED
 IM_MSVC_RUNTIME_CHECKS_OFF
-static inline ImVec2  operator*(const ImVec2& lhs, const float rhs)     { return ImVec2(lhs.x * rhs, lhs.y * rhs); }
-static inline ImVec2  operator/(const ImVec2& lhs, const float rhs)     { return ImVec2(lhs.x / rhs, lhs.y / rhs); }
-static inline ImVec2  operator+(const ImVec2& lhs, const ImVec2& rhs)   { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); }
-static inline ImVec2  operator-(const ImVec2& lhs, const ImVec2& rhs)   { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); }
-static inline ImVec2  operator*(const ImVec2& lhs, const ImVec2& rhs)   { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); }
-static inline ImVec2  operator/(const ImVec2& lhs, const ImVec2& rhs)   { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); }
-static inline ImVec2  operator-(const ImVec2& lhs)                      { return ImVec2(-lhs.x, -lhs.y); }
-static inline ImVec2& operator*=(ImVec2& lhs, const float rhs)          { lhs.x *= rhs; lhs.y *= rhs; return lhs; }
-static inline ImVec2& operator/=(ImVec2& lhs, const float rhs)          { lhs.x /= rhs; lhs.y /= rhs; return lhs; }
-static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs)        { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; }
-static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs)        { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; }
-static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs)        { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; }
-static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs)        { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; }
-static inline bool    operator==(const ImVec2& lhs, const ImVec2& rhs)  { return lhs.x == rhs.x && lhs.y == rhs.y; }
-static inline bool    operator!=(const ImVec2& lhs, const ImVec2& rhs)  { return lhs.x != rhs.x || lhs.y != rhs.y; }
-static inline ImVec4  operator+(const ImVec4& lhs, const ImVec4& rhs)   { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); }
-static inline ImVec4  operator-(const ImVec4& lhs, const ImVec4& rhs)   { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); }
-static inline ImVec4  operator*(const ImVec4& lhs, const ImVec4& rhs)   { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); }
-static inline bool    operator==(const ImVec4& lhs, const ImVec4& rhs)  { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; }
-static inline bool    operator!=(const ImVec4& lhs, const ImVec4& rhs)  { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; }
+// ImVec2 operators
+inline ImVec2  operator*(const ImVec2& lhs, const float rhs)    { return ImVec2(lhs.x * rhs, lhs.y * rhs); }
+inline ImVec2  operator/(const ImVec2& lhs, const float rhs)    { return ImVec2(lhs.x / rhs, lhs.y / rhs); }
+inline ImVec2  operator+(const ImVec2& lhs, const ImVec2& rhs)  { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); }
+inline ImVec2  operator-(const ImVec2& lhs, const ImVec2& rhs)  { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); }
+inline ImVec2  operator*(const ImVec2& lhs, const ImVec2& rhs)  { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); }
+inline ImVec2  operator/(const ImVec2& lhs, const ImVec2& rhs)  { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); }
+inline ImVec2  operator-(const ImVec2& lhs)                     { return ImVec2(-lhs.x, -lhs.y); }
+inline ImVec2& operator*=(ImVec2& lhs, const float rhs)         { lhs.x *= rhs; lhs.y *= rhs; return lhs; }
+inline ImVec2& operator/=(ImVec2& lhs, const float rhs)         { lhs.x /= rhs; lhs.y /= rhs; return lhs; }
+inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs)       { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; }
+inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs)       { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; }
+inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs)       { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; }
+inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs)       { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; }
+inline bool    operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; }
+inline bool    operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; }
+// ImVec4 operators
+inline ImVec4  operator*(const ImVec4& lhs, const float rhs)    { return ImVec4(lhs.x * rhs, lhs.y * rhs, lhs.z * rhs, lhs.w * rhs); }
+inline ImVec4  operator/(const ImVec4& lhs, const float rhs)    { return ImVec4(lhs.x / rhs, lhs.y / rhs, lhs.z / rhs, lhs.w / rhs); }
+inline ImVec4  operator+(const ImVec4& lhs, const ImVec4& rhs)  { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); }
+inline ImVec4  operator-(const ImVec4& lhs, const ImVec4& rhs)  { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); }
+inline ImVec4  operator*(const ImVec4& lhs, const ImVec4& rhs)  { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); }
+inline ImVec4  operator/(const ImVec4& lhs, const ImVec4& rhs)  { return ImVec4(lhs.x / rhs.x, lhs.y / rhs.y, lhs.z / rhs.z, lhs.w / rhs.w); }
+inline ImVec4  operator-(const ImVec4& lhs)                     { return ImVec4(-lhs.x, -lhs.y, -lhs.z, -lhs.w); }
+inline bool    operator==(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; }
+inline bool    operator!=(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; }
 IM_MSVC_RUNTIME_CHECKS_RESTORE
 #endif
 
@@ -2983,11 +3128,11 @@
 // - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled,
 //   this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices.
 //   Backends made for <1.71. will typically ignore the VtxOffset fields.
-// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for).
+// - The ClipRect/TexRef/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for).
 struct ImDrawCmd
 {
     ImVec4          ClipRect;           // 4*4  // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates
-    ImTextureID     TextureId;          // 4-8  // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas.
+    ImTextureRef    TexRef;             // 16   // Reference to a font/texture atlas (where backend called ImTextureData::SetTexID()) or to a user-provided texture ID (via e.g. ImGui::Image() calls). Both will lead to a ImTextureID value.
     unsigned int    VtxOffset;          // 4    // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices.
     unsigned int    IdxOffset;          // 4    // Start offset in index buffer.
     unsigned int    ElemCount;          // 4    // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[].
@@ -2999,7 +3144,8 @@
     ImDrawCmd()     { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed
 
     // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature)
-    inline ImTextureID GetTexID() const { return TextureId; }
+    // Since 1.92: removed ImDrawCmd::TextureId field, the getter function must be used!
+    inline ImTextureID GetTexID() const;    // == (TexRef._TexData ? TexRef._TexData->TexID : TexRef._TexID)
 };
 
 // Vertex layout
@@ -3022,7 +3168,7 @@
 struct ImDrawCmdHeader
 {
     ImVec4          ClipRect;
-    ImTextureID     TextureId;
+    ImTextureRef    TexRef;
     unsigned int    VtxOffset;
 };
 
@@ -3107,7 +3253,7 @@
     ImDrawCmdHeader         _CmdHeader;         // [Internal] template of active commands. Fields should match those of CmdBuffer.back().
     ImDrawListSplitter      _Splitter;          // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!)
     ImVector<ImVec4>        _ClipRectStack;     // [Internal]
-    ImVector<ImTextureID>   _TextureIdStack;    // [Internal]
+    ImVector<ImTextureRef>  _TextureStack;      // [Internal]
     ImVector<ImU8>          _CallbacksDataBuf;  // [Internal]
     float                   _FringeScale;       // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content
     const char*             _OwnerName;         // Pointer to owner window's name for debugging
@@ -3120,8 +3266,8 @@
     IMGUI_API void  PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect = false);  // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling)
     IMGUI_API void  PushClipRectFullScreen();
     IMGUI_API void  PopClipRect();
-    IMGUI_API void  PushTextureID(ImTextureID texture_id);
-    IMGUI_API void  PopTextureID();
+    IMGUI_API void  PushTexture(ImTextureRef tex_ref);
+    IMGUI_API void  PopTexture();
     inline ImVec2   GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); }
     inline ImVec2   GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); }
 
@@ -3159,12 +3305,12 @@
     IMGUI_API void  AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col);
 
     // Image primitives
-    // - Read FAQ to understand what ImTextureID is.
+    // - Read FAQ to understand what ImTextureID/ImTextureRef are.
     // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle.
     // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture.
-    IMGUI_API void  AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE);
-    IMGUI_API void  AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE);
-    IMGUI_API void  AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0);
+    IMGUI_API void  AddImage(ImTextureRef tex_ref, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE);
+    IMGUI_API void  AddImageQuad(ImTextureRef tex_ref, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE);
+    IMGUI_API void  AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0);
 
     // Stateful path API, add points then finish with PathFillConvex() or PathStroke()
     // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing.
@@ -3195,7 +3341,7 @@
 
     // Advanced: Miscellaneous
     IMGUI_API void  AddDrawCmd();                                               // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible
-    IMGUI_API ImDrawList* CloneOutput() const;                                  // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer.
+    IMGUI_API ImDrawList* CloneOutput() const;                                  // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. For multi-threaded rendering, consider using `imgui_threaded_rendering` from https://github.com/ocornut/imgui_club instead.
 
     // Advanced: Channels
     // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives)
@@ -3220,6 +3366,10 @@
     inline    void  PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col)         { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index
 
     // Obsolete names
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    inline    void  PushTextureID(ImTextureRef tex_ref) { PushTexture(tex_ref); }   // RENAMED in 1.92.x
+    inline    void  PopTextureID()                      { PopTexture(); }           // RENAMED in 1.92.x
+#endif
     //inline  void  AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024)
     //inline  void  AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); }                        // OBSOLETED in 1.90.5 (Mar 2024)
     //inline  void  PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0) { PathEllipticalArcTo(center, ImVec2(radius_x, radius_y), rot, a_min, a_max, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024)
@@ -3227,14 +3377,15 @@
     //inline  void  PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); }                                                                                // OBSOLETED in 1.80 (Jan 2021)
 
     // [Internal helpers]
+    IMGUI_API void  _SetDrawListSharedData(ImDrawListSharedData* data);
     IMGUI_API void  _ResetForNewFrame();
     IMGUI_API void  _ClearFreeMemory();
     IMGUI_API void  _PopUnusedDrawCmd();
     IMGUI_API void  _TryMergeDrawCmds();
     IMGUI_API void  _OnChangedClipRect();
-    IMGUI_API void  _OnChangedTextureID();
+    IMGUI_API void  _OnChangedTexture();
     IMGUI_API void  _OnChangedVtxOffset();
-    IMGUI_API void  _SetTextureID(ImTextureID texture_id);
+    IMGUI_API void  _SetTexture(ImTextureRef tex_ref);
     IMGUI_API int   _CalcCircleAutoSegmentCount(float radius) const;
     IMGUI_API void  _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step);
     IMGUI_API void  _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments);
@@ -3246,14 +3397,15 @@
 struct ImDrawData
 {
     bool                Valid;              // Only valid after Render() is called and before the next NewFrame() is called.
-    int                 CmdListsCount;      // Number of ImDrawList* to render (should always be == CmdLists.size)
+    int                 CmdListsCount;      // == CmdLists.Size. (OBSOLETE: exists for legacy reasons). Number of ImDrawList* to render.
     int                 TotalIdxCount;      // For convenience, sum of all ImDrawList's IdxBuffer.Size
     int                 TotalVtxCount;      // For convenience, sum of all ImDrawList's VtxBuffer.Size
     ImVector<ImDrawList*> CmdLists;         // Array of ImDrawList* to render. The ImDrawLists are owned by ImGuiContext and only pointed to from here.
     ImVec2              DisplayPos;         // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications)
     ImVec2              DisplaySize;        // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications)
-    ImVec2              FramebufferScale;   // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display.
+    ImVec2              FramebufferScale;   // Amount of pixels for each unit of DisplaySize. Copied from viewport->FramebufferScale (== io.DisplayFramebufferScale for main viewport). Generally (1,1) on normal display, (2,2) on OSX with Retina display.
     ImGuiViewport*      OwnerViewport;      // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not).
+    ImVector<ImTextureData*>* Textures;     // List of textures to update. Most of the times the list is shared by all ImDrawData, has only 1 texture and it doesn't need any update. This almost always points to ImGui::GetPlatformIO().Textures[]. May be overriden or set to NULL if you want to manually update textures.
 
     // Functions
     ImDrawData()    { Clear(); }
@@ -3264,49 +3416,145 @@
 };
 
 //-----------------------------------------------------------------------------
+// [SECTION] Texture API (ImTextureFormat, ImTextureStatus, ImTextureRect, ImTextureData)
+//-----------------------------------------------------------------------------
+// In principle, the only data types that user/application code should care about are 'ImTextureRef' and 'ImTextureID'.
+// They are defined above in this header file. Read their description to the difference between ImTextureRef and ImTextureID.
+// FOR ALL OTHER ImTextureXXXX TYPES: ONLY CORE LIBRARY AND RENDERER BACKENDS NEED TO KNOW AND CARE ABOUT THEM.
+//-----------------------------------------------------------------------------
+
+#undef Status // X11 headers are leaking this.
+
+// We intentionally support a limited amount of texture formats to limit burden on CPU-side code and extension.
+// Most standard backends only support RGBA32 but we provide a single channel option for low-resource/embedded systems.
+enum ImTextureFormat
+{
+    ImTextureFormat_RGBA32,         // 4 components per pixel, each is unsigned 8-bit. Total size = TexWidth * TexHeight * 4
+    ImTextureFormat_Alpha8,         // 1 component per pixel, each is unsigned 8-bit. Total size = TexWidth * TexHeight
+};
+
+// Status of a texture to communicate with Renderer Backend.
+enum ImTextureStatus
+{
+    ImTextureStatus_OK,
+    ImTextureStatus_Destroyed,      // Backend destroyed the texture.
+    ImTextureStatus_WantCreate,     // Requesting backend to create the texture. Set status OK when done.
+    ImTextureStatus_WantUpdates,    // Requesting backend to update specific blocks of pixels (write to texture portions which have never been used before). Set status OK when done.
+    ImTextureStatus_WantDestroy,    // Requesting backend to destroy the texture. Set status to Destroyed when done.
+};
+
+// Coordinates of a rectangle within a texture.
+// When a texture is in ImTextureStatus_WantUpdates state, we provide a list of individual rectangles to copy to the graphics system.
+// You may use ImTextureData::Updates[] for the list, or ImTextureData::UpdateBox for a single bounding box.
+struct ImTextureRect
+{
+    unsigned short      x, y;       // Upper-left coordinates of rectangle to update
+    unsigned short      w, h;       // Size of rectangle to update (in pixels)
+};
+
+// Specs and pixel storage for a texture used by Dear ImGui.
+// This is only useful for (1) core library and (2) backends. End-user/applications do not need to care about this.
+// Renderer Backends will create a GPU-side version of this.
+// Why does we store two identifiers: TexID and BackendUserData?
+// - ImTextureID    TexID           = lower-level identifier stored in ImDrawCmd. ImDrawCmd can refer to textures not created by the backend, and for which there's no ImTextureData.
+// - void*          BackendUserData = higher-level opaque storage for backend own book-keeping. Some backends may have enough with TexID and not need both.
+ // In columns below: who reads/writes each fields? 'r'=read, 'w'=write, 'core'=main library, 'backend'=renderer backend
+struct ImTextureData
+{
+    //------------------------------------------ core / backend ---------------------------------------
+    int                 UniqueID;               // w    -   // [DEBUG] Sequential index to facilitate identifying a texture when debugging/printing. Unique per atlas.
+    ImTextureStatus     Status;                 // rw   rw  // ImTextureStatus_OK/_WantCreate/_WantUpdates/_WantDestroy. Always use SetStatus() to modify!
+    void*               BackendUserData;        // -    rw  // Convenience storage for backend. Some backends may have enough with TexID.
+    ImTextureID         TexID;                  // r    w   // Backend-specific texture identifier. Always use SetTexID() to modify! The identifier will stored in ImDrawCmd::GetTexID() and passed to backend's RenderDrawData function.
+    ImTextureFormat     Format;                 // w    r   // ImTextureFormat_RGBA32 (default) or ImTextureFormat_Alpha8
+    int                 Width;                  // w    r   // Texture width
+    int                 Height;                 // w    r   // Texture height
+    int                 BytesPerPixel;          // w    r   // 4 or 1
+    unsigned char*      Pixels;                 // w    r   // Pointer to buffer holding 'Width*Height' pixels and 'Width*Height*BytesPerPixels' bytes.
+    ImTextureRect       UsedRect;               // w    r   // Bounding box encompassing all past and queued Updates[].
+    ImTextureRect       UpdateRect;             // w    r   // Bounding box encompassing all queued Updates[].
+    ImVector<ImTextureRect> Updates;            // w    r   // Array of individual updates.
+    int                 UnusedFrames;           // w    r   // In order to facilitate handling Status==WantDestroy in some backend: this is a count successive frames where the texture was not used. Always >0 when Status==WantDestroy.
+    unsigned short      RefCount;               // w    r   // Number of contexts using this texture. Used during backend shutdown.
+    bool                UseColors;              // w    r   // Tell whether our texture data is known to use colors (rather than just white + alpha).
+    bool                WantDestroyNextFrame;   // rw   -   // [Internal] Queued to set ImTextureStatus_WantDestroy next frame. May still be used in the current frame.
+
+    // Functions
+    ImTextureData()     { memset(this, 0, sizeof(*this)); Status = ImTextureStatus_Destroyed; TexID = ImTextureID_Invalid; }
+    ~ImTextureData()    { DestroyPixels(); }
+    IMGUI_API void      Create(ImTextureFormat format, int w, int h);
+    IMGUI_API void      DestroyPixels();
+    void*               GetPixels()                 { IM_ASSERT(Pixels != NULL); return Pixels; }
+    void*               GetPixelsAt(int x, int y)   { IM_ASSERT(Pixels != NULL); return Pixels + (x + y * Width) * BytesPerPixel; }
+    int                 GetSizeInBytes() const      { return Width * Height * BytesPerPixel; }
+    int                 GetPitch() const            { return Width * BytesPerPixel; }
+    ImTextureRef        GetTexRef()                 { ImTextureRef tex_ref; tex_ref._TexData = this; tex_ref._TexID = ImTextureID_Invalid; return tex_ref; }
+    ImTextureID         GetTexID() const            { return TexID; }
+
+    // Called by Renderer backend
+    // - Call SetTexID() and SetStatus() after honoring texture requests. Never modify TexID and Status directly!
+    // - A backend may decide to destroy a texture that we did not request to destroy, which is fine (e.g. freeing resources), but we immediately set the texture back in _WantCreate mode.
+    void    SetTexID(ImTextureID tex_id)            { TexID = tex_id; }
+    void    SetStatus(ImTextureStatus status)       { Status = status; if (status == ImTextureStatus_Destroyed && !WantDestroyNextFrame) Status = ImTextureStatus_WantCreate; }
+};
+
+//-----------------------------------------------------------------------------
 // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont)
 //-----------------------------------------------------------------------------
 
 // A font input/source (we may rename this to ImFontSource in the future)
 struct ImFontConfig
 {
+    // Data Source
+    char            Name[40];               // <auto>   // Name (strictly to ease debugging, hence limited size buffer)
     void*           FontData;               //          // TTF/OTF data
     int             FontDataSize;           //          // TTF/OTF data size
     bool            FontDataOwnedByAtlas;   // true     // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself).
+
+    // Options
     bool            MergeMode;              // false    // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights.
     bool            PixelSnapH;             // false    // Align every glyph AdvanceX to pixel boundaries. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1.
-    int             FontNo;                 // 0        // Index of font within TTF/OTF file
-    int             OversampleH;            // 0 (2)    // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1 or 2 depending on size. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details.
-    int             OversampleV;            // 0 (1)    // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1. This is not really useful as we don't use sub-pixel positions on the Y axis.
+    bool            PixelSnapV;             // true     // Align Scaled GlyphOffset.y to pixel boundaries.
+    ImS8            OversampleH;            // 0 (2)    // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1 or 2 depending on size. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details.
+    ImS8            OversampleV;            // 0 (1)    // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1. This is not really useful as we don't use sub-pixel positions on the Y axis.
+    ImWchar         EllipsisChar;           // 0        // Explicitly specify Unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used.
     float           SizePixels;             //          // Size in pixels for rasterizer (more or less maps to the resulting font height).
-    //ImVec2        GlyphExtraSpacing;      // 0, 0     // (REMOVED IN 1.91.9: use GlyphExtraAdvanceX)
-    ImVec2          GlyphOffset;            // 0, 0     // Offset all glyphs from this font input.
-    const ImWchar*  GlyphRanges;            // NULL     // THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list).
-    float           GlyphMinAdvanceX;       // 0        // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font
+    const ImWchar*  GlyphRanges;            // NULL     // *LEGACY* THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list).
+    const ImWchar*  GlyphExcludeRanges;     // NULL     // Pointer to a small user-provided list of Unicode ranges (2 value per range, values are inclusive, zero-terminated list). This is very close to GlyphRanges[] but designed to exclude ranges from a font source, when merging fonts with overlapping glyphs. Use "Input Glyphs Overlap Detection Tool" to find about your overlapping ranges.
+    //ImVec2        GlyphExtraSpacing;      // 0, 0     // (REMOVED AT IT SEEMS LARGELY OBSOLETE. PLEASE REPORT IF YOU WERE USING THIS). Extra spacing (in pixels) between glyphs when rendered: essentially add to glyph->AdvanceX. Only X axis is supported for now.
+    ImVec2          GlyphOffset;            // 0, 0     // Offset (in pixels) all glyphs from this font input. Absolute value for default size, other sizes will scale this value.
+    float           GlyphMinAdvanceX;       // 0        // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font. Absolute value for default size, other sizes will scale this value.
     float           GlyphMaxAdvanceX;       // FLT_MAX  // Maximum AdvanceX for glyphs
-    float           GlyphExtraAdvanceX;     // 0        // Extra spacing (in pixels) between glyphs. Please contact us if you are using this.
-    unsigned int    FontBuilderFlags;       // 0        // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure.
+    float           GlyphExtraAdvanceX;     // 0        // Extra spacing (in pixels) between glyphs. Please contact us if you are using this. // FIXME-NEWATLAS: Intentionally unscaled
+    ImU32           FontNo;                 // 0        // Index of font within TTF/OTF file
+    unsigned int    FontLoaderFlags;        // 0        // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure.
+    //unsigned int  FontBuilderFlags;       // --       // [Renamed in 1.92] Ue FontLoaderFlags.
     float           RasterizerMultiply;     // 1.0f     // Linearly brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. This is a silly thing we may remove in the future.
-    float           RasterizerDensity;      // 1.0f     // DPI scale for rasterization, not altering other font metrics: make it easy to swap between e.g. a 100% and a 400% fonts for a zooming display. IMPORTANT: If you increase this it is expected that you increase font scale accordingly, otherwise quality may look lowered.
-    ImWchar         EllipsisChar;           // 0        // Explicitly specify Unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used.
+    float           RasterizerDensity;      // 1.0f     // [LEGACY: this only makes sense when ImGuiBackendFlags_RendererHasTextures is not supported] DPI scale multiplier for rasterization. Not altering other font metrics: makes it easy to swap between e.g. a 100% and a 400% fonts for a zooming display, or handle Retina screen. IMPORTANT: If you change this it is expected that you increase/decrease font scale roughly to the inverse of this, otherwise quality may look lowered.
 
     // [Internal]
-    char            Name[40];               // Name (strictly to ease debugging)
-    ImFont*         DstFont;
+    ImFontFlags     Flags;                  // Font flags (don't use just yet, will be exposed in upcoming 1.92.X updates)
+    ImFont*         DstFont;                // Target font (as we merging fonts, multiple ImFontConfig may target the same font)
+    const ImFontLoader* FontLoader;         // Custom font backend for this source (default source is the one stored in ImFontAtlas)
+    void*           FontLoaderData;         // Font loader opaque storage (per font config)
 
     IMGUI_API ImFontConfig();
 };
 
 // Hold rendering data for one glyph.
-// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this)
+// (Note: some language parsers may fail to convert the bitfield members, in this case maybe drop store a single u32 or we can rework this)
 struct ImFontGlyph
 {
     unsigned int    Colored : 1;        // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops)
     unsigned int    Visible : 1;        // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering.
-    unsigned int    Codepoint : 30;     // 0x0000..0x10FFFF
-    float           AdvanceX;           // Horizontal distance to advance layout with
-    float           X0, Y0, X1, Y1;     // Glyph corners
-    float           U0, V0, U1, V1;     // Texture coordinates
+    unsigned int    SourceIdx : 4;      // Index of source in parent font
+    unsigned int    Codepoint : 26;     // 0x0000..0x10FFFF
+    float           AdvanceX;           // Horizontal distance to advance cursor/layout position.
+    float           X0, Y0, X1, Y1;     // Glyph corners. Offsets from current cursor/layout position.
+    float           U0, V0, U1, V1;     // Texture coordinates for the current value of ImFontAtlas->TexRef. Cached equivalent of calling GetCustomRect() with PackId.
+    int             PackId;             // [Internal] ImFontAtlasRectId value (FIXME: Cold data, could be moved elsewhere?)
+
+    ImFontGlyph()   { memset(this, 0, sizeof(*this)); PackId = -1; }
 };
 
 // Helper to build glyph ranges from text/string data. Feed your application strings/characters to it then call BuildRanges().
@@ -3325,20 +3573,21 @@
     IMGUI_API void  BuildRanges(ImVector<ImWchar>* out_ranges);                 // Output new ranges
 };
 
-// See ImFontAtlas::AddCustomRectXXX functions.
-struct ImFontAtlasCustomRect
-{
-    unsigned short  X, Y;           // Output   // Packed position in Atlas
+// An opaque identifier to a rectangle in the atlas. -1 when invalid.
+// The rectangle may move and UV may be invalidated, use GetCustomRect() to retrieve it.
+typedef int ImFontAtlasRectId;
+#define ImFontAtlasRectId_Invalid -1
+
+// Output of ImFontAtlas::GetCustomRect() when using custom rectangles.
+// Those values may not be cached/stored as they are only valid for the current value of atlas->TexRef
+// (this is in theory derived from ImTextureRect but we use separate structures for reasons)
+struct ImFontAtlasRect
+{
+    unsigned short  x, y;               // Position (in current texture)
+    unsigned short  w, h;               // Size
+    ImVec2          uv0, uv1;           // UV coordinates (in current texture)
 
-    // [Internal]
-    unsigned short  Width, Height;  // Input    // Desired rectangle dimension
-    unsigned int    GlyphID : 31;   // Input    // For custom font glyphs only (ID < 0x110000)
-    unsigned int    GlyphColored : 1; // Input  // For custom font glyphs only: glyph is colored, removed tinting.
-    float           GlyphAdvanceX;  // Input    // For custom font glyphs only: glyph xadvance
-    ImVec2          GlyphOffset;    // Input    // For custom font glyphs only: glyph display offset
-    ImFont*         Font;           // Input    // For custom font glyphs only: target font
-    ImFontAtlasCustomRect()         { X = Y = 0xFFFF; Width = Height = 0; GlyphID = 0; GlyphColored = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; }
-    bool IsPacked() const           { return X != 0xFFFF; }
+    ImFontAtlasRect() { memset(this, 0, sizeof(*this)); }
 };
 
 // Flags for ImFontAtlas build
@@ -3354,12 +3603,14 @@
 //  - One or more fonts.
 //  - Custom graphics data needed to render the shapes needed by Dear ImGui.
 //  - Mouse cursor shapes for software cursor rendering (unless setting 'Flags |= ImFontAtlasFlags_NoMouseCursors' in the font atlas).
-// It is the user-code responsibility to setup/build the atlas, then upload the pixel data into a texture accessible by your graphics api.
-//  - Optionally, call any of the AddFont*** functions. If you don't call any, the default font embedded in the code will be loaded for you.
-//  - Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data.
-//  - Upload the pixels data into a texture within your graphics system (see imgui_impl_xxxx.cpp examples)
+//  - If you don't call any AddFont*** functions, the default font embedded in the code will be loaded for you.
+// It is the rendering backend responsibility to upload texture into your graphics API:
+//  - ImGui_ImplXXXX_RenderDrawData() functions generally iterate platform_io->Textures[] to create/update/destroy each ImTextureData instance.
+//  - Backend then set ImTextureData's TexID and BackendUserData.
+//  - Texture id are passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID/ImTextureRef for more details.
+// Legacy path:
+//  - Call Build() + GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data.
 //  - Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture in a format natural to your graphics API.
-//    This value will be passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID for more details.
 // Common pitfalls:
 // - If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the
 //   atlas is build (when calling GetTexData*** or Build()). We only copy the pointer, not the data.
@@ -3373,35 +3624,49 @@
     IMGUI_API ~ImFontAtlas();
     IMGUI_API ImFont*           AddFont(const ImFontConfig* font_cfg);
     IMGUI_API ImFont*           AddFontDefault(const ImFontConfig* font_cfg = NULL);
-    IMGUI_API ImFont*           AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL);
-    IMGUI_API ImFont*           AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed.
-    IMGUI_API ImFont*           AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp.
-    IMGUI_API ImFont*           AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL);              // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter.
-    IMGUI_API void              ClearInputData();           // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts.
-    IMGUI_API void              ClearFonts();               // Clear input+output font data (same as ClearInputData() + glyphs storage, UV coordinates).
-    IMGUI_API void              ClearTexData();             // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory.
-    IMGUI_API void              Clear();                    // Clear all input and output.
-
-    // Build atlas, retrieve pixel data.
-    // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID().
-    // The pitch is always = Width * BytesPerPixels (1 or 4)
-    // Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into
-    // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste.
-    IMGUI_API bool              Build();                    // Build pixels data. This is called automatically for you by the GetTexData*** functions.
-    IMGUI_API void              GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL);  // 1 byte per-pixel
-    IMGUI_API void              GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL);  // 4 bytes-per-pixel
-    bool                        IsBuilt() const             { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent...
-    void                        SetTexID(ImTextureID id)    { TexID = id; }
+    IMGUI_API ImFont*           AddFontFromFileTTF(const char* filename, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL);
+    IMGUI_API ImFont*           AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed.
+    IMGUI_API ImFont*           AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp.
+    IMGUI_API ImFont*           AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels = 0.0f, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL);              // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter.
+    IMGUI_API void              RemoveFont(ImFont* font);
+
+    IMGUI_API void              Clear();                    // Clear everything (input fonts, output glyphs/textures)
+    IMGUI_API void              CompactCache();             // Compact cached glyphs and texture.
+    IMGUI_API void              SetFontLoader(const ImFontLoader* font_loader); // Change font loader at runtime.
+
+    // As we are transitioning toward a new font system, we expect to obsolete those soon:
+    IMGUI_API void              ClearInputData();           // [OBSOLETE] Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts.
+    IMGUI_API void              ClearFonts();               // [OBSOLETE] Clear input+output font data (same as ClearInputData() + glyphs storage, UV coordinates).
+    IMGUI_API void              ClearTexData();             // [OBSOLETE] Clear CPU-side copy of the texture data. Saves RAM once the texture has been copied to graphics memory.
+
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    // Legacy path for build atlas + retrieving pixel data.
+    // - User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID().
+    // - The pitch is always = Width * BytesPerPixels (1 or 4)
+    // - Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into
+    //   the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste).
+    // - From 1.92 with backends supporting ImGuiBackendFlags_RendererHasTextures:
+    //   - Calling Build(), GetTexDataAsAlpha8(), GetTexDataAsRGBA32() is not needed.
+    //   - In backend: replace calls to ImFontAtlas::SetTexID() with calls to ImTextureData::SetTexID() after honoring texture creation.
+    IMGUI_API bool  Build();                    // Build pixels data. This is called automatically for you by the GetTexData*** functions.
+    IMGUI_API void  GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel
+    IMGUI_API void  GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel
+    void            SetTexID(ImTextureID id)    { IM_ASSERT(TexRef._TexID == ImTextureID_Invalid); TexRef._TexData->TexID = id; }                               // Called by legacy backends. May be called before texture creation.
+    void            SetTexID(ImTextureRef id)   { IM_ASSERT(TexRef._TexID == ImTextureID_Invalid && id._TexData == NULL); TexRef._TexData->TexID = id._TexID; } // Called by legacy backends.
+    bool            IsBuilt() const { return Fonts.Size > 0 && TexIsBuilt; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent..
+#endif
 
     //-------------------------------------------
     // Glyph Ranges
     //-------------------------------------------
 
+    // Since 1.92: specifying glyph ranges is only useful/necessary if your backend doesn't support ImGuiBackendFlags_RendererHasTextures!
+    IMGUI_API const ImWchar*    GetGlyphRangesDefault();                // Basic Latin, Extended Latin
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
     // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list)
     // NB: Make sure that your string are UTF-8 and NOT in your local code page.
     // Read https://github.com/ocornut/imgui/blob/master/docs/FONTS.md/#about-utf-8-encoding for details.
     // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data.
-    IMGUI_API const ImWchar*    GetGlyphRangesDefault();                // Basic Latin, Extended Latin
     IMGUI_API const ImWchar*    GetGlyphRangesGreek();                  // Default + Greek and Coptic
     IMGUI_API const ImWchar*    GetGlyphRangesKorean();                 // Default + Korean characters
     IMGUI_API const ImWchar*    GetGlyphRangesJapanese();               // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs
@@ -3410,24 +3675,32 @@
     IMGUI_API const ImWchar*    GetGlyphRangesCyrillic();               // Default + about 400 Cyrillic characters
     IMGUI_API const ImWchar*    GetGlyphRangesThai();                   // Default + Thai characters
     IMGUI_API const ImWchar*    GetGlyphRangesVietnamese();             // Default + Vietnamese characters
+#endif
 
     //-------------------------------------------
     // [ALPHA] Custom Rectangles/Glyphs API
     //-------------------------------------------
 
-    // You can request arbitrary rectangles to be packed into the atlas, for your own purposes.
-    // - After calling Build(), you can query the rectangle position and render your pixels.
-    // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of preferred texture format.
-    // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point),
-    //   so you can render e.g. custom colorful icons and use them as regular glyphs.
+    // Register and retrieve custom rectangles
+    // - You can request arbitrary rectangles to be packed into the atlas, for your own purpose.
+    // - Since 1.92.X, packing is done immediately in the function call (previously packing was done during the Build call)
+    // - You can render your pixels into the texture right after calling the AddCustomRect() functions.
+    // - VERY IMPORTANT:
+    //   - Texture may be created/resized at any time when calling ImGui or ImFontAtlas functions.
+    //   - IT WILL INVALIDATE RECTANGLE DATA SUCH AS UV COORDINATES. Always use latest values from GetCustomRect().
+    //   - UV coordinates are associated to the current texture identifier aka 'atlas->TexRef'. Both TexRef and UV coordinates are typically changed at the same time.
+    // - If you render colored output into your custom rectangles: set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of preferred texture format.
     // - Read docs/FONTS.md for more details about using colorful icons.
-    // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings.
-    IMGUI_API int               AddCustomRectRegular(int width, int height);
-    IMGUI_API int               AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0));
-    ImFontAtlasCustomRect*      GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; }
-
-    // [Internal]
-    IMGUI_API void              CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const;
+    // - Note: this API may be reworked further in order to facilitate supporting e.g. multi-monitor, varying DPI settings.
+    // - (Pre-1.92 names) ------------> (1.92 names)
+    //   - GetCustomRectByIndex()   --> Use GetCustomRect()
+    //   - CalcCustomRectUV()       --> Use GetCustomRect() and read uv0, uv1 fields.
+    //   - AddCustomRectRegular()   --> Renamed to AddCustomRect()
+    //   - AddCustomRectFontGlyph() --> Prefer using custom ImFontLoader inside ImFontConfig
+    //   - ImFontAtlasCustomRect    --> Renamed to ImFontAtlasRect
+    IMGUI_API ImFontAtlasRectId AddCustomRect(int width, int height, ImFontAtlasRect* out_r = NULL);// Register a rectangle. Return -1 (ImFontAtlasRectId_Invalid) on error.
+    IMGUI_API void              RemoveCustomRect(ImFontAtlasRectId id);                             // Unregister a rectangle. Existing pixels will stay in texture until resized / garbage collected.
+    IMGUI_API bool              GetCustomRect(ImFontAtlasRectId id, ImFontAtlasRect* out_r) const;  // Get rectangle coordinates for current texture. Valid immediately, never store this (read above)!
 
     //-------------------------------------------
     // Members
@@ -3435,96 +3708,178 @@
 
     // Input
     ImFontAtlasFlags            Flags;              // Build flags (see ImFontAtlasFlags_)
-    ImTextureID                 TexID;              // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure.
-    int                         TexDesiredWidth;    // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height.
+    ImTextureFormat             TexDesiredFormat;   // Desired texture format (default to ImTextureFormat_RGBA32 but may be changed to ImTextureFormat_Alpha8).
     int                         TexGlyphPadding;    // FIXME: Should be called "TexPackPadding". Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0 (will also need to set AntiAliasedLinesUseTex = false).
+    int                         TexMinWidth;        // Minimum desired texture width. Must be a power of two. Default to 512.
+    int                         TexMinHeight;       // Minimum desired texture height. Must be a power of two. Default to 128.
+    int                         TexMaxWidth;        // Maximum desired texture width. Must be a power of two. Default to 8192.
+    int                         TexMaxHeight;       // Maximum desired texture height. Must be a power of two. Default to 8192.
     void*                       UserData;           // Store your own atlas related user-data (if e.g. you have multiple font atlas).
 
+    // Output
+    // - Because textures are dynamically created/resized, the current texture identifier may changed at *ANY TIME* during the frame.
+    // - This should not affect you as you can always use the latest value. But note that any precomputed UV coordinates are only valid for the current TexRef.
+#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    ImTextureRef                TexRef;             // Latest texture identifier == TexData->GetTexRef().
+#else
+    union { ImTextureRef TexRef; ImTextureRef TexID; }; // Latest texture identifier == TexData->GetTexRef(). // RENAMED TexID to TexRef in 1.92.x
+#endif
+    ImTextureData*              TexData;            // Latest texture.
+
     // [Internal]
-    // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you.
-    bool                        Locked;             // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert.
-    bool                        TexReady;           // Set when texture was built matching current font input
-    bool                        TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format.
-    unsigned char*              TexPixelsAlpha8;    // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight
-    unsigned int*               TexPixelsRGBA32;    // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4
-    int                         TexWidth;           // Texture width calculated during Build().
-    int                         TexHeight;          // Texture height calculated during Build().
-    ImVec2                      TexUvScale;         // = (1.0f/TexWidth, 1.0f/TexHeight)
-    ImVec2                      TexUvWhitePixel;    // Texture coordinates to a white pixel
+    ImVector<ImTextureData*>    TexList;            // Texture list (most often TexList.Size == 1). TexData is always == TexList.back(). DO NOT USE DIRECTLY, USE GetDrawData().Textures[]/GetPlatformIO().Textures[] instead!
+    bool                        Locked;             // Marked as locked during ImGui::NewFrame()..EndFrame() scope if TexUpdates are not supported. Any attempt to modify the atlas will assert.
+    bool                        RendererHasTextures;// Copy of (BackendFlags & ImGuiBackendFlags_RendererHasTextures) from supporting context.
+    bool                        TexIsBuilt;         // Set when texture was built matching current font input. Mostly useful for legacy IsBuilt() call.
+    bool                        TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format or conversion process.
+    ImVec2                      TexUvScale;         // = (1.0f/TexData->TexWidth, 1.0f/TexData->TexHeight). May change as new texture gets created.
+    ImVec2                      TexUvWhitePixel;    // Texture coordinates to a white pixel. May change as new texture gets created.
     ImVector<ImFont*>           Fonts;              // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font.
-    ImVector<ImFontAtlasCustomRect> CustomRects;    // Rectangles for packing custom texture data into the atlas.
     ImVector<ImFontConfig>      Sources;            // Source/configuration data
     ImVec4                      TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1];  // UVs for baked anti-aliased lines
-
-    // [Internal] Font builder
-    const ImFontBuilderIO*      FontBuilderIO;      // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE).
-    unsigned int                FontBuilderFlags;   // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig.
-
-    // [Internal] Packing data
-    int                         PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors
-    int                         PackIdLines;        // Custom texture rectangle ID for baked anti-aliased lines
+    int                         TexNextUniqueID;    // Next value to be stored in TexData->UniqueID
+    int                         FontNextUniqueID;   // Next value to be stored in ImFont->FontID
+    ImVector<ImDrawListSharedData*> DrawListSharedDatas; // List of users for this atlas. Typically one per Dear ImGui context.
+    ImFontAtlasBuilder*         Builder;            // Opaque interface to our data that doesn't need to be public and may be discarded when rebuilding.
+    const ImFontLoader*         FontLoader;         // Font loader opaque interface (default to use FreeType when IMGUI_ENABLE_FREETYPE is defined, otherwise default to use stb_truetype). Use SetFontLoader() to change this at runtime.
+    const char*                 FontLoaderName;     // Font loader name (for display e.g. in About box) == FontLoader->Name
+    void*                       FontLoaderData;     // Font backend opaque storage
+    unsigned int                FontLoaderFlags;    // Shared flags (for all fonts) for font loader. THIS IS BUILD IMPLEMENTATION DEPENDENT (e.g. Per-font override is also available in ImFontConfig).
+    int                         RefCount;           // Number of contexts using this atlas
+    ImGuiContext*               OwnerContext;       // Context which own the atlas will be in charge of updating and destroying it.
 
     // [Obsolete]
-    //typedef ImFontAtlasCustomRect    CustomRect;              // OBSOLETED in 1.72+
-    //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder;      // OBSOLETED in 1.67+
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    // Legacy: You can request your rectangles to be mapped as font glyph (given a font + Unicode point), so you can render e.g. custom colorful icons and use them as regular glyphs. --> Prefer using a custom ImFontLoader.
+    ImFontAtlasRect             TempRect;           // For old GetCustomRectByIndex() API
+    inline ImFontAtlasRectId    AddCustomRectRegular(int w, int h)                                                          { return AddCustomRect(w, h); }                             // RENAMED in 1.92.X
+    inline const ImFontAtlasRect* GetCustomRectByIndex(ImFontAtlasRectId id)                                                { return GetCustomRect(id, &TempRect) ? &TempRect : NULL; } // OBSOLETED in 1.92.X
+    inline void                 CalcCustomRectUV(const ImFontAtlasRect* r, ImVec2* out_uv_min, ImVec2* out_uv_max) const    { *out_uv_min = r->uv0; *out_uv_max = r->uv1; }             // OBSOLETED in 1.92.X
+    IMGUI_API ImFontAtlasRectId AddCustomRectFontGlyph(ImFont* font, ImWchar codepoint, int w, int h, float advance_x, const ImVec2& offset = ImVec2(0, 0));                            // OBSOLETED in 1.92.X: Use custom ImFontLoader in ImFontConfig
+    IMGUI_API ImFontAtlasRectId AddCustomRectFontGlyphForSize(ImFont* font, float font_size, ImWchar codepoint, int w, int h, float advance_x, const ImVec2& offset = ImVec2(0, 0));    // ADDED AND OBSOLETED in 1.92.X
+#endif
+    //unsigned int                      FontBuilderFlags;        // OBSOLETED in 1.92.X: Renamed to FontLoaderFlags.
+    //int                               TexDesiredWidth;         // OBSOLETED in 1.92.X: Force texture width before calling Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height.
+    //typedef ImFontAtlasRect           ImFontAtlasCustomRect;   // OBSOLETED in 1.92.X
+    //typedef ImFontAtlasCustomRect     CustomRect;              // OBSOLETED in 1.72+
+    //typedef ImFontGlyphRangesBuilder  GlyphRangesBuilder;      // OBSOLETED in 1.67+
 };
 
-// Font runtime data and rendering
-// ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32().
-struct ImFont
+// Font runtime data for a given size
+// Important: pointers to ImFontBaked are only valid for the current frame.
+struct ImFontBaked
 {
     // [Internal] Members: Hot ~20/24 bytes (for CalcTextSize)
     ImVector<float>             IndexAdvanceX;      // 12-16 // out // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this info, and are often bottleneck in large UI).
-    float                       FallbackAdvanceX;   // 4     // out // = FallbackGlyph->AdvanceX
-    float                       FontSize;           // 4     // in  // Height of characters/line, set during loading (don't change after loading)
+    float                       FallbackAdvanceX;   // 4     // out // FindGlyph(FallbackChar)->AdvanceX
+    float                       Size;               // 4     // in  // Height of characters/line, set during loading (doesn't change after loading)
+    float                       RasterizerDensity;  // 4     // in  // Density this is baked at
 
-    // [Internal] Members: Hot ~28/40 bytes (for RenderText loop)
+    // [Internal] Members: Hot ~28/36 bytes (for RenderText loop)
     ImVector<ImU16>             IndexLookup;        // 12-16 // out // Sparse. Index glyphs by Unicode code-point.
     ImVector<ImFontGlyph>       Glyphs;             // 12-16 // out // All glyphs.
-    ImFontGlyph*                FallbackGlyph;      // 4-8   // out // = FindGlyph(FontFallbackChar)
+    int                         FallbackGlyphIndex; // 4     // out // Index of FontFallbackChar
+
+    // [Internal] Members: Cold
+    float                       Ascent, Descent;    // 4+4   // out // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] (unscaled)
+    unsigned int                MetricsTotalSurface:26;// 3  // out // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs)
+    unsigned int                WantDestroy:1;         // 0  //     // Queued for destroy
+    unsigned int                LoadNoFallback:1;      // 0  //     // Disable loading fallback in lower-level calls.
+    unsigned int                LoadNoRenderOnLayout:1;// 0  //     // Enable a two-steps mode where CalcTextSize() calls will load AdvanceX *without* rendering/packing glyphs. Only advantagous if you know that the glyph is unlikely to actually be rendered, otherwise it is slower because we'd do one query on the first CalcTextSize and one query on the first Draw.
+    int                         LastUsedFrame;         // 4  //     // Record of that time this was bounds
+    ImGuiID                     BakedId;            // 4     //     // Unique ID for this baked storage
+    ImFont*                     ContainerFont;      // 4-8   // in  // Parent font
+    void*                       FontLoaderDatas;    // 4-8   //     // Font loader opaque storage (per baked font * sources): single contiguous buffer allocated by imgui, passed to loader.
+
+    // Functions
+    IMGUI_API ImFontBaked();
+    IMGUI_API void              ClearOutputData();
+    IMGUI_API ImFontGlyph*      FindGlyph(ImWchar c);               // Return U+FFFD glyph if requested glyph doesn't exists.
+    IMGUI_API ImFontGlyph*      FindGlyphNoFallback(ImWchar c);     // Return NULL if glyph doesn't exist
+    IMGUI_API float             GetCharAdvance(ImWchar c);
+    IMGUI_API bool              IsGlyphLoaded(ImWchar c);
+};
+
+// Font flags
+// (in future versions as we redesign font loading API, this will become more important and better documented. for now please consider this as internal/advanced use)
+enum ImFontFlags_
+{
+    ImFontFlags_None                    = 0,
+    ImFontFlags_NoLoadError             = 1 << 1,   // Disable throwing an error/assert when calling AddFontXXX() with missing file/data. Calling code is expected to check AddFontXXX() return value.
+    ImFontFlags_NoLoadGlyphs            = 1 << 2,   // [Internal] Disable loading new glyphs.
+    ImFontFlags_LockBakedSizes          = 1 << 3,   // [Internal] Disable loading new baked sizes, disable garbage collecting current ones. e.g. if you want to lock a font to a single size. Important: if you use this to preload given sizes, consider the possibility of multiple font density used on Retina display.
+};
+
+// Font runtime data and rendering
+// - ImFontAtlas automatically loads a default embedded font for you if you didn't load one manually.
+// - Since 1.92.X a font may be rendered as any size! Therefore a font doesn't have one specific size.
+// - Use 'font->GetFontBaked(size)' to retrieve the ImFontBaked* corresponding to a given size.
+// - If you used g.Font + g.FontSize (which is frequent from the ImGui layer), you can use g.FontBaked as a shortcut, as g.FontBaked == g.Font->GetFontBaked(g.FontSize).
+struct ImFont
+{
+    // [Internal] Members: Hot ~12-20 bytes
+    ImFontBaked*                LastBaked;          // 4-8   // Cache last bound baked. NEVER USE DIRECTLY. Use GetFontBaked().
+    ImFontAtlas*                ContainerAtlas;     // 4-8   // What we have been loaded into.
+    ImFontFlags                 Flags;              // 4     // Font flags.
+    float                       CurrentRasterizerDensity;    // Current rasterizer density. This is a varying state of the font.
 
-    // [Internal] Members: Cold ~32/40 bytes
+    // [Internal] Members: Cold ~24-52 bytes
     // Conceptually Sources[] is the list of font sources merged to create this font.
-    ImFontAtlas*                ContainerAtlas;     // 4-8   // out // What we has been loaded into
-    ImFontConfig*               Sources;            // 4-8   // in  // Pointer within ContainerAtlas->Sources[], to SourcesCount instances
-    short                       SourcesCount;       // 2     // in  // Number of ImFontConfig involved in creating this font. Usually 1, or >1 when merging multiple font sources into one ImFont.
-    short                       EllipsisCharCount;  // 1     // out // 1 or 3
+    ImGuiID                     FontId;             // Unique identifier for the font
+    float                       LegacySize;         // 4     // in  // Font size passed to AddFont(). Use for old code calling PushFont() expecting to use that size. (use ImGui::GetFontBaked() to get font baked at current bound size).
+    ImVector<ImFontConfig*>     Sources;            // 16    // in  // List of sources. Pointers within ContainerAtlas->Sources[]
     ImWchar                     EllipsisChar;       // 2-4   // out // Character used for ellipsis rendering ('...').
     ImWchar                     FallbackChar;       // 2-4   // out // Character used if a glyph isn't found (U+FFFD, '?')
-    float                       EllipsisWidth;      // 4     // out // Total ellipsis Width
-    float                       EllipsisCharStep;   // 4     // out // Step between characters when EllipsisCount > 0
-    float                       Scale;              // 4     // in  // Base font scale (1.0f), multiplied by the per-window font scale which you can adjust with SetWindowFontScale()
-    float                       Ascent, Descent;    // 4+4   // out // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] (unscaled)
-    int                         MetricsTotalSurface;// 4     // out // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs)
-    bool                        DirtyLookupTables;  // 1     // out //
     ImU8                        Used8kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/8192/8]; // 1 bytes if ImWchar=ImWchar16, 16 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints.
+    bool                        EllipsisAutoBake;   // 1     //     // Mark when the "..." glyph needs to be generated.
+    ImGuiStorage                RemapPairs;         // 16    //     // Remapping pairs when using AddRemapChar(), otherwise empty.
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    float                       Scale;              // 4     // in  // Legacy base font scale (~1.0f), multiplied by the per-window font scale which you can adjust with SetWindowFontScale()
+#endif
 
     // Methods
     IMGUI_API ImFont();
     IMGUI_API ~ImFont();
-    IMGUI_API ImFontGlyph*      FindGlyph(ImWchar c);
-    IMGUI_API ImFontGlyph*      FindGlyphNoFallback(ImWchar c);
-    float                       GetCharAdvance(ImWchar c)       { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; }
+    IMGUI_API bool              IsGlyphInFont(ImWchar c);
     bool                        IsLoaded() const                { return ContainerAtlas != NULL; }
-    const char*                 GetDebugName() const            { return Sources ? Sources->Name : "<unknown>"; }
+    const char*                 GetDebugName() const            { return Sources.Size ? Sources[0]->Name : "<unknown>"; } // Fill ImFontConfig::Name.
 
     // [Internal] Don't use!
     // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable.
     // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable.
-    IMGUI_API ImVec2            CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL); // utf8
-    IMGUI_API const char*       CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width);
-    IMGUI_API void              RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c);
-    IMGUI_API void              RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false);
+    IMGUI_API ImFontBaked*      GetFontBaked(float font_size, float density = -1.0f);  // Get or create baked data for given size
+    IMGUI_API ImVec2            CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** out_remaining = NULL);
+    IMGUI_API const char*       CalcWordWrapPosition(float size, const char* text, const char* text_end, float wrap_width);
+    IMGUI_API void              RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c, const ImVec4* cpu_fine_clip = NULL);
+    IMGUI_API void              RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, ImDrawTextFlags flags = 0);
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+    inline const char*          CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) { return CalcWordWrapPosition(LegacySize * scale, text, text_end, wrap_width); }
+#endif
 
     // [Internal] Don't use!
-    IMGUI_API void              BuildLookupTable();
     IMGUI_API void              ClearOutputData();
-    IMGUI_API void              GrowIndex(int new_size);
-    IMGUI_API void              AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x);
-    IMGUI_API void              AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built.
+    IMGUI_API void              AddRemapChar(ImWchar from_codepoint, ImWchar to_codepoint); // Makes 'from_codepoint' character points to 'to_codepoint' glyph.
     IMGUI_API bool              IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last);
 };
 
+// This is provided for consistency (but we don't actually use this)
+inline ImTextureID ImTextureRef::GetTexID() const
+{
+    IM_ASSERT(!(_TexData != NULL && _TexID != ImTextureID_Invalid));
+    return _TexData ? _TexData->TexID : _TexID;
+}
+
+// Using an indirection to avoid patching ImDrawCmd after a SetTexID() call (but this could be an alternative solution too)
+inline ImTextureID ImDrawCmd::GetTexID() const
+{
+    // If you are getting this assert: A renderer backend with support for ImGuiBackendFlags_RendererHasTextures (1.92)
+    // must iterate and handle ImTextureData requests stored in ImDrawData::Textures[].
+    ImTextureID tex_id = TexRef._TexData ? TexRef._TexData->TexID : TexRef._TexID; // == TexRef.GetTexID() above.
+    if (TexRef._TexData != NULL)
+        IM_ASSERT(tex_id != ImTextureID_Invalid && "ImDrawCmd is referring to ImTextureData that wasn't uploaded to graphics system. Backend must call ImTextureData::SetTexID() after handling ImTextureStatus_WantCreate request!");
+    return tex_id;
+}
+
 //-----------------------------------------------------------------------------
 // [SECTION] Viewports
 //-----------------------------------------------------------------------------
@@ -3551,6 +3906,7 @@
     ImGuiViewportFlags  Flags;                  // See ImGuiViewportFlags_
     ImVec2              Pos;                    // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates)
     ImVec2              Size;                   // Main Area: Size of the viewport.
+    ImVec2              FramebufferScale;       // Density of the viewport for Retina display (always 1,1 on Windows, may be 2,2 etc on macOS/iOS). This will affect font rasterizer density.
     ImVec2              WorkPos;                // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos)
     ImVec2              WorkSize;               // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size)
 
@@ -3603,18 +3959,39 @@
     // Input - Interface with Renderer Backend
     //------------------------------------------------------------------
 
+    // Optional: Maximum texture size supported by renderer (used to adjust how we size textures). 0 if not known.
+    int         Renderer_TextureMaxWidth;
+    int         Renderer_TextureMaxHeight;
+
     // Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure.
     void*       Renderer_RenderState;
+
+    //------------------------------------------------------------------
+    // Output
+    //------------------------------------------------------------------
+
+    // Textures list (the list is updated by calling ImGui::EndFrame or ImGui::Render)
+    // The ImGui_ImplXXXX_RenderDrawData() function of each backend generally access this via ImDrawData::Textures which points to this. The array is available here mostly because backends will want to destroy textures on shutdown.
+    ImVector<ImTextureData*>        Textures;           // List of textures used by Dear ImGui (most often 1) + contents of external texture list is automatically appended into this.
+
+    //------------------------------------------------------------------
+    // Functions
+    //------------------------------------------------------------------
+
+    void    ClearPlatformHandlers();    // Clear all Platform_XXX fields. Typically called on Platform Backend shutdown.
+    void    ClearRendererHandlers();    // Clear all Renderer_XXX fields. Typically called on Renderer Backend shutdown.
 };
 
-// (Optional) Support for IME (Input Method Editor) via the platform_io.Platform_SetImeDataFn() function.
+// (Optional) Support for IME (Input Method Editor) via the platform_io.Platform_SetImeDataFn() function. Handler is called during EndFrame().
 struct ImGuiPlatformImeData
 {
-    bool    WantVisible;        // A widget wants the IME to be visible
-    ImVec2  InputPos;           // Position of the input cursor
-    float   InputLineHeight;    // Line height
+    bool    WantVisible;            // A widget wants the IME to be visible.
+    bool    WantTextInput;          // A widget wants text input, not necessarily IME to be visible. This is automatically set to the upcoming value of io.WantTextInput.
+    ImVec2  InputPos;               // Position of input cursor (for IME).
+    float   InputLineHeight;        // Line height (for IME).
+    ImGuiID ViewportId;             // ID of platform window/viewport.
 
-    ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); }
+    ImGuiPlatformImeData()          { memset(this, 0, sizeof(*this)); }
 };
 
 //-----------------------------------------------------------------------------
@@ -3626,31 +4003,34 @@
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
 namespace ImGui
 {
+    // OBSOLETED in 1.92.0 (from June 2025)
+    inline void         PushFont(ImFont* font)                                  { PushFont(font, font ? font->LegacySize : 0.0f); }
+    IMGUI_API void      SetWindowFontScale(float scale);                        // Set font scale factor for current window. Prefer using PushFont(NULL, style.FontSizeBase * factor) or use style.FontScaleMain to scale all windows.
     // OBSOLETED in 1.91.9 (from February 2025)
-    IMGUI_API void      Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col); // <-- border_col was removed in favor of ImGuiCol_ImageBorder.
+    IMGUI_API void      Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col); // <-- 'border_col' was removed in favor of ImGuiCol_ImageBorder. If you use 'tint_col', use ImageWithBg() instead.
     // OBSOLETED in 1.91.0 (from July 2024)
-    static inline void  PushButtonRepeat(bool repeat)                           { PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); }
-    static inline void  PopButtonRepeat()                                       { PopItemFlag(); }
-    static inline void  PushTabStop(bool tab_stop)                              { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); }
-    static inline void  PopTabStop()                                            { PopItemFlag(); }
+    inline void         PushButtonRepeat(bool repeat)                           { PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); }
+    inline void         PopButtonRepeat()                                       { PopItemFlag(); }
+    inline void         PushTabStop(bool tab_stop)                              { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); }
+    inline void         PopTabStop()                                            { PopItemFlag(); }
     IMGUI_API ImVec2    GetContentRegionMax();                                  // Content boundaries max (e.g. window boundaries including scrolling, or current column boundaries). You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()!
     IMGUI_API ImVec2    GetWindowContentRegionMin();                            // Content boundaries min for the window (roughly (0,0)-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()!
     IMGUI_API ImVec2    GetWindowContentRegionMax();                            // Content boundaries max for the window (roughly (0,0)+Size-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()!
     // OBSOLETED in 1.90.0 (from September 2023)
-    static inline bool  BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0)  { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); }
-    static inline void  EndChildFrame()                                                                     { EndChild(); }
-    //static inline bool BeginChild(const char* str_id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders
-    //static inline bool BeginChild(ImGuiID id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags)        { return BeginChild(id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags);     } // Unnecessary as true == ImGuiChildFlags_Borders
-    static inline void  ShowStackToolWindow(bool* p_open = NULL)                { ShowIDStackToolWindow(p_open); }
+    inline bool         BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0)  { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); }
+    inline void         EndChildFrame()                                                                     { EndChild(); }
+    //inline bool       BeginChild(const char* str_id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders
+    //inline bool       BeginChild(ImGuiID id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags)        { return BeginChild(id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags);     } // Unnecessary as true == ImGuiChildFlags_Borders
+    inline void         ShowStackToolWindow(bool* p_open = NULL)                { ShowIDStackToolWindow(p_open); }
     IMGUI_API bool      Combo(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int popup_max_height_in_items = -1);
     IMGUI_API bool      ListBox(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int height_in_items = -1);
     // OBSOLETED in 1.89.7 (from June 2023)
     IMGUI_API void      SetItemAllowOverlap();                                  // Use SetNextItemAllowOverlap() before item.
-    // OBSOLETED in 1.89.4 (from March 2023)
-    static inline void  PushAllowKeyboardFocus(bool tab_stop)                   { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); }
-    static inline void  PopAllowKeyboardFocus()                                 { PopItemFlag(); }
 
     // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE)
+    //-- OBSOLETED in 1.89.4 (from March 2023)
+    //static inline void  PushAllowKeyboardFocus(bool tab_stop)                                       { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); }
+    //static inline void  PopAllowKeyboardFocus()                                                     { PopItemFlag(); }
     //-- OBSOLETED in 1.89 (from August 2022)
     //IMGUI_API bool      ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // --> Use new ImageButton() signature (explicit item id, regular FramePadding). Refer to code in 1.91 if you want to grab a copy of this version.
     //-- OBSOLETED in 1.88 (from May 2022)
@@ -3713,6 +4093,25 @@
     //static inline void  SetScrollPosHere()                    { SetScrollHere(); }                                                // OBSOLETED in 1.42
 }
 
+//-- OBSOLETED in 1.92.x: ImFontAtlasCustomRect becomes ImTextureRect
+// - ImFontAtlasCustomRect::X,Y          --> ImTextureRect::x,y
+// - ImFontAtlasCustomRect::Width,Height --> ImTextureRect::w,h
+// - ImFontAtlasCustomRect::GlyphColored --> if you need to write to this, instead you can write to 'font->Glyphs.back()->Colored' after calling AddCustomRectFontGlyph()
+// We could make ImTextureRect an union to use old names, but 1) this would be confusing 2) the fix is easy 3) ImFontAtlasCustomRect was always a rather esoteric api.
+typedef ImFontAtlasRect ImFontAtlasCustomRect;
+/*struct ImFontAtlasCustomRect
+{
+    unsigned short  X, Y;           // Output   // Packed position in Atlas
+    unsigned short  Width, Height;  // Input    // [Internal] Desired rectangle dimension
+    unsigned int    GlyphID:31;     // Input    // [Internal] For custom font glyphs only (ID < 0x110000)
+    unsigned int    GlyphColored:1; // Input    // [Internal] For custom font glyphs only: glyph is colored, removed tinting.
+    float           GlyphAdvanceX;  // Input    // [Internal] For custom font glyphs only: glyph xadvance
+    ImVec2          GlyphOffset;    // Input    // [Internal] For custom font glyphs only: glyph display offset
+    ImFont*         Font;           // Input    // [Internal] For custom font glyphs only: target font
+    ImFontAtlasCustomRect()         { X = Y = 0xFFFF; Width = Height = 0; GlyphID = 0; GlyphColored = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; }
+    bool IsPacked() const           { return X != 0xFFFF; }
+};*/
+
 //-- OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect()
 //typedef ImDrawFlags ImDrawCornerFlags;
 //enum ImDrawCornerFlags_
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui_internal.h f3d-3.4.1/external/imgui/imgui_internal.h
--- f3d-3.2.0+dfsg/external/imgui/imgui_internal.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui_internal.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91.9b
+// dear imgui, v1.92.4
 // (internal structures/api)
 
 // You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility.
@@ -37,6 +37,7 @@
 // [SECTION] Tab bar, Tab item support
 // [SECTION] Table support
 // [SECTION] ImGui internal API
+// [SECTION] ImFontLoader
 // [SECTION] ImFontAtlas internal API
 // [SECTION] Test Engine specific hooks (imgui_test_engine)
 
@@ -76,8 +77,8 @@
 #ifdef _MSC_VER
 #pragma warning (push)
 #pragma warning (disable: 4251)     // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport)
-#pragma warning (disable: 26812)    // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer)
 #pragma warning (disable: 26495)    // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6).
+#pragma warning (disable: 26812)    // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
 #if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later
 #pragma warning (disable: 5054)     // operator '|': deprecated between enumerations of different types
 #endif
@@ -132,7 +133,7 @@
 //-----------------------------------------------------------------------------
 
 // Utilities
-// (other types which are not forwarded declared are: ImBitArray<>, ImSpan<>, ImSpanAllocator<>, ImPool<>, ImChunkStream<>)
+// (other types which are not forwarded declared are: ImBitArray<>, ImSpan<>, ImSpanAllocator<>, ImStableVector<>, ImPool<>, ImChunkStream<>)
 struct ImBitVector;                 // Store 1-bit per value
 struct ImRect;                      // An axis-aligned rectangle (2 points)
 struct ImGuiTextIndex;              // Maintain a line index for a text buffer.
@@ -140,6 +141,9 @@
 // ImDrawList/ImFontAtlas
 struct ImDrawDataBuilder;           // Helper to build a ImDrawData instance
 struct ImDrawListSharedData;        // Data shared between all ImDrawList instances
+struct ImFontAtlasBuilder;          // Internal storage for incrementally packing and building a ImFontAtlas
+struct ImFontAtlasPostProcessData;  // Data available to potential texture post-processing functions
+struct ImFontAtlasRectEntry;        // Packed rectangle lookup entry
 
 // ImGui
 struct ImGuiBoxSelectState;         // Box-selection state (currently used by multi-selection, could potentially be used by others)
@@ -189,6 +193,7 @@
 typedef int ImGuiLayoutType;            // -> enum ImGuiLayoutType_         // Enum: Horizontal or vertical
 
 // Flags
+typedef int ImDrawTextFlags;            // -> enum ImDrawTextFlags_         // Flags: for ImTextCalcWordWrapPositionEx()
 typedef int ImGuiActivateFlags;         // -> enum ImGuiActivateFlags_      // Flags: for navigation/focus function (will be for ActivateItem() later)
 typedef int ImGuiDebugLogFlags;         // -> enum ImGuiDebugLogFlags_      // Flags: for ShowDebugLogWindow(), g.DebugLogFlags
 typedef int ImGuiFocusRequestFlags;     // -> enum ImGuiFocusRequestFlags_  // Flags: for FocusWindow()
@@ -206,6 +211,10 @@
 typedef int ImGuiTypingSelectFlags;     // -> enum ImGuiTypingSelectFlags_  // Flags: for GetTypingSelectRequest()
 typedef int ImGuiWindowRefreshFlags;    // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy()
 
+// Table column indexing
+typedef ImS16 ImGuiTableColumnIdx;
+typedef ImU16 ImGuiTableDrawChannelIdx;
+
 //-----------------------------------------------------------------------------
 // [SECTION] Context pointer
 // See implementation of this variable in imgui.cpp for comments and details.
@@ -238,7 +247,7 @@
 #define IMGUI_DEBUG_LOG_SELECTION(...)  do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)   IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
 #define IMGUI_DEBUG_LOG_CLIPPER(...)    do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper)     IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
 #define IMGUI_DEBUG_LOG_IO(...)         do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO)          IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
-#define IMGUI_DEBUG_LOG_FONT(...)       do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFont)        IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
+#define IMGUI_DEBUG_LOG_FONT(...)       do { ImGuiContext* g2 = GImGui; if (g2 && g2->DebugLogFlags & ImGuiDebugLogFlags_EventFont) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) // Called from ImFontAtlas function which may operate without a context.
 #define IMGUI_DEBUG_LOG_INPUTROUTING(...) do{if (g.DebugLogFlags & ImGuiDebugLogFlags_EventInputRouting)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0)
 
 // Static Asserts
@@ -327,6 +336,7 @@
 #define IM_PRIu64   "llu"
 #define IM_PRIX64   "llX"
 #endif
+#define IM_TEXTUREID_TO_U64(_TEXID) ((ImU64)(intptr_t)(_TEXID))
 
 //-----------------------------------------------------------------------------
 // [SECTION] Generic helpers
@@ -348,6 +358,7 @@
 // - Helper: ImBitArray
 // - Helper: ImBitVector
 // - Helper: ImSpan<>, ImSpanAllocator<>
+// - Helper: ImStableVector<>
 // - Helper: ImPool<>
 // - Helper: ImChunkStream<>
 // - Helper: ImGuiTextIndex
@@ -357,20 +368,21 @@
 // Helpers: Hashing
 IMGUI_API ImGuiID       ImHashData(const void* data, size_t data_size, ImGuiID seed = 0);
 IMGUI_API ImGuiID       ImHashStr(const char* data, size_t data_size = 0, ImGuiID seed = 0);
+IMGUI_API const char*   ImHashSkipUncontributingPrefix(const char* label);
 
 // Helpers: Sorting
 #ifndef ImQsort
-static inline void      ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); }
+inline void             ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); }
 #endif
 
 // Helpers: Color Blending
 IMGUI_API ImU32         ImAlphaBlendColors(ImU32 col_a, ImU32 col_b);
 
 // Helpers: Bit manipulation
-static inline bool      ImIsPowerOfTwo(int v)               { return v != 0 && (v & (v - 1)) == 0; }
-static inline bool      ImIsPowerOfTwo(ImU64 v)             { return v != 0 && (v & (v - 1)) == 0; }
-static inline int       ImUpperPowerOfTwo(int v)            { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; }
-static inline unsigned int ImCountSetBits(unsigned int v)   { unsigned int count = 0; while (v > 0) { v = v & (v - 1); count++; } return count; }
+inline bool             ImIsPowerOfTwo(int v)               { return v != 0 && (v & (v - 1)) == 0; }
+inline bool             ImIsPowerOfTwo(ImU64 v)             { return v != 0 && (v & (v - 1)) == 0; }
+inline int              ImUpperPowerOfTwo(int v)            { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; }
+inline unsigned int     ImCountSetBits(unsigned int v)      { unsigned int count = 0; while (v > 0) { v = v & (v - 1); count++; } return count; }
 
 // Helpers: String
 #define ImStrlen strlen
@@ -379,6 +391,7 @@
 IMGUI_API int           ImStrnicmp(const char* str1, const char* str2, size_t count);       // Case insensitive compare to a certain count.
 IMGUI_API void          ImStrncpy(char* dst, const char* src, size_t count);                // Copy to a certain count and always zero terminate (strncpy doesn't).
 IMGUI_API char*         ImStrdup(const char* str);                                          // Duplicate a string.
+IMGUI_API void*         ImMemdup(const void* src, size_t size);                             // Duplicate a chunk of memory.
 IMGUI_API char*         ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str);        // Copy in provided buffer, recreate buffer if needed.
 IMGUI_API const char*   ImStrchrRange(const char* str_begin, const char* str_end, char c);  // Find first occurrence of 'c' in string range.
 IMGUI_API const char*   ImStreolRange(const char* str, const char* str_end);                // End end-of-line
@@ -388,10 +401,10 @@
 IMGUI_API int           ImStrlenW(const ImWchar* str);                                      // Computer string length (ImWchar string)
 IMGUI_API const char*   ImStrbol(const char* buf_mid_line, const char* buf_begin);          // Find beginning-of-line
 IM_MSVC_RUNTIME_CHECKS_OFF
-static inline char      ImToUpper(char c)               { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; }
-static inline bool      ImCharIsBlankA(char c)          { return c == ' ' || c == '\t'; }
-static inline bool      ImCharIsBlankW(unsigned int c)  { return c == ' ' || c == '\t' || c == 0x3000; }
-static inline bool      ImCharIsXdigitA(char c)         { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); }
+inline char             ImToUpper(char c)               { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; }
+inline bool             ImCharIsBlankA(char c)          { return c == ' ' || c == '\t'; }
+inline bool             ImCharIsBlankW(unsigned int c)  { return c == ' ' || c == '\t' || c == 0x3000; }
+inline bool             ImCharIsXdigitA(char c)         { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); }
 IM_MSVC_RUNTIME_CHECKS_RESTORE
 
 // Helpers: Formatting
@@ -407,7 +420,7 @@
 IMGUI_API int           ImParseFormatPrecision(const char* format, int default_value);
 
 // Helpers: UTF-8 <> wchar conversions
-IMGUI_API const char*   ImTextCharToUtf8(char out_buf[5], unsigned int c);                                                      // return out_buf
+IMGUI_API int           ImTextCharToUtf8(char out_buf[5], unsigned int c);                                                      // return output UTF-8 bytes count
 IMGUI_API int           ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end);   // return output UTF-8 bytes count
 IMGUI_API int           ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end);               // read one character. return input UTF-8 bytes count
 IMGUI_API int           ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL);   // return input UTF-8 bytes count
@@ -417,15 +430,27 @@
 IMGUI_API const char*   ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr);                   // return previous UTF-8 code-point.
 IMGUI_API int           ImTextCountLines(const char* in_text, const char* in_text_end);                                         // return number of lines taken by text. trailing carriage return doesn't count as an extra line.
 
+// Helpers: High-level text functions (DO NOT USE!!! THIS IS A MINIMAL SUBSET OF LARGER UPCOMING CHANGES)
+enum ImDrawTextFlags_
+{
+    ImDrawTextFlags_None                = 0,
+    ImDrawTextFlags_CpuFineClip         = 1 << 0,    // Must be == 1/true for legacy with 'bool cpu_fine_clip' arg to RenderText()
+    ImDrawTextFlags_WrapKeepBlanks      = 1 << 1,
+    ImDrawTextFlags_StopOnNewLine       = 1 << 2,
+};
+IMGUI_API ImVec2        ImFontCalcTextSizeEx(ImFont* font, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags);
+IMGUI_API const char*   ImFontCalcWordWrapPositionEx(ImFont* font, float size, const char* text, const char* text_end, float wrap_width, ImDrawTextFlags flags = 0);
+IMGUI_API const char*   ImTextCalcWordWrapNextLineStart(const char* text, const char* text_end, ImDrawTextFlags flags = 0); // trim trailing space and find beginning of next line
+
 // Helpers: File System
 #ifdef IMGUI_DISABLE_FILE_FUNCTIONS
 #define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS
 typedef void* ImFileHandle;
-static inline ImFileHandle  ImFileOpen(const char*, const char*)                    { return NULL; }
-static inline bool          ImFileClose(ImFileHandle)                               { return false; }
-static inline ImU64         ImFileGetSize(ImFileHandle)                             { return (ImU64)-1; }
-static inline ImU64         ImFileRead(void*, ImU64, ImU64, ImFileHandle)           { return 0; }
-static inline ImU64         ImFileWrite(const void*, ImU64, ImU64, ImFileHandle)    { return 0; }
+inline ImFileHandle         ImFileOpen(const char*, const char*)                    { return NULL; }
+inline bool                 ImFileClose(ImFileHandle)                               { return false; }
+inline ImU64                ImFileGetSize(ImFileHandle)                             { return (ImU64)-1; }
+inline ImU64                ImFileRead(void*, ImU64, ImU64, ImFileHandle)           { return 0; }
+inline ImU64                ImFileWrite(const void*, ImU64, ImU64, ImFileHandle)    { return 0; }
 #endif
 #ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS
 typedef FILE* ImFileHandle;
@@ -452,54 +477,56 @@
 #define ImAtan2(Y, X)       atan2f((Y), (X))
 #define ImAtof(STR)         atof(STR)
 #define ImCeil(X)           ceilf(X)
-static inline float  ImPow(float x, float y)    { return powf(x, y); }          // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision
-static inline double ImPow(double x, double y)  { return pow(x, y); }
-static inline float  ImLog(float x)             { return logf(x); }             // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision
-static inline double ImLog(double x)            { return log(x); }
-static inline int    ImAbs(int x)               { return x < 0 ? -x : x; }
-static inline float  ImAbs(float x)             { return fabsf(x); }
-static inline double ImAbs(double x)            { return fabs(x); }
-static inline float  ImSign(float x)            { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument
-static inline double ImSign(double x)           { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; }
+inline float  ImPow(float x, float y)    { return powf(x, y); }          // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision
+inline double ImPow(double x, double y)  { return pow(x, y); }
+inline float  ImLog(float x)             { return logf(x); }             // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision
+inline double ImLog(double x)            { return log(x); }
+inline int    ImAbs(int x)               { return x < 0 ? -x : x; }
+inline float  ImAbs(float x)             { return fabsf(x); }
+inline double ImAbs(double x)            { return fabs(x); }
+inline float  ImSign(float x)            { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument
+inline double ImSign(double x)           { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; }
 #ifdef IMGUI_ENABLE_SSE
-static inline float  ImRsqrt(float x)           { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); }
+inline float  ImRsqrt(float x)           { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); }
 #else
-static inline float  ImRsqrt(float x)           { return 1.0f / sqrtf(x); }
+inline float  ImRsqrt(float x)           { return 1.0f / sqrtf(x); }
 #endif
-static inline double ImRsqrt(double x)          { return 1.0 / sqrt(x); }
+inline double ImRsqrt(double x)          { return 1.0 / sqrt(x); }
 #endif
 // - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double
 // (Exceptionally using templates here but we could also redefine them for those types)
-template<typename T> static inline T ImMin(T lhs, T rhs)                        { return lhs < rhs ? lhs : rhs; }
-template<typename T> static inline T ImMax(T lhs, T rhs)                        { return lhs >= rhs ? lhs : rhs; }
-template<typename T> static inline T ImClamp(T v, T mn, T mx)                   { return (v < mn) ? mn : (v > mx) ? mx : v; }
-template<typename T> static inline T ImLerp(T a, T b, float t)                  { return (T)(a + (b - a) * t); }
-template<typename T> static inline void ImSwap(T& a, T& b)                      { T tmp = a; a = b; b = tmp; }
-template<typename T> static inline T ImAddClampOverflow(T a, T b, T mn, T mx)   { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; }
-template<typename T> static inline T ImSubClampOverflow(T a, T b, T mn, T mx)   { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; }
+template<typename T> T ImMin(T lhs, T rhs)                              { return lhs < rhs ? lhs : rhs; }
+template<typename T> T ImMax(T lhs, T rhs)                              { return lhs >= rhs ? lhs : rhs; }
+template<typename T> T ImClamp(T v, T mn, T mx)                         { return (v < mn) ? mn : (v > mx) ? mx : v; }
+template<typename T> T ImLerp(T a, T b, float t)                        { return (T)(a + (b - a) * t); }
+template<typename T> void ImSwap(T& a, T& b)                            { T tmp = a; a = b; b = tmp; }
+template<typename T> T ImAddClampOverflow(T a, T b, T mn, T mx)         { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; }
+template<typename T> T ImSubClampOverflow(T a, T b, T mn, T mx)         { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; }
 // - Misc maths helpers
-static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs)                { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); }
-static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs)                { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); }
-static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2&mn, const ImVec2&mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); }
-static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t)          { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); }
-static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t)  { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); }
-static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t)          { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); }
-static inline float  ImSaturate(float f)                                        { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; }
-static inline float  ImLengthSqr(const ImVec2& lhs)                             { return (lhs.x * lhs.x) + (lhs.y * lhs.y); }
-static inline float  ImLengthSqr(const ImVec4& lhs)                             { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); }
-static inline float  ImInvLength(const ImVec2& lhs, float fail_value)           { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; }
-static inline float  ImTrunc(float f)                                           { return (float)(int)(f); }
-static inline ImVec2 ImTrunc(const ImVec2& v)                                   { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); }
-static inline float  ImFloor(float f)                                           { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf()
-static inline ImVec2 ImFloor(const ImVec2& v)                                   { return ImVec2(ImFloor(v.x), ImFloor(v.y)); }
-static inline int    ImModPositive(int a, int b)                                { return (a + b) % b; }
-static inline float  ImDot(const ImVec2& a, const ImVec2& b)                    { return a.x * b.x + a.y * b.y; }
-static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a)        { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); }
-static inline float  ImLinearSweep(float current, float target, float speed)    { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; }
-static inline float  ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; }
-static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs)                { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); }
-static inline bool   ImIsFloatAboveGuaranteedIntegerPrecision(float f)          { return f <= -16777216 || f >= 16777216; }
-static inline float  ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; }
+inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs)               { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); }
+inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs)               { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); }
+inline ImVec2 ImClamp(const ImVec2& v, const ImVec2&mn, const ImVec2&mx){ return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); }
+inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t)         { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); }
+inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); }
+inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t)         { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); }
+inline float  ImSaturate(float f)                                       { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; }
+inline float  ImLengthSqr(const ImVec2& lhs)                            { return (lhs.x * lhs.x) + (lhs.y * lhs.y); }
+inline float  ImLengthSqr(const ImVec4& lhs)                            { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); }
+inline float  ImInvLength(const ImVec2& lhs, float fail_value)          { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; }
+inline float  ImTrunc(float f)                                          { return (float)(int)(f); }
+inline ImVec2 ImTrunc(const ImVec2& v)                                  { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); }
+inline float  ImFloor(float f)                                          { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf()
+inline ImVec2 ImFloor(const ImVec2& v)                                  { return ImVec2(ImFloor(v.x), ImFloor(v.y)); }
+inline float  ImTrunc64(float f)                                        { return (float)(ImS64)(f); }
+inline float  ImRound64(float f)                                        { return (float)(ImS64)(f + 0.5f); }
+inline int    ImModPositive(int a, int b)                               { return (a + b) % b; }
+inline float  ImDot(const ImVec2& a, const ImVec2& b)                   { return a.x * b.x + a.y * b.y; }
+inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a)       { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); }
+inline float  ImLinearSweep(float current, float target, float speed)   { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; }
+inline float  ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; }
+inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs)               { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); }
+inline bool   ImIsFloatAboveGuaranteedIntegerPrecision(float f)         { return f <= -16777216 || f >= 16777216; }
+inline float  ImExponentialMovingAverage(float avg, float sample, int n){ avg -= avg / n; avg += sample / n; return avg; }
 IM_MSVC_RUNTIME_CHECKS_RESTORE
 
 // Helpers: Geometry
@@ -524,6 +551,14 @@
     constexpr ImVec1(float _x) : x(_x) { }
 };
 
+// Helper: ImVec2i (2D vector, integer)
+struct ImVec2i
+{
+    int         x, y;
+    constexpr ImVec2i()                             : x(0), y(0) {}
+    constexpr ImVec2i(int _x, int _y)               : x(_x), y(_y) {}
+};
+
 // Helper: ImVec2ih (2D vector, half-size integer, for long-term packed storage)
 struct ImVec2ih
 {
@@ -570,6 +605,7 @@
     void        Floor()                             { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); }
     bool        IsInverted() const                  { return Min.x > Max.x || Min.y > Max.y; }
     ImVec4      ToVec4() const                      { return ImVec4(Min.x, Min.y, Max.x, Max.y); }
+    const ImVec4& AsVec4() const                    { return *(const ImVec4*)&Min.x; }
 };
 
 // Helper: ImBitArray
@@ -675,6 +711,39 @@
     inline void  GetSpan(int n, ImSpan<T>* span)    { span->set((T*)GetSpanPtrBegin(n), (T*)GetSpanPtrEnd(n)); }
 };
 
+// Helper: ImStableVector<>
+// Allocating chunks of BLOCK_SIZE items. Objects pointers are never invalidated when growing, only by clear().
+// Important: does not destruct anything!
+// Implemented only the minimum set of functions we need for it.
+template<typename T, int BLOCK_SIZE>
+struct ImStableVector
+{
+    int                 Size = 0;
+    int                 Capacity = 0;
+    ImVector<T*>        Blocks;
+
+    // Functions
+    inline ~ImStableVector()                        { for (T* block : Blocks) IM_FREE(block); }
+
+    inline void         clear()                     { Size = Capacity = 0; Blocks.clear_delete(); }
+    inline void         resize(int new_size)        { if (new_size > Capacity) reserve(new_size); Size = new_size; }
+    inline void         reserve(int new_cap)
+    {
+        new_cap = IM_MEMALIGN(new_cap, BLOCK_SIZE);
+        int old_count = Capacity / BLOCK_SIZE;
+        int new_count = new_cap / BLOCK_SIZE;
+        if (new_count <= old_count)
+            return;
+        Blocks.resize(new_count);
+        for (int n = old_count; n < new_count; n++)
+            Blocks[n] = (T*)IM_ALLOC(sizeof(T) * BLOCK_SIZE);
+        Capacity = new_cap;
+    }
+    inline T&           operator[](int i)           { IM_ASSERT(i >= 0 && i < Size); return Blocks[i / BLOCK_SIZE][i % BLOCK_SIZE]; }
+    inline const T&     operator[](int i) const     { IM_ASSERT(i >= 0 && i < Size); return Blocks[i / BLOCK_SIZE][i % BLOCK_SIZE]; }
+    inline T*           push_back(const T& v)       { int i = Size; IM_ASSERT(i >= 0); if (Size == Capacity) reserve(Capacity + BLOCK_SIZE); void* ptr = &Blocks[i / BLOCK_SIZE][i % BLOCK_SIZE]; memcpy(ptr, &v, sizeof(v)); Size++; return (T*)ptr; }
+};
+
 // Helper: ImPool<>
 // Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer,
 // Honor constructor/destructor. Add/remove invalidate all pointers. Indexes have the same lifetime as the associated object.
@@ -735,13 +804,13 @@
 // Maintain a line index for a text buffer. This is a strong candidate to be moved into the public API.
 struct ImGuiTextIndex
 {
-    ImVector<int>   LineOffsets;
+    ImVector<int>   Offsets;
     int             EndOffset = 0;                          // Because we don't own text buffer we need to maintain EndOffset (may bake in LineOffsets?)
 
-    void            clear()                                 { LineOffsets.clear(); EndOffset = 0; }
-    int             size()                                  { return LineOffsets.Size; }
-    const char*     get_line_begin(const char* base, int n) { return base + LineOffsets[n]; }
-    const char*     get_line_end(const char* base, int n)   { return base + (n + 1 < LineOffsets.Size ? (LineOffsets[n + 1] - 1) : EndOffset); }
+    void            clear()                                 { Offsets.clear(); EndOffset = 0; }
+    int             size()                                  { return Offsets.Size; }
+    const char*     get_line_begin(const char* base, int n) { return base + (Offsets.Size != 0 ? Offsets[n] : 0); }
+    const char*     get_line_end(const char* base, int n)   { return base + (n + 1 < Offsets.Size ? (Offsets[n + 1] - 1) : EndOffset); }
     void            append(const char* base, int old_size, int new_size);
 };
 
@@ -783,17 +852,20 @@
 // You may want to create your own instance of you try to ImDrawList completely without ImGui. In that case, watch out for future changes to this structure.
 struct IMGUI_API ImDrawListSharedData
 {
-    ImVec2          TexUvWhitePixel;            // UV of white pixel in the atlas
-    const ImVec4*   TexUvLines;                 // UV of anti-aliased lines in the atlas
-    ImFont*         Font;                       // Current/default font (optional, for simplified AddText overload)
-    float           FontSize;                   // Current/default font size (optional, for simplified AddText overload)
-    float           FontScale;                  // Current/default font scale (== FontSize / Font->FontSize)
+    ImVec2          TexUvWhitePixel;            // UV of white pixel in the atlas (== FontAtlas->TexUvWhitePixel)
+    const ImVec4*   TexUvLines;                 // UV of anti-aliased lines in the atlas (== FontAtlas->TexUvLines)
+    ImFontAtlas*    FontAtlas;                  // Current font atlas
+    ImFont*         Font;                       // Current font (used for simplified AddText overload)
+    float           FontSize;                   // Current font size (used for for simplified AddText overload)
+    float           FontScale;                  // Current font scale (== FontSize / Font->FontSize)
     float           CurveTessellationTol;       // Tessellation tolerance when using PathBezierCurveTo()
     float           CircleSegmentMaxError;      // Number of circle segments to use per pixel of radius for AddCircle() etc
     float           InitialFringeScale;         // Initial scale to apply to AA fringe
     ImDrawListFlags InitialFlags;               // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards)
     ImVec4          ClipRectFullscreen;         // Value for PushClipRectFullscreen()
     ImVector<ImVec2> TempBuffer;                // Temporary write buffer
+    ImVector<ImDrawList*> DrawLists;            // All draw lists associated to this ImDrawListSharedData
+    ImGuiContext*   Context;                    // [OPTIONAL] Link to Dear ImGui context. 99% of ImDrawList/ImFontAtlas can function without an ImGui context, but this facilitate handling one legacy edge case.
 
     // Lookup tables
     ImVec2          ArcFastVtx[IM_DRAWLIST_ARCFAST_TABLE_SIZE]; // Sample points on the quarter of the circle.
@@ -801,6 +873,7 @@
     ImU8            CircleSegmentCounts[64];    // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead)
 
     ImDrawListSharedData();
+    ~ImDrawListSharedData();
     void SetCircleTessellationMaxError(float max_error);
 };
 
@@ -812,6 +885,13 @@
     ImDrawDataBuilder()                     { memset(this, 0, sizeof(*this)); }
 };
 
+struct ImFontStackData
+{
+    ImFont*     Font;
+    float       FontSizeBeforeScaling;      // ~~ style.FontSizeBase
+    float       FontSizeAfterScaling;       // ~~ g.FontSize
+};
+
 //-----------------------------------------------------------------------------
 // [SECTION] Style support
 //-----------------------------------------------------------------------------
@@ -883,6 +963,7 @@
     ImGuiItemFlags_AllowOverlap             = 1 << 14, // false     // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame.
     ImGuiItemFlags_NoNavDisableMouseHover   = 1 << 15, // false     // Nav keyboard/gamepad mode doesn't disable hover highlight (behave as if NavHighlightItemUnderNav==false).
     ImGuiItemFlags_NoMarkEdited             = 1 << 16, // false     // Skip calling MarkItemEdited()
+    ImGuiItemFlags_NoFocus                  = 1 << 17, // false     // [EXPERIMENTAL: Not very well specced] Clicking doesn't take focus. Automatically sets ImGuiButtonFlags_NoFocus + ImGuiButtonFlags_NoNavFocus in ButtonBehavior().
 
     // Controlled by widget code
     ImGuiItemFlags_Inputable                = 1 << 20, // false     // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature.
@@ -911,6 +992,7 @@
     ImGuiItemStatusFlags_Visible            = 1 << 8,   // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()).
     ImGuiItemStatusFlags_HasClipRect        = 1 << 9,   // g.LastItemData.ClipRect is valid.
     ImGuiItemStatusFlags_HasShortcut        = 1 << 10,  // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd().
+    //ImGuiItemStatusFlags_FocusedByTabbing = 1 << 8,   // Removed IN 1.90.1 (Dec 2023). The trigger is part of g.NavActivateId. See commit 54c1bdeceb.
 
     // Additional status + semantic for ImGuiTestEngine
 #ifdef IMGUI_ENABLE_TEST_ENGINE
@@ -960,8 +1042,10 @@
     ImGuiButtonFlags_NoHoveredOnFocus       = 1 << 19,  // don't report as hovered when nav focus is on this item
     ImGuiButtonFlags_NoSetKeyOwner          = 1 << 20,  // don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!)
     ImGuiButtonFlags_NoTestKeyOwner         = 1 << 21,  // don't test key/input owner when polling the key (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!)
+    ImGuiButtonFlags_NoFocus                = 1 << 22,  // [EXPERIMENTAL: Not very well specced]. Don't focus parent window when clicking.
     ImGuiButtonFlags_PressedOnMask_         = ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_PressedOnDragDropHold,
     ImGuiButtonFlags_PressedOnDefault_      = ImGuiButtonFlags_PressedOnClickRelease,
+    //ImGuiButtonFlags_NoKeyModifiers       = ImGuiButtonFlags_NoKeyModsAllowed, // Renamed in 1.91.4
 };
 
 // Extend ImGuiComboFlags_
@@ -982,7 +1066,6 @@
 {
     // NB: need to be in sync with last value of ImGuiSelectableFlags_
     ImGuiSelectableFlags_NoHoldingActiveID      = 1 << 20,
-    ImGuiSelectableFlags_SelectOnNav            = 1 << 21,  // (WIP) Auto-select when moved into. This is not exposed in public API as to handle multi-select and modifiers we will need user to explicitly control focus scope. May be replaced with a BeginSelection() API.
     ImGuiSelectableFlags_SelectOnClick          = 1 << 22,  // Override button behavior to react on Click (default is Click+Release)
     ImGuiSelectableFlags_SelectOnRelease        = 1 << 23,  // Override button behavior to react on Release (default is Click+Release)
     ImGuiSelectableFlags_SpanAvailWidth         = 1 << 24,  // Span all avail width even if we declared less for layout purpose. FIXME: We may be able to remove this (added in 6251d379, 2bcafc86 for menus)
@@ -994,9 +1077,11 @@
 // Extend ImGuiTreeNodeFlags_
 enum ImGuiTreeNodeFlagsPrivate_
 {
+    ImGuiTreeNodeFlags_NoNavFocus                 = 1 << 27,// Don't claim nav focus when interacting with this item (#8551)
     ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 28,// FIXME-WIP: Hard-coded for CollapsingHeader()
     ImGuiTreeNodeFlags_UpsideDownArrow            = 1 << 29,// FIXME-WIP: Turn Down arrow into an Up arrow, for reversed trees (#6517)
     ImGuiTreeNodeFlags_OpenOnMask_                = ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow,
+    ImGuiTreeNodeFlags_DrawLinesMask_             = ImGuiTreeNodeFlags_DrawLinesNone | ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes,
 };
 
 enum ImGuiSeparatorFlags_
@@ -1148,11 +1233,15 @@
     ImVector<char>          CallbackTextBackup;     // temporary storage for callback to support automatic reconcile of undo-stack
     int                     BufCapacity;            // end-user buffer capacity (include zero terminator)
     ImVec2                  Scroll;                 // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y)
+    int                     LineCount;              // last line count (solely for debugging)
+    float                   WrapWidth;              // word-wrapping width
     float                   CursorAnim;             // timer for cursor blink, reset on every user action so the cursor reappears immediately
     bool                    CursorFollow;           // set when we want scrolling to follow the current cursor position (not always!)
+    bool                    CursorCenterY;          // set when we want scrolling to be centered over the cursor position (while resizing a word-wrapping field)
     bool                    SelectedAllMouseLock;   // after a double-click to select all, we ignore further mouse drags to update selection
     bool                    Edited;                 // edited this frame
     bool                    WantReloadUserBuf;      // force a reload of user buf so it may be modified externally. may be automatic in future version.
+    ImS8                    LastMoveDirectionLR;    // ImGuiDir_Left or ImGuiDir_Right. track last movement direction so when cursor cross over a word-wrapping boundaries we can display it on either line depending on last move.s
     int                     ReloadSelectionStart;
     int                     ReloadSelectionEnd;
 
@@ -1162,6 +1251,7 @@
     void        ClearFreeMemory()           { TextA.clear(); TextToRevertTo.clear(); }
     void        OnKeyPressed(int key);      // Cannot be inline because we call in code in stb_textedit.h implementation
     void        OnCharPressed(unsigned int c);
+    float       GetPreferredOffsetX() const;
 
     // Cursor & Selection
     void        CursorAnimReset();
@@ -1283,15 +1373,18 @@
 };
 
 // Store data emitted by TreeNode() for usage by TreePop()
-// - To implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere: store the minimum amount of data
+// - To implement ImGuiTreeNodeFlags_NavLeftJumpsToParent: store the minimum amount of data
 //   which we can't infer in TreePop(), to perform the equivalent of NavApplyItemToResult().
 //   Only stored when the node is a potential candidate for landing on a Left arrow jump.
 struct ImGuiTreeNodeStackData
 {
     ImGuiID                 ID;
     ImGuiTreeNodeFlags      TreeFlags;
-    ImGuiItemFlags          ItemFlags;  // Used for nav landing
-    ImRect                  NavRect;    // Used for nav landing
+    ImGuiItemFlags          ItemFlags;      // Used for nav landing
+    ImRect                  NavRect;        // Used for nav landing
+    float                   DrawLinesX1;
+    float                   DrawLinesToNodesY2;
+    ImGuiTableColumnIdx     DrawLinesTableColumn;
 };
 
 // sizeof() = 20
@@ -1414,7 +1507,7 @@
     ImGuiInputEventType_COUNT
 };
 
-enum ImGuiInputSource
+enum ImGuiInputSource : int
 {
     ImGuiInputSource_None = 0,
     ImGuiInputSource_Mouse,         // Note: may be Mouse or TouchScreen or Pen. See io.MouseSource to distinguish them.
@@ -1463,12 +1556,12 @@
 {
     ImGuiKeyRoutingIndex            NextEntryIndex;
     ImU16                           Mods;               // Technically we'd only need 4-bits but for simplify we store ImGuiMod_ values which need 16-bits.
-    ImU8                            RoutingCurrScore;   // [DEBUG] For debug display
-    ImU8                            RoutingNextScore;   // Lower is better (0: perfect score)
+    ImU16                           RoutingCurrScore;   // [DEBUG] For debug display
+    ImU16                           RoutingNextScore;   // Lower is better (0: perfect score)
     ImGuiID                         RoutingCurr;
     ImGuiID                         RoutingNext;
 
-    ImGuiKeyRoutingData()           { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_NoOwner; }
+    ImGuiKeyRoutingData()           { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 0; RoutingCurr = RoutingNext = ImGuiKeyOwner_NoOwner; }
 };
 
 // Routing table: maintain a desired owner for each possible key-chord (key + mods), and setup owner in NewFrame() when mods are matching.
@@ -1577,8 +1670,9 @@
     ImGuiActivateFlags_PreferInput          = 1 << 0,       // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default for Enter key.
     ImGuiActivateFlags_PreferTweak          = 1 << 1,       // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default for Space key and if keyboard is not used.
     ImGuiActivateFlags_TryToPreserveState   = 1 << 2,       // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection)
-    ImGuiActivateFlags_FromTabbing          = 1 << 3,       // Activation requested by a tabbing request
+    ImGuiActivateFlags_FromTabbing          = 1 << 3,       // Activation requested by a tabbing request (ImGuiNavMoveFlags_IsTabbing)
     ImGuiActivateFlags_FromShortcut         = 1 << 4,       // Activation requested by an item shortcut via SetNextItemShortcut() function.
+    ImGuiActivateFlags_FromFocusApi         = 1 << 5,       // Activation requested by an api request (ImGuiNavMoveFlags_FocusApi)
 };
 
 // Early work-in-progress API for ScrollToItem()
@@ -1607,6 +1701,7 @@
     ImGuiNavHighlightFlags_Compact          = ImGuiNavRenderCursorFlags_Compact,    // Renamed in 1.91.4
     ImGuiNavHighlightFlags_AlwaysDraw       = ImGuiNavRenderCursorFlags_AlwaysDraw, // Renamed in 1.91.4
     ImGuiNavHighlightFlags_NoRounding       = ImGuiNavRenderCursorFlags_NoRounding, // Renamed in 1.91.4
+    //ImGuiNavHighlightFlags_TypeThin       = ImGuiNavRenderCursorFlags_Compact,    // Renamed in 1.90.2
 #endif
 };
 
@@ -2007,10 +2102,12 @@
     bool        ShowDrawCmdMesh = true;
     bool        ShowDrawCmdBoundingBoxes = true;
     bool        ShowTextEncodingViewer = false;
+    bool        ShowTextureUsedRect = false;
     int         ShowWindowsRectsType = -1;
     int         ShowTablesRectsType = -1;
     int         HighlightMonitorIdx = -1;
     ImGuiID     HighlightViewportID = 0;
+    bool        ShowFontPreview = true;
 };
 
 struct ImGuiStackLevelInfo
@@ -2018,10 +2115,10 @@
     ImGuiID                 ID;
     ImS8                    QueryFrameCount;            // >= 1: Query in progress
     bool                    QuerySuccess;               // Obtained result from DebugHookIdInfo()
-    ImGuiDataType           DataType : 8;
-    char                    Desc[57];                   // Arbitrarily sized buffer to hold a result (FIXME: could replace Results[] with a chunk stream?) FIXME: Now that we added CTRL+C this should be fixed.
+    ImS8                    DataType;                   // ImGuiDataType
+    int                     DescOffset;                 // -1 or offset into parent's ResultPathsBuf
 
-    ImGuiStackLevelInfo()   { memset(this, 0, sizeof(*this)); }
+    ImGuiStackLevelInfo()   { memset(this, 0, sizeof(*this)); DescOffset = -1; }
 };
 
 // State for ID Stack tool queries
@@ -2029,13 +2126,16 @@
 {
     int                     LastActiveFrame;
     int                     StackLevel;                 // -1: query stack and resize Results, >= 0: individual stack level
-    ImGuiID                 QueryId;                    // ID to query details for
+    ImGuiID                 QueryMainId;                // ID to query details for
     ImVector<ImGuiStackLevelInfo> Results;
-    bool                    CopyToClipboardOnCtrlC;
+    bool                    QueryHookActive;            // Used to disambiguate the case where DebugHookIdInfoId == 0 which is valid.
+    bool                    OptHexEncodeNonAsciiChars;
+    bool                    OptCopyToClipboardOnCtrlC;
     float                   CopyToClipboardLastTime;
-    ImGuiTextBuffer         ResultPathBuf;
+    ImGuiTextBuffer         ResultPathsBuf;
+    ImGuiTextBuffer         ResultTempBuf;
 
-    ImGuiIDStackTool()      { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; }
+    ImGuiIDStackTool()      { memset(this, 0, sizeof(*this)); LastActiveFrame = -1; OptHexEncodeNonAsciiChars = true; CopyToClipboardLastTime = -FLT_MAX; }
 };
 
 //-----------------------------------------------------------------------------
@@ -2063,15 +2163,17 @@
 struct ImGuiContext
 {
     bool                    Initialized;
-    bool                    FontAtlasOwnedByContext;            // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it.
     ImGuiIO                 IO;
     ImGuiPlatformIO         PlatformIO;
     ImGuiStyle              Style;
-    ImFont*                 Font;                               // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back()
-    float                   FontSize;                           // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window.
-    float                   FontBaseSize;                       // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height.
-    float                   FontScale;                          // == FontSize / Font->FontSize
-    float                   CurrentDpiScale;                    // Current window/viewport DpiScale
+    ImVector<ImFontAtlas*>  FontAtlases;                        // List of font atlases used by the context (generally only contains g.IO.Fonts aka the main font atlas)
+    ImFont*                 Font;                               // Currently bound font. (== FontStack.back().Font)
+    ImFontBaked*            FontBaked;                          // Currently bound font at currently bound size. (== Font->GetFontBaked(FontSize))
+    float                   FontSize;                           // Currently bound font size == line height (== FontSizeBase + externals scales applied in the UpdateCurrentFontSize() function).
+    float                   FontSizeBase;                       // Font size before scaling == style.FontSizeBase == value passed to PushFont() when specified.
+    float                   FontBakedScale;                     // == FontBaked->Size / FontSize. Scale factor over baked size. Rarely used nowadays, very often == 1.0f.
+    float                   FontRasterizerDensity;              // Current font density. Used by all calls to GetFontBaked().
+    float                   CurrentDpiScale;                    // Current window/viewport DpiScale == CurrentViewport->DpiScale
     ImDrawListSharedData    DrawListSharedData;
     double                  Time;
     int                     FrameCount;
@@ -2114,8 +2216,8 @@
     ImVec2                  WheelingAxisAvg;
 
     // Item/widgets state and tracking information
-    ImGuiID                 DebugDrawIdConflicts;               // Set when we detect multiple items with the same identifier
-    ImGuiID                 DebugHookIdInfo;                    // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line]
+    ImGuiID                 DebugDrawIdConflictsId;             // Set when we detect multiple items with the same identifier
+    ImGuiID                 DebugHookIdInfoId;                  // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line]
     ImGuiID                 HoveredId;                          // Hovered widget, filled during the frame
     ImGuiID                 HoveredIdPreviousFrame;
     int                     HoveredIdPreviousFrameItemCount;    // Count numbers of items using the same ID as last frame's hovered id
@@ -2134,6 +2236,7 @@
     bool                    ActiveIdHasBeenEditedBefore;        // Was the value associated to the widget Edited over the course of the Active state.
     bool                    ActiveIdHasBeenEditedThisFrame;
     bool                    ActiveIdFromShortcut;
+    ImGuiID                 ActiveIdDisabledId;                 // When clicking a disabled item we set ActiveId=window->MoveId to avoid interference with widget code. Actual item ID is stored here.
     int                     ActiveIdMouseButton : 8;
     ImVec2                  ActiveIdClickOffset;                // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior)
     ImGuiWindow*            ActiveIdWindow;
@@ -2172,7 +2275,7 @@
     ImGuiCol                        DebugFlashStyleColorIdx;    // (Keep close to ColorStack to share cache line)
     ImVector<ImGuiColorMod>         ColorStack;                 // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin()
     ImVector<ImGuiStyleMod>         StyleVarStack;              // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin()
-    ImVector<ImFont*>               FontStack;                  // Stack for PushFont()/PopFont() - inherited by Begin()
+    ImVector<ImFontStackData>       FontStack;                  // Stack for PushFont()/PopFont() - inherited by Begin()
     ImVector<ImGuiFocusScopeData>   FocusScopeStack;            // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin()
     ImVector<ImGuiItemFlags>        ItemFlagsStack;             // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin()
     ImVector<ImGuiGroupData>        GroupStack;                 // Stack for BeginGroup()/EndGroup() - not inherited by Begin()
@@ -2194,18 +2297,19 @@
     ImGuiWindow*            NavWindow;                          // Focused window for navigation. Could be called 'FocusedWindow'
     ImGuiID                 NavFocusScopeId;                    // Focused focus scope (e.g. selection code often wants to "clear other items" when landing on an item of the same scope)
     ImGuiNavLayer           NavLayer;                           // Focused layer (main scrolling layer, or menu/title bar layer)
-    ImGuiID                 NavActivateId;                      // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItem()
+    ImGuiID                 NavActivateId;                      // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItemByID()
     ImGuiID                 NavActivateDownId;                  // ~~ IsKeyDown(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyDown(ImGuiKey_NavGamepadActivate) ? NavId : 0
     ImGuiID                 NavActivatePressedId;               // ~~ IsKeyPressed(ImGuiKey_Space) || IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate) ? NavId : 0 (no repeat)
     ImGuiActivateFlags      NavActivateFlags;
     ImVector<ImGuiFocusScopeData> NavFocusRoute;                // Reversed copy focus scope stack for NavId (should contains NavFocusScopeId). This essentially follow the window->ParentWindowForFocusRoute chain.
     ImGuiID                 NavHighlightActivatedId;
     float                   NavHighlightActivatedTimer;
-    ImGuiID                 NavNextActivateId;                  // Set by ActivateItem(), queued until next frame.
+    ImGuiID                 NavNextActivateId;                  // Set by ActivateItemByID(), queued until next frame.
     ImGuiActivateFlags      NavNextActivateFlags;
-    ImGuiInputSource        NavInputSource;                     // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Mouse
+    ImGuiInputSource        NavInputSource;                     // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Gamepad
     ImGuiSelectionUserData  NavLastValidSelectionUserData;      // Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data.
     ImS8                    NavCursorHideFrames;
+    //ImGuiID               NavActivateInputId;                 // Removed in 1.89.4 (July 2023). This is now part of g.NavActivateId and sets g.NavActivateFlags |= ImGuiActivateFlags_PreferInput. See commit c9a53aa74, issue #5606.
 
     // Navigation: Init & Move Requests
     bool                    NavAnyRequest;                      // ~~ NavMoveRequest || NavInitRequest this is to perform early out in ItemAdd()
@@ -2240,6 +2344,7 @@
     bool                    NavJustMovedToHasSelectionData;     // Copy of move result's ItemFlags & ImGuiItemFlags_HasSelectionUserData). Maybe we should just store ImGuiNavItemData.
 
     // Navigation: Windowing (CTRL+TAB for list, or Menu button + keys or directional pads to move/resize)
+    bool                    ConfigNavWindowingWithGamepad;      // = true. Enable CTRL+TAB by holding ImGuiKey_GamepadFaceLeft (== ImGuiKey_NavGamepadMenu). When false, the button may still be used to toggle Menu layer.
     ImGuiKeyChord           ConfigNavWindowingKeyNext;          // = ImGuiMod_Ctrl | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiKey_Tab on OS X). For reconfiguration (see #4828)
     ImGuiKeyChord           ConfigNavWindowingKeyPrev;          // = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab on OS X)
     ImGuiWindow*            NavWindowingTarget;                 // Target window when doing CTRL+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most!
@@ -2247,8 +2352,9 @@
     ImGuiWindow*            NavWindowingListWindow;             // Internal window actually listing the CTRL+Tab contents
     float                   NavWindowingTimer;
     float                   NavWindowingHighlightAlpha;
-    bool                    NavWindowingToggleLayer;
-    ImGuiKey                NavWindowingToggleKey;
+    ImGuiInputSource        NavWindowingInputSource;
+    bool                    NavWindowingToggleLayer;            // Set while Alt or GamepadMenu is held, may be cleared by other operations, and processed when releasing the key.
+    ImGuiKey                NavWindowingToggleKey;              // Keyboard/gamepad key used when toggling to menu layer.
     ImVec2                  NavWindowingAccumDeltaPos;
     ImVec2                  NavWindowingAccumDeltaSize;
 
@@ -2266,6 +2372,7 @@
     ImRect                  DragDropTargetRect;                 // Store rectangle of current target candidate (we favor small targets when overlapping)
     ImRect                  DragDropTargetClipRect;             // Store ClipRect at the time of item's drawing
     ImGuiID                 DragDropTargetId;
+    ImGuiID                 DragDropTargetFullViewport;
     ImGuiDragDropFlags      DragDropAcceptFlags;
     float                   DragDropAcceptIdCurrRectSurface;    // Target item surface (we resolve overlapping targets by prioritizing the smaller surface)
     ImGuiID                 DragDropAcceptIdCurr;               // Target item id (set at the time of accepting the payload)
@@ -2316,8 +2423,10 @@
 
     // Widget state
     ImGuiInputTextState     InputTextState;
+    ImGuiTextIndex          InputTextLineIndex;                 // Temporary storage
     ImGuiInputTextDeactivatedState InputTextDeactivatedState;
-    ImFont                  InputTextPasswordFont;
+    ImFontBaked             InputTextPasswordFontBackupBaked;
+    ImFontFlags             InputTextPasswordFontBackupFlags;
     ImGuiID                 TempInputId;                        // Temporary text input when CTRL+clicking on a slider, etc.
     ImGuiDataTypeStorage    DataTypeZeroValue;                  // 0 for all data types
     int                     BeginMenuDepth;
@@ -2349,9 +2458,13 @@
     ImGuiTypingSelectState  TypingSelectState;                  // State for GetTypingSelectRequest()
 
     // Platform support
-    ImGuiPlatformImeData    PlatformImeData;                    // Data updated by current frame
+    ImGuiPlatformImeData    PlatformImeData;                    // Data updated by current frame. Will be applied at end of the frame. For some backends, this is required to have WantVisible=true in order to receive text message.
     ImGuiPlatformImeData    PlatformImeDataPrev;                // Previous frame data. When changed we call the platform_io.Platform_SetImeDataFn() handler.
 
+    // Extensions
+    // FIXME: We could provide an API to register one slot in an array held in ImGuiContext?
+    ImVector<ImTextureData*> UserTextures;                      // List of textures created/managed by user or third-party extension. Automatically appended into platform_io.Textures[].
+
     // Settings
     bool                    SettingsLoaded;
     float                   SettingsDirtyTimer;                 // Save .ini Settings to memory when time reaches zero
@@ -2371,7 +2484,7 @@
     ImGuiWindow*            LogWindow;
     ImFileHandle            LogFile;                            // If != NULL log to stdout/ file
     ImGuiTextBuffer         LogBuffer;                          // Accumulation buffer when log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators.
-    const char*             LogNextPrefix;
+    const char*             LogNextPrefix;                      // See comment in LogSetNextTextDecoration(): doesn't copy underlying data, use carefully!
     const char*             LogNextSuffix;
     float                   LogLinePosY;
     bool                    LogLineFirstItem;
@@ -2409,6 +2522,10 @@
     ImGuiMetricsConfig      DebugMetricsConfig;
     ImGuiIDStackTool        DebugIDStackTool;
     ImGuiDebugAllocInfo     DebugAllocInfo;
+#if defined(IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS) && !defined(IMGUI_DISABLE_DEBUG_TOOLS)
+    ImGuiStorage            DebugDrawIdConflictsAliveCount;
+    ImGuiStorage            DebugDrawIdConflictsHighlightSet;
+#endif
 
     // Misc
     float                   FramerateSecPerFrame[60];           // Calculate estimate of framerate for user over the last 60 frames..
@@ -2417,7 +2534,7 @@
     float                   FramerateSecPerFrameAccum;
     int                     WantCaptureMouseNextFrame;          // Explicit capture override via SetNextFrameWantCaptureMouse()/SetNextFrameWantCaptureKeyboard(). Default to -1.
     int                     WantCaptureKeyboardNextFrame;       // "
-    int                     WantTextInputNextFrame;
+    int                     WantTextInputNextFrame;             // Copied in EndFrame() from g.PlatformImeData.WantTextInput. Needs to be set for some backends (SDL3) to emit character inputs.
     ImVector<char>          TempBuffer;                         // Temporary text buffer
     char                    TempKeychordName[64];
 
@@ -2463,7 +2580,8 @@
     ImVec2                  MenuBarOffset;          // MenuBarOffset.x is sort of equivalent of a per-layer CursorPos.x, saved/restored as we switch to the menu bar. The only situation when MenuBarOffset.y is > 0 if when (SafeAreaPadding.y > FramePadding.y), often used on TVs.
     ImGuiMenuColumns        MenuColumns;            // Simplified columns storage for menu items measurement
     int                     TreeDepth;              // Current tree depth.
-    ImU32                   TreeHasStackDataDepthMask; // Store whether given depth has ImGuiTreeNodeStackData data. Could be turned into a ImU64 if necessary.
+    ImU32                   TreeHasStackDataDepthMask;      // Store whether given depth has ImGuiTreeNodeStackData data. Could be turned into a ImU64 if necessary.
+    ImU32                   TreeRecordsClippedNodesY2Mask;  // Store whether we should keep recording Y2. Cleared when passing clip max. Equivalent TreeHasStackDataDepthMask value should always be set.
     ImVector<ImGuiWindow*>  ChildWindows;
     ImGuiStorage*           StateStorage;           // Current persistent per-window storage (store e.g. tree node open/close state)
     ImGuiOldColumns*        CurrentColumns;         // Current columns set
@@ -2606,9 +2724,11 @@
 
     // We don't use g.FontSize because the window may be != g.CurrentWindow.
     ImRect      Rect() const            { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); }
-    float       CalcFontSize() const    { ImGuiContext& g = *Ctx; return g.FontBaseSize * FontWindowScale * FontWindowScaleParents; }
     ImRect      TitleBarRect() const    { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight)); }
     ImRect      MenuBarRect() const     { float y1 = Pos.y + TitleBarHeight; return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight); }
+
+    // [Obsolete] ImGuiWindow::CalcFontSize() was removed in 1.92.x because error-prone/misleading. You can use window->FontRefSize for a copy of g.FontSize at the time of the last Begin() call for this window.
+    //float     CalcFontSize() const    { ImGuiContext& g = *Ctx; return g.FontSizeBase * FontWindowScale * FontWindowScaleParents;
 };
 
 //-----------------------------------------------------------------------------
@@ -2642,7 +2762,7 @@
     int                 LastFrameSelected;      // This allows us to infer an ordered list of the last activated tabs with little maintenance
     float               Offset;                 // Position relative to beginning of tab
     float               Width;                  // Width currently displayed
-    float               ContentWidth;           // Width of label, stored during BeginTabItem() call
+    float               ContentWidth;           // Width of label + padding, stored during BeginTabItem() call (misnamed as "Content" would normally imply width of label only)
     float               RequestedWidth;         // Width optionally requested by caller, -1.0f is unused
     ImS32               NameOffset;             // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames
     ImS16               BeginOrder;             // BeginTabItem() order, used to re-order tabs after toggling ImGuiTabBarFlags_Reorderable
@@ -2665,6 +2785,7 @@
     int                 CurrFrameVisible;
     int                 PrevFrameVisible;
     ImRect              BarRect;
+    float               BarRectPrevWidth;       // Backup of previous width. When width change we enforce keep horizontal scroll on focused tab.
     float               CurrTabsContentsHeight;
     float               PrevTabsContentsHeight; // Record the height of contents submitted below the tab bar
     float               WidthAllTabs;           // Actual width of all tabs (locked during layout)
@@ -2683,6 +2804,7 @@
     bool                WantLayout;
     bool                VisibleTabWasSubmitted;
     bool                TabsAddedNew;           // Set to true when a new tab item or button has been added to the tab bar during last frame
+    bool                ScrollButtonEnabled;
     ImS16               TabsActiveCount;        // Number of tabs submitted this frame.
     ImS16               LastTabItemIdx;         // Index of last BeginTabItem() tab for use by EndTabItem()
     float               ItemSpacingY;
@@ -2698,11 +2820,7 @@
 //-----------------------------------------------------------------------------
 
 #define IM_COL32_DISABLE                IM_COL32(0,0,0,1)   // Special sentinel code which cannot be used as a regular color.
-#define IMGUI_TABLE_MAX_COLUMNS         512                 // May be further lifted
-
-// Our current column maximum is 64 but we may raise that in the future.
-typedef ImS16 ImGuiTableColumnIdx;
-typedef ImU16 ImGuiTableDrawChannelIdx;
+#define IMGUI_TABLE_MAX_COLUMNS         512                 // Arbitrary "safety" maximum, may be lifted in the future if needed. Must fit in ImGuiTableColumnIdx/ImGuiTableDrawChannelIdx.
 
 // [Internal] sizeof() ~ 112
 // We use the terminology "Enabled" to refer to a column that is not Hidden by user/api.
@@ -2901,7 +3019,7 @@
     bool                        IsSortSpecsDirty;
     bool                        IsUsingHeaders;             // Set when the first row had the ImGuiTableRowFlags_Headers flag.
     bool                        IsContextPopupOpen;         // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted).
-    bool                        DisableDefaultContextMenu;  // Disable default context menu contents. You may submit your own using TableBeginContextMenuPopup()/EndPopup()
+    bool                        DisableDefaultContextMenu;  // Disable default context menu. You may submit your own using TableBeginContextMenuPopup()/EndPopup()
     bool                        IsSettingsRequestLoad;
     bool                        IsSettingsDirty;            // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data.
     bool                        IsDefaultDisplayOrder;      // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1)
@@ -3035,9 +3153,18 @@
     IMGUI_API void          SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags);
 
     // Fonts, drawing
-    IMGUI_API void          SetCurrentFont(ImFont* font);
-    inline ImFont*          GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; }
+    IMGUI_API void          RegisterUserTexture(ImTextureData* tex); // Register external texture. EXPERIMENTAL: DO NOT USE YET.
+    IMGUI_API void          UnregisterUserTexture(ImTextureData* tex);
+    IMGUI_API void          RegisterFontAtlas(ImFontAtlas* atlas);
+    IMGUI_API void          UnregisterFontAtlas(ImFontAtlas* atlas);
+    IMGUI_API void          SetCurrentFont(ImFont* font, float font_size_before_scaling, float font_size_after_scaling);
+    IMGUI_API void          UpdateCurrentFontSize(float restore_font_size_after_scaling);
+    IMGUI_API void          SetFontRasterizerDensity(float rasterizer_density);
+    inline float            GetFontRasterizerDensity() { return GImGui->FontRasterizerDensity; }
+    inline float            GetRoundedFontSize(float size) { return IM_ROUND(size); }
+    IMGUI_API ImFont*       GetDefaultFont();
     IMGUI_API void          PushPasswordFont();
+    IMGUI_API void          PopPasswordFont();
     inline ImDrawList*      GetForegroundDrawList(ImGuiWindow* window) { IM_UNUSED(window); return GetForegroundDrawList(); } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches.
     IMGUI_API ImDrawList*   GetBackgroundDrawList(ImGuiViewport* viewport);                     // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents.
     IMGUI_API ImDrawList*   GetForegroundDrawList(ImGuiViewport* viewport);                     // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents.
@@ -3049,9 +3176,10 @@
 
     // NewFrame
     IMGUI_API void          UpdateInputEvents(bool trickle_fast_inputs);
-    IMGUI_API void          UpdateHoveredWindowAndCaptureFlags();
+    IMGUI_API void          UpdateHoveredWindowAndCaptureFlags(const ImVec2& mouse_pos);
     IMGUI_API void          FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window);
     IMGUI_API void          StartMouseMovingWindow(ImGuiWindow* window);
+    IMGUI_API void          StopMouseMovingWindow();
     IMGUI_API void          UpdateMouseMovingWindowNewFrame();
     IMGUI_API void          UpdateMouseMovingWindowEndFrame();
 
@@ -3123,7 +3251,8 @@
     IMGUI_API ImVec2        CalcItemSize(ImVec2 size, float default_w, float default_h);
     IMGUI_API float         CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x);
     IMGUI_API void          PushMultiItemsWidths(int components, float width_full);
-    IMGUI_API void          ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess);
+    IMGUI_API void          ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min);
+    IMGUI_API void          CalcClipRectVisibleItemsY(const ImRect& clip_rect, const ImVec2& pos, float items_height, int* out_visible_start, int* out_visible_end);
 
     // Parameter stacks (shared)
     IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx);
@@ -3175,7 +3304,7 @@
     IMGUI_API void          NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags);
     IMGUI_API void          NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags);
     IMGUI_API void          NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result);
-    IMGUI_API void          NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data);
+    IMGUI_API void          NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, const ImGuiTreeNodeStackData* tree_node_data);
     IMGUI_API void          NavMoveRequestCancel();
     IMGUI_API void          NavMoveRequestApplyResult();
     IMGUI_API void          NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags);
@@ -3191,7 +3320,7 @@
     // This should be part of a larger set of API: FocusItem(offset = -1), FocusItemByID(id), ActivateItem(offset = -1), ActivateItemByID(id) etc. which are
     // much harder to design and implement than expected. I have a couple of private branches on this matter but it's not simple. For now implementing the easy ones.
     IMGUI_API void          FocusItem();                    // Focus last item (no selection/activation).
-    IMGUI_API void          ActivateItemByID(ImGuiID id);   // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again.
+    IMGUI_API void          ActivateItemByID(ImGuiID id);   // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again. Was called 'ActivateItem()' before 1.89.7.
 
     // Inputs
     // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions.
@@ -3293,9 +3422,11 @@
     // Drag and Drop
     IMGUI_API bool          IsDragDropActive();
     IMGUI_API bool          BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id);
+    IMGUI_API bool          BeginDragDropTargetViewport(ImGuiViewport* viewport, const ImRect* p_bb = NULL);
     IMGUI_API void          ClearDragDrop();
     IMGUI_API bool          IsDragDropPayloadBeingAccepted();
-    IMGUI_API void          RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect);
+    IMGUI_API void          RenderDragDropTargetRectForItem(const ImRect& bb);
+    IMGUI_API void          RenderDragDropTargetRectEx(ImDrawList* draw_list, const ImRect& bb);
 
     // Typing-Select API
     // (provide Windows Explorer style "select items by typing partial name" + "cycle through items by typing same letter" feature)
@@ -3338,6 +3469,8 @@
     IMGUI_API float         TableGetHeaderAngledMaxLabelWidth();
     IMGUI_API void          TablePushBackgroundChannel();
     IMGUI_API void          TablePopBackgroundChannel();
+    IMGUI_API void          TablePushColumnChannel(int column_n);
+    IMGUI_API void          TablePopColumnChannel();
     IMGUI_API void          TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count);
 
     // Tables: Internals
@@ -3387,6 +3520,8 @@
 
     // Tab Bars
     inline    ImGuiTabBar*  GetCurrentTabBar() { ImGuiContext& g = *GImGui; return g.CurrentTabBar; }
+    IMGUI_API ImGuiTabBar*  TabBarFindByID(ImGuiID id);
+    IMGUI_API void          TabBarRemove(ImGuiTabBar* tab_bar);
     IMGUI_API bool          BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags);
     IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id);
     IMGUI_API ImGuiTabItem* TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order);
@@ -3414,7 +3549,7 @@
     IMGUI_API void          RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width);
     IMGUI_API void          RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL);
     IMGUI_API void          RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL);
-    IMGUI_API void          RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known);
+    IMGUI_API void          RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known);
     IMGUI_API void          RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders = true, float rounding = 0.0f);
     IMGUI_API void          RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f);
     IMGUI_API void          RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0);
@@ -3433,11 +3568,15 @@
     IMGUI_API void          RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding);
     IMGUI_API void          RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding);
 
-    // Widgets
+    // Widgets: Text
     IMGUI_API void          TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0);
+    IMGUI_API void          TextAligned(float align_x, float size_x, const char* fmt, ...);               // FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
+    IMGUI_API void          TextAlignedV(float align_x, float size_x, const char* fmt, va_list args);
+
+    // Widgets
     IMGUI_API bool          ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0);
     IMGUI_API bool          ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0);
-    IMGUI_API bool          ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0);
+    IMGUI_API bool          ImageButtonEx(ImGuiID id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0);
     IMGUI_API void          SeparatorEx(ImGuiSeparatorFlags flags, float thickness = 1.0f);
     IMGUI_API void          SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_width);
     IMGUI_API bool          CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value);
@@ -3461,6 +3600,8 @@
 
     // Widgets: Tree Nodes
     IMGUI_API bool          TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL);
+    IMGUI_API void          TreeNodeDrawLineToChildNode(const ImVec2& target_pos);
+    IMGUI_API void          TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data);
     IMGUI_API void          TreePushOverrideID(ImGuiID id);
     IMGUI_API bool          TreeNodeGetOpen(ImGuiID storage_id);
     IMGUI_API void          TreeNodeSetOpen(ImGuiID storage_id, bool open);
@@ -3541,7 +3682,9 @@
     IMGUI_API void          DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label);
     IMGUI_API void          DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb);
     IMGUI_API void          DebugNodeFont(ImFont* font);
+    IMGUI_API void          DebugNodeFontGlyphesForSrcMask(ImFont* font, ImFontBaked* baked, int src_mask);
     IMGUI_API void          DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph);
+    IMGUI_API void          DebugNodeTexture(ImTextureData* tex, int int_id, const ImFontAtlasRect* highlight_rect = NULL); // ID used to facilitate persisting the "current" texture.
     IMGUI_API void          DebugNodeStorage(ImGuiStorage* storage, const char* label);
     IMGUI_API void          DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label);
     IMGUI_API void          DebugNodeTable(ImGuiTable* table);
@@ -3575,30 +3718,191 @@
 
 
 //-----------------------------------------------------------------------------
-// [SECTION] ImFontAtlas internal API
+// [SECTION] ImFontLoader
 //-----------------------------------------------------------------------------
 
+// Hooks and storage for a given font backend.
 // This structure is likely to evolve as we add support for incremental atlas updates.
-// Conceptually this could be in ImGuiPlatformIO, but we are far from ready to make this public.
-struct ImFontBuilderIO
+// Conceptually this could be public, but API is still going to be evolve.
+struct ImFontLoader
 {
-    bool    (*FontBuilder_Build)(ImFontAtlas* atlas);
+    const char*     Name;
+    bool            (*LoaderInit)(ImFontAtlas* atlas);
+    void            (*LoaderShutdown)(ImFontAtlas* atlas);
+    bool            (*FontSrcInit)(ImFontAtlas* atlas, ImFontConfig* src);
+    void            (*FontSrcDestroy)(ImFontAtlas* atlas, ImFontConfig* src);
+    bool            (*FontSrcContainsGlyph)(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint);
+    bool            (*FontBakedInit)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src);
+    void            (*FontBakedDestroy)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src);
+    bool            (*FontBakedLoadGlyph)(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x);
+
+    // Size of backend data, Per Baked * Per Source. Buffers are managed by core to avoid excessive allocations.
+    // FIXME: At this point the two other types of buffers may be managed by core to be consistent?
+    size_t          FontBakedSrcLoaderDataSize;
+
+    ImFontLoader()  { memset(this, 0, sizeof(*this)); }
 };
 
-// Helper for font builder
 #ifdef IMGUI_ENABLE_STB_TRUETYPE
-IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype();
+IMGUI_API const ImFontLoader* ImFontAtlasGetFontLoaderForStbTruetype();
+#endif
+#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
+typedef ImFontLoader ImFontBuilderIO; // [renamed/changed in 1.92] The types are not actually compatible but we provide this as a compile-time error report helper.
+#endif
+
+//-----------------------------------------------------------------------------
+// [SECTION] ImFontAtlas internal API
+//-----------------------------------------------------------------------------
+
+#define IMGUI_FONT_SIZE_MAX                                     (512.0f)
+#define IMGUI_FONT_SIZE_THRESHOLD_FOR_LOADADVANCEXONLYMODE      (128.0f)
+
+// Helpers: ImTextureRef ==/!= operators provided as convenience
+// (note that _TexID and _TexData are never set simultaneously)
+inline bool operator==(const ImTextureRef& lhs, const ImTextureRef& rhs)    { return lhs._TexID == rhs._TexID && lhs._TexData == rhs._TexData; }
+inline bool operator!=(const ImTextureRef& lhs, const ImTextureRef& rhs)    { return lhs._TexID != rhs._TexID || lhs._TexData != rhs._TexData; }
+
+// Refer to ImFontAtlasPackGetRect() to better understand how this works.
+#define ImFontAtlasRectId_IndexMask_        (0x0007FFFF)    // 20-bits signed: index to access builder->RectsIndex[].
+#define ImFontAtlasRectId_GenerationMask_   (0x3FF00000)    // 10-bits: entry generation, so each ID is unique and get can safely detected old identifiers.
+#define ImFontAtlasRectId_GenerationShift_  (20)
+inline int               ImFontAtlasRectId_GetIndex(ImFontAtlasRectId id)       { return (id & ImFontAtlasRectId_IndexMask_); }
+inline unsigned int      ImFontAtlasRectId_GetGeneration(ImFontAtlasRectId id)  { return (unsigned int)(id & ImFontAtlasRectId_GenerationMask_) >> ImFontAtlasRectId_GenerationShift_; }
+inline ImFontAtlasRectId ImFontAtlasRectId_Make(int index_idx, int gen_idx)     { IM_ASSERT(index_idx >= 0 && index_idx <= ImFontAtlasRectId_IndexMask_ && gen_idx <= (ImFontAtlasRectId_GenerationMask_ >> ImFontAtlasRectId_GenerationShift_)); return (ImFontAtlasRectId)(index_idx | (gen_idx << ImFontAtlasRectId_GenerationShift_)); }
+
+// Packed rectangle lookup entry (we need an indirection to allow removing/reordering rectangles)
+// User are returned ImFontAtlasRectId values which are meant to be persistent.
+// We handle this with an indirection. While Rects[] may be in theory shuffled, compacted etc., RectsIndex[] cannot it is keyed by ImFontAtlasRectId.
+// RectsIndex[] is used both as an index into Rects[] and an index into itself. This is basically a free-list. See ImFontAtlasBuildAllocRectIndexEntry() code.
+// Having this also makes it easier to e.g. sort rectangles during repack.
+struct ImFontAtlasRectEntry
+{
+    int                 TargetIndex : 20;   // When Used: ImFontAtlasRectId -> into Rects[]. When unused: index to next unused RectsIndex[] slot to consume free-list.
+    unsigned int        Generation : 10;    // Increased each time the entry is reused for a new rectangle.
+    unsigned int        IsUsed : 1;
+};
+
+// Data available to potential texture post-processing functions
+struct ImFontAtlasPostProcessData
+{
+    ImFontAtlas*        FontAtlas;
+    ImFont*             Font;
+    ImFontConfig*       FontSrc;
+    ImFontBaked*        FontBaked;
+    ImFontGlyph*        Glyph;
+
+    // Pixel data
+    void*               Pixels;
+    ImTextureFormat     Format;
+    int                 Pitch;
+    int                 Width;
+    int                 Height;
+};
+
+// We avoid dragging imstb_rectpack.h into public header (partly because binding generators are having issues with it)
+#ifdef IMGUI_STB_NAMESPACE
+namespace IMGUI_STB_NAMESPACE { struct stbrp_node; }
+typedef IMGUI_STB_NAMESPACE::stbrp_node stbrp_node_im;
+#else
+struct stbrp_node;
+typedef stbrp_node stbrp_node_im;
+#endif
+struct stbrp_context_opaque { char data[80]; };
+
+// Internal storage for incrementally packing and building a ImFontAtlas
+struct ImFontAtlasBuilder
+{
+    stbrp_context_opaque        PackContext;            // Actually 'stbrp_context' but we don't want to define this in the header file.
+    ImVector<stbrp_node_im>     PackNodes;
+    ImVector<ImTextureRect>     Rects;
+    ImVector<ImFontAtlasRectEntry> RectsIndex;          // ImFontAtlasRectId -> index into Rects[]
+    ImVector<unsigned char>     TempBuffer;             // Misc scratch buffer
+    int                         RectsIndexFreeListStart;// First unused entry
+    int                         RectsPackedCount;       // Number of packed rectangles.
+    int                         RectsPackedSurface;     // Number of packed pixels. Used when compacting to heuristically find the ideal texture size.
+    int                         RectsDiscardedCount;
+    int                         RectsDiscardedSurface;
+    int                         FrameCount;             // Current frame count
+    ImVec2i                     MaxRectSize;            // Largest rectangle to pack (de-facto used as a "minimum texture size")
+    ImVec2i                     MaxRectBounds;          // Bottom-right most used pixels
+    bool                        LockDisableResize;      // Disable resizing texture
+    bool                        PreloadedAllGlyphsRanges; // Set when missing ImGuiBackendFlags_RendererHasTextures features forces atlas to preload everything.
+
+    // Cache of all ImFontBaked
+    ImStableVector<ImFontBaked,32> BakedPool;
+    ImGuiStorage                BakedMap;               // BakedId --> ImFontBaked*
+    int                         BakedDiscardedCount;
+
+    // Custom rectangle identifiers
+    ImFontAtlasRectId           PackIdMouseCursors;     // White pixel + mouse cursors. Also happen to be fallback in case of packing failure.
+    ImFontAtlasRectId           PackIdLinesTexData;
+
+    ImFontAtlasBuilder()        { memset(this, 0, sizeof(*this)); FrameCount = -1; RectsIndexFreeListStart = -1; PackIdMouseCursors = PackIdLinesTexData = -1; }
+};
+
+IMGUI_API void              ImFontAtlasBuildInit(ImFontAtlas* atlas);
+IMGUI_API void              ImFontAtlasBuildDestroy(ImFontAtlas* atlas);
+IMGUI_API void              ImFontAtlasBuildMain(ImFontAtlas* atlas);
+IMGUI_API void              ImFontAtlasBuildSetupFontLoader(ImFontAtlas* atlas, const ImFontLoader* font_loader);
+IMGUI_API void              ImFontAtlasBuildUpdatePointers(ImFontAtlas* atlas);
+IMGUI_API void              ImFontAtlasBuildRenderBitmapFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char);
+IMGUI_API void              ImFontAtlasBuildClear(ImFontAtlas* atlas); // Clear output and custom rects
+
+IMGUI_API ImTextureData*    ImFontAtlasTextureAdd(ImFontAtlas* atlas, int w, int h);
+IMGUI_API void              ImFontAtlasTextureMakeSpace(ImFontAtlas* atlas);
+IMGUI_API void              ImFontAtlasTextureRepack(ImFontAtlas* atlas, int w, int h);
+IMGUI_API void              ImFontAtlasTextureGrow(ImFontAtlas* atlas, int old_w = -1, int old_h = -1);
+IMGUI_API void              ImFontAtlasTextureCompact(ImFontAtlas* atlas);
+IMGUI_API ImVec2i           ImFontAtlasTextureGetSizeEstimate(ImFontAtlas* atlas);
+
+IMGUI_API void              ImFontAtlasBuildSetupFontSpecialGlyphs(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src);
+IMGUI_API void              ImFontAtlasBuildLegacyPreloadAllGlyphRanges(ImFontAtlas* atlas); // Legacy
+IMGUI_API void              ImFontAtlasBuildGetOversampleFactors(ImFontConfig* src, ImFontBaked* baked, int* out_oversample_h, int* out_oversample_v);
+IMGUI_API void              ImFontAtlasBuildDiscardBakes(ImFontAtlas* atlas, int unused_frames);
+
+IMGUI_API bool              ImFontAtlasFontSourceInit(ImFontAtlas* atlas, ImFontConfig* src);
+IMGUI_API void              ImFontAtlasFontSourceAddToFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src);
+IMGUI_API void              ImFontAtlasFontDestroySourceData(ImFontAtlas* atlas, ImFontConfig* src);
+IMGUI_API bool              ImFontAtlasFontInitOutput(ImFontAtlas* atlas, ImFont* font); // Using FontDestroyOutput/FontInitOutput sequence useful notably if font loader params have changed
+IMGUI_API void              ImFontAtlasFontDestroyOutput(ImFontAtlas* atlas, ImFont* font);
+IMGUI_API void              ImFontAtlasFontDiscardBakes(ImFontAtlas* atlas, ImFont* font, int unused_frames);
+
+IMGUI_API ImGuiID           ImFontAtlasBakedGetId(ImGuiID font_id, float baked_size, float rasterizer_density);
+IMGUI_API ImFontBaked*      ImFontAtlasBakedGetOrAdd(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density);
+IMGUI_API ImFontBaked*      ImFontAtlasBakedGetClosestMatch(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density);
+IMGUI_API ImFontBaked*      ImFontAtlasBakedAdd(ImFontAtlas* atlas, ImFont* font, float font_size, float font_rasterizer_density, ImGuiID baked_id);
+IMGUI_API void              ImFontAtlasBakedDiscard(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked);
+IMGUI_API ImFontGlyph*      ImFontAtlasBakedAddFontGlyph(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, const ImFontGlyph* in_glyph);
+IMGUI_API void              ImFontAtlasBakedAddFontGlyphAdvancedX(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImWchar codepoint, float advance_x);
+IMGUI_API void              ImFontAtlasBakedDiscardFontGlyph(ImFontAtlas* atlas, ImFont* font, ImFontBaked* baked, ImFontGlyph* glyph);
+IMGUI_API void              ImFontAtlasBakedSetFontGlyphBitmap(ImFontAtlas* atlas, ImFontBaked* baked, ImFontConfig* src, ImFontGlyph* glyph, ImTextureRect* r, const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch);
+
+IMGUI_API void              ImFontAtlasPackInit(ImFontAtlas* atlas);
+IMGUI_API ImFontAtlasRectId ImFontAtlasPackAddRect(ImFontAtlas* atlas, int w, int h, ImFontAtlasRectEntry* overwrite_entry = NULL);
+IMGUI_API ImTextureRect*    ImFontAtlasPackGetRect(ImFontAtlas* atlas, ImFontAtlasRectId id);
+IMGUI_API ImTextureRect*    ImFontAtlasPackGetRectSafe(ImFontAtlas* atlas, ImFontAtlasRectId id);
+IMGUI_API void              ImFontAtlasPackDiscardRect(ImFontAtlas* atlas, ImFontAtlasRectId id);
+
+IMGUI_API void              ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, int frame_count, bool renderer_has_textures);
+IMGUI_API void              ImFontAtlasAddDrawListSharedData(ImFontAtlas* atlas, ImDrawListSharedData* data);
+IMGUI_API void              ImFontAtlasRemoveDrawListSharedData(ImFontAtlas* atlas, ImDrawListSharedData* data);
+IMGUI_API void              ImFontAtlasUpdateDrawListsTextures(ImFontAtlas* atlas, ImTextureRef old_tex, ImTextureRef new_tex);
+IMGUI_API void              ImFontAtlasUpdateDrawListsSharedData(ImFontAtlas* atlas);
+
+IMGUI_API void              ImFontAtlasTextureBlockConvert(const unsigned char* src_pixels, ImTextureFormat src_fmt, int src_pitch, unsigned char* dst_pixels, ImTextureFormat dst_fmt, int dst_pitch, int w, int h);
+IMGUI_API void              ImFontAtlasTextureBlockPostProcess(ImFontAtlasPostProcessData* data);
+IMGUI_API void              ImFontAtlasTextureBlockPostProcessMultiply(ImFontAtlasPostProcessData* data, float multiply_factor);
+IMGUI_API void              ImFontAtlasTextureBlockFill(ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h, ImU32 col);
+IMGUI_API void              ImFontAtlasTextureBlockCopy(ImTextureData* src_tex, int src_x, int src_y, ImTextureData* dst_tex, int dst_x, int dst_y, int w, int h);
+IMGUI_API void              ImFontAtlasTextureBlockQueueUpload(ImFontAtlas* atlas, ImTextureData* tex, int x, int y, int w, int h);
+
+IMGUI_API int               ImTextureDataGetFormatBytesPerPixel(ImTextureFormat format);
+IMGUI_API const char*       ImTextureDataGetStatusName(ImTextureStatus status);
+IMGUI_API const char*       ImTextureDataGetFormatName(ImTextureFormat format);
+
+#ifndef IMGUI_DISABLE_DEBUG_TOOLS
+IMGUI_API void              ImFontAtlasDebugLogTextureRequests(ImFontAtlas* atlas);
 #endif
-IMGUI_API void      ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas);
-IMGUI_API void      ImFontAtlasBuildInit(ImFontAtlas* atlas);
-IMGUI_API void      ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src, float ascent, float descent);
-IMGUI_API void      ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque);
-IMGUI_API void      ImFontAtlasBuildFinish(ImFontAtlas* atlas);
-IMGUI_API void      ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value);
-IMGUI_API void      ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value);
-IMGUI_API void      ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor);
-IMGUI_API void      ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride);
-IMGUI_API void      ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v);
 
 IMGUI_API bool      ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]);
 
@@ -3617,7 +3921,7 @@
 #define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS)      if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS)    // Register item label and status flags (optional)
 #define IMGUI_TEST_ENGINE_LOG(_FMT,...)                     ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__)                                      // Custom log entry from user land into test log
 #else
-#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID)                 ((void)0)
+#define IMGUI_TEST_ENGINE_ITEM_ADD(_ID,_BB,_ITEM_DATA)      ((void)0)
 #define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS)      ((void)g)
 #endif
 
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui_tables.cpp f3d-3.4.1/external/imgui/imgui_tables.cpp
--- f3d-3.2.0+dfsg/external/imgui/imgui_tables.cpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui_tables.cpp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91b
+// dear imgui, v1.92.4
 // (tables and columns code)
 
 /*
@@ -437,7 +437,7 @@
         if (table->InnerWindow->SkipItems && outer_window_is_measuring_size)
             table->InnerWindow->SkipItems = false;
 
-        // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned)
+        // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned
         if (instance_no == 0)
         {
             table->HasScrollbarYPrev = table->HasScrollbarYCurr;
@@ -451,6 +451,7 @@
         // But at this point we do NOT have a correct value for .Max.y (unless a height has been explicitly passed in). It will only be updated in EndTable().
         table->WorkRect = table->OuterRect = table->InnerRect = outer_rect;
         table->HasScrollbarYPrev = table->HasScrollbarYCurr = false;
+        table->InnerWindow->DC.TreeDepth++; // This is designed to always linking ImGuiTreeNodeFlags_DrawLines linking accross a table
     }
 
     // Push a standardized ID for both child-using and not-child-using tables
@@ -541,7 +542,7 @@
 
     // Make table current
     g.CurrentTable = table;
-    outer_window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX();
+    inner_window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX();
     outer_window->DC.CurrentTableIdx = table_idx;
     if (inner_window != outer_window) // So EndChild() within the inner window can restore the table properly.
         inner_window->DC.CurrentTableIdx = table_idx;
@@ -1250,7 +1251,7 @@
 
     // [Part 11] Default context menu
     // - To append to this menu: you can call TableBeginContextMenuPopup()/.../EndPopup().
-    // - To modify or replace this: set table->IsContextPopupNoDefaultContents = true, then call TableBeginContextMenuPopup()/.../EndPopup().
+    // - To modify or replace this: set table->DisableDefaultContextMenu = true, then call TableBeginContextMenuPopup()/.../EndPopup().
     // - You may call TableDrawDefaultContextMenu() with selected flags to display specific sections of the default menu,
     //   e.g. TableDrawDefaultContextMenu(table, table->Flags & ~ImGuiTableFlags_Hideable) will display everything EXCEPT columns visibility options.
     if (table->DisableDefaultContextMenu == false && TableBeginContextMenuPopup(table))
@@ -1510,6 +1511,7 @@
     }
     else
     {
+        table->InnerWindow->DC.TreeDepth--;
         ItemSize(table->OuterRect.GetSize());
         ItemAdd(table->OuterRect, 0);
     }
@@ -1823,6 +1825,11 @@
     ImGuiContext& g = *GImGui;
     ImGuiTable* table = g.CurrentTable;
     IM_ASSERT(target != ImGuiTableBgTarget_None);
+    if (table == NULL)
+    {
+        IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!");
+        return;
+    }
 
     if (color == IM_COL32_DISABLE)
         color = 0;
@@ -1951,7 +1958,10 @@
     IM_ASSERT(table->IsInsideRow);
 
     if (table->CurrentColumn != -1)
+    {
         TableEndCell(table);
+        table->CurrentColumn = -1;
+    }
 
     // Logging
     if (g.LogEnabled)
@@ -2044,10 +2054,11 @@
     }
 
     // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle)
-    // We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark end of row and
-    // get the new cursor position.
+    // - We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark
+    //   end of row and get the new cursor position.
     if (unfreeze_rows_request)
     {
+        IM_ASSERT(table->FreezeRowsRequest > 0);
         for (int column_n = 0; column_n < table->ColumnsCount; column_n++)
             table->Columns[column_n].NavLayerCurrent = table->NavLayer;
         const float y0 = ImMax(table->RowPosY2 + 1, table->InnerClipRect.Min.y);
@@ -2191,6 +2202,7 @@
         g.LastItemData.StatusFlags = 0;
     }
 
+    // Also see TablePushColumnChannel()
     if (table->Flags & ImGuiTableFlags_NoClip)
     {
         // FIXME: if we end up drawing all borders/bg in EndTable, could remove this and just assert that channel hasn't changed.
@@ -2464,10 +2476,38 @@
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
     ImGuiTable* table = g.CurrentTable;
-    ImGuiTableColumn* column = &table->Columns[table->CurrentColumn];
 
     // Optimization: avoid PopClipRect() + SetCurrentChannel()
     SetWindowClipRectBeforeSetChannel(window, table->HostBackupInnerClipRect);
+    table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Columns[table->CurrentColumn].DrawChannelCurrent);
+}
+
+// Also see TableBeginCell()
+void ImGui::TablePushColumnChannel(int column_n)
+{
+    ImGuiContext& g = *GImGui;
+    ImGuiTable* table = g.CurrentTable;
+
+    // Optimization: avoid SetCurrentChannel() + PushClipRect()
+    if (table->Flags & ImGuiTableFlags_NoClip)
+        return;
+    ImGuiWindow* window = g.CurrentWindow;
+    const ImGuiTableColumn* column = &table->Columns[column_n];
+    SetWindowClipRectBeforeSetChannel(window, column->ClipRect);
+    table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent);
+}
+
+void ImGui::TablePopColumnChannel()
+{
+    ImGuiContext& g = *GImGui;
+    ImGuiTable* table = g.CurrentTable;
+
+    // Optimization: avoid PopClipRect() + SetCurrentChannel()
+    if ((table->Flags & ImGuiTableFlags_NoClip) || (table->CurrentColumn == -1)) // Calling TreePop() after TableNextRow() is supported.
+        return;
+    ImGuiWindow* window = g.CurrentWindow;
+    const ImGuiTableColumn* column = &table->Columns[table->CurrentColumn];
+    SetWindowClipRectBeforeSetChannel(window, column->ClipRect);
     table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent);
 }
 
@@ -2778,8 +2818,13 @@
                 continue;
 
             // Draw in outer window so right-most column won't be clipped
-            // Always draw full height border when being resized/hovered, or on the delimitation of frozen column scrolling.
-            float draw_y2 = (is_hovered || is_resized || is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) == 0) ? draw_y2_body : draw_y2_head;
+            float draw_y2 = draw_y2_head;
+            if (is_frozen_separator)
+                draw_y2 = draw_y2_body;
+            else if ((table->Flags & ImGuiTableFlags_NoBordersInBodyUntilResize) != 0 && (is_hovered || is_resized))
+                draw_y2 = draw_y2_body;
+            else if ((table->Flags & (ImGuiTableFlags_NoBordersInBodyUntilResize | ImGuiTableFlags_NoBordersInBody)) == 0)
+                draw_y2 = draw_y2_body;
             if (draw_y2 > draw_y1)
                 inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), TableGetColumnBorderCol(table, order_n, column_n), border_size);
         }
@@ -2842,9 +2887,7 @@
 {
     ImGuiContext& g = *GImGui;
     ImGuiTable* table = g.CurrentTable;
-    IM_ASSERT(table != NULL);
-
-    if (!(table->Flags & ImGuiTableFlags_Sortable))
+    if (table == NULL || !(table->Flags & ImGuiTableFlags_Sortable))
         return NULL;
 
     // Require layout (in case TableHeadersRow() hasn't been called) as it may alter IsSortSpecsDirty in some paths.
@@ -3244,7 +3287,7 @@
     // Render clipped label. Clipping here ensure that in the majority of situations, all our header cells will
     // be merged into a single draw call.
     //window->DrawList->AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE);
-    RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + label_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size);
+    RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, bb.Max.y), ellipsis_max, label, label_end, &label_size);
 
     const bool text_clipped = label_size.x > (ellipsis_max - label_pos.x);
     if (text_clipped && hovered && g.ActiveId == 0)
@@ -3341,7 +3384,7 @@
     ButtonBehavior(row_r, row_id, NULL, NULL);
     KeepAliveID(row_id);
 
-    const float ascent_scaled = g.Font->Ascent * g.FontScale; // FIXME: Standardize those scaling factors better
+    const float ascent_scaled = g.FontBaked->Ascent * g.FontBakedScale; // FIXME: Standardize those scaling factors better
     const float line_off_for_ascent_x = (ImMax((g.FontSize - ascent_scaled) * 0.5f, 0.0f) / -sin_a) * (flip_label ? -1.0f : 1.0f);
     const ImVec2 padding = g.Style.CellPadding; // We will always use swapped component
     const ImVec2 align = g.Style.TableAngledHeadersTextAlign;
@@ -3396,7 +3439,7 @@
                     ImRect clip_r(window->ClipRect.Min, window->ClipRect.Min + ImVec2(clip_width, clip_height));
                     int vtx_idx_begin = draw_list->_VtxCurrentIdx;
                     PushStyleColor(ImGuiCol_Text, request->TextColor);
-                    RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, clip_r.Max.x, label_name, label_name_eol, &label_size);
+                    RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, label_name, label_name_eol, &label_size);
                     PopStyleColor();
                     int vtx_idx_end = draw_list->_VtxCurrentIdx;
 
@@ -3905,7 +3948,7 @@
 // - TableGcCompactSettings() [Internal]
 //-------------------------------------------------------------------------
 
-// Remove Table (currently only used by TestEngine)
+// Remove Table data (currently only used by TestEngine)
 void ImGui::TableRemove(ImGuiTable* table)
 {
     //IMGUI_DEBUG_PRINT("TableRemove() id=0x%08X\n", table->ID);
diff -Nru f3d-3.2.0+dfsg/external/imgui/imgui_widgets.cpp f3d-3.4.1/external/imgui/imgui_widgets.cpp
--- f3d-3.2.0+dfsg/external/imgui/imgui_widgets.cpp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imgui_widgets.cpp	2026-01-08 08:41:55.000000000 +0200
@@ -1,4 +1,4 @@
-// dear imgui, v1.91b
+// dear imgui, v1.92.4
 // (widgets code)
 
 /*
@@ -135,8 +135,7 @@
 
 // For InputTextEx()
 static bool     InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
-static int      InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
-static ImVec2   InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
+static ImVec2   InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
 
 //-------------------------------------------------------------------------
 // [SECTION] Widgets: Text, etc.
@@ -339,6 +338,46 @@
         PopTextWrapPos();
 }
 
+void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    TextAlignedV(align_x, size_x, fmt, args);
+    va_end(args);
+}
+
+// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
+// size_x : 0.0f = shortcut for GetContentRegionAvail().x
+// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
+void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
+{
+    ImGuiWindow* window = GetCurrentWindow();
+    if (window->SkipItems)
+        return;
+
+    const char* text, *text_end;
+    ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
+    const ImVec2 text_size = CalcTextSize(text, text_end);
+    size_x = CalcItemSize(ImVec2(size_x, 0.0f), 0.0f, text_size.y).x;
+
+    ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
+    ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
+    ImVec2 size(ImMin(size_x, text_size.x), text_size.y);
+    window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, pos.x + text_size.x);
+    window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, pos.x + text_size.x);
+    if (align_x > 0.0f && text_size.x < size_x)
+        pos.x += ImTrunc((size_x - text_size.x) * align_x);
+    RenderTextEllipsis(window->DrawList, pos, pos_max, pos_max.x, text, text_end, &text_size);
+
+    const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
+    ItemSize(size);
+    ItemAdd(ImRect(pos, pos + size), 0);
+    window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
+
+    if (size_x < text_size.x && IsItemHovered(ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
+        SetTooltip("%.*s", (int)(text_end - text), text);
+}
+
 void ImGui::LabelText(const char* label, const char* fmt, ...)
 {
     va_list args;
@@ -494,7 +533,7 @@
 //   And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
 //   For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
 // - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
-//   One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior()
+//   One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
 //   with same ID and different MouseButton (see #8030). You can fix it by:
 //       (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
 //    or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
@@ -508,6 +547,8 @@
     ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
     if (flags & ImGuiButtonFlags_AllowOverlap)
         item_flags |= ImGuiItemFlags_AllowOverlap;
+    if (item_flags & ImGuiItemFlags_NoFocus)
+        flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
 
     // Default only reacts to left mouse button
     if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
@@ -518,7 +559,7 @@
         flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
 
     ImGuiWindow* backup_hovered_window = g.HoveredWindow;
-    const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window;
+    const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window->RootWindow;
     if (flatten_hovered_children)
         g.HoveredWindow = window;
 
@@ -531,7 +572,8 @@
     bool pressed = false;
     bool hovered = ItemHoverable(bb, id, item_flags);
 
-    // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
+    // Special mode for Drag and Drop used by openables (tree nodes, tabs etc.)
+    // where holding the button pressed for a long time while drag a payload item triggers the button.
     if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
         if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
         {
@@ -583,7 +625,7 @@
                         SetFocusID(id, window);
                         FocusWindow(window);
                     }
-                    else
+                    else if (!(flags & ImGuiButtonFlags_NoFocus))
                     {
                         FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
                     }
@@ -601,7 +643,7 @@
                         SetFocusID(id, window);
                         FocusWindow(window);
                     }
-                    else
+                    else if (!(flags & ImGuiButtonFlags_NoFocus))
                     {
                         FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
                     }
@@ -867,11 +909,12 @@
     if (hovered)
         window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
     RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
-    ImU32 cross_col = GetColorU32(ImGuiCol_Text);
-    ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
-    float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
-    window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f);
-    window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f);
+    const ImU32 cross_col = GetColorU32(ImGuiCol_Text);
+    const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
+    const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
+    const float cross_thickness = 1.0f; // FIXME-DPI
+    window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness);
+    window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness);
 
     return pressed;
 }
@@ -982,7 +1025,8 @@
     const bool allow_interaction = (alpha >= 1.0f);
 
     ImRect bb = bb_frame;
-    bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f)));
+    float padding = IM_TRUNC(ImMin(style.ScrollbarPadding, ImMin(bb_frame_width, bb_frame_height) * 0.5f));
+    bb.Expand(-padding);
 
     // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
     const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
@@ -1062,9 +1106,9 @@
     return held;
 }
 
-// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
+// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
 // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
-void ImGui::ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
+void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = GetCurrentWindow();
@@ -1082,28 +1126,28 @@
         window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, ImDrawFlags_None, g.Style.ImageBorderSize);
     if (bg_col.w > 0.0f)
         window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
-    window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
+    window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
 }
 
-void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
+void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
 {
-    ImageWithBg(user_texture_id, image_size, uv0, uv1);
+    ImageWithBg(tex_ref, image_size, uv0, uv1);
 }
 
 // 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
-void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
+void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
 {
     ImGuiContext& g = *GImGui;
     PushStyleVar(ImGuiStyleVar_ImageBorderSize, (border_col.w > 0.0f) ? ImMax(1.0f, g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f
     PushStyleColor(ImGuiCol_Border, border_col);
-    ImageWithBg(user_texture_id, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
+    ImageWithBg(tex_ref, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
     PopStyleColor();
     PopStyleVar();
 }
 #endif
 
-bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
+bool ImGui::ImageButtonEx(ImGuiID id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = GetCurrentWindow();
@@ -1125,21 +1169,21 @@
     RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding));
     if (bg_col.w > 0.0f)
         window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
-    window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
+    window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
 
     return pressed;
 }
 
 // - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
 // - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
-bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
+bool ImGui::ImageButton(const char* str_id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
     if (window->SkipItems)
         return false;
 
-    return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
+    return ImageButtonEx(window->GetID(str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
 }
 
 #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
@@ -1477,8 +1521,8 @@
         ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
     }
 
-    float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f);
-    window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode.
+    float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
+    window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode // FIXME-DPI
 
     PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
     RenderText(bb.Min, label, label_end);
@@ -1488,14 +1532,14 @@
     return pressed;
 }
 
-void ImGui::TextLinkOpenURL(const char* label, const char* url)
+bool ImGui::TextLinkOpenURL(const char* label, const char* url)
 {
     ImGuiContext& g = *GImGui;
     if (url == NULL)
         url = label;
-    if (TextLink(label))
-        if (g.PlatformIO.Platform_OpenInShellFn != NULL)
-            g.PlatformIO.Platform_OpenInShellFn(&g, url);
+    bool pressed = TextLink(label);
+    if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
+        g.PlatformIO.Platform_OpenInShellFn(&g, url);
     SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
     if (BeginPopupContextItem())
     {
@@ -1503,6 +1547,7 @@
             SetClipboardText(url);
         EndPopup();
     }
+    return pressed;
 }
 
 //-------------------------------------------------------------------------
@@ -1689,7 +1734,7 @@
             window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
         if (g.LogEnabled)
             LogSetNextTextDecoration("---", NULL);
-        RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size);
+        RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
     }
     else
     {
@@ -1785,27 +1830,31 @@
 
 // Shrink excess width from a set of item, by removing width from the larger items first.
 // Set items Width to -1.0f to disable shrinking this item.
-void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
+void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min)
 {
     if (count == 1)
     {
         if (items[0].Width >= 0.0f)
-            items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
+            items[0].Width = ImMax(items[0].Width - width_excess, width_min);
         return;
     }
-    ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer);
+    ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); // Sort largest first, smallest last.
     int count_same_width = 1;
-    while (width_excess > 0.0f && count_same_width < count)
+    while (width_excess > 0.001f && count_same_width < count)
     {
         while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
             count_same_width++;
         float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f);
+        max_width_to_remove_per_item = ImMin(items[0].Width - width_min, max_width_to_remove_per_item);
         if (max_width_to_remove_per_item <= 0.0f)
             break;
-        float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
+        float base_width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
         for (int item_n = 0; item_n < count_same_width; item_n++)
-            items[item_n].Width -= width_to_remove_per_item;
-        width_excess -= width_to_remove_per_item * count_same_width;
+        {
+            float width_to_remove_for_this_item = ImMin(base_width_to_remove_per_item, items[item_n].Width - width_min);
+            items[item_n].Width -= width_to_remove_for_this_item;
+            width_excess -= width_to_remove_for_this_item;
+        }
     }
 
     // Round width and redistribute remainder
@@ -1982,7 +2031,8 @@
     if (!ret)
     {
         EndPopup();
-        IM_ASSERT(0);   // This should never happen as we tested for IsPopupOpen() above
+        if (!g.IO.ConfigDebugBeginReturnValueOnce && !g.IO.ConfigDebugBeginReturnValueLoop) // Begin may only return false with those debug tools activated.
+            IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
         return false;
     }
     g.BeginComboDepth++;
@@ -3861,9 +3911,6 @@
 // - InputText()
 // - InputTextWithHint()
 // - InputTextMultiline()
-// - InputTextGetCharInfo() [Internal]
-// - InputTextReindexLines() [Internal]
-// - InputTextReindexLinesRange() [Internal]
 // - InputTextEx() [Internal]
 // - DebugNodeInputTextState() [Internal]
 //-------------------------------------------------------------------------
@@ -3890,75 +3937,12 @@
     return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
 }
 
-// This is only used in the path where the multiline widget is inactivate.
-static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
-{
-    int line_count = 0;
-    const char* s = text_begin;
-    while (true)
-    {
-        const char* s_eol = strchr(s, '\n');
-        line_count++;
-        if (s_eol == NULL)
-        {
-            s = s + ImStrlen(s);
-            break;
-        }
-        s = s_eol + 1;
-    }
-    *out_text_end = s;
-    return line_count;
-}
-
-// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
-static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
+static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
 {
     ImGuiContext& g = *ctx;
-    ImFont* font = g.Font;
-    const float line_height = g.FontSize;
-    const float scale = line_height / font->FontSize;
-
-    ImVec2 text_size = ImVec2(0, 0);
-    float line_width = 0.0f;
-
-    const char* s = text_begin;
-    while (s < text_end)
-    {
-        unsigned int c = (unsigned int)*s;
-        if (c < 0x80)
-            s += 1;
-        else
-            s += ImTextCharFromUtf8(&c, s, text_end);
-
-        if (c == '\n')
-        {
-            text_size.x = ImMax(text_size.x, line_width);
-            text_size.y += line_height;
-            line_width = 0.0f;
-            if (stop_on_new_line)
-                break;
-            continue;
-        }
-        if (c == '\r')
-            continue;
-
-        const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale;
-        line_width += char_width;
-    }
-
-    if (text_size.x < line_width)
-        text_size.x = line_width;
-
-    if (out_offset)
-        *out_offset = ImVec2(line_width, text_size.y + line_height);  // offset allow for the possibility of sitting after a trailing \n
-
-    if (line_width > 0 || text_size.y == 0.0f)                        // whereas size.y will ignore the trailing \n
-        text_size.y += line_height;
-
-    if (remaining)
-        *remaining = s;
-
-    return text_size;
+    ImGuiInputTextState* obj = &g.InputTextState;
+    IM_ASSERT(text_end_display >= text_begin && text_end_display <= text_end);
+    return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, obj->WrapWidth, text_begin, text_end_display, text_end, out_remaining, out_offset, flags);
 }
 
 // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar)
@@ -3969,14 +3953,14 @@
 namespace ImStb
 {
 static int     STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj)                             { return obj->TextLen; }
-static char    STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx)                      { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
-static float   STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx)  { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; }
+static char    STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx)                      { IM_ASSERT(idx >= 0 && idx <= obj->TextLen); return obj->TextSrc[idx]; }
+static float   STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx)  { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.FontBaked->GetCharAdvance((ImWchar)c) * g.FontBakedScale; }
 static char    STB_TEXTEDIT_NEWLINE = '\n';
 static void    STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
 {
     const char* text = obj->TextSrc;
     const char* text_remaining = NULL;
-    const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true);
+    const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepBlanks);
     r->x0 = 0.0f;
     r->x1 = size.x;
     r->baseline_y_delta = size.y;
@@ -4078,6 +4062,75 @@
 #define STB_TEXTEDIT_MOVEWORDLEFT       STB_TEXTEDIT_MOVEWORDLEFT_IMPL  // They need to be #define for stb_textedit.h
 #define STB_TEXTEDIT_MOVEWORDRIGHT      STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
 
+// Reimplementation of stb_textedit_move_line_start()/stb_textedit_move_line_end() which supports word-wrapping.
+static int STB_TEXTEDIT_MOVELINESTART_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
+{
+    if (state->single_line)
+        return 0;
+
+    if (obj->WrapWidth > 0.0f)
+    {
+        ImGuiContext& g = *obj->Ctx;
+        const char* p_cursor = obj->TextSrc + cursor;
+        const char* p_bol = ImStrbol(p_cursor, obj->TextSrc);
+        const char* p = p_bol;
+        const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
+        while (p >= p_bol)
+        {
+            const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
+            if (p == p_cursor) // If we are already on a visible beginning-of-line, return real beginning-of-line (would be same as regular handler below)
+                return (int)(p_bol - obj->TextSrc);
+            if (p_eol == p_cursor && obj->TextA[cursor] != '\n' && obj->LastMoveDirectionLR == ImGuiDir_Left)
+                return (int)(p_bol - obj->TextSrc);
+            if (p_eol >= p_cursor)
+                return (int)(p - obj->TextSrc);
+            p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
+        }
+    }
+
+    // Regular handler, same as stb_textedit_move_line_start()
+    while (cursor > 0)
+    {
+        int prev_cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, cursor);
+        if (STB_TEXTEDIT_GETCHAR(obj, prev_cursor) == STB_TEXTEDIT_NEWLINE)
+            break;
+        cursor = prev_cursor;
+    }
+    return cursor;
+}
+
+static int STB_TEXTEDIT_MOVELINEEND_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
+{
+    int n = STB_TEXTEDIT_STRINGLEN(obj);
+    if (state->single_line)
+        return n;
+
+    if (obj->WrapWidth > 0.0f)
+    {
+        ImGuiContext& g = *obj->Ctx;
+        const char* p_cursor = obj->TextSrc + cursor;
+        const char* p = ImStrbol(p_cursor, obj->TextSrc);
+        const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
+        while (p < text_end)
+        {
+            const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
+            cursor = (int)(p_eol - obj->TextSrc);
+            if (p_eol == p_cursor && obj->LastMoveDirectionLR != ImGuiDir_Left) // If we are already on a visible end-of-line, switch to regular handle
+                break;
+            if (p_eol > p_cursor)
+                return cursor;
+            p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
+        }
+    }
+    // Regular handler, same as stb_textedit_move_line_end()
+    while (cursor < n && STB_TEXTEDIT_GETCHAR(obj, cursor) != STB_TEXTEDIT_NEWLINE)
+        cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, cursor);
+    return cursor;
+}
+
+#define STB_TEXTEDIT_MOVELINESTART      STB_TEXTEDIT_MOVELINESTART_IMPL
+#define STB_TEXTEDIT_MOVELINEEND        STB_TEXTEDIT_MOVELINEEND_IMPL
+
 static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
 {
     // Offset remaining text (+ copy zero terminator)
@@ -4181,6 +4234,11 @@
     stb_textedit_key(this, Stb, key);
     CursorFollow = true;
     CursorAnimReset();
+    const int key_u = (key & ~STB_TEXTEDIT_K_SHIFT);
+    if (key_u == STB_TEXTEDIT_K_LEFT || key_u == STB_TEXTEDIT_K_LINESTART || key_u == STB_TEXTEDIT_K_TEXTSTART || key_u == STB_TEXTEDIT_K_BACKSPACE || key_u == STB_TEXTEDIT_K_WORDLEFT)
+        LastMoveDirectionLR = ImGuiDir_Left;
+    else if (key_u == STB_TEXTEDIT_K_RIGHT || key_u == STB_TEXTEDIT_K_LINEEND || key_u == STB_TEXTEDIT_K_TEXTEND || key_u == STB_TEXTEDIT_K_DELETE || key_u == STB_TEXTEDIT_K_WORDRIGHT)
+        LastMoveDirectionLR = ImGuiDir_Right;
 }
 
 void ImGuiInputTextState::OnCharPressed(unsigned int c)
@@ -4202,6 +4260,7 @@
 int  ImGuiInputTextState::GetCursorPos() const              { return Stb->cursor; }
 int  ImGuiInputTextState::GetSelectionStart() const         { return Stb->select_start; }
 int  ImGuiInputTextState::GetSelectionEnd() const           { return Stb->select_end; }
+float ImGuiInputTextState::GetPreferredOffsetX() const      { return Stb->has_preferred_x ? Stb->preferred_x : -1; }
 void ImGuiInputTextState::SelectAll()                       { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
 void ImGuiInputTextState::ReloadUserBufAndSelectAll()       { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
 void ImGuiInputTextState::ReloadUserBufAndKeepSelection()   { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
@@ -4238,23 +4297,24 @@
     if (new_text == new_text_end)
         return;
 
+    ImGuiContext& g = *Ctx;
+    ImGuiInputTextState* obj = &g.InputTextState;
+    IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
+
     // Grow internal buffer if needed
     const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
     const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text);
-    if (new_text_len + BufTextLen >= BufSize)
+    if (new_text_len + BufTextLen + 1 > obj->TextA.Size && (Flags & ImGuiInputTextFlags_ReadOnly) == 0)
     {
         if (!is_resizable)
             return;
 
-        ImGuiContext& g = *Ctx;
-        ImGuiInputTextState* edit_state = &g.InputTextState;
-        IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
-        IM_ASSERT(Buf == edit_state->TextA.Data);
+        IM_ASSERT(Buf == obj->TextA.Data);
         int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
-        edit_state->TextA.resize(new_buf_size + 1);
-        edit_state->TextSrc = edit_state->TextA.Data;
-        Buf = edit_state->TextA.Data;
-        BufSize = edit_state->BufCapacity = new_buf_size;
+        obj->TextA.resize(new_buf_size + 1);
+        obj->TextSrc = obj->TextA.Data;
+        Buf = obj->TextA.Data;
+        BufSize = obj->BufCapacity = new_buf_size;
     }
 
     if (BufTextLen != pos)
@@ -4262,28 +4322,40 @@
     memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
     Buf[BufTextLen + new_text_len] = '\0';
 
+    BufDirty = true;
+    BufTextLen += new_text_len;
     if (CursorPos >= pos)
         CursorPos += new_text_len;
+    CursorPos = ImClamp(CursorPos, 0, BufTextLen);
     SelectionStart = SelectionEnd = CursorPos;
-    BufDirty = true;
-    BufTextLen += new_text_len;
 }
 
 void ImGui::PushPasswordFont()
 {
     ImGuiContext& g = *GImGui;
-    ImFont* in_font = g.Font;
-    ImFont* out_font = &g.InputTextPasswordFont;
-    ImFontGlyph* glyph = in_font->FindGlyph('*');
-    out_font->FontSize = in_font->FontSize;
-    out_font->Scale = in_font->Scale;
-    out_font->Ascent = in_font->Ascent;
-    out_font->Descent = in_font->Descent;
-    out_font->ContainerAtlas = in_font->ContainerAtlas;
-    out_font->FallbackGlyph = glyph;
-    out_font->FallbackAdvanceX = glyph->AdvanceX;
-    IM_ASSERT(out_font->Glyphs.Size == 0 && out_font->IndexAdvanceX.Size == 0 && out_font->IndexLookup.Size == 0);
-    PushFont(out_font);
+    ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
+    IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
+    ImFontGlyph* glyph = g.FontBaked->FindGlyph('*');
+    g.InputTextPasswordFontBackupFlags = g.Font->Flags;
+    backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
+    backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
+    backup->IndexLookup.swap(g.FontBaked->IndexLookup);
+    backup->IndexAdvanceX.swap(g.FontBaked->IndexAdvanceX);
+    g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
+    g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(glyph);
+    g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
+}
+
+void ImGui::PopPasswordFont()
+{
+    ImGuiContext& g = *GImGui;
+    ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
+    g.Font->Flags = g.InputTextPasswordFontBackupFlags;
+    g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
+    g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
+    g.FontBaked->IndexLookup.swap(backup->IndexLookup);
+    g.FontBaked->IndexAdvanceX.swap(backup->IndexAdvanceX);
+    IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
 }
 
 // Return false to discard a character.
@@ -4339,7 +4411,7 @@
             if (c == '.' || c == ',')
                 c = c_decimal_point;
 
-        // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
+        // Full-width -> half-width conversion for numeric fields: https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
         // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
         // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
         if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
@@ -4444,6 +4516,97 @@
     }
 }
 
+static int* ImLowerBound(int* in_begin, int* in_end, int v)
+{
+    int* in_p = in_begin;
+    for (size_t count = (size_t)(in_end - in_p); count > 0; )
+    {
+        size_t count2 = count >> 1;
+        int* mid = in_p + count2;
+        if (*mid < v)
+        {
+            in_p = ++mid;
+            count -= count2 + 1;
+        }
+        else
+        {
+            count = count2;
+        }
+    }
+    return in_p;
+}
+
+// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool?
+// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive)
+static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size, const char** out_buf_end)
+{
+    ImGuiContext& g = *GImGui;
+    int size = 0;
+    const char* s;
+    if (flags & ImGuiInputTextFlags_WordWrap)
+    {
+        for (s = buf; s < buf_end; s = (*s == '\n') ? s + 1 : s)
+        {
+            if (size++ <= max_output_buffer_size)
+                line_index->Offsets.push_back((int)(s - buf));
+            s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
+        }
+    }
+    else if (buf_end != NULL)
+    {
+        for (s = buf; s < buf_end; s = s ? s + 1 : buf_end)
+        {
+            if (size++ <= max_output_buffer_size)
+                line_index->Offsets.push_back((int)(s - buf));
+            s = (const char*)ImMemchr(s, '\n', buf_end - s);
+        }
+    }
+    else
+    {
+        const char* s_eol;
+        for (s = buf; ; s = s_eol + 1)
+        {
+            if (size++ <= max_output_buffer_size)
+                line_index->Offsets.push_back((int)(s - buf));
+            if ((s_eol = strchr(s, '\n')) != NULL)
+                continue;
+            s += strlen(s);
+            break;
+        }
+    }
+    if (out_buf_end != NULL)
+        *out_buf_end = buf_end = s;
+    if (size == 0)
+    {
+        line_index->Offsets.push_back(0);
+        size++;
+    }
+    if (buf_end > buf && buf_end[-1] == '\n' && size <= max_output_buffer_size)
+    {
+        line_index->Offsets.push_back((int)(buf_end - buf));
+        size++;
+    }
+    return size;
+}
+
+static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n)
+{
+    const char* cursor_ptr = buf + cursor_n;
+    int* it_begin = line_index->Offsets.begin();
+    int* it_end = line_index->Offsets.end();
+    const int* it = ImLowerBound(it_begin, it_end, cursor_n);
+    if (it > it_begin)
+        if (it == it_end || *it != cursor_n || (state != NULL && state->WrapWidth > 0.0f && state->LastMoveDirectionLR == ImGuiDir_Right && cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0))
+            it--;
+
+    const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it);
+    const char* line_start = line_index->get_line_begin(buf, line_no);
+    ImVec2 offset;
+    offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
+    offset.y = (line_no + 1) * g.FontSize;
+    return offset;
+}
+
 // Edit a string of text
 // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
 //   This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
@@ -4461,7 +4624,9 @@
     IM_ASSERT(buf != NULL && buf_size >= 0);
     IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline)));        // Can't use both together (they both use up/down keys)
     IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
-    IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline)));               // Multiline will not work with left-trimming
+    IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline)));              // Multiline does not not work with left-trimming
+    IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Password) == 0);         // WordWrap does not work with Password mode.
+    IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Multiline) != 0);        // WordWrap does not work in single-line mode.
 
     ImGuiContext& g = *GImGui;
     ImGuiIO& io = g.IO;
@@ -4495,8 +4660,8 @@
         item_data_backup = g.LastItemData;
         window->DC.CursorPos = backup_pos;
 
-        // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
-        if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
+        // Prevent NavActivation from explicit Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
+        if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && !(g.NavActivateFlags & ImGuiActivateFlags_FromFocusApi) && (flags & ImGuiInputTextFlags_AllowTabInput))
             g.NavActivateId = 0;
 
         // Prevent NavActivate reactivating in BeginChild() when we are already active.
@@ -4552,6 +4717,13 @@
     if (is_resizable)
         IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
 
+    // Word-wrapping: enforcing a fixed width not altered by vertical scrollbar makes things easier, notably to track cursor reliably and avoid one-frame glitches.
+    // Instead of using ImGuiWindowFlags_AlwaysVerticalScrollbar we account for that space if the scrollbar is not visible.
+    const bool is_wordwrap = (flags & ImGuiInputTextFlags_WordWrap) != 0;
+    float wrap_width = 0.0f;
+    if (is_wordwrap)
+        wrap_width = ImMax(1.0f, GetContentRegionAvail().x + (draw_window->ScrollbarY ? 0.0f : -g.Style.ScrollbarSize));
+
     const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
 
     const bool user_clicked = hovered && io.MouseClicked[0];
@@ -4591,7 +4763,7 @@
         // Take a copy of the initial buffer value.
         // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
         const int buf_len = (int)ImStrlen(buf);
-        IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
+        IM_ASSERT(((buf_len + 1 <= buf_size) || (buf_len == 0 && buf_size == 0)) && "Is your input buffer properly zero-terminated?");
         state->TextToRevertTo.resize(buf_len + 1);    // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
         memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
 
@@ -4647,7 +4819,7 @@
     if (g.ActiveId == id)
     {
         // Declare some inputs, the other are registered and polled via Shortcut() routing system.
-        // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combinaison into individual shortcuts.
+        // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combination into individual shortcuts.
         const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
         for (ImGuiKey key : always_owned_keys)
             SetKeyOwner(key, id);
@@ -4704,6 +4876,15 @@
     if (is_password && !is_displaying_hint)
         PushPasswordFont();
 
+    // Word-wrapping: attempt to keep cursor in view while resizing frame/parent
+    // FIXME-WORDWRAP: It would be better to preserve same relative offset.
+    if (is_wordwrap && state != NULL && state->ID == id && state->WrapWidth != wrap_width)
+    {
+        state->CursorCenterY = true;
+        state->WrapWidth = wrap_width;
+        render_cursor = true;
+    }
+
     // Process mouse inputs and character inputs
     if (g.ActiveId == id)
     {
@@ -4711,6 +4892,7 @@
         state->Edited = false;
         state->BufCapacity = buf_size;
         state->Flags = flags;
+        state->WrapWidth = wrap_width;
 
         // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
         // Down the line we should have a cleaner library-wide concept of Selected vs Active.
@@ -4748,9 +4930,11 @@
             {
                 // Triple-click: Select line
                 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
+                state->WrapWidth = 0.0f; // Temporarily disable wrapping so we use real line start.
                 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
                 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
                 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
+                state->WrapWidth = wrap_width;
                 if (!is_eol && is_multiline)
                 {
                     ImSwap(state->Stb->select_start, state->Stb->select_end);
@@ -4883,7 +5067,7 @@
         {
             // Determine if we turn Enter into a \n character
             bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
-            if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
+            if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line != io.KeyCtrl))
             {
                 validated = true;
                 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
@@ -4902,7 +5086,7 @@
         {
             if (flags & ImGuiInputTextFlags_EscapeClearsAll)
             {
-                if (buf[0] != 0)
+                if (state->TextA.Data[0] != 0)
                 {
                     revert_edit = true;
                 }
@@ -4994,14 +5178,14 @@
             if (flags & ImGuiInputTextFlags_EscapeClearsAll)
             {
                 // Clear input
-                IM_ASSERT(buf[0] != 0);
+                IM_ASSERT(state->TextA.Data[0] != 0);
                 apply_new_text = "";
                 apply_new_text_length = 0;
                 value_changed = true;
-                IMSTB_TEXTEDIT_CHARTYPE empty_string;
+                char empty_string = 0;
                 stb_textedit_replace(state, state->Stb, &empty_string, 0);
             }
-            else if (strcmp(buf, state->TextToRevertTo.Data) != 0)
+            else if (strcmp(state->TextA.Data, state->TextToRevertTo.Data) != 0)
             {
                 apply_new_text = state->TextToRevertTo.Data;
                 apply_new_text_length = state->TextToRevertTo.Size - 1;
@@ -5160,8 +5344,6 @@
     // Otherwise request text input ahead for next frame.
     if (g.ActiveId == id && clear_active_id)
         ClearActiveID();
-    else if (g.ActiveId == id)
-        g.WantTextInputNextFrame = 1;
 
     // Render frame
     if (!is_multiline)
@@ -5170,9 +5352,11 @@
         RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
     }
 
-    const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
     ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
     ImVec2 text_size(0.0f, 0.0f);
+    ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
+    if (is_multiline)
+        clip_rect.ClipWith(draw_window->ClipRect);
 
     // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
     // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
@@ -5187,7 +5371,7 @@
     if (new_is_displaying_hint != is_displaying_hint)
     {
         if (is_password && !is_displaying_hint)
-            PopFont();
+            PopPasswordFont();
         is_displaying_hint = new_is_displaying_hint;
         if (is_password && !is_displaying_hint)
             PushPasswordFont();
@@ -5197,15 +5381,42 @@
         buf_display = hint;
         buf_display_end = hint + ImStrlen(hint);
     }
+    else
+    {
+        if (render_cursor || render_selection || g.ActiveId == id)
+            buf_display_end = buf_display + state->TextLen; //-V595
+        else if (is_multiline && !is_wordwrap)
+            buf_display_end = NULL; // Inactive multi-line: end of buffer will be output by InputTextLineIndexBuild() special strchr() path.
+        else
+            buf_display_end = buf_display + ImStrlen(buf_display);
+    }
+
+    // Calculate visibility
+    int line_visible_n0 = 0, line_visible_n1 = 1;
+    if (is_multiline)
+        CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
+
+    // Build line index for easy data access (makes code below simpler and faster)
+    ImGuiTextIndex* line_index = &g.InputTextLineIndex;
+    line_index->Offsets.resize(0);
+    int line_count = 1;
+    if (is_multiline)
+        line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, (render_cursor && state && state->CursorFollow) ? INT_MAX : line_visible_n1 + 1, buf_display_end ? NULL : &buf_display_end);
+    line_index->EndOffset = (int)(buf_display_end - buf_display);
+    line_visible_n1 = ImMin(line_visible_n1, line_count);
+
+    // Store text height (we don't need width)
+    text_size = ImVec2(inner_size.x, line_count * g.FontSize);
+    //GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255));
+
+    // Calculate blinking cursor position
+    const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f);
+    ImVec2 draw_scroll;
 
     // Render text. We currently only render selection when the widget is active or while scrolling.
-    // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
+    const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
     if (render_cursor || render_selection)
     {
-        IM_ASSERT(state != NULL);
-        if (!is_displaying_hint)
-            buf_display_end = buf_display + state->TextLen;
-
         // Render text (with cursor and selection)
         // This is going to be messy. We need to:
         // - Display the text (this alone can be more easily clipped)
@@ -5213,48 +5424,11 @@
         // - Measure text height (for scrollbar)
         // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort)
         // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
-        const char* text_begin = buf_display;
-        const char* text_end = text_begin + state->TextLen;
-        ImVec2 cursor_offset, select_start_offset;
-
-        {
-            // Find lines numbers straddling cursor and selection min position
-            int cursor_line_no = render_cursor ? -1 : -1000;
-            int selmin_line_no = render_selection ? -1 : -1000;
-            const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
-            const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
-
-            // Count lines and find line number for cursor and selection ends
-            int line_count = 1;
-            if (is_multiline)
-            {
-                for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
-                {
-                    if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
-                    if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
-                    line_count++;
-                }
-            }
-            if (cursor_line_no == -1)
-                cursor_line_no = line_count;
-            if (selmin_line_no == -1)
-                selmin_line_no = line_count;
-
-            // Calculate 2d position by finding the beginning of the line and measuring distance
-            cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x;
-            cursor_offset.y = cursor_line_no * g.FontSize;
-            if (selmin_line_no >= 0)
-            {
-                select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x;
-                select_start_offset.y = selmin_line_no * g.FontSize;
-            }
-
-            // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
-            if (is_multiline)
-                text_size = ImVec2(inner_size.x, line_count * g.FontSize);
-        }
+        IM_ASSERT(state != NULL);
+        state->LineCount = line_count;
 
         // Scroll
+        float new_scroll_y = scroll_y;
         if (render_cursor && state->CursorFollow)
         {
             // Horizontal scroll in chunks of quarter width
@@ -5269,7 +5443,7 @@
             }
             else
             {
-                state->Scroll.y = 0.0f;
+                state->Scroll.x = 0.0f;
             }
 
             // Vertical scroll
@@ -5277,103 +5451,110 @@
             {
                 // Test if cursor is vertically visible
                 if (cursor_offset.y - g.FontSize < scroll_y)
-                    scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
+                    new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
                 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
-                    scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
-                const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
-                scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
-                draw_pos.y += (draw_window->Scroll.y - scroll_y);   // Manipulate cursor pos immediately avoid a frame of lag
-                draw_window->Scroll.y = scroll_y;
+                    new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
             }
-
             state->CursorFollow = false;
         }
-
-        // Draw selection
-        const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
-        if (render_selection)
+        if (state->CursorCenterY)
         {
-            const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
-            const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
-
-            ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
-            float bg_offy_up = is_multiline ? 0.0f : -1.0f;    // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
-            float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
-            ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
-            for (const char* p = text_selected_begin; p < text_selected_end; )
-            {
-                if (rect_pos.y > clip_rect.w + g.FontSize)
-                    break;
-                if (rect_pos.y < clip_rect.y)
-                {
-                    p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p);
-                    p = p ? p + 1 : text_selected_end;
-                }
-                else
-                {
-                    ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true);
-                    if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
-                    ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
-                    rect.ClipWith(clip_rect);
-                    if (rect.Overlaps(clip_rect))
-                        draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
-                    rect_pos.x = draw_pos.x - draw_scroll.x;
-                }
-                rect_pos.y += g.FontSize;
-            }
-        }
-
-        // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
-        // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
-        if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
-        {
-            ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
-            draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
-        }
-
-        // Draw blinking cursor
-        if (render_cursor)
-        {
-            state->CursorAnim += io.DeltaTime;
-            bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
-            ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
-            ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
-            if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
-                draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text));
-
-            // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
-            if (!is_readonly)
-            {
-                g.PlatformImeData.WantVisible = true;
-                g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
-                g.PlatformImeData.InputLineHeight = g.FontSize;
-            }
+            if (is_multiline)
+                new_scroll_y = cursor_offset.y - g.FontSize - (inner_size.y * 0.5f - style.FramePadding.y);
+            state->CursorCenterY = false;
+            render_cursor = false;
+        }
+        if (new_scroll_y != scroll_y)
+        {
+            const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
+            scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
+            draw_pos.y += (draw_window->Scroll.y - scroll_y);   // Manipulate cursor pos immediately avoid a frame of lag
+            draw_window->Scroll.y = scroll_y;
+            CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
+            line_visible_n1 = ImMin(line_visible_n1, line_count);
         }
-    }
-    else
-    {
-        // Render text only (no selection, no cursor)
-        if (is_multiline)
-            text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width
-        else if (!is_displaying_hint && g.ActiveId == id)
-            buf_display_end = buf_display + state->TextLen;
-        else if (!is_displaying_hint)
-            buf_display_end = buf_display + ImStrlen(buf_display);
 
-        if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
+        // Draw selection
+        draw_scroll.x = state->Scroll.x;
+        if (render_selection)
         {
-            // Find render position for right alignment
-            if (flags & ImGuiInputTextFlags_ElideLeft)
-                draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
-
-            const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
-            ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
-            draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
+            const ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
+            const float bg_offy_up = is_multiline ? 0.0f : -1.0f;    // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
+            const float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
+            const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
+
+            const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end);
+            const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end);
+            for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++)
+            {
+                const char* p = line_index->get_line_begin(buf_display, line_n);
+                const char* p_eol = line_index->get_line_end(buf_display, line_n);
+                const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n');
+                if (p_eol_is_wrap)
+                    p_eol++;
+                const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
+                const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
+
+                float rect_width = 0.0f;
+                if (line_selected_begin < line_selected_end)
+                    rect_width += CalcTextSize(line_selected_begin, line_selected_end).x;
+                if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap)
+                    rect_width += bg_eol_width; // So we can see selected empty lines
+                if (rect_width == 0.0f)
+                    continue;
+
+                ImRect rect;
+                rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x;
+                rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize;
+                rect.Max.x = rect.Min.x + rect_width;
+                rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize;
+                rect.Min.y -= bg_offy_up;
+                rect.ClipWith(clip_rect);
+                draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
+            }
+        }
+    }
+
+    // Find render position for right alignment (single-line only)
+    if (g.ActiveId != id && flags & ImGuiInputTextFlags_ElideLeft)
+        draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
+    //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive?
+
+    // Render text
+    if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
+        g.Font->RenderText(draw_window->DrawList, g.FontSize,
+            draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
+            text_col, clip_rect.AsVec4(),
+            line_index->get_line_begin(buf_display, line_visible_n0),
+            line_index->get_line_end(buf_display, line_visible_n1 - 1),
+            wrap_width, ImDrawTextFlags_WrapKeepBlanks | ImDrawTextFlags_CpuFineClip);
+
+    // Render blinking cursor
+    if (render_cursor)
+    {
+        state->CursorAnim += io.DeltaTime;
+        bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
+        ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
+        ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f);
+        if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
+            draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f); // FIXME-DPI: Cursor thickness (#7031)
+
+        // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.)
+        // This is required for some backends (SDL3) to start emitting character/text inputs.
+        // As per #6341, make sure we don't set that on the deactivating frame.
+        if (!is_readonly && g.ActiveId == id)
+        {
+            ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
+            ime_data->WantVisible = true;
+            ime_data->WantTextInput = true;
+            ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
+            ime_data->InputLineHeight = g.FontSize;
+            ime_data->ViewportId = window->Viewport->ID;
         }
     }
 
     if (is_password && !is_displaying_hint)
-        PopFont();
+        PopPasswordFont();
 
     if (is_multiline)
     {
@@ -5424,8 +5605,10 @@
     ImStb::StbUndoState* undo_state = &stb_state->undostate;
     Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
     DebugLocateItemOnHover(state->ID);
-    Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
-    Text("BufCapacityA: %d", state->BufCapacity);
+    Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state->TextLen, stb_state->cursor,
+        (state->Flags & ImGuiInputTextFlags_WordWrap) ? (state->LastMoveDirectionLR == ImGuiDir_Left ? " (L)" : " (R)") : "",
+        stb_state->select_start, stb_state->select_end);
+    Text("BufCapacity: %d, LineCount: %d", state->BufCapacity, state->LineCount);
     Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
     Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
     Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point);
@@ -6207,7 +6390,7 @@
         if (g.Style.FrameBorderSize > 0.0f)
             RenderFrameBorder(bb.Min, bb.Max, rounding);
         else
-            window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border
+            window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border // FIXME-DPI
     }
 
     // Drag and Drop Source
@@ -6389,6 +6572,7 @@
 // - TreeNodeV()
 // - TreeNodeEx()
 // - TreeNodeExV()
+// - TreeNodeStoreStackData() [Internal]
 // - TreeNodeBehavior() [Internal]
 // - TreePush()
 // - TreePop()
@@ -6547,18 +6731,26 @@
 
 // Store ImGuiTreeNodeStackData for just submitted node.
 // Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
-static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags)
+static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
 {
     ImGuiContext& g = *GImGui;
     ImGuiWindow* window = g.CurrentWindow;
 
     g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
-    ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back();
+    ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
     tree_node_data->ID = g.LastItemData.ID;
     tree_node_data->TreeFlags = flags;
     tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
     tree_node_data->NavRect = g.LastItemData.NavRect;
+
+    // Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
+    const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
+    tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
+    tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
+    tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
     window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
+    if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
+        window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
 }
 
 // When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop.
@@ -6628,14 +6820,18 @@
     g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
     g.LastItemData.DisplayRect = frame_bb;
 
-    // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
+    // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
     // Store data for the current depth to allow returning to this node from any child item.
     // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
-    // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
+    // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
     bool store_tree_node_stack_data = false;
+    if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
+        flags |= g.Style.TreeLinesFlags;
+    const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
     if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
     {
-        if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive)
+        store_tree_node_stack_data = draw_tree_lines;
+        if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
             if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
                 store_tree_node_stack_data = true;
     }
@@ -6643,8 +6839,15 @@
     const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
     if (!is_visible)
     {
-        if (store_tree_node_stack_data && is_open)
-            TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
+        if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
+        {
+            ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
+            parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, window->DC.CursorPos.y); // Don't need to aim to mid Y position as we are clipped anyway.
+            if (frame_bb.Min.y >= window->ClipRect.Max.y)
+                window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
+        }
+        if (is_open && store_tree_node_stack_data)
+            TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
         if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
             TreePushOverrideID(id);
         IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
@@ -6690,6 +6893,8 @@
         button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
     else
         button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
+    if (flags & ImGuiTreeNodeFlags_NoNavFocus)
+        button_flags |= ImGuiButtonFlags_NoNavFocus;
 
     bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
     const bool was_selected = selected;
@@ -6776,6 +6981,8 @@
             const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
             RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
             RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
+            if (span_all_columns && !span_all_columns_label)
+                TablePopBackgroundChannel();
             if (flags & ImGuiTreeNodeFlags_Bullet)
                 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
             else if (!is_leaf)
@@ -6796,6 +7003,8 @@
                 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
             }
             RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
+            if (span_all_columns && !span_all_columns_label)
+                TablePopBackgroundChannel();
             if (flags & ImGuiTreeNodeFlags_Bullet)
                 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
             else if (!is_leaf)
@@ -6804,8 +7013,8 @@
                 LogSetNextTextDecoration(">", NULL);
         }
 
-        if (span_all_columns && !span_all_columns_label)
-            TablePopBackgroundChannel();
+        if (draw_tree_lines)
+            TreeNodeDrawLineToChildNode(ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
 
         // Label
         if (display_frame)
@@ -6817,8 +7026,8 @@
             TablePopBackgroundChannel();
     }
 
-    if (store_tree_node_stack_data && is_open)
-        TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
+    if (is_open && store_tree_node_stack_data)
+        TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
     if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
         TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
 
@@ -6826,6 +7035,64 @@
     return is_open;
 }
 
+// Draw horizontal line from our parent node
+// This is only called for visible child nodes so we are not too fussy anymore about performances
+void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
+{
+    ImGuiContext& g = *GImGui;
+    ImGuiWindow* window = g.CurrentWindow;
+    if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
+        return;
+
+    ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
+    float x1 = ImTrunc(parent_data->DrawLinesX1);
+    float x2 = ImTrunc(target_pos.x - g.Style.ItemInnerSpacing.x);
+    float y = ImTrunc(target_pos.y);
+    float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(x2 - x1, g.Style.TreeLinesRounding) : 0.0f;
+    parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, y - rounding);
+    if (x1 >= x2)
+        return;
+    if (rounding > 0.0f)
+    {
+        x1 += 0.5f + rounding;
+        window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
+        if (x1 < x2)
+            window->DrawList->PathLineTo(ImVec2(x2, y));
+        window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), ImDrawFlags_None, g.Style.TreeLinesSize);
+    }
+    else
+    {
+        window->DrawList->AddLine(ImVec2(x1, y), ImVec2(x2, y), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
+    }
+}
+
+// Draw vertical line of the hierarchy
+void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
+{
+    ImGuiContext& g = *GImGui;
+    ImGuiWindow* window = g.CurrentWindow;
+    float y1 = ImMax(data->NavRect.Max.y, window->ClipRect.Min.y);
+    float y2 = data->DrawLinesToNodesY2;
+    if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
+    {
+        float y2_full = window->DC.CursorPos.y;
+        if (g.CurrentTable)
+            y2_full = ImMax(g.CurrentTable->RowPosY2, y2_full);
+        y2_full = ImTrunc(y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
+        if (y2 + (g.Style.ItemSpacing.y + g.Style.TreeLinesRounding) < y2_full) // FIXME: threshold to use ToNodes Y2 instead of Full Y2 when close by ItemSpacing.y
+            y2 = y2_full;
+    }
+    y2 = ImMin(y2, window->ClipRect.Max.y);
+    if (y2 <= y1)
+        return;
+    float x = ImTrunc(data->DrawLinesX1);
+    if (data->DrawLinesTableColumn != -1)
+        TablePushColumnChannel(data->DrawLinesTableColumn);
+    window->DrawList->AddLine(ImVec2(x, y1), ImVec2(x, y2), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
+    if (data->DrawLinesTableColumn != -1)
+        TablePopColumnChannel();
+}
+
 void ImGui::TreePush(const char* str_id)
 {
     ImGuiWindow* window = GetCurrentWindow();
@@ -6860,18 +7127,23 @@
     window->DC.TreeDepth--;
     ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
 
-    if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request
+    if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
     {
-        ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back();
+        const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
         IM_ASSERT(data->ID == window->IDStack.back());
-        if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere)
-        {
-            // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
+
+        // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
+        if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
             if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
                 NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
-        }
+
+        // Draw hierarchy lines
+        if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
+            TreeNodeDrawLineToTreePop(data);
+
         g.TreeNodeStack.pop_back();
         window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
+        window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
     }
 
     IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much.
@@ -7064,6 +7336,7 @@
 
     bool hovered, held;
     bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
+    bool auto_selected = false;
 
     // Multi-selection support (footer)
     if (is_multi_select)
@@ -7080,8 +7353,8 @@
         //   - (2) usage will fail with clipped items
         //   The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
         if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
-            if (g.NavJustMovedToId == id)
-                selected = pressed = true;
+            if (g.NavJustMovedToId == id && (g.NavJustMovedToKeyMods & ImGuiMod_Ctrl) == 0)
+                selected = pressed = auto_selected = true;
     }
 
     // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
@@ -7132,7 +7405,7 @@
         RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb);
 
     // Automatically close popups
-    if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
+    if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
         CloseCurrentPopup();
 
     if (disabled_item && !disabled_global)
@@ -7486,7 +7759,7 @@
     ImRect box_select_r = bs->BoxSelectRectCurr;
     box_select_r.ClipWith(scope_rect);
     window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
-    window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling
+    window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
 
     // Scroll
     const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
@@ -7690,7 +7963,7 @@
     if (ms->IsFocused)
     {
         // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
-        if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure)
+        if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at beginning of the scope (see tests for easy failure)
         {
             IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
             storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
@@ -8210,7 +8483,7 @@
     // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
     // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector<ImGuiID> to reduce bandwidth, but this is a reasonable trade off to reuse code.
     // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling
-    //   left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
+    //   left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.
     // FIXME-OPT: For each block of consecutive SetRange request:
     // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
     // - rewrite sorted storage a single time.
@@ -8730,6 +9003,7 @@
 
     PopClipRect();
     PopID();
+    IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
     window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos.
 
     // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
@@ -8924,7 +9198,7 @@
     {
         // Menu inside a regular/vertical menu
         // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
-        //  Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
+        //  Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
         popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
         float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
         float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
@@ -9131,7 +9405,7 @@
     {
         // Menu item inside a vertical menu
         // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
-        //  Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
+        //  Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
         float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
         float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
         float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
@@ -9209,6 +9483,7 @@
 {
     int                 TabCount;               // Number of tabs in this section.
     float               Width;                  // Sum of width of tabs in this section (after shrinking down)
+    float               WidthAfterShrinkMinWidth;
     float               Spacing;                // Horizontal spacing at the end of the section.
 
     ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
@@ -9269,6 +9544,19 @@
     return ImGuiPtrOrIndex(tab_bar);
 }
 
+ImGuiTabBar* ImGui::TabBarFindByID(ImGuiID id)
+{
+    ImGuiContext& g = *GImGui;
+    return g.TabBars.GetByKey(id);
+}
+
+// Remove TabBar data (currently only used by TestEngine)
+void    ImGui::TabBarRemove(ImGuiTabBar* tab_bar)
+{
+    ImGuiContext& g = *GImGui;
+    g.TabBars.Remove(tab_bar->ID, tab_bar);
+}
+
 bool    ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
 {
     ImGuiContext& g = *GImGui;
@@ -9280,8 +9568,8 @@
     ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
     ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2);
     tab_bar->ID = id;
-    tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
-    tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
+    tab_bar->SeparatorMinX = tab_bar_bb.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
+    tab_bar->SeparatorMaxX = tab_bar_bb.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
     //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
     flags |= ImGuiTabBarFlags_IsFocused;
     return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
@@ -9401,6 +9689,10 @@
     ImGuiContext& g = *GImGui;
     tab_bar->WantLayout = false;
 
+    // Track selected tab when resizing our parent down
+    const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
+    tab_bar->BarRectPrevWidth = tab_bar->BarRect.GetWidth();
+
     // Garbage collect by compacting list
     // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
     int tab_dst_n = 0;
@@ -9477,6 +9769,9 @@
     int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
     g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
 
+    // Minimum shrink width
+    const float shrink_min_width = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed) ? g.Style.TabMinWidthShrink : 1.0f;
+
     // Compute ideal tabs widths + store them into shrink buffer
     ImGuiTabItem* most_recently_selected_tab = NULL;
     int curr_section_n = -1;
@@ -9499,10 +9794,13 @@
         const char* tab_name = TabBarGetTabName(tab_bar, tab);
         const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
         tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
+        if ((tab->Flags & ImGuiTabItemFlags_Button) == 0)
+            tab->ContentWidth = ImMax(tab->ContentWidth, g.Style.TabMinWidthBase);
 
         int section_n = TabItemGetSectionIdx(tab);
         ImGuiTabBarSection* section = &sections[section_n];
         section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
+        section->WidthAfterShrinkMinWidth += ImMin(tab->ContentWidth, shrink_min_width) + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
         curr_section_n = section_n;
 
         // Store data so we can build an array sorted by width if we need to shrink tabs down
@@ -9514,19 +9812,28 @@
     }
 
     // Compute total ideal width (used for e.g. auto-resizing a window)
+    float width_all_tabs_after_min_width_shrink = 0.0f;
     tab_bar->WidthAllTabsIdeal = 0.0f;
     for (int section_n = 0; section_n < 3; section_n++)
+    {
         tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
+        width_all_tabs_after_min_width_shrink += sections[section_n].WidthAfterShrinkMinWidth + sections[section_n].Spacing;
+    }
 
     // Horizontal scrolling buttons
-    // (note that TabBarScrollButtons() will alter BarRect.Max.x)
-    if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
+    // Important: note that TabBarScrollButtons() will alter BarRect.Max.x.
+    const bool can_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
+    const float width_all_tabs_to_use_for_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) ? tab_bar->WidthAllTabs : width_all_tabs_after_min_width_shrink;
+    tab_bar->ScrollButtonEnabled = ((width_all_tabs_to_use_for_scroll > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && can_scroll);
+    if (tab_bar->ScrollButtonEnabled)
         if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
         {
             scroll_to_tab_id = scroll_and_select_tab->ID;
             if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
                 tab_bar->SelectedTabId = scroll_to_tab_id;
         }
+    if (scroll_to_tab_id == 0 && scroll_to_selected_tab)
+        scroll_to_tab_id = tab_bar->SelectedTabId;
 
     // Shrink widths if full tabs don't fit in their allocated space
     float section_0_w = sections[0].Width + sections[0].Spacing;
@@ -9540,11 +9847,12 @@
         width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
 
     // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
-    if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
+    const bool can_shrink = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyShrink) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
+    if (width_excess >= 1.0f && (can_shrink || !central_section_is_visible))
     {
         int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
         int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
-        ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess);
+        ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess, shrink_min_width);
 
         // Apply shrunk values into tabs and sections
         for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
@@ -9587,7 +9895,8 @@
     tab_bar->TabsNames.Buf.resize(0);
 
     // If we have lost the selected tab, select the next most recently active one
-    if (found_selected_tab_id == false)
+    const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
+    if (found_selected_tab_id == false && !tab_bar_appearing)
         tab_bar->SelectedTabId = 0;
     if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
         scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
@@ -9599,7 +9908,7 @@
     // Apply request requests
     if (scroll_to_tab_id != 0)
         TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
-    else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
+    else if (tab_bar->ScrollButtonEnabled && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
     {
         const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
         const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
@@ -9873,7 +10182,7 @@
 
     PushStyleColor(ImGuiCol_Text, arrow_col);
     PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
-    PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
+    PushItemFlag(ImGuiItemFlags_ButtonRepeat | ImGuiItemFlags_NoNav, true);
     const float backup_repeat_delay = g.IO.KeyRepeatDelay;
     const float backup_repeat_rate = g.IO.KeyRepeatRate;
     g.IO.KeyRepeatDelay = 0.250f;
@@ -10376,13 +10685,12 @@
 #endif
 
     // Render text label (with clipping + alpha gradient) + unsaved marker
-    ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
-    ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
+    ImRect text_ellipsis_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
 
     // Return clipped state ignoring the close button
     if (out_text_clipped)
     {
-        *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
+        *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
         //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255));
     }
 
@@ -10410,8 +10718,8 @@
     const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
     if (unsaved_marker_visible)
     {
-        const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
-        RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text));
+        ImVec2 bullet_pos = button_pos + ImVec2(button_sz, button_sz) * 0.5f;
+        RenderBullet(draw_list, bullet_pos, GetColorU32(ImGuiCol_UnsavedMarker));
     }
     else if (close_button_visible)
     {
@@ -10428,15 +10736,22 @@
     // This is all rather complicated
     // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
     // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist..
-    float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
+    float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
     if (close_button_visible || unsaved_marker_visible)
     {
-        text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
-        text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
-        ellipsis_max_x = text_pixel_clip_bb.Max.x;
+        const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
+        if (visible_without_hover)
+        {
+            text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
+            ellipsis_max_x -= button_sz * 0.90f;
+        }
+        else
+        {
+            text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
+        }
     }
     LogSetNextTextDecoration("/", "\\");
-    RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size);
+    RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, NULL, &label_size);
 
 #if 0
     if (!is_contents_visible)
diff -Nru f3d-3.2.0+dfsg/external/imgui/imstb_textedit.h f3d-3.4.1/external/imgui/imstb_textedit.h
--- f3d-3.2.0+dfsg/external/imgui/imstb_textedit.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imstb_textedit.h	2026-01-08 08:41:55.000000000 +0200
@@ -141,6 +141,7 @@
 //                                        with previous char)
 //    STB_TEXTEDIT_KEYTOTEXT(k)         maps a keyboard input to an insertable character
 //                                        (return type is int, -1 means not valid to insert)
+//                                        (not supported if you want to use UTF-8, see below)
 //    STB_TEXTEDIT_GETCHAR(obj,i)       returns the i'th character of obj, 0-based
 //    STB_TEXTEDIT_NEWLINE              the character returned by _GETCHAR() we recognize
 //                                        as manually wordwrapping for end-of-line positioning
@@ -178,6 +179,13 @@
 //    STB_TEXTEDIT_K_TEXTSTART2          secondary keyboard input to move cursor to start of text
 //    STB_TEXTEDIT_K_TEXTEND2            secondary keyboard input to move cursor to end of text
 //
+// To support UTF-8:
+//
+//    STB_TEXTEDIT_GETPREVCHARINDEX      returns index of previous character
+//    STB_TEXTEDIT_GETNEXTCHARINDEX      returns index of next character
+//    Do NOT define STB_TEXTEDIT_KEYTOTEXT.
+//    Instead, call stb_textedit_text() directly for text contents.
+//
 // Keyboard input must be encoded as a single integer value; e.g. a character code
 // and some bitflags that represent shift states. to simplify the interface, SHIFT must
 // be a bitflag, so we can test the shifted state of cursor movements to allow selection,
@@ -250,8 +258,10 @@
 //          if the STB_TEXTEDIT_KEYTOTEXT function is defined, selected keys are
 //          transformed into text and stb_textedit_text() is automatically called.
 //
-//      text: [DEAR IMGUI] added 2024-09
-//          call this to text inputs sent to the textfield.
+//      text: (added 2025)
+//          call this to directly send text input the textfield, which is required
+//          for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT()
+//          cannot infer text length.
 //
 //
 //   When rendering, you can read the cursor position and selection state from
@@ -400,6 +410,16 @@
 #define IMSTB_TEXTEDIT_memmove memmove
 #endif
 
+// [DEAR IMGUI]
+// Functions must be implemented for UTF8 support
+// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
+// There is not necessarily a '[DEAR IMGUI]' at the usage sites.
+#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX
+#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(OBJ, IDX) ((IDX) - 1)
+#endif
+#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX
+#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(OBJ, IDX) ((IDX) + 1)
+#endif
 
 /////////////////////////////////////////////////////////////////////////////
 //
@@ -407,7 +427,7 @@
 //
 
 // traverse the layout to locate the nearest character to a display position
-static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
+static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y, int* out_side_on_line)
 {
    StbTexteditRow r;
    int n = STB_TEXTEDIT_STRINGLEN(str);
@@ -417,6 +437,7 @@
    r.x0 = r.x1 = 0;
    r.ymin = r.ymax = 0;
    r.num_chars = 0;
+   *out_side_on_line = 0;
 
    // search rows to find one that straddles 'y'
    while (i < n) {
@@ -436,7 +457,10 @@
 
    // below all text, return 'after' last character
    if (i >= n)
+   {
+      *out_side_on_line = 1;
       return n;
+   }
 
    // check if it's before the beginning of the line
    if (x < r.x0)
@@ -449,6 +473,7 @@
       for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) {
          float w = STB_TEXTEDIT_GETWIDTH(str, i, k);
          if (x < prev_x+w) {
+            *out_side_on_line = (k == 0) ? 0 : 1;
             if (x < prev_x+w/2)
                return k+i;
             else
@@ -460,6 +485,7 @@
    }
 
    // if the last character is a newline, return that. otherwise return 'after' the last character
+   *out_side_on_line = 1;
    if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE)
       return i+r.num_chars-1;
    else
@@ -471,6 +497,7 @@
 {
    // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
    // goes off the top or bottom of the text
+   int side_on_line;
    if( state->single_line )
    {
       StbTexteditRow r;
@@ -478,16 +505,18 @@
       y = r.ymin;
    }
 
-   state->cursor = stb_text_locate_coord(str, x, y);
+   state->cursor = stb_text_locate_coord(str, x, y, &side_on_line);
    state->select_start = state->cursor;
    state->select_end = state->cursor;
    state->has_preferred_x = 0;
+   str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left);
 }
 
 // API drag: on mouse drag, move the cursor and selection endpoint to the clicked location
 static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y)
 {
    int p = 0;
+   int side_on_line;
 
    // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
    // goes off the top or bottom of the text
@@ -501,8 +530,9 @@
    if (state->select_start == state->select_end)
       state->select_start = state->cursor;
 
-   p = stb_text_locate_coord(str, x, y);
+   p = stb_text_locate_coord(str, x, y, &side_on_line);
    state->cursor = state->select_end = p;
+   str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left);
 }
 
 /////////////////////////////////////////////////////////////////////////////
@@ -552,6 +582,8 @@
       STB_TEXTEDIT_LAYOUTROW(&r, str, i);
       if (n < i + r.num_chars)
          break;
+      if (str->LastMoveDirectionLR == ImGuiDir_Right && str->Stb->cursor > 0 && str->Stb->cursor == i + r.num_chars && STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] Wrapping point handling
+         break;
       if (i + r.num_chars == z && z > 0 && STB_TEXTEDIT_GETCHAR(str, z - 1) != STB_TEXTEDIT_NEWLINE)  // [DEAR IMGUI] special handling for last line
          break;   // [DEAR IMGUI]
       prev_start = i;
@@ -648,15 +680,33 @@
    }
 }
 
-// [DEAR IMGUI]
-// Functions must be implemented for UTF8 support
-// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
-// There is not necessarily a '[DEAR IMGUI]' at the usage sites.
-#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX
-#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx) (idx - 1)
+// [DEAR IMGUI] Extracted this function so we can more easily add support for word-wrapping.
+#ifndef STB_TEXTEDIT_MOVELINESTART
+static int stb_textedit_move_line_start(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor)
+{
+   if (state->single_line)
+      return 0;
+   while (cursor > 0) {
+      int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, cursor);
+      if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE)
+         break;
+      cursor = prev;
+   }
+   return cursor;
+}
+#define STB_TEXTEDIT_MOVELINESTART stb_textedit_move_line_start
 #endif
-#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX
-#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) (idx + 1)
+#ifndef STB_TEXTEDIT_MOVELINEEND
+static int stb_textedit_move_line_end(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor)
+{
+   int n = STB_TEXTEDIT_STRINGLEN(str);
+   if (state->single_line)
+      return n;
+   while (cursor < n && STB_TEXTEDIT_GETCHAR(str, cursor) != STB_TEXTEDIT_NEWLINE)
+      cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, cursor);
+   return cursor;
+}
+#define STB_TEXTEDIT_MOVELINEEND stb_textedit_move_line_end
 #endif
 
 #ifdef STB_TEXTEDIT_IS_SPACE
@@ -668,9 +718,9 @@
 #ifndef STB_TEXTEDIT_MOVEWORDLEFT
 static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c )
 {
-   --c; // always move at least one character
-   while( c >= 0 && !is_word_boundary( str, c ) )
-      --c;
+   c = IMSTB_TEXTEDIT_GETPREVCHARINDEX( str, c ); // always move at least one character
+   while (c >= 0 && !is_word_boundary(str, c))
+      c = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, c);
 
    if( c < 0 )
       c = 0;
@@ -684,9 +734,9 @@
 static int stb_textedit_move_to_word_next( IMSTB_TEXTEDIT_STRING *str, int c )
 {
    const int len = STB_TEXTEDIT_STRINGLEN(str);
-   ++c; // always move at least one character
+   c = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, c); // always move at least one character
    while( c < len && !is_word_boundary( str, c ) )
-      ++c;
+      c = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, c);
 
    if( c > len )
       c = len;
@@ -739,6 +789,7 @@
 #define STB_TEXTEDIT_KEYTYPE int
 #endif
 
+// API key: process text input
 // [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility.
 static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
 {
@@ -753,8 +804,7 @@
          state->cursor += text_len;
          state->has_preferred_x = 0;
       }
-   }
-   else {
+   } else {
       stb_textedit_delete_selection(str, state); // implicitly clamps
       if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) {
          stb_text_makeundo_insert(state, state->cursor, text_len);
@@ -771,6 +821,7 @@
    switch (key) {
       default: {
 #ifdef STB_TEXTEDIT_KEYTOTEXT
+         // This is not suitable for UTF-8 support.
          int c = STB_TEXTEDIT_KEYTOTEXT(key);
          if (c > 0) {
             IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE)c;
@@ -911,15 +962,16 @@
 
             // [DEAR IMGUI]
             // going down while being on the last line shouldn't bring us to that line end
-            if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE)
-               break;
+            //if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE)
+            //   break;
 
             // now find character position down a row
             state->cursor = start;
             STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
             x = row.x0;
-            for (i=0; i < row.num_chars; ++i) {
+            for (i=0; i < row.num_chars; ) {
                float dx = STB_TEXTEDIT_GETWIDTH(str, start, i);
+               int next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
                #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE
                if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE)
                   break;
@@ -927,10 +979,13 @@
                x += dx;
                if (x > goal_x)
                   break;
-               state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
+               i += next - state->cursor;
+               state->cursor = next;
             }
             stb_textedit_clamp(str, state);
 
+            if (state->cursor == find.first_char + find.length)
+               str->LastMoveDirectionLR = ImGuiDir_Left;
             state->has_preferred_x = 1;
             state->preferred_x = goal_x;
 
@@ -980,8 +1035,9 @@
             state->cursor = find.prev_first;
             STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
             x = row.x0;
-            for (i=0; i < row.num_chars; ++i) {
+            for (i=0; i < row.num_chars; ) {
                float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i);
+               int next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
                #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE
                if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE)
                   break;
@@ -989,10 +1045,15 @@
                x += dx;
                if (x > goal_x)
                   break;
-               state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
+               i += next - state->cursor;
+               state->cursor = next;
             }
             stb_textedit_clamp(str, state);
 
+            if (state->cursor == find.first_char)
+               str->LastMoveDirectionLR = ImGuiDir_Right;
+            else if (state->cursor == find.prev_first)
+               str->LastMoveDirectionLR = ImGuiDir_Left;
             state->has_preferred_x = 1;
             state->preferred_x = goal_x;
 
@@ -1002,10 +1063,15 @@
             // go to previous line
             // (we need to scan previous line the hard way. maybe we could expose this as a new API function?)
             prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0;
-            while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE)
-               --prev_scan;
+            while (prev_scan > 0)
+            {
+               int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, prev_scan);
+               if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE)
+                  break;
+               prev_scan = prev;
+            }
             find.first_char = find.prev_first;
-            find.prev_first = prev_scan;
+            find.prev_first = STB_TEXTEDIT_MOVELINESTART(str, state, prev_scan);
          }
          break;
       }
@@ -1079,10 +1145,7 @@
       case STB_TEXTEDIT_K_LINESTART:
          stb_textedit_clamp(str, state);
          stb_textedit_move_to_first(state);
-         if (state->single_line)
-            state->cursor = 0;
-         else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
-            --state->cursor;
+         state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor);
          state->has_preferred_x = 0;
          break;
 
@@ -1090,13 +1153,9 @@
       case STB_TEXTEDIT_K_LINEEND2:
 #endif
       case STB_TEXTEDIT_K_LINEEND: {
-         int n = STB_TEXTEDIT_STRINGLEN(str);
          stb_textedit_clamp(str, state);
-         stb_textedit_move_to_first(state);
-         if (state->single_line)
-             state->cursor = n;
-         else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
-             ++state->cursor;
+         stb_textedit_move_to_last(str, state);
+         state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor);
          state->has_preferred_x = 0;
          break;
       }
@@ -1107,10 +1166,7 @@
       case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT:
          stb_textedit_clamp(str, state);
          stb_textedit_prep_selection_at_cursor(state);
-         if (state->single_line)
-            state->cursor = 0;
-         else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
-            --state->cursor;
+         state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor);
          state->select_end = state->cursor;
          state->has_preferred_x = 0;
          break;
@@ -1119,13 +1175,9 @@
       case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT:
 #endif
       case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: {
-         int n = STB_TEXTEDIT_STRINGLEN(str);
          stb_textedit_clamp(str, state);
          stb_textedit_prep_selection_at_cursor(state);
-         if (state->single_line)
-             state->cursor = n;
-         else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
-            ++state->cursor;
+         state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor);
          state->select_end = state->cursor;
          state->has_preferred_x = 0;
          break;
diff -Nru f3d-3.2.0+dfsg/external/imgui/imstb_truetype.h f3d-3.4.1/external/imgui/imstb_truetype.h
--- f3d-3.2.0+dfsg/external/imgui/imstb_truetype.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/imgui/imstb_truetype.h	2026-01-08 08:41:55.000000000 +0200
@@ -4516,8 +4516,8 @@
             q2[0] = (float)x2;
             q2[1] = (float)y2;
             if (equal(q0,q1) || equal(q1,q2)) {
-               x0 = (int)verts[i-1].x;
-               y0 = (int)verts[i-1].y;
+               x0 = (int)verts[i-1].x; //-V1048
+               y0 = (int)verts[i-1].y; //-V1048
                x1 = (int)verts[i  ].x;
                y1 = (int)verts[i  ].y;
                if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) {
diff -Nru f3d-3.2.0+dfsg/external/tinyfiledialogs/tinyfiledialogs.cxx f3d-3.4.1/external/tinyfiledialogs/tinyfiledialogs.cxx
--- f3d-3.2.0+dfsg/external/tinyfiledialogs/tinyfiledialogs.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/tinyfiledialogs/tinyfiledialogs.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,13 +1,17 @@
 /* SPDX-License-Identifier: Zlib
-Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
 	 ________________________________________________________________
 	|                                                                |
 	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
+	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
+	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
+	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
+	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
 	|________________________________________________________________|
 
 ********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
   _________
- /         \ tinyfiledialogs.c v3.19.1 [Jan 27, 2025] zlib licence
+ /         \ tinyfiledialogs.c v3.21.2 [Oct 25, 2025] zlib licence
  |tiny file| Unique code file created [November 9, 2014]
  | dialogs |
  \____  ___/ http://tinyfiledialogs.sourceforge.net
@@ -58,7 +62,10 @@
 #ifndef _GNU_SOURCE
  #define _GNU_SOURCE /* used only to resolve symbolic links. Can be commented out */
  #ifndef _POSIX_C_SOURCE
-  #ifdef __FreeBSD__
+  #ifdef __NetBSD__
+    #define _POSIX_C_SOURCE 200112L
+    #define _XOPEN_SOURCE 600 /* https://pubs.opengroup.org/onlinepubs/007904875/functions/xsh_chap02_02.html */
+  #elif defined(__FreeBSD__)
     #define _POSIX_C_SOURCE 199506L /* 199506L is enough for freebsd for realpath() */
   #elif defined(__illumos__) || defined(__solaris__)
     #define _POSIX_C_SOURCE 200112L /* illumos/solaris needs 200112L for realpath() */
@@ -108,7 +115,7 @@
 #endif
 #define LOW_MULTIPLE_FILES 32
 
-char tinyfd_version[8] = "3.19.1";
+char tinyfd_version[8] = "3.21.2";
 
 /******************************************************************************************************/
 /**************************************** UTF-8 on Windows ********************************************/
@@ -148,7 +155,7 @@
 possible values for tinyfd_response are (all lowercase)
 for graphic mode:
   windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
-  shellementary qarma python2-tkinter python3-tkinter python-dbus
+  shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus
   perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
 for console mode:
   dialog whiptail basicinput no_solution */
@@ -178,7 +185,7 @@
       \\|\
 \ntiny file dialogs on UNIX needs:\
 \n   applescript or kdialog or yad or Xdialog\
-\nor zenity (or matedialog or shellementary or qarma)\
+\nor zenity (or matedialog, shellementary, qarma, shanty, boxer)\
 \nor python (2 or 3) + tkinter + python-dbus (optional)\
 \nor dialog (opens console if needed) ** Disabled by default **\
 \nor xterm + bash (opens console for basic input)\
@@ -196,7 +203,7 @@
 
 static int getenvDISPLAY(void)
 {
-		/* return tinyfd_assumeGraphicDisplay || getenv("DISPLAY") || getenv("WAYLAND_DISPLAY") ; */
+	/* return tinyfd_assumeGraphicDisplay || getenv("DISPLAY") || getenv("WAYLAND_DISPLAY") ; */
 	static int lReturnValue = -1 ;
 
 	if ( lReturnValue < 0 )
@@ -317,88 +324,88 @@
 
 static void RGB2Hex( unsigned char const aRGB[3], char aoResultHexRGB[8] )
 {
-		if ( aoResultHexRGB )
+	if ( aoResultHexRGB )
+	{
+		if ( aRGB )
 		{
-				if ( aRGB )
-				{
 #if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
-	sprintf(aoResultHexRGB, "#%02hhx%02hhx%02hhx", aRGB[0], aRGB[1], aRGB[2]);
+			sprintf(aoResultHexRGB, "#%02hhx%02hhx%02hhx", aRGB[0], aRGB[1], aRGB[2]);
 #else
-	sprintf(aoResultHexRGB, "#%02hx%02hx%02hx", aRGB[0], aRGB[1], aRGB[2]);
+			sprintf(aoResultHexRGB, "#%02hx%02hx%02hx", aRGB[0], aRGB[1], aRGB[2]);
 #endif
-						 /*printf("aoResultHexRGB %s\n", aoResultHexRGB);*/
-				}
-				else
-				{
-						aoResultHexRGB[0]=0;
-						aoResultHexRGB[1]=0;
-						aoResultHexRGB[2]=0;
-				}
+		 /*printf("aoResultHexRGB %s\n", aoResultHexRGB);*/
 		}
+		else
+		{
+			aoResultHexRGB[0]=0;
+			aoResultHexRGB[1]=0;
+			aoResultHexRGB[2]=0;
+		}
+	}
 }
 
 
 void tfd_replaceSubStr( char const * aSource, char const * aOldSubStr,
 						char const * aNewSubStr, char * aoDestination )
 {
-		char const * pOccurence ;
-		char const * p ;
-		char const * lNewSubStr = "" ;
-		size_t lOldSubLen = strlen( aOldSubStr ) ;
+	char const * pOccurence ;
+	char const * p ;
+	char const * lNewSubStr = "" ;
+	size_t lOldSubLen = strlen( aOldSubStr ) ;
 
-		if ( ! aSource )
-		{
-				* aoDestination = '\0' ;
-				return ;
-		}
-		if ( ! aOldSubStr )
-		{
-				strcpy( aoDestination , aSource ) ;
-				return ;
-		}
-		if ( aNewSubStr )
-		{
-				lNewSubStr = aNewSubStr ;
-		}
-		p = aSource ;
+	if ( ! aSource )
+	{
 		* aoDestination = '\0' ;
-		while ( ( pOccurence = strstr( p , aOldSubStr ) ) != NULL )
-		{
-				strncat( aoDestination , p , pOccurence - p ) ;
-				strcat( aoDestination , lNewSubStr ) ;
-				p = pOccurence + lOldSubLen ;
-		}
-		strcat( aoDestination , p ) ;
+		return ;
+	}
+	if ( ! aOldSubStr )
+	{
+		strcpy( aoDestination , aSource ) ;
+		return ;
+	}
+	if ( aNewSubStr )
+	{
+		lNewSubStr = aNewSubStr ;
+	}
+	p = aSource ;
+	* aoDestination = '\0' ;
+	while ( ( pOccurence = strstr( p , aOldSubStr ) ) != NULL )
+	{
+		strncat( aoDestination , p , pOccurence - p ) ;
+		strcat( aoDestination , lNewSubStr ) ;
+		p = pOccurence + lOldSubLen ;
+	}
+	strcat( aoDestination , p ) ;
 }
 
 
 static int filenameValid( char const * aFileNameWithoutPath )
 {
-		if ( ! aFileNameWithoutPath
-		  || ! strlen(aFileNameWithoutPath)
-		  || strpbrk(aFileNameWithoutPath , "\\/:*?\"<>|") )
-		{
-				return 0 ;
-		}
-		return 1 ;
+	if ( ! aFileNameWithoutPath
+	  || ! strlen(aFileNameWithoutPath)
+	  || strpbrk(aFileNameWithoutPath , "\\/:*?\"<>|") )
+	{
+		return 0 ;
+	}
+	return 1 ;
 }
 
 #ifndef _WIN32
 
 static int fileExists( char const * aFilePathAndName )
 {
-		FILE * lIn ;
-		if ( ! aFilePathAndName || ! strlen(aFilePathAndName) )
-		{
-				return 0 ;
-		}
-		lIn = fopen( aFilePathAndName , "r" ) ;
-		if ( ! lIn )
-		{
-				return 0 ;
-		}
-		fclose( lIn ) ;
-		return 1 ;
+	FILE * lIn ;
+	if ( ! aFilePathAndName || ! strlen(aFilePathAndName) )
+	{
+			return 0 ;
+	}
+	lIn = ( FILE * ) fopen( aFilePathAndName , "r" ) ;
+	if ( ! lIn )
+	{
+			return 0 ;
+	}
+	fclose( lIn ) ;
+	return 1 ;
 }
 
 #endif
@@ -406,21 +413,22 @@
 
 static void wipefile(char const * aFilename)
 {
-		int i;
-		struct stat st;
-		FILE * lIn;
+	int i;
+	struct stat st;
+	FILE * lIn;
 
-		if (stat(aFilename, &st) == 0)
+	if (stat(aFilename, &st) == 0)
+	{
+		lIn = ( FILE * ) fopen(aFilename, "w") ;
+		if ( lIn )
 		{
-				if ((lIn = fopen(aFilename, "w")))
-				{
-						for (i = 0; i < st.st_size; i++)
-						{
-								fputc('A', lIn);
-						}
-						fclose(lIn);
-				}
+			for (i = 0; i < st.st_size; i++)
+			{
+					fputc('A', lIn);
+			}
+			fclose(lIn);
 		}
+	}
 }
 
 
@@ -446,12 +454,12 @@
 		return 1;
 	}
 
-	p = aString;
+/*	p = aString;
 	while ((p = strchr(p, '$')))
 	{
 		p ++ ;
 		if ( ( * p == '(' ) || ( * p == '_' ) || isalpha( * p) ) return 1 ;
-	}
+	}*/
 
 	return 0;
 }
@@ -459,41 +467,41 @@
 
 char const * tinyfd_getGlobalChar(char const * aCharVariableName) /* to be called from C# (you don't need this in C or C++) */
 {
-		if (!aCharVariableName || !strlen(aCharVariableName)) return NULL;
-		else if (!strcmp(aCharVariableName, "tinyfd_version")) return tinyfd_version;
-		else if (!strcmp(aCharVariableName, "tinyfd_needs")) return tinyfd_needs;
-		else if (!strcmp(aCharVariableName, "tinyfd_response")) return tinyfd_response;
-		else return NULL ;
+	if (!aCharVariableName || !strlen(aCharVariableName)) return NULL;
+	else if (!strcmp(aCharVariableName, "tinyfd_version")) return tinyfd_version;
+	else if (!strcmp(aCharVariableName, "tinyfd_needs")) return tinyfd_needs;
+	else if (!strcmp(aCharVariableName, "tinyfd_response")) return tinyfd_response;
+	else return NULL ;
 }
 
 
 int tinyfd_getGlobalInt(char const * aIntVariableName) /* to be called from C# (you don't need this in C or C++) */
 {
-		if ( !aIntVariableName || !strlen(aIntVariableName) ) return -1 ;
-		else if ( !strcmp(aIntVariableName, "tinyfd_verbose") ) return tinyfd_verbose ;
-		else if ( !strcmp(aIntVariableName, "tinyfd_silent") ) return tinyfd_silent ;
-		else if ( !strcmp(aIntVariableName, "tinyfd_allowCursesDialogs") ) return tinyfd_allowCursesDialogs ;
-		else if ( !strcmp(aIntVariableName, "tinyfd_forceConsole") ) return tinyfd_forceConsole ;
-		/* else if ( !strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay") ) return tinyfd_assumeGraphicDisplay ; */
+	if ( !aIntVariableName || !strlen(aIntVariableName) ) return -1 ;
+	else if ( !strcmp(aIntVariableName, "tinyfd_verbose") ) return tinyfd_verbose ;
+	else if ( !strcmp(aIntVariableName, "tinyfd_silent") ) return tinyfd_silent ;
+	else if ( !strcmp(aIntVariableName, "tinyfd_allowCursesDialogs") ) return tinyfd_allowCursesDialogs ;
+	else if ( !strcmp(aIntVariableName, "tinyfd_forceConsole") ) return tinyfd_forceConsole ;
+	/* else if ( !strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay") ) return tinyfd_assumeGraphicDisplay ; */
 #ifdef _WIN32
-		else if ( !strcmp(aIntVariableName, "tinyfd_winUtf8") ) return tinyfd_winUtf8 ;
+	else if ( !strcmp(aIntVariableName, "tinyfd_winUtf8") ) return tinyfd_winUtf8 ;
 #endif
-		else return -1;
+	else return -1;
 }
 
 
 int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue) /* to be called from C# (you don't need this in C or C++) */
 {
-		if (!aIntVariableName || !strlen(aIntVariableName)) return -1 ;
-		else if (!strcmp(aIntVariableName, "tinyfd_verbose")) { tinyfd_verbose = aValue; return tinyfd_verbose; }
-		else if (!strcmp(aIntVariableName, "tinyfd_silent")) { tinyfd_silent = aValue; return tinyfd_silent; }
-		else if (!strcmp(aIntVariableName, "tinyfd_allowCursesDialogs")) { tinyfd_allowCursesDialogs = aValue; return tinyfd_allowCursesDialogs; }
-		else if (!strcmp(aIntVariableName, "tinyfd_forceConsole")) { tinyfd_forceConsole = aValue; return tinyfd_forceConsole; }
-		/* else if (!strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay")) { tinyfd_assumeGraphicDisplay = aValue; return tinyfd_assumeGraphicDisplay; } */
+	if (!aIntVariableName || !strlen(aIntVariableName)) return -1 ;
+	else if (!strcmp(aIntVariableName, "tinyfd_verbose")) { tinyfd_verbose = aValue; return tinyfd_verbose; }
+	else if (!strcmp(aIntVariableName, "tinyfd_silent")) { tinyfd_silent = aValue; return tinyfd_silent; }
+	else if (!strcmp(aIntVariableName, "tinyfd_allowCursesDialogs")) { tinyfd_allowCursesDialogs = aValue; return tinyfd_allowCursesDialogs; }
+	else if (!strcmp(aIntVariableName, "tinyfd_forceConsole")) { tinyfd_forceConsole = aValue; return tinyfd_forceConsole; }
+	/* else if (!strcmp(aIntVariableName, "tinyfd_assumeGraphicDisplay")) { tinyfd_assumeGraphicDisplay = aValue; return tinyfd_assumeGraphicDisplay; } */
 #ifdef _WIN32
-		else if (!strcmp(aIntVariableName, "tinyfd_winUtf8")) { tinyfd_winUtf8 = aValue; return tinyfd_winUtf8; }
+	else if (!strcmp(aIntVariableName, "tinyfd_winUtf8")) { tinyfd_winUtf8 = aValue; return tinyfd_winUtf8; }
 #endif
-		else return -1;
+	else return -1;
 }
 
 
@@ -508,7 +516,8 @@
 
 	if (lPowershellPresent < 0)
 	{
-		if (!(lIn = _popen("where powershell.exe", "r")))
+		lIn = ( FILE * ) _popen("where powershell.exe", "r") ;
+		if (! lIn )
 		{
 			lPowershellPresent = 0;
 			return 0;
@@ -535,7 +544,7 @@
 
 static int windowsVersion(void)
 {
-#if !defined(__MINGW32__) || defined(__MINGW64_VERSION_MAJOR)
+#if ( !defined(__MINGW32__) || defined(__MINGW64_VERSION_MAJOR) ) && !defined(__DMC__)
 	typedef LONG NTSTATUS  ;
 	typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
 	HMODULE hMod;
@@ -562,18 +571,18 @@
 
 static void replaceChr(char * aString, char aOldChr, char aNewChr)
 {
-		char * p;
+	char * p;
 
-		if (!aString) return;
-		if (aOldChr == aNewChr) return;
+	if (!aString) return;
+	if (aOldChr == aNewChr) return;
 
-		p = aString;
-		while ((p = strchr(p, aOldChr)))
-		{
-				*p = aNewChr;
-				p++;
-		}
-		return;
+	p = aString;
+	while ((p = strchr(p, aOldChr)))
+	{
+		*p = aNewChr;
+		p++;
+	}
+	return;
 }
 
 
@@ -584,31 +593,31 @@
 
 static int sizeUtf16From8(char const * aUtf8string)
 {
-		return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
-				aUtf8string, -1, NULL, 0);
+	return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+			aUtf8string, -1, NULL, 0);
 }
 
 
 static int sizeUtf16FromMbcs(char const * aMbcsString)
 {
-		return MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS,
-				aMbcsString, -1, NULL, 0);
+	return MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS,
+			aMbcsString, -1, NULL, 0);
 }
 
 
 static int sizeUtf8(wchar_t const * aUtf16string)
 {
-		return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
-				aUtf16string, -1, NULL, 0, NULL, NULL);
+	return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
+			aUtf16string, -1, NULL, 0, NULL, NULL);
 }
 
 
 static int sizeMbcs(wchar_t const * aMbcsString)
 {
-		int lRes = WideCharToMultiByte(CP_ACP, 0,
-				aMbcsString, -1, NULL, 0, NULL, NULL);
-		/* DWORD licic = GetLastError(); */
-		return lRes;
+	int lRes = WideCharToMultiByte(CP_ACP, 0,
+			aMbcsString, -1, NULL, 0, NULL, NULL);
+	/* DWORD licic = GetLastError(); */
+	return lRes;
 }
 
 
@@ -632,12 +641,12 @@
 
 wchar_t * tinyfd_utf8to16(char const * aUtf8string)
 {
-		static wchar_t * lUtf16string = NULL;
-		int lSize;
+	static wchar_t * lUtf16string = NULL;
+	int lSize;
 
-		free(lUtf16string);
-		if (!aUtf8string) {lUtf16string = NULL; return NULL;}
-		lSize = sizeUtf16From8(aUtf8string);
+	free(lUtf16string);
+	if (!aUtf8string) {lUtf16string = NULL; return NULL;}
+	lSize = sizeUtf16From8(aUtf8string);
 	if (lSize)
 	{
 		lUtf16string = (wchar_t*) malloc(lSize * sizeof(wchar_t));
@@ -656,12 +665,12 @@
 
 char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string)
 {
-		static char * lMbcsString = NULL;
-		int lSize;
+	static char * lMbcsString = NULL;
+	int lSize;
 
-		free(lMbcsString);
-		if (!aUtf16string) { lMbcsString = NULL; return NULL; }
-		lSize = sizeMbcs(aUtf16string);
+	free(lMbcsString);
+	if (!aUtf16string) { lMbcsString = NULL; return NULL; }
+	lSize = sizeMbcs(aUtf16string);
 	if (lSize)
 	{
 		lMbcsString = (char*) malloc(lSize);
@@ -674,35 +683,35 @@
 
 char * tinyfd_utf8toMbcs(char const * aUtf8string)
 {
-		wchar_t const * lUtf16string;
-		lUtf16string = tinyfd_utf8to16(aUtf8string);
-		return tinyfd_utf16toMbcs(lUtf16string);
+	wchar_t const * lUtf16string;
+	lUtf16string = tinyfd_utf8to16(aUtf8string);
+	return tinyfd_utf16toMbcs(lUtf16string);
 }
 
 
 char * tinyfd_utf16to8(wchar_t const * aUtf16string)
 {
-		static char * lUtf8string = NULL;
-		int lSize;
+	static char * lUtf8string = NULL;
+	int lSize;
 
-		free(lUtf8string);
-		if (!aUtf16string) { lUtf8string = NULL; return NULL; }
-		lSize = sizeUtf8(aUtf16string);
+	free(lUtf8string);
+	if (!aUtf16string) { lUtf8string = NULL; return NULL; }
+	lSize = sizeUtf8(aUtf16string);
 	if (lSize)
 	{
 		lUtf8string = (char*) malloc(lSize);
 		lSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, aUtf16string, -1, lUtf8string, lSize, NULL, NULL);
 	}
 	else strcpy(lUtf8string, "");
-		return lUtf8string;
+	return lUtf8string;
 }
 
 
 char * tinyfd_mbcsTo8(char const * aMbcsString)
 {
-		wchar_t const * lUtf16string;
-		lUtf16string = tinyfd_mbcsTo16(aMbcsString);
-		return tinyfd_utf16to8(lUtf16string);
+	wchar_t const * lUtf16string;
+	lUtf16string = tinyfd_mbcsTo16(aMbcsString);
+	return tinyfd_utf16to8(lUtf16string);
 }
 
 
@@ -715,25 +724,26 @@
 
 static void wipefileW(wchar_t const * aFilename)
 {
-		int i;
-		FILE * lIn;
-#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
-		struct _stat st;
-		if (_wstat(aFilename, &st) == 0)
+	int i;
+	FILE * lIn;
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
+	struct _stat st;
+	if (_wstat(aFilename, &st) == 0)
 #else
-		struct __stat64 st;
-		if (_wstat64(aFilename, &st) == 0)
+	struct __stat64 st;
+	if (_wstat64(aFilename, &st) == 0)
 #endif
+	{
+		lIn = ( FILE * ) _wfopen(aFilename, L"w");
+		if ( lIn )
 		{
-				if ((lIn = _wfopen(aFilename, L"w")))
-				{
-						for (i = 0; i < st.st_size; i++)
-						{
-								fputc('A', lIn);
-						}
-						fclose(lIn);
-				}
+			for (i = 0; i < st.st_size; i++)
+			{
+				fputc('A', lIn);
+			}
+			fclose(lIn);
 		}
+	}
 }
 
 
@@ -741,29 +751,29 @@
 		wchar_t * aoDestination, /* make sure it is allocated, use _MAX_PATH */
 		wchar_t const * aSource) /* aoDestination and aSource can be the same */
 {
-		wchar_t const * lTmp;
-		if (aSource)
+	wchar_t const * lTmp;
+	if (aSource)
+	{
+		lTmp = wcsrchr(aSource, L'/');
+		if (!lTmp)
 		{
-				lTmp = wcsrchr(aSource, L'/');
-				if (!lTmp)
-				{
-						lTmp = wcsrchr(aSource, L'\\');
-				}
-				if (lTmp)
-				{
-						wcsncpy(aoDestination, aSource, lTmp - aSource);
-						aoDestination[lTmp - aSource] = L'\0';
-				}
-				else
-				{
-						*aoDestination = L'\0';
-				}
+			lTmp = wcsrchr(aSource, L'\\');
+		}
+		if (lTmp)
+		{
+			wcsncpy(aoDestination, aSource, lTmp - aSource);
+			aoDestination[lTmp - aSource] = L'\0';
 		}
 		else
 		{
-				*aoDestination = L'\0';
+			*aoDestination = L'\0';
 		}
-		return aoDestination;
+	}
+	else
+	{
+		*aoDestination = L'\0';
+	}
+	return aoDestination;
 }
 
 
@@ -825,36 +835,35 @@
 static void RGB2HexW( unsigned char const aRGB[3], wchar_t aoResultHexRGB[8])
 {
 #if (defined(__cplusplus ) && __cplusplus >= 201103L) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__clang__)
-		wchar_t const * const lPrintFormat = L"#%02hhx%02hhx%02hhx";
+	wchar_t const * const lPrintFormat = L"#%02hhx%02hhx%02hhx";
 #else
-		wchar_t const * const lPrintFormat = L"#%02hx%02hx%02hx";
+	wchar_t const * const lPrintFormat = L"#%02hx%02hx%02hx";
 #endif
 
-		if (aoResultHexRGB)
+	if (aoResultHexRGB)
+	{
+		if (aRGB)
 		{
-				if (aRGB)
-				{
-						/* wprintf(L"aoResultHexRGB %s\n", aoResultHexRGB); */
+			/* wprintf(L"aoResultHexRGB %s\n", aoResultHexRGB); */
 #if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
-										swprintf(aoResultHexRGB, 8, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
+			swprintf(aoResultHexRGB, 8, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
 #else
-										swprintf(aoResultHexRGB, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
+			swprintf(aoResultHexRGB, lPrintFormat, aRGB[0], aRGB[1], aRGB[2]);
 #endif
-
-				}
-				else
-				{
-						aoResultHexRGB[0] = 0;
-						aoResultHexRGB[1] = 0;
-						aoResultHexRGB[2] = 0;
-				}
 		}
+		else
+		{
+			aoResultHexRGB[0] = 0;
+			aoResultHexRGB[1] = 0;
+			aoResultHexRGB[2] = 0;
+		}
+	}
 }
 
 
 static int dirExists(char const * aDirPath)
 {
-#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
 	struct _stat lInfo;
 #else
 	struct __stat64 lInfo;
@@ -901,7 +910,7 @@
 
 static int fileExists(char const * aFilePathAndName)
 {
-#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__)
+#if (defined(__MINGW32_MAJOR_VERSION) && !defined(__MINGW64__) && (__MINGW32_MAJOR_VERSION <= 3)) || defined(__BORLANDC__) || defined(__WATCOMC__) || defined(__DMC__)
 	struct _stat lInfo;
 #else
 	struct __stat64 lInfo;
@@ -933,7 +942,7 @@
 		}
 		else
 		{
-				lIn = fopen(aFilePathAndName, "r");
+				lIn = ( FILE * ) fopen(aFilePathAndName, "r");
 				if (!lIn)
 				{
 						return 0;
@@ -1278,7 +1287,7 @@
 #endif
 		L"%ls\\tinyfd.hta", _wgetenv(L"TEMP"));
 
-	lIn = _wfopen(lDialogString, L"w");
+	lIn = ( FILE * ) _wfopen(lDialogString, L"w");
 	if (!lIn)
 	{
 		free(lDialogString);
@@ -1403,7 +1412,7 @@
 #endif
 				L"%ls\\tinyfd.hta", _wgetenv(L"TEMP"));
 		}
-		lIn = _wfopen(lDialogString, L"w");
+		lIn = ( FILE * ) _wfopen(lDialogString, L"w");
 		if (!lIn)
 		{
 				free(lDialogString);
@@ -1567,20 +1576,21 @@
 #if !defined(__BORLANDC__) && !defined(__TINYC__) && !(defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR))
 				lDialogStringLen,
 #endif
-								L"%ls\\tinyfd.txt", _wgetenv(L"TEMP"));
-				/* wprintf(L"lDialogString: %ls\n", lDialogString); */
+				L"%ls\\tinyfd.txt", _wgetenv(L"TEMP"));
+		/* wprintf(L"lDialogString: %ls\n", lDialogString); */
 #ifdef TINYFD_NOCCSUNICODE
-				if (!(lIn = _wfopen(lDialogString, L"r")))
+		lIn = ( FILE * ) _wfopen(lDialogString, L"r") ;
 #else
-				if (!(lIn = _wfopen(lDialogString, L"rt, ccs=UNICODE"))) /*or ccs=UTF-16LE*/
+		lIn = ( FILE * ) _wfopen(lDialogString, L"rt, ccs=UNICODE") /*or ccs=UTF-16LE*/
 #endif
-				{
-				_wremove(lDialogString);
-				free(lDialogString);
-				return NULL;
+		if ( ! lIn )
+		{
+			_wremove(lDialogString);
+			free(lDialogString);
+			return NULL;
 		}
 
-				memset(lBuff, 0, MAX_PATH_OR_CMD * sizeof(wchar_t) );
+		memset(lBuff, 0, MAX_PATH_OR_CMD * sizeof(wchar_t) );
 
 #ifdef TINYFD_NOCCSUNICODE
 				fgets((char *)lBuff, 2*MAX_PATH_OR_CMD, lIn);
@@ -1962,7 +1972,9 @@
 		bInfo.lpszTitle = aTitle && wcslen(aTitle) ? aTitle : NULL;
 		if (lHResult == S_OK || lHResult == S_FALSE)
 		{
+#ifndef __DMC__
 				bInfo.ulFlags = BIF_USENEWUI;
+#endif
 		}
 		bInfo.lpfn = BrowseCallbackProcW;
 		bInfo.lParam = (LPARAM)aDefaultPath;
@@ -2423,7 +2435,7 @@
 	if (!tinyfd_allowCursesDialogs) return 0;
 	if (lDialogPresent < 0)
 	{
-		lIn = _popen("where dialog.exe", "r");
+		lIn = ( FILE * ) _popen("where dialog.exe", "r");
 		if ( ! lIn )
 		{
 				lDialogPresent = 0 ;
@@ -2535,7 +2547,8 @@
 		/*if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;*/
 		system( lDialogString ) ;
 
-		if (!(lIn = fopen(lDialogFile, "r")))
+		lIn = ( FILE * ) fopen(lDialogFile, "r");
+		if ( ! lIn )
 		{
 				remove(lDialogFile);
 				return 0 ;
@@ -2630,7 +2643,8 @@
 		/* printf( "lDialogString: %s\n" , lDialogString ) ; */
 		system( lDialogString ) ;
 
-		if (!(lIn = fopen(lDialogFile, "r")))
+		lIn = ( FILE * ) fopen(lDialogFile, "r");
+		if ( ! lIn )
 		{
 				remove(lDialogFile);
 								aoBuff[0] = '\0';
@@ -2706,7 +2720,8 @@
 		/* printf( "lDialogString: %s\n" , lDialogString ) ; */
 		system( lDialogString ) ;
 
-		if (!(lIn = fopen(lPathAndFile, "r")))
+		lIn = ( FILE * ) fopen(lPathAndFile, "r");
+		if ( ! lIn )
 		{
 				remove(lPathAndFile);
 				return NULL;
@@ -2771,7 +2786,8 @@
 		/* printf( "lDialogString: %s\n" , lDialogString ) ; */
 		system( lDialogString ) ;
 
-		if (!(lIn = fopen(lFilterPatterns, "r")))
+		lIn = ( FILE * ) fopen(lFilterPatterns, "r");
+		if ( ! lIn )
 		{
 				remove(lFilterPatterns);
 				return NULL;
@@ -2830,7 +2846,8 @@
 		/* printf( "lDialogString: %s\n" , lDialogString ) ; */
 		system( lDialogString ) ;
 
-		if (!(lIn = fopen(lString, "r")))
+		lIn = ( FILE * ) fopen(lString, "r") ;
+		if ( ! lIn )
 		{
 				remove(lString);
 				return NULL;
@@ -2982,19 +2999,17 @@
 		char const * aMessage , /* NULL or "" may contain \n \t */
 		char const * aIconType ) /* "info" "warning" "error" */
 {
-		if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
-		if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
+	if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
+	if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
 
-	if ( powershellPresent() && (!tinyfd_forceConsole || !(
-			GetConsoleWindow() ||
-			dialogPresent()))
-						&& (!getenv("SSH_CLIENT") || getenvDISPLAY()))
+	if ( powershellPresent()
+		&& ( !tinyfd_forceConsole || !(GetConsoleWindow() || dialogPresent()) )
+		&& (!getenv("SSH_CLIENT") || getenvDISPLAY()) )
 	{
-			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return 1;}
-			return notifyWinGui(aTitle, aMessage, aIconType);
+		if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"windows");return 1;}
+		return notifyWinGui(aTitle, aMessage, aIconType);
 	}
-	else
-			return tinyfd_messageBox(aTitle, aMessage, "ok" , aIconType, 0);
+	else return tinyfd_messageBox(aTitle, aMessage, "ok" , aIconType, 0);
 }
 
 
@@ -3328,16 +3343,16 @@
 		unsigned char const aDefaultRGB[3], /* { 0 , 255 , 255 } */
 		unsigned char aoResultRGB[3]) /* { 0 , 0 , 0 } */
 {
-		static char lDefaultHexRGB[16];
+	static char lDefaultHexRGB[16];
 	int i;
 	char * p ;
-		char * lPointerInputBox;
-		char lString[MAX_PATH_OR_CMD];
+	char * lPointerInputBox;
+	char lString[MAX_PATH_OR_CMD];
 
-		lDefaultHexRGB[0] = '\0';
+	lDefaultHexRGB[0] = '\0';
 
-		if (tfd_quoteDetected(aTitle)) return tinyfd_colorChooser("INVALID TITLE WITH QUOTES", aDefaultHexRGB, aDefaultRGB, aoResultRGB);
-		if (tfd_quoteDetected(aDefaultHexRGB)) return tinyfd_colorChooser(aTitle, "INVALID DEFAULT_HEX_RGB WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultRGB, aoResultRGB);
+	if (tfd_quoteDetected(aTitle)) return tinyfd_colorChooser("INVALID TITLE WITH QUOTES", aDefaultHexRGB, aDefaultRGB, aoResultRGB);
+	if (tfd_quoteDetected(aDefaultHexRGB)) return tinyfd_colorChooser(aTitle, "INVALID DEFAULT_HEX_RGB WITH QUOTES: use the GRAVE ACCENT \\x60 instead.", aDefaultRGB, aoResultRGB);
 
 	if ( (!tinyfd_forceConsole || !( GetConsoleWindow() || dialogPresent()) )
 				&& (!getenv("SSH_CLIENT") || getenvDISPLAY()))
@@ -3376,22 +3391,22 @@
 
 	if ( !p || (strlen(p) != 7) || (p[0] != '#') )
 	{
-			return NULL ;
+		return NULL ;
 	}
 	for ( i = 1 ; i < 7 ; i ++ )
 	{
-			if ( ! isxdigit( (int) p[i] ) )
-			{
-					return NULL ;
-			}
+		if ( ! isxdigit( (int) p[i] ) )
+		{
+				return NULL ;
+		}
 	}
 	Hex2RGB(p,aoResultRGB);
 
-		strcpy(lDefaultHexRGB, p);
+	strcpy(lDefaultHexRGB, p);
 
-		if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
+	if (lPointerInputBox) strcpy(lPointerInputBox, lString); /* restore its previous content to tinyfd_inputBox */
 
-		return lDefaultHexRGB;
+	return lDefaultHexRGB;
 }
 
 
@@ -3403,13 +3418,19 @@
 
 int tfd_isDarwin(void)
 {
-		static int lsIsDarwin = -1 ;
-		struct utsname lUtsname ;
-		if ( lsIsDarwin < 0 )
-		{
-				lsIsDarwin = !uname(&lUtsname) && !strcmp(lUtsname.sysname,"Darwin") ;
-		}
-		return lsIsDarwin ;
+	static int lIsDarwin = -1 ;
+	struct utsname lUtsname ;
+	if ( lIsDarwin < 0 ) lIsDarwin = !uname(&lUtsname) && !strcmp(lUtsname.sysname,"Darwin") ;
+	return lIsDarwin ;
+}
+
+
+int tfd_isHaiku(void)
+{
+	static int lIsHaiku = -1 ;
+	struct utsname lUtsname ;
+	if ( lIsHaiku < 0 ) lIsHaiku = !uname(&lUtsname) && !strcmp(lUtsname.sysname,"Haiku") ;
+	return lIsHaiku ;
 }
 
 
@@ -3441,7 +3462,7 @@
 
    strcat( lTestedString , aExecutable ) ;
    strcat( lTestedString, " 2>/dev/null ");
-   lIn = popen( lTestedString , "r" ) ;
+   lIn = ( FILE * ) popen( lTestedString , "r" ) ;
    if ( ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
 	&& ( ! strchr( lBuff , ':' ) ) && ( strncmp(lBuff, "no ", 3) ) )
    {   /* present */
@@ -3473,21 +3494,21 @@
 
 static char * getVersion( char const * aExecutable ) /*version must be first numeral*/
 {
-		static char lBuff[MAX_PATH_OR_CMD] ;
-		char lTestedString[MAX_PATH_OR_CMD] ;
-		FILE * lIn ;
-		char * lTmp ;
+	static char lBuff[MAX_PATH_OR_CMD] ;
+	char lTestedString[MAX_PATH_OR_CMD] ;
+	FILE * lIn ;
+	char * lTmp ;
 
 	strcpy( lTestedString , aExecutable ) ;
 	strcat( lTestedString , " --version" ) ;
 
-	lIn = popen( lTestedString , "r" ) ;
-		lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
-		pclose( lIn ) ;
+	lIn = ( FILE * ) popen( lTestedString , "r" ) ;
+	lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
+	pclose( lIn ) ;
 
-		lTmp += strcspn(lTmp,"0123456789");
-		 /* printf("lTmp:%s\n", lTmp); */
-		return lTmp ;
+	lTmp += strcspn(lTmp,"0123456789");
+	 /* printf("lTmp:%s\n", lTmp); */
+	return lTmp ;
 }
 
 
@@ -3511,20 +3532,20 @@
 
 static int tryCommand( char const * aCommand )
 {
-		char lBuff[MAX_PATH_OR_CMD] ;
-		FILE * lIn ;
+	char lBuff[MAX_PATH_OR_CMD] ;
+	FILE * lIn ;
 
-		lIn = popen( aCommand , "r" ) ;
-		if ( fgets( lBuff , sizeof( lBuff ) , lIn ) == NULL )
-		{       /* present */
-				pclose( lIn ) ;
-				return 1 ;
-		}
-		else
-		{
-				pclose( lIn ) ;
-				return 0 ;
-		}
+	lIn = ( FILE * ) popen( aCommand , "r" ) ;
+	if ( fgets( lBuff , sizeof( lBuff ) , lIn ) == NULL )
+	{       /* present */
+			pclose( lIn ) ;
+			return 1 ;
+	}
+	else
+	{
+			pclose( lIn ) ;
+			return 0 ;
+	}
 
 }
 
@@ -3773,9 +3794,22 @@
 
 static int graphicMode(void)
 {
-		return !( tinyfd_forceConsole && (isTerminalRunning() || terminalName()) )
-						&& ( getenvDISPLAY()
-						|| (tfd_isDarwin() && (!getenv("SSH_TTY") || getenvDISPLAY() ) ) ) ;
+	static int lGraphicMode = -1 ;
+	
+	if ( lGraphicMode < 0 )
+	{
+		if ( getenvDISPLAY() ) lGraphicMode = 1 ;
+		else if ( tfd_isDarwin() || tfd_isHaiku() )
+		{
+			if ( getenv("SSH_TTY") ) lGraphicMode = 0 ;
+			else lGraphicMode = 1 ;
+		}
+		else lGraphicMode = 0 ;
+	}
+	/*printf("lGraphicMode %d %d\n", lGraphicMode);*/
+	if ( tinyfd_forceConsole && ( isTerminalRunning() || terminalName() ) ) return 0 ;	
+	
+	return lGraphicMode ;
 }
 
 
@@ -3801,7 +3835,7 @@
 		lPactlPresent = detectPresence("pactl") ;
 		if ( lPactlPresent )
 		{
-			lIn = popen( "pactl info | grep -iF pulseaudio" , "r" ) ;
+			lIn = ( FILE * ) popen( "pactl info | grep -iF pulseaudio" , "r" ) ;
 			if ( ! (fgets( lBuff , sizeof( lBuff ) , lIn ) && ! strstr(lBuff, "PipeWire") ) )
 			{
 				lPactlPresent = 0 ;
@@ -3932,6 +3966,17 @@
 }
 
 
+static int notifyPresent(void)
+{
+	static int lNotifyPresent = -1 ;
+	if ( lNotifyPresent < 0 )
+	{
+		lNotifyPresent = detectPresence("notify") ;
+	}
+	return lNotifyPresent && graphicMode( ) ;
+}
+
+
 static int perlPresent(void)
 {
    static int lPerlPresent = -1 ;
@@ -3943,7 +3988,7 @@
 	  lPerlPresent = detectPresence("perl") ;
 	  if (lPerlPresent)
 	  {
-		 lIn = popen("perl -MNet::DBus -e \"Net::DBus->session->get_service('org.freedesktop.Notifications')\" 2>&1", "r");
+		 lIn = ( FILE * ) popen("perl -MNet::DBus -e \"Net::DBus->session->get_service('org.freedesktop.Notifications')\" 2>&1", "r");
 		 if (fgets(lBuff, sizeof(lBuff), lIn) == NULL)
 		 {
 			lPerlPresent = 2;
@@ -3967,7 +4012,7 @@
 				lAfplayPresent = detectPresence("afplay") ;
 				if ( lAfplayPresent )
 				{
-						lIn = popen( "test -e /System/Library/Sounds/Ping.aiff || echo Ping" , "r" ) ;
+						lIn = ( FILE * ) popen( "test -e /System/Library/Sounds/Ping.aiff || echo Ping" , "r" ) ;
 						if ( fgets( lBuff , sizeof( lBuff ) , lIn ) == NULL )
 						{
 								lAfplayPresent = 2 ;
@@ -4026,7 +4071,7 @@
 		lDunstifyPresent = detectPresence( "dunstify" ) ;
 		if ( lDunstifyPresent )
 		{
-			lIn = popen( "dunstify -s" , "r" ) ;
+			lIn = ( FILE * ) popen( "dunstify -s" , "r" ) ;
 			lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
 			pclose( lIn ) ;
 			/* printf("lTmp:%s\n", lTmp); */
@@ -4050,7 +4095,7 @@
 		lDunstPresent = detectPresence( "dunst" ) ;
 		if ( lDunstPresent )
 		{
-			lIn = popen( "ps -e | grep dunst | grep -v grep" , "r" ) ; /* add "| wc -l" to receive the number of lines */
+			lIn = ( FILE * ) popen( "ps -e | grep dunst | grep -v grep" , "r" ) ; /* add "| wc -l" to receive the number of lines */
 			lTmp = fgets( lBuff , sizeof( lBuff ) , lIn ) ;
 			pclose( lIn ) ;
 			/* if ( lTmp ) printf("lTmp:%s\n", lTmp); */
@@ -4063,36 +4108,60 @@
 }
 
 
+int tfd_shantyPresent(void)
+{
+	static int lShantyPresent = -1 ;
+	if ( lShantyPresent < 0 )
+	{
+		lShantyPresent = detectPresence("shanty") ;
+	}
+	/*printf("lShantyPresent %d\n", lShantyPresent);*/
+	return lShantyPresent && graphicMode( ) ;
+}
+
+
+int tfd_boxerPresent(void)
+{
+	static int lBoxerPresent = -1 ;
+	if ( lBoxerPresent < 0 )
+	{
+		lBoxerPresent = detectPresence("boxer") ;
+	}
+	/*printf("lBoxerPresent %d\n", lBoxerPresent);*/
+	return lBoxerPresent && graphicMode( ) ;
+}
+
+
 int tfd_qarmaPresent(void)
 {
-		static int lQarmaPresent = -1 ;
-		if ( lQarmaPresent < 0 )
-		{
-				lQarmaPresent = detectPresence("qarma") ;
-		}
-		return lQarmaPresent && graphicMode( ) ;
+	static int lQarmaPresent = -1 ;
+	if ( lQarmaPresent < 0 )
+	{
+		lQarmaPresent = detectPresence("qarma") ;
+	}
+	return lQarmaPresent && graphicMode( ) ;
 }
 
 
 int tfd_matedialogPresent(void)
 {
-		static int lMatedialogPresent = -1 ;
-		if ( lMatedialogPresent < 0 )
-		{
-				lMatedialogPresent = detectPresence("matedialog") ;
-		}
-		return lMatedialogPresent && graphicMode( ) ;
+	static int lMatedialogPresent = -1 ;
+	if ( lMatedialogPresent < 0 )
+	{
+		lMatedialogPresent = detectPresence("matedialog") ;
+	}
+	return lMatedialogPresent && graphicMode( ) ;
 }
 
 
 int tfd_shellementaryPresent(void)
 {
-		static int lShellementaryPresent = -1 ;
-		if ( lShellementaryPresent < 0 )
-		{
-				lShellementaryPresent = 0 ; /*detectPresence("shellementary"); shellementary is not ready yet */
-		}
-		return lShellementaryPresent && graphicMode( ) ;
+	static int lShellementaryPresent = -1 ;
+	if ( lShellementaryPresent < 0 )
+	{
+		lShellementaryPresent = 0 ; /*detectPresence("shellementary"); shellementary is not ready yet */
+	}
+	return lShellementaryPresent && graphicMode( ) ;
 }
 
 
@@ -4113,7 +4182,7 @@
 
 	if ( ! lXpropReady )
 	{	/* xwayland Debian issue reported by Kay F. Jahnke and solved with his help */
-		lIn = popen( "xprop -root 32x '	$0' _NET_ACTIVE_WINDOW" , "r" ) ;
+		lIn = ( FILE * ) popen( "xprop -root 32x '	$0' _NET_ACTIVE_WINDOW" , "r" ) ;
 		if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
 		{
 			if ( ! strstr( lBuff , "not found" ) )
@@ -4128,66 +4197,51 @@
 }
 
 
-int tfd_zenityPresent(void)
+int tfd_yadPresent(void)
 {
-		static int lZenityPresent = -1 ;
-		if ( lZenityPresent < 0 )
-		{
-				lZenityPresent = detectPresence("zenity") ;
-		}
-		return lZenityPresent && graphicMode( ) ;
+	static int lYadPresent = -1;
+	if (lYadPresent < 0)
+	{
+		lYadPresent = detectPresence("yad");
+	}
+	return lYadPresent && graphicMode();
 }
 
 
-int tfd_yadPresent(void)
+int tfd_zenityPresent(void)
 {
-   static int lYadPresent = -1;
-   if (lYadPresent < 0)
-   {
-	  lYadPresent = detectPresence("yad");
-   }
-   return lYadPresent && graphicMode();
+	static int lZenityPresent = -1 ;
+	if ( lZenityPresent < 0 )
+	{
+		lZenityPresent = detectPresence("zenity") ;
+	}
+	return lZenityPresent && graphicMode( ) ;
 }
 
 
-int tfd_zenity3Present(void)
+int tfd_zenityVersion(void)
 {
-		static int lZenity3Present = -1 ;
-		char lBuff[MAX_PATH_OR_CMD] ;
-		FILE * lIn ;
-				int lIntTmp ;
+	static int lZenityVersion = -1 ;
+	char lBuff[MAX_PATH_OR_CMD] ;
+	FILE * lIn ;
 
-		if ( lZenity3Present < 0 )
+	if ( lZenityVersion < 0 )
+	{
+		if ( tfd_zenityPresent() )
 		{
-				lZenity3Present = 0 ;
-				if ( tfd_zenityPresent() )
-				{
-						lIn = popen( "zenity --version" , "r" ) ;
-						if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
-						{
-								if ( atoi(lBuff) >= 3 )
-								{
-									lZenity3Present = 3 ;
-									lIntTmp = atoi(strtok(lBuff,".")+2 ) ;
-									if ( lIntTmp >= 18 )
-									{
-											lZenity3Present = 5 ;
-									}
-									else if ( lIntTmp >= 10 )
-									{
-											lZenity3Present = 4 ;
-									}
-																}
-								else if ( ( atoi(lBuff) == 2 ) && ( atoi(strtok(lBuff,".")+2 ) >= 32 ) )
-								{
-										lZenity3Present = 2 ;
-								}
-								if (tinyfd_verbose) printf("zenity type %d\n", lZenity3Present);
-						}
-						pclose( lIn ) ;
-				}
+			lIn = ( FILE * ) popen( "zenity --version" , "r" ) ;
+			if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
+			{
+				lZenityVersion = 1000 * atoi( lBuff ) ;
+				lZenityVersion += atoi( strtok( lBuff, "." ) + 2 ) ;
+				/* 3018 , 3010 , 3000, 2032 was 5 , 4 , 3, 2 */ 
+			}
+			pclose( lIn ) ;
 		}
-		return graphicMode() ? lZenity3Present : 0 ;
+		else lZenityVersion = 0 ;
+		if (tinyfd_verbose) printf("lZenityVersion %d\n", lZenityVersion ) ;	
+	}
+	return graphicMode() ? lZenityVersion : 0 ;
 }
 
 
@@ -4213,7 +4267,7 @@
         lKdialogPresent = detectPresence("kdialog") ;
         if ( lKdialogPresent && !getenv("SSH_TTY") )
         {
-            lIn = popen( "kdialog --attach 2>&1" , "r" ) ;
+            lIn = ( FILE * ) popen( "kdialog --attach 2>&1" , "r" ) ;
             if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
             {
                 if ( ! strstr( "Unknown" , lBuff ) )
@@ -4227,7 +4281,7 @@
             if (lKdialogPresent == 2)
             {
                 lKdialogPresent = 1 ;
-                lIn = popen( "kdialog --passivepopup 2>&1" , "r" ) ;
+                lIn = ( FILE * ) popen( "kdialog --passivepopup 2>&1" , "r" ) ;
                 if ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
                 {
                     if ( ! strstr( "Unknown" , lBuff ) )
@@ -4254,7 +4308,7 @@
 		if ( lOsx9orBetter < 0 )
 		{
 				lOsx9orBetter = 0 ;
-				lIn = popen( "osascript -e 'set osver to system version of (system info)'" , "r" ) ;
+				lIn = ( FILE * ) popen( "osascript -e 'set osver to system version of (system info)'" , "r" ) ;
 				V = 0 ;
 				if ( ( fgets( lBuff , sizeof( lBuff ) , lIn ) != NULL )
 						&& ( 2 == sscanf(lBuff, "%d.%d", &V, &v) ) )
@@ -4381,8 +4435,8 @@
 
 static void sigHandler(int signum)
 {
-    FILE * lIn ;
-    if ( ( lIn = popen( "pactl unload-module module-sine" , "r" ) ) )
+    FILE * lIn = ( FILE * ) popen( "pactl unload-module module-sine" , "r" );
+    if ( lIn )
     {
         pclose( lIn ) ;
     }
@@ -4448,9 +4502,10 @@
 
     if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
 
-    if ( ( lIn = popen( lDialogString , "r" ) ) )
+	lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+    if ( lIn )
     {
-            pclose( lIn ) ;
+		pclose( lIn ) ;
     }
 
     if ( pactlPresent() )
@@ -4467,114 +4522,115 @@
 		char const * aIconType , /* "info" "warning" "error" "question" */
 		int aDefaultButton ) /* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
 {
-		char lBuff[MAX_PATH_OR_CMD] ;
-		char * lDialogString = NULL ;
-		char * lpDialogString;
-		FILE * lIn ;
-		int lWasGraphicDialog = 0 ;
-		int lWasXterm = 0 ;
-		int lResult ;
-		char lChar ;
-		struct termios infoOri;
-		struct termios info;
-		size_t lTitleLen ;
-		size_t lMessageLen ;
-
-		lBuff[0]='\0';
+	char lBuff[MAX_PATH_OR_CMD] ;
+	char lBuff2[MAX_PATH_OR_CMD] ;
+	char * lDialogString = NULL ;
+	char * lpDialogString;
+	FILE * lIn ;
+	int lWasGraphicDialog = 0 ;
+	int lWasXterm = 0 ;
+	int lResult ;
+	char lChar ;
+	struct termios infoOri;
+	struct termios info;
+	size_t lTitleLen ;
+	size_t lMessageLen ;
+
+	lBuff[0]='\0';
+
+	if (tfd_quoteDetected(aTitle)) return tinyfd_messageBox("INVALID TITLE WITH QUOTES", aMessage, aDialogType, aIconType, aDefaultButton);
+	if (tfd_quoteDetected(aMessage)) return tinyfd_messageBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDialogType, aIconType, aDefaultButton);
+
+	lTitleLen =  aTitle ? strlen(aTitle) : 0 ;
+	lMessageLen =  aMessage ? strlen(aMessage) : 0 ;
+	if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
+	{
+			lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+	}
 
-				if (tfd_quoteDetected(aTitle)) return tinyfd_messageBox("INVALID TITLE WITH QUOTES", aMessage, aDialogType, aIconType, aDefaultButton);
-				if (tfd_quoteDetected(aMessage)) return tinyfd_messageBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDialogType, aIconType, aDefaultButton);
+	if ( osascriptPresent( ) )
+	{
+		if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return 1;}
 
-		lTitleLen =  aTitle ? strlen(aTitle) : 0 ;
-		lMessageLen =  aMessage ? strlen(aMessage) : 0 ;
-		if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
+		strcpy( lDialogString , "osascript ");
+		if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+		strcat( lDialogString , " -e 'try' -e 'set {vButton} to {button returned} of ( display dialog \"") ;
+		if ( aMessage && strlen(aMessage) )
 		{
-				lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+				strcat(lDialogString, aMessage) ;
 		}
-
-		if ( osascriptPresent( ) )
+		strcat(lDialogString, "\" ") ;
+		if ( aTitle && strlen(aTitle) )
 		{
-				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return 1;}
-
-				strcpy( lDialogString , "osascript ");
-				if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
-				strcat( lDialogString , " -e 'try' -e 'set {vButton} to {button returned} of ( display dialog \"") ;
-				if ( aMessage && strlen(aMessage) )
-				{
-						strcat(lDialogString, aMessage) ;
-				}
+				strcat(lDialogString, "with title \"") ;
+				strcat(lDialogString, aTitle) ;
 				strcat(lDialogString, "\" ") ;
-				if ( aTitle && strlen(aTitle) )
-				{
-						strcat(lDialogString, "with title \"") ;
-						strcat(lDialogString, aTitle) ;
-						strcat(lDialogString, "\" ") ;
-				}
-				strcat(lDialogString, "with icon ") ;
-				if ( aIconType && ! strcmp( "error" , aIconType ) )
-				{
-						strcat(lDialogString, "stop " ) ;
-				}
-				else if ( aIconType && ! strcmp( "warning" , aIconType ) )
-				{
-						strcat(lDialogString, "caution " ) ;
-				}
-				else /* question or info */
-				{
-						strcat(lDialogString, "note " ) ;
-				}
-				if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+		}
+		strcat(lDialogString, "with icon ") ;
+		if ( aIconType && ! strcmp( "error" , aIconType ) )
+		{
+				strcat(lDialogString, "stop " ) ;
+		}
+		else if ( aIconType && ! strcmp( "warning" , aIconType ) )
+		{
+				strcat(lDialogString, "caution " ) ;
+		}
+		else /* question or info */
+		{
+				strcat(lDialogString, "note " ) ;
+		}
+		if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
+		{
+				if ( ! aDefaultButton )
 				{
-						if ( ! aDefaultButton )
-						{
-								strcat( lDialogString ,"default button \"Cancel\" " ) ;
-						}
+						strcat( lDialogString ,"default button \"Cancel\" " ) ;
 				}
-				else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+		}
+		else if ( aDialogType && ! strcmp( "yesno" , aDialogType ) )
+		{
+				strcat( lDialogString ,"buttons {\"No\", \"Yes\"} " ) ;
+				if (aDefaultButton)
 				{
-						strcat( lDialogString ,"buttons {\"No\", \"Yes\"} " ) ;
-						if (aDefaultButton)
-						{
-								strcat( lDialogString ,"default button \"Yes\" " ) ;
-						}
-						else
-						{
-								strcat( lDialogString ,"default button \"No\" " ) ;
-						}
-						strcat( lDialogString ,"cancel button \"No\"" ) ;
+						strcat( lDialogString ,"default button \"Yes\" " ) ;
 				}
-				else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+				else
 				{
-						strcat( lDialogString ,"buttons {\"No\", \"Yes\", \"Cancel\"} " ) ;
-						switch (aDefaultButton)
-						{
-								case 1: strcat( lDialogString ,"default button \"Yes\" " ) ; break;
-								case 2: strcat( lDialogString ,"default button \"No\" " ) ; break;
-								case 0: strcat( lDialogString ,"default button \"Cancel\" " ) ; break;
-						}
-						strcat( lDialogString ,"cancel button \"Cancel\"" ) ;
+						strcat( lDialogString ,"default button \"No\" " ) ;
 				}
-				else
+				strcat( lDialogString ,"cancel button \"No\"" ) ;
+		}
+		else if ( aDialogType && ! strcmp( "yesnocancel" , aDialogType ) )
+		{
+				strcat( lDialogString ,"buttons {\"No\", \"Yes\", \"Cancel\"} " ) ;
+				switch (aDefaultButton)
 				{
-						strcat( lDialogString ,"buttons {\"OK\"} " ) ;
-						strcat( lDialogString ,"default button \"OK\" " ) ;
+						case 1: strcat( lDialogString ,"default button \"Yes\" " ) ; break;
+						case 2: strcat( lDialogString ,"default button \"No\" " ) ; break;
+						case 0: strcat( lDialogString ,"default button \"Cancel\" " ) ; break;
 				}
-				strcat( lDialogString, ")' ") ;
+				strcat( lDialogString ,"cancel button \"Cancel\"" ) ;
+		}
+		else
+		{
+				strcat( lDialogString ,"buttons {\"OK\"} " ) ;
+				strcat( lDialogString ,"default button \"OK\" " ) ;
+		}
+		strcat( lDialogString, ")' ") ;
 
-				strcat( lDialogString,
-"-e 'if vButton is \"Yes\" then' -e 'return 1'\
- -e 'else if vButton is \"OK\" then' -e 'return 1'\
- -e 'else if vButton is \"No\" then' -e 'return 2'\
- -e 'else' -e 'return 0' -e 'end if' " );
+		strcat( lDialogString,
+"-e 'if vButton is \"Yes\" then' -e 'return 1' \
+-e 'else if vButton is \"OK\" then' -e 'return 1' \
+-e 'else if vButton is \"No\" then' -e 'return 2' \
+-e 'else' -e 'return 0' -e 'end if' " );
 
-				strcat( lDialogString, "-e 'on error number -128' " ) ;
-				strcat( lDialogString, "-e '0' " );
+		strcat( lDialogString, "-e 'on error number -128' " ) ;
+		strcat( lDialogString, "-e '0' " );
 
-				strcat( lDialogString, "-e 'end try'") ;
-				if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
-		}
-		else if ( tfd_kdialogPresent() )
-		{
+		strcat( lDialogString, "-e 'end try'") ;
+		if ( ! osx9orBetter() ) strcat( lDialogString, " -e 'end tell'") ;
+	}
+	else if ( tfd_kdialogPresent() )
+	{
 				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return 1;}
 
 				strcpy( lDialogString , "kdialog" ) ;
@@ -4640,13 +4696,14 @@
 						strcat( lDialogString , ";if [ $? = 0 ];then echo 1;else echo 0;fi");
 				}
 		}
-		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
 				if ( tfd_zenityPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return 1;}
 						strcpy( lDialogString , "szAnswer=$(zenity" ) ;
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat(lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
@@ -4661,15 +4718,27 @@
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return 1;}
 						strcpy( lDialogString , "szAnswer=$(shellementary" ) ;
 				}
-				else
+				else if ( tfd_qarmaPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return 1;}
 						strcpy( lDialogString , "szAnswer=$(qarma" ) ;
-												if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
+				else if ( tfd_boxerPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return 1;}
+						strcpy( lDialogString , "szAnswer=$(boxer" ) ;
+				}
+				else if ( tfd_shantyPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return 1;}
+						strcpy( lDialogString , "szAnswer=$(shanty" ) ;
+				}
+				else {}
+
 				strcat(lDialogString, " --");
 
 				if ( aDialogType && ! strcmp( "okcancel" , aDialogType ) )
@@ -4708,19 +4777,21 @@
 				if (aMessage && strlen(aMessage)) strcat(lDialogString, aMessage) ;
 				strcat(lDialogString, "\"") ;
 
-				if ( (tfd_zenity3Present() >= 3) || (!tfd_zenityPresent() && (tfd_shellementaryPresent() || tfd_qarmaPresent()) ) )
+				if ( (tfd_zenityVersion()>=3000) || tfd_boxerPresent() )
 				{
-						strcat( lDialogString , " --icon-name=dialog-" ) ;
-						if ( aIconType && (! strcmp( "question" , aIconType )
-						  || ! strcmp( "error" , aIconType )
-						  || ! strcmp( "warning" , aIconType ) ) )
-						{
-								strcat( lDialogString , aIconType ) ;
-						}
-						else
-						{
-								strcat( lDialogString , "information" ) ;
-						}
+					if ( (tfd_zenityVersion()>=3900) || tfd_boxerPresent() ) strcat( lDialogString , " --icon=dialog-" ) ;
+					else strcat( lDialogString , " --icon-name=dialog-" ) ;
+
+					if ( aIconType && (! strcmp( "question" , aIconType )
+					  || ! strcmp( "error" , aIconType )
+					  || ! strcmp( "warning" , aIconType ) ) )
+					{
+						strcat( lDialogString , aIconType ) ;
+					}
+					else
+					{
+						strcat( lDialogString , "information" ) ;
+					}
 				}
 
 				if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
@@ -4732,7 +4803,7 @@
 				}
 				else
 				{
-						strcat( lDialogString , ");if [ $? = 0 ];then echo 1;else echo 0;fi");
+					strcat( lDialogString , ");if [ $? = 0 ];then echo 1;else echo 0;fi");
 				}
 	  }
 
@@ -5308,28 +5379,56 @@
 		else if ( !isTerminalRunning() && notifysendPresent() && !strcmp("ok" , aDialogType) )
 		{
 
-				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notifysend");return 1;}
-				strcpy( lDialogString , "notify-send" ) ;
-				if ( aIconType && strlen(aIconType) )
-				{
-						strcat( lDialogString , " -i '" ) ;
-						strcat( lDialogString , aIconType ) ;
-						strcat( lDialogString , "'" ) ;
-				}
-		strcat( lDialogString , " \"" ) ;
-				if ( aTitle && strlen(aTitle) )
-				{
-						strcat(lDialogString, aTitle) ;
-						strcat( lDialogString , " | " ) ;
-				}
-				if ( aMessage && strlen(aMessage) )
-				{
-			tfd_replaceSubStr( aMessage , "\n\t" , " |  " , lBuff ) ;
-			tfd_replaceSubStr( aMessage , "\n" , " | " , lBuff ) ;
-			tfd_replaceSubStr( aMessage , "\t" , "  " , lBuff ) ;
-						strcat(lDialogString, lBuff) ;
-				}
-				strcat( lDialogString , "\"" ) ;
+			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notifysend");return 1;}
+			strcpy( lDialogString , "notify-send" ) ;
+			if ( aIconType && strlen(aIconType) )
+			{
+					strcat( lDialogString , " -i '" ) ;
+					strcat( lDialogString , aIconType ) ;
+					strcat( lDialogString , "'" ) ;
+			}
+			strcat( lDialogString , " \"" ) ;
+			if ( aTitle && strlen(aTitle) )
+			{
+					strcat(lDialogString, aTitle) ;
+					strcat( lDialogString , " | " ) ;
+			}
+			if ( aMessage && strlen(aMessage) )
+			{
+				tfd_replaceSubStr( aMessage , "\n\t" , " |  " , lBuff ) ;
+				tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+				tfd_replaceSubStr( lBuff2 , "\t" , "  " , lBuff ) ;
+				strcat(lDialogString, lBuff) ;
+			}
+			strcat( lDialogString , "\"" ) ;
+		}
+		else if ( !isTerminalRunning() && notifyPresent() && !strcmp("ok" , aDialogType) )
+		{
+
+			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notify");return 1;}
+			strcpy( lDialogString , "notify" ) ;
+			if ( aIconType && strlen(aIconType) )
+			{
+				strcat( lDialogString , " --icon '" ) ;
+				strcat( lDialogString , aIconType ) ;
+				strcat( lDialogString , "'" ) ;
+			}
+			if ( aTitle && strlen(aTitle) )
+			{
+				strcat( lDialogString , " --title '" ) ;
+				strcat( lDialogString , aTitle ) ;
+				strcat( lDialogString , "'" ) ;
+			}
+			strcat( lDialogString , " \"" ) ;
+			if ( aMessage && strlen(aMessage) )
+			{
+				tfd_replaceSubStr( aMessage , "\n\t" , " |  " , lBuff ) ;
+				tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+				tfd_replaceSubStr( lBuff2 , "\t" , "  " , lBuff ) ;
+				strcat(lDialogString, lBuff) ;
+			}
+			else strcat(lDialogString, "''");
+			strcat( lDialogString , "\"" ) ;
 		}
 		else
 		{
@@ -5403,7 +5502,7 @@
 								printf("\n%s\n\n",aMessage);
 						}
 						printf("press enter to continue "); fflush(stdout);
-						getchar() ;
+						(void) getchar() ;
 						printf("\n\n");
 						lResult = 1 ;
 				}
@@ -5414,7 +5513,8 @@
 
 		if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
 
-		if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+		lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+		if ( ! lIn )
 		{
 				free(lDialogString);
 				return 0 ;
@@ -5455,15 +5555,16 @@
 		char const * aMessage , /* NULL or ""  may contain \n and \t */
 		char const * aIconType ) /* "info" "warning" "error" */
 {
-		char lBuff[MAX_PATH_OR_CMD];
+	char lBuff[MAX_PATH_OR_CMD];
+	char lBuff2[MAX_PATH_OR_CMD];
 		char * lDialogString = NULL ;
 		char * lpDialogString ;
 		FILE * lIn ;
 		size_t lTitleLen ;
 		size_t lMessageLen ;
 
-				if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
-				if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
+		if (tfd_quoteDetected(aTitle)) return tinyfd_notifyPopup("INVALID TITLE WITH QUOTES", aMessage, aIconType);
+		if (tfd_quoteDetected(aMessage)) return tinyfd_notifyPopup(aTitle, "INVALID MESSAGE WITH QUOTES", aIconType);
 
 		if ( getenv("SSH_TTY") && !dunstifyPresent() && !dunstPresent() )
 		{
@@ -5622,20 +5723,47 @@
 			if ( aMessage && strlen(aMessage) )
 			{
 				tfd_replaceSubStr( aMessage , "\n\t" , " |  " , lBuff ) ;
-				tfd_replaceSubStr( aMessage , "\n" , " | " , lBuff ) ;
-				tfd_replaceSubStr( aMessage , "\t" , "  " , lBuff ) ;
+				tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+				tfd_replaceSubStr( lBuff2 , "\t" , "  " , lBuff ) ;
 				strcat(lDialogString, lBuff) ;
 			}
 			strcat( lDialogString , "\"" ) ;
 		}
-		else if ( (tfd_zenity3Present()>=5) )
+		else if ( notifyPresent() ) /* haiku */
+		{
+			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"notify");return 1;}
+			strcpy( lDialogString , "notify" ) ;
+			if ( aIconType && strlen(aIconType) )
+			{
+				strcat( lDialogString , " --icon '" ) ;
+				strcat( lDialogString , aIconType ) ;
+				strcat( lDialogString , "'" ) ;
+			}
+			if ( aTitle && strlen(aTitle) )
+			{
+				strcat( lDialogString , " --title '" ) ;
+				strcat(lDialogString, aTitle) ;
+				strcat( lDialogString , "'" ) ;
+			}
+			strcat( lDialogString , " \"" ) ;
+			if ( aMessage && strlen(aMessage) )
+			{
+				tfd_replaceSubStr( aMessage , "\n\t" , " |  " , lBuff ) ;
+				tfd_replaceSubStr( lBuff , "\n" , " | " , lBuff2 ) ;
+				tfd_replaceSubStr( lBuff2 , "\t" , "  " , lBuff ) ;
+				strcat(lDialogString, lBuff) ;
+			}
+			else strcat(lDialogString, "''" ) ;
+			strcat( lDialogString , "\"" ) ;
+		}
+		else if ( tfd_zenityVersion()>=3018 )
 		{
 				/* zenity 2.32 & 3.14 has the notification but with a bug: it doesnt return from it */
 				/* zenity 3.8 show the notification as an alert ok cancel box */
 				/* zenity 3.44 doesn't have the notification (3.42 has it) */
 				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return 1;}
 				strcpy( lDialogString , "zenity --notification");
-
+				
 				if ( aIconType && strlen( aIconType ) )
 				{
 						strcat( lDialogString , " --window-icon '");
@@ -5663,7 +5791,8 @@
 
 		if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
 
-		if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+		lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+		if ( ! lIn )
 		{
 				free(lDialogString);
 				return 0 ;
@@ -5696,147 +5825,160 @@
 		size_t lTitleLen ;
 		size_t lMessageLen ;
 
-				if (!aTitle && !aMessage && !aDefaultInput) return lBuff; /* now I can fill lBuff from outside */
+		if (!aTitle && !aMessage && !aDefaultInput) return lBuff; /* now I can fill lBuff from outside */
 
 		lBuff[0]='\0';
 
-				if (tfd_quoteDetected(aTitle)) return tinyfd_inputBox("INVALID TITLE WITH QUOTES", aMessage, aDefaultInput);
-				if (tfd_quoteDetected(aMessage)) return tinyfd_inputBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDefaultInput);
-				if (tfd_quoteDetected(aDefaultInput)) return tinyfd_inputBox(aTitle, aMessage, "INVALID DEFAULT_INPUT WITH QUOTES: use the GRAVE ACCENT \\x60 instead.");
+		if (tfd_quoteDetected(aTitle)) return tinyfd_inputBox("INVALID TITLE WITH QUOTES", aMessage, aDefaultInput);
+		if (tfd_quoteDetected(aMessage)) return tinyfd_inputBox(aTitle, "INVALID MESSAGE WITH QUOTES", aDefaultInput);
+		if (tfd_quoteDetected(aDefaultInput)) return tinyfd_inputBox(aTitle, aMessage, "INVALID DEFAULT_INPUT WITH QUOTES: use the GRAVE ACCENT \\x60 instead.");
 
 		lTitleLen =  aTitle ? strlen(aTitle) : 0 ;
 		lMessageLen =  aMessage ? strlen(aMessage) : 0 ;
 		if ( !aTitle || strcmp(aTitle,"tinyfd_query") )
 		{
-				lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
+			lDialogString = (char *) malloc( MAX_PATH_OR_CMD + lTitleLen + lMessageLen );
 		}
 
 		if ( osascriptPresent( ) )
 		{
-				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
-				strcpy( lDialogString , "osascript ");
-				if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
-				strcat( lDialogString , " -e 'try' -e 'display dialog \"") ;
-				if ( aMessage && strlen(aMessage) )
-				{
-						strcat(lDialogString, aMessage) ;
-				}
-				strcat(lDialogString, "\" ") ;
-				strcat(lDialogString, "default answer \"") ;
-				if ( aDefaultInput && strlen(aDefaultInput) )
-				{
-						strcat(lDialogString, aDefaultInput) ;
-				}
-				strcat(lDialogString, "\" ") ;
-				if ( ! aDefaultInput )
-				{
-						strcat(lDialogString, "hidden answer true ") ;
-				}
-				if ( aTitle && strlen(aTitle) )
-				{
-						strcat(lDialogString, "with title \"") ;
-						strcat(lDialogString, aTitle) ;
-						strcat(lDialogString, "\" ") ;
-				}
-				strcat(lDialogString, "with icon note' ") ;
-				strcat(lDialogString, "-e '\"1\" & text returned of result' " );
-				strcat(lDialogString, "-e 'on error number -128' " ) ;
-				strcat(lDialogString, "-e '0' " );
-				strcat(lDialogString, "-e 'end try'") ;
-				if ( ! osx9orBetter() ) strcat(lDialogString, " -e 'end tell'") ;
+			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"applescript");return (char *)1;}
+			strcpy( lDialogString , "osascript ");
+			if ( ! osx9orBetter() ) strcat( lDialogString , " -e 'tell application \"System Events\"' -e 'Activate'");
+			strcat( lDialogString , " -e 'try' -e 'display dialog \"") ;
+			if ( aMessage && strlen(aMessage) )
+			{
+					strcat(lDialogString, aMessage) ;
+			}
+			strcat(lDialogString, "\" ") ;
+			strcat(lDialogString, "default answer \"") ;
+			if ( aDefaultInput && strlen(aDefaultInput) )
+			{
+					strcat(lDialogString, aDefaultInput) ;
+			}
+			strcat(lDialogString, "\" ") ;
+			if ( ! aDefaultInput )
+			{
+					strcat(lDialogString, "hidden answer true ") ;
+			}
+			if ( aTitle && strlen(aTitle) )
+			{
+					strcat(lDialogString, "with title \"") ;
+					strcat(lDialogString, aTitle) ;
+					strcat(lDialogString, "\" ") ;
+			}
+			strcat(lDialogString, "with icon note' ") ;
+			strcat(lDialogString, "-e '\"1\" & text returned of result' " );
+			strcat(lDialogString, "-e 'on error number -128' " ) ;
+			strcat(lDialogString, "-e '0' " );
+			strcat(lDialogString, "-e 'end try'") ;
+			if ( ! osx9orBetter() ) strcat(lDialogString, " -e 'end tell'") ;
 		}
 		else if ( tfd_kdialogPresent() )
 		{
-				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
-				strcpy( lDialogString , "szAnswer=$(kdialog" ) ;
+			if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"kdialog");return (char *)1;}
+			strcpy( lDialogString , "szAnswer=$(kdialog" ) ;
 
-								if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
-				{
-						strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
-				}
+							if ( (tfd_kdialogPresent() == 2) && tfd_xpropPresent() )
+			{
+					strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+			}
 
-				if ( ! aDefaultInput )
-				{
-						strcat(lDialogString, " --password ") ;
-				}
-				else
-				{
-						strcat(lDialogString, " --inputbox ") ;
+			if ( ! aDefaultInput )
+			{
+					strcat(lDialogString, " --password ") ;
+			}
+			else
+			{
+					strcat(lDialogString, " --inputbox ") ;
 
-				}
-				strcat(lDialogString, "\"") ;
-				if ( aMessage && strlen(aMessage) )
-				{
-						strcat(lDialogString, aMessage ) ;
-				}
-				strcat(lDialogString , "\" \"" ) ;
-				if ( aDefaultInput && strlen(aDefaultInput) )
-				{
-						strcat(lDialogString, aDefaultInput ) ;
-				}
-				strcat(lDialogString , "\"" ) ;
-				if ( aTitle && strlen(aTitle) )
-				{
-						strcat(lDialogString, " --title \"") ;
-						strcat(lDialogString, aTitle) ;
-						strcat(lDialogString, "\"") ;
-				}
-				strcat( lDialogString ,
-						");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
+			}
+			strcat(lDialogString, "\"") ;
+			if ( aMessage && strlen(aMessage) )
+			{
+					strcat(lDialogString, aMessage ) ;
+			}
+			strcat(lDialogString , "\" \"" ) ;
+			if ( aDefaultInput && strlen(aDefaultInput) )
+			{
+					strcat(lDialogString, aDefaultInput ) ;
+			}
+			strcat(lDialogString , "\"" ) ;
+			if ( aTitle && strlen(aTitle) )
+			{
+					strcat(lDialogString, " --title \"") ;
+					strcat(lDialogString, aTitle) ;
+					strcat(lDialogString, "\"") ;
+			}
+			strcat( lDialogString ,
+					");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
 		}
-		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
-				if ( tfd_zenityPresent() )
-				{
-						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
-						strcpy( lDialogString , "szAnswer=$(zenity" ) ;
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
-						{
-								strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
-						}
-				}
-				else if ( tfd_matedialogPresent() )
-				{
-						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
-						strcpy( lDialogString ,  "szAnswer=$(matedialog" ) ;
-				}
-				else if ( tfd_shellementaryPresent() )
-				{
-						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
-						strcpy( lDialogString , "szAnswer=$(shellementary" ) ;
-				}
-				else
-				{
-						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
-						strcpy( lDialogString ,  "szAnswer=$(qarma" ) ;
-												if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
-						{
-								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
-						}
-				}
-				strcat( lDialogString ," --entry" ) ;
-
-				strcat(lDialogString, " --title=\"") ;
-				if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
-				strcat(lDialogString, "\"") ;
-
-				strcat(lDialogString, " --text=\"") ;
-				if (aMessage && strlen(aMessage)) strcat(lDialogString, aMessage) ;
-				strcat(lDialogString, "\"") ;
+			if ( tfd_zenityPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
+					strcpy( lDialogString , "szAnswer=$(zenity" ) ;
+					if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+					{
+							strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+					}
+			}
+			else if ( tfd_matedialogPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"matedialog");return (char *)1;}
+					strcpy( lDialogString ,  "szAnswer=$(matedialog" ) ;
+			}
+			else if ( tfd_shellementaryPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
+					strcpy( lDialogString , "szAnswer=$(shellementary" ) ;
+			}
+			else if ( tfd_qarmaPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
+					strcpy( lDialogString ,  "szAnswer=$(qarma" ) ;
+					if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+					{
+							strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+					}
+			}
+			else if ( tfd_boxerPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+					strcpy( lDialogString ,  "szAnswer=$(boxer" ) ;
+			}
+			else if ( tfd_shantyPresent() )
+			{
+					if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+					strcpy( lDialogString ,  "szAnswer=$(shanty" ) ;
+			}
+			else {}
+			
+			strcat( lDialogString ," --entry" ) ;
+
+			strcat(lDialogString, " --title=\"") ;
+			if (aTitle && strlen(aTitle)) strcat(lDialogString, aTitle) ;
+			strcat(lDialogString, "\"") ;
+
+			strcat(lDialogString, " --text=\"") ;
+			if (aMessage && strlen(aMessage)) strcat(lDialogString, aMessage) ;
+			strcat(lDialogString, "\"") ;
 
-				if ( aDefaultInput )
-				{
-						strcat(lDialogString, " --entry-text=\"") ;
-						strcat(lDialogString, aDefaultInput) ;
-						strcat(lDialogString, "\"") ;
-				}
-				else
-				{
-						strcat(lDialogString, " --hide-text") ;
-				}
-				if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
-				strcat( lDialogString ,
-								");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
+			if ( aDefaultInput )
+			{
+					strcat(lDialogString, " --entry-text=\"") ;
+					strcat(lDialogString, aDefaultInput) ;
+					strcat(lDialogString, "\"") ;
+			}
+			else
+			{
+					strcat(lDialogString, " --hide-text") ;
+			}
+			if (tinyfd_silent) strcat( lDialogString , " 2>/dev/null ");
+			strcat( lDialogString ,
+							");if [ $? = 0 ];then echo 1$szAnswer;else echo 0$szAnswer;fi");
 		}
 		else if (tfd_yadPresent())
 		{
@@ -6199,7 +6341,7 @@
 		}
 
 		if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
-		lIn = popen( lDialogString , "r" );
+		lIn = ( FILE * ) popen( lDialogString , "r" );
 		if ( ! lIn  )
 		{
 				if ( fileExists("/tmp/tinyfd.txt") )
@@ -6374,13 +6516,14 @@
 						strcat(lDialogString, "\"") ;
 				}
 		}
-		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
 				if ( tfd_zenityPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
 						strcpy( lDialogString , "zenity" ) ;
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
@@ -6395,7 +6538,7 @@
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
 						strcpy( lDialogString , "shellementary" ) ;
 				}
-				else
+				else if ( tfd_qarmaPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
 						strcpy( lDialogString , "qarma" ) ;
@@ -6404,6 +6547,18 @@
 								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
+				else if ( tfd_boxerPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+						strcpy( lDialogString , "boxer" ) ;
+				}
+				else if ( tfd_shantyPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+						strcpy( lDialogString , "shanty" ) ;
+				}
+				else {}
+				
 				strcat(lDialogString, " --file-selection --save --confirm-overwrite" ) ;
 
 				strcat(lDialogString, " --title=\"") ;
@@ -6422,12 +6577,13 @@
 						if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
 						{
 								strcat( lDialogString , aSingleFilterDescription ) ;
-								strcat( lDialogString , " |" ) ;
+								strcat( lDialogString , " | " ) ;
 						}
-						for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+						strcat( lDialogString , aFilterPatterns[0] ) ;
+						for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
 						{
-														strcat( lDialogString , " " ) ;
-														strcat( lDialogString , aFilterPatterns[i] ) ;
+							strcat( lDialogString , " " ) ;
+							strcat( lDialogString , aFilterPatterns[i] ) ;
 						}
 						strcat( lDialogString , "' --file-filter='All files | *'" ) ;
 				}
@@ -6451,18 +6607,19 @@
 		   }
 		   if (aNumOfFilterPatterns > 0)
 		   {
-			  strcat(lDialogString, " --file-filter='");
-			  if (aSingleFilterDescription && strlen(aSingleFilterDescription))
-			  {
-				 strcat(lDialogString, aSingleFilterDescription);
-				 strcat(lDialogString, " |");
-			  }
-			  for (i = 0; i < aNumOfFilterPatterns; i++)
-			  {
-				 strcat(lDialogString, " ");
-				 strcat(lDialogString, aFilterPatterns[i]);
-			  }
-			  strcat(lDialogString, "' --file-filter='All files | *'");
+				strcat(lDialogString, " --file-filter='");
+				if (aSingleFilterDescription && strlen(aSingleFilterDescription))
+				{
+					strcat(lDialogString, aSingleFilterDescription);
+					strcat(lDialogString, " | ");
+				}
+				strcat(lDialogString, aFilterPatterns[0]);
+				for ( i = 1 ; i < aNumOfFilterPatterns; i++)
+				{
+					strcat(lDialogString, " ");
+					strcat(lDialogString, aFilterPatterns[i]);
+				}
+				strcat(lDialogString, "' --file-filter='All files | *'");
 		   }
 		   if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
 	  }
@@ -6533,8 +6690,8 @@
 		if ( tfd_isDarwin( ) )
 		{
                 strcat( lDialogString ,
-"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set\
- frontmost of process \\\"Python\\\" to true' ''');");
+"import os;os.system('''/usr/bin/osascript -e 'tell app \\\"Finder\\\" to set \
+frontmost of process \\\"Python\\\" to true' ''');");
 				}
 
 				strcat( lDialogString , "res=tkFileDialog.asksaveasfilename(");
@@ -6683,8 +6840,10 @@
 				return p ;
 		}
 
-		if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
-	if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+	if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
+	
+	lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+	if ( ! lIn )
 	{
 		return NULL ;
 	}
@@ -6897,15 +7056,16 @@
 						strcat(lDialogString, "\"") ;
 				}
 		}
-		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
 				if ( tfd_zenityPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
 						strcpy( lDialogString , "zenity" ) ;
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
-								strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
+							strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
 				else if ( tfd_matedialogPresent() )
@@ -6918,7 +7078,7 @@
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
 						strcpy( lDialogString , "shellementary" ) ;
 				}
-				else
+				else if ( tfd_qarmaPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
 						strcpy( lDialogString , "qarma" ) ;
@@ -6927,6 +7087,18 @@
 								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
+				else if ( tfd_boxerPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+						strcpy( lDialogString , "boxer" ) ;
+				}
+				else if ( tfd_shantyPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+						strcpy( lDialogString , "shanty" ) ;
+				}
+				else {}
+				
 				strcat( lDialogString , " --file-selection" ) ;
 
 				if ( aAllowMultipleSelects )
@@ -6950,12 +7122,13 @@
 						if ( aSingleFilterDescription && strlen(aSingleFilterDescription) )
 						{
 								strcat( lDialogString , aSingleFilterDescription ) ;
-								strcat( lDialogString , " |" ) ;
+								strcat( lDialogString , " | " ) ;
 						}
-						for ( i = 0 ; i < aNumOfFilterPatterns ; i ++ )
+						strcat( lDialogString , aFilterPatterns[0] ) ;
+						for ( i = 1 ; i < aNumOfFilterPatterns ; i ++ )
 						{
-														strcat( lDialogString , " " ) ;
-														strcat( lDialogString , aFilterPatterns[i] ) ;
+							strcat( lDialogString , " " ) ;
+							strcat( lDialogString , aFilterPatterns[i] ) ;
 						}
 						strcat( lDialogString , "' --file-filter='All files | *'" ) ;
 				}
@@ -6981,24 +7154,25 @@
 			  strcat(lDialogString, aDefaultPathAndOrFile);
 			  strcat(lDialogString, "\"");
 		   }
-		   if (aNumOfFilterPatterns > 0)
-		   {
-			  strcat(lDialogString, " --file-filter='");
-			  if (aSingleFilterDescription && strlen(aSingleFilterDescription))
-			  {
-				 strcat(lDialogString, aSingleFilterDescription);
-				 strcat(lDialogString, " |");
-			  }
-			  for (i = 0; i < aNumOfFilterPatterns; i++)
-			  {
+			if (aNumOfFilterPatterns > 0)
+			{
+				strcat(lDialogString, " --file-filter='");
+				if (aSingleFilterDescription && strlen(aSingleFilterDescription))
+				{
+					strcat(lDialogString, aSingleFilterDescription);
+					strcat(lDialogString, " | ");
+				}
+				strcat(lDialogString, aFilterPatterns[0]);
+				for ( i = 1 ; i < aNumOfFilterPatterns; i++)
+				{
 				 strcat(lDialogString, " ");
 				 strcat(lDialogString, aFilterPatterns[i]);
-			  }
-			  strcat(lDialogString, "' --file-filter='All files | *'");
-		   }
-		   if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
-	  }
-	  else if ( tkinter3Present( ) )
+				}
+				strcat(lDialogString, "' --file-filter='All files | *'");
+			}
+			if (tinyfd_silent) strcat(lDialogString, " 2>/dev/null ");
+		}
+		else if ( tkinter3Present( ) )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python3-tkinter");return (char *)1;}
 						strcpy( lDialogString , gPython3Name ) ;
@@ -7226,7 +7400,8 @@
 		}
 
 	if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
-	if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+	lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+	if ( ! lIn )
 	{
 				free(lBuff);
 				lBuff = NULL;
@@ -7369,13 +7544,14 @@
 						strcat(lDialogString, "\"") ;
 				}
 		}
-		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( tfd_zenityPresent() || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
 				if ( tfd_zenityPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity");return (char *)1;}
 						strcpy( lDialogString , "zenity" ) ;
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( (tfd_zenityVersion() >= 3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
@@ -7390,15 +7566,27 @@
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
 						strcpy( lDialogString , "shellementary" ) ;
 				}
-				else
+				else if ( tfd_qarmaPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
 						strcpy( lDialogString , "qarma" ) ;
-												if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
+				else if ( tfd_boxerPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+						strcpy( lDialogString , "boxer" ) ;
+				}
+				else if ( tfd_shantyPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+						strcpy( lDialogString , "shanty" ) ;
+				}
+				else {}
+
 				strcat( lDialogString , " --file-selection --directory" ) ;
 
 				strcat(lDialogString, " --title=\"") ;
@@ -7577,7 +7765,8 @@
 				return p ;
 		}
 	if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
-	if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+	lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+	if ( ! lIn )
 	{
 		return NULL ;
 	}
@@ -7624,7 +7813,7 @@
 				char * lPointerInputBox;
 		FILE * lIn ;
 		int i ;
-		int lWasZenity3 = 0 ;
+		int lWasZenity2032 = 0 ;
 		int lWasOsascript = 0 ;
 		int lWasXdialog = 0 ;
 		lBuff[0]='\0';
@@ -7701,14 +7890,15 @@
 						strcat(lDialogString, "\"") ;
 				}
 		}
-		else if ( tfd_zenity3Present() || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() )
+		else if ( (tfd_zenityVersion()>=2032) || tfd_matedialogPresent() || tfd_shellementaryPresent()
+				|| tfd_qarmaPresent() || tfd_shantyPresent() || tfd_boxerPresent() )
 		{
-				lWasZenity3 = 1 ;
-				if ( tfd_zenity3Present() )
+				lWasZenity2032 = 1 ;
+				if ( tfd_zenityVersion()>=2032 )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"zenity3");return (char *)1;}
 						strcpy( lDialogString , "zenity" );
-												if ( (tfd_zenity3Present() >= 4) && !getenv("SSH_TTY") && tfd_xpropPresent() )
+						if ( (tfd_zenityVersion() >=3010) && !getenv("SSH_TTY") && tfd_xpropPresent() )
 						{
 								strcat( lDialogString, " --attach=$(sleep .01;xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
@@ -7723,7 +7913,7 @@
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shellementary");return (char *)1;}
 						strcpy( lDialogString , "shellementary" ) ;
 				}
-				else
+				else if ( tfd_qarmaPresent() )
 				{
 						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"qarma");return (char *)1;}
 						strcpy( lDialogString , "qarma" ) ;
@@ -7732,7 +7922,23 @@
 								strcat(lDialogString, " --attach=$(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2)"); /* contribution: Paul Rouget */
 						}
 				}
-				strcat( lDialogString , " --color-selection --show-palette" ) ;
+				else if ( tfd_boxerPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"boxer");return (char *)1;}
+						strcpy( lDialogString , "boxer" ) ;
+				}
+				else if ( tfd_shantyPresent() )
+				{
+						if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"shanty");return (char *)1;}
+						strcpy( lDialogString , "shanty" ) ;
+				}
+				else {}
+				
+				strcat( lDialogString , " --color-selection" ) ;
+				if ( tfd_zenityVersion()>=2032 || tfd_matedialogPresent() || tfd_shellementaryPresent() || tfd_qarmaPresent() || tfd_boxerPresent() )
+				{
+					strcat( lDialogString , " --show-palette" ) ;
+				}
 				sprintf( lDialogString + strlen(lDialogString), " --color=%s" , lDefaultHexRGB ) ;
 
 				strcat(lDialogString, " --title=\"") ;
@@ -7792,7 +7998,7 @@
 \nif res[1] is not None:\n\tprint(res[1])\"" ) ;
 				}
 				else if ( tkinter2Present( ) )
-		{
+				{
 				if (aTitle&&!strcmp(aTitle,"tinyfd_query")){strcpy(tinyfd_response,"python2-tkinter");return (char *)1;}
 								strcpy( lDialogString , "export PYTHONIOENCODING=utf-8;" ) ;
 								strcat( lDialogString , gPython2Name ) ;
@@ -7850,7 +8056,8 @@
 		}
 
 		if (tinyfd_verbose) printf( "lDialogString: %s\n" , lDialogString ) ;
-		if ( ! ( lIn = popen( lDialogString , "r" ) ) )
+		lIn = ( FILE * ) popen( lDialogString , "r" ) ;
+		if ( ! lIn )
 		{
 				return NULL ;
 	}
@@ -7869,7 +8076,7 @@
 		lBuff[strlen( lBuff ) -1] = '\0' ;
 	}
 
-		if ( lWasZenity3 )
+	if ( lWasZenity2032 )
 	{
 				if ( lBuff[0] == '#' )
 				{
diff -Nru f3d-3.2.0+dfsg/external/tinyfiledialogs/tinyfiledialogs.h f3d-3.4.1/external/tinyfiledialogs/tinyfiledialogs.h
--- f3d-3.2.0+dfsg/external/tinyfiledialogs/tinyfiledialogs.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/tinyfiledialogs/tinyfiledialogs.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,5 @@
 /* SPDX-License-Identifier: Zlib
-Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
+Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
 	 ____________________________________________________________________
 	|                                                                    |
 	| 100% compatible C C++  ->  You can rename tinfiledialogs.c as .cpp |
@@ -7,7 +7,7 @@
 
 ********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
   _________
- /         \ tinyfiledialogs.h v3.19.1 [Jan 27, 2025]
+ /         \ tinyfiledialogs.h v3.21.2 [Oct 25, 2025]
  |tiny file| Unique header file created [November 9, 2014]
  | dialogs |
  \____  ___/ http://tinyfiledialogs.sourceforge.net
@@ -124,7 +124,7 @@
 possible values for tinyfd_response are (all lowercase)
 for graphic mode:
   windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
-  shellementary qarma python2-tkinter python3-tkinter python-dbus
+  shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus
   perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
 for console mode:
   dialog whiptail basicinput no_solution */
@@ -288,8 +288,8 @@
   (on windows the no linking claim is a lie)
 - On unix: it tries command line calls, so no such need (NO LINKING).
 - On unix you need one of the following:
-  applescript, kdialog, zenity, matedialog, shellementary, qarma, yad,
-  python (2 or 3)/tkinter/python-dbus (optional), Xdialog
+  applescript, kdialog, zenity, matedialog, shellementary, qarma, shanty, boxer,
+  yad, python (2 or 3)/tkinter/python-dbus (optional), Xdialog
   or curses dialogs (opens terminal if running without console).
 - One of those is already included on most (if not all) desktops.
 - In the absence of those it will use gdialog, gxmessage or whiptail
diff -Nru f3d-3.2.0+dfsg/external/VERSIONS.md f3d-3.4.1/external/VERSIONS.md
--- f3d-3.2.0+dfsg/external/VERSIONS.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/external/VERSIONS.md	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,5 @@
-- cxxopts: v3.2.0
-- dmon: 1.3.7
-- imgui: v1.91.9b
+- cxxopts: v3.3.1
+- dmon: 1.3.8
+- imgui: v1.92.2
 - nlohmann_json: v3.12.0
 - tinyfiledialogs: v3.19.1
diff -Nru f3d-3.2.0+dfsg/.github/actions/alembic-install-dep/action.yml f3d-3.4.1/.github/actions/alembic-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/alembic-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/alembic-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,12 @@
   version:
     description: "Version of alembic to build"
     required: true
+  imath_version:
+    description: "Version of imath to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,13 +22,15 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.imath_version }}" ]] || { echo "imath_version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache Alembic
       id: cache-alembic
       uses: actions/cache/restore@v4
       with:
         path: dependencies/alembic_install
-        key: alembic-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-0
+        key: alembic-${{inputs.version}}-${{inputs.imath_version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-3
 
     - name: Checkout Alembic
       if: steps.cache-alembic.outputs.cache-hit != 'true'
@@ -32,6 +40,13 @@
         path: "./dependencies/alembic"
         ref: ${{inputs.version}}
 
+    # https://github.com/alembic/alembic/issues/476
+    - name: Patch Alembic rpath issue
+      if: steps.cache-alembic.outputs.cache-hit != 'true' && inputs.version == '1.8.9'
+      working-directory: ${{github.workspace}}/dependencies/alembic
+      shell: bash
+      run: patch -p1 < $GITHUB_ACTION_PATH/fix_cmake_rpath.patch
+
     - name: Setup Alembic
       if: steps.cache-alembic.outputs.cache-hit != 'true'
       working-directory: ${{github.workspace}}/dependencies
diff -Nru f3d-3.2.0+dfsg/.github/actions/alembic-install-dep/fix_cmake_rpath.patch f3d-3.4.1/.github/actions/alembic-install-dep/fix_cmake_rpath.patch
--- f3d-3.2.0+dfsg/.github/actions/alembic-install-dep/fix_cmake_rpath.patch	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/actions/alembic-install-dep/fix_cmake_rpath.patch	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,26 @@
+From 99caa8156fb65a1c0a97ef4b168ca720787e04b8 Mon Sep 17 00:00:00 2001
+From: Mathieu Westphal <mathieu.westphal@kitware.com>
+Date: Thu, 30 Oct 2025 08:49:47 +0100
+Subject: [PATCH] fix_cmake_rpath
+
+---
+ lib/Alembic/CMakeLists.txt | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/lib/Alembic/CMakeLists.txt b/lib/Alembic/CMakeLists.txt
+index 8cb8ddb9..b5a73a6d 100644
+--- a/lib/Alembic/CMakeLists.txt
++++ b/lib/Alembic/CMakeLists.txt
+@@ -67,6 +67,9 @@ IF (ALEMBIC_SHARED_LIBS)
+     VERSION ${PROJECT_VERSION})
+ ENDIF()
+ 
++if (APPLE OR UNIX)
++  set_target_properties(Alembic PROPERTIES INSTALL_RPATH "$<TARGET_FILE_DIR:Imath::Imath>")
++endif ()
+ 
+ TARGET_LINK_LIBRARIES(Alembic
+     LINK_PUBLIC
+-- 
+2.51.1
+
diff -Nru f3d-3.2.0+dfsg/.github/actions/assimp-install-dep/action.yml f3d-3.4.1/.github/actions/assimp-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/assimp-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/assimp-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,9 @@
   version:
     description: "Version of assimp to build"
     required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,13 +19,14 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache ASSIMP
       id: cache-assimp
       uses: actions/cache/restore@v4
       with:
         path: dependencies/assimp_install
-        key: assimp-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-0
+        key: assimp-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-1
 
     - name: Checkout ASSIMP
       if: steps.cache-assimp.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/blosc-install-dep/action.yml f3d-3.4.1/.github/actions/blosc-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/blosc-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/blosc-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -5,16 +5,32 @@
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  version:
+    description: "Version of blosc to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.zlib_version }}" ]] || { echo "zlib_version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache blosc
       id: cache-blosc
       uses: actions/cache/restore@v4
       with:
         path: dependencies/blosc_install
-        key: blosc-v1.21.6-${{runner.os}}-${{inputs.cpu}}-0
+        key: blosc-${{inputs.version}}-${{inputs.zlib_version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-1
 
     # Dependents: openvdb vtk
     - name: Checkout blosc
@@ -23,7 +39,7 @@
       with:
         repository: Blosc/c-blosc
         path: "./dependencies/blosc"
-        ref: v1.21.6
+        ref: ${{inputs.version}}
 
     - name: Setup blosc
       if: steps.cache-blosc.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/coverage-ci/action.yml f3d-3.4.1/.github/actions/coverage-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/coverage-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/coverage-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -14,9 +14,15 @@
   assimp_version:
     description: "Version of assimp to build"
     required: false
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
@@ -26,12 +32,27 @@
   openvdb_version:
     description: "Version of openvdb to build"
     required: false
+  ospray_version:
+    description: "Version of ospray to build"
+    required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   usd_version:
     description: "Version of usd to build"
     required: false
   vtk_version:
     description: "Version of vtk to build"
     required: true
+  webp_version:
+    description: "Version of webp to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -43,7 +64,7 @@
         [[ "${{ inputs.vtk_version }}" ]] || { echo "vtk_version input is empty" ; exit 1; }
 
     - name: Recover LFS Data
-      uses: f3d-app/lfs-data-cache-action@v1
+      uses: f3d-app/lfs-data-cache-action@v2
       with:
         type: "consumer"
         lfs_sha: ${{inputs.lfs_sha}}
@@ -52,21 +73,31 @@
     - name: Install all F3D dependencies
       uses: ./source/.github/actions/generic-dependencies
       with:
-        raytracing_label: raytracing
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
+        blosc_version: ${{inputs.blosc_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        ospray_version: ${{inputs.ospray_version}}
+        tbb_version: ${{inputs.tbb_version}}
         usd_version: ${{inputs.usd_version}}
+        webp_version: ${{inputs.webp_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/.github/actions/vtk-install-dep
       with:
-        vtk_version: ${{inputs.vtk_version}}
-        raytracing_label: raytracing
+        blosc_version: ${{inputs.blosc_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        ospray_version: ${{inputs.ospray_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     # coverage build is done in source as it seems to be required for codecov
     # CMAKE_MODULE_PATH is required because of
@@ -86,6 +117,7 @@
         -DCMAKE_PREFIX_PATH:PATH=$(pwd)/../dependencies/install/
         -DF3D_COVERAGE=ON
         -DF3D_MODULE_EXR=ON
+        -DF3D_MODULE_WEBP=ON
         -DF3D_MODULE_RAYTRACING=ON
         -DF3D_MODULE_UI=ON
         -DF3D_PLUGINS_STATIC_BUILD=OFF
diff -Nru f3d-3.2.0+dfsg/.github/actions/draco-install-dep/action.yml f3d-3.4.1/.github/actions/draco-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/draco-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/draco-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,9 @@
   version:
     description: "Version of draco to build"
     required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,13 +19,14 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache Draco
       id: cache-draco
       uses: actions/cache/restore@v4
       with:
         path: dependencies/draco_install
-        key: draco-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-3
+        key: draco-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
     - name: Checkout Draco
       if: steps.cache-draco.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/external-build-ci/action.yml f3d-3.4.1/.github/actions/external-build-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/external-build-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/external-build-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,9 +1,21 @@
 name: "External build CI"
 description: "External build CI"
 inputs:
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   vtk_version:
     description: "Version of vtk to build"
     required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -30,11 +42,19 @@
       uses: ./source/f3d/.github/actions/vtk-dependencies
       with:
         source_dir: ./source/f3d
+        blosc_version: ${{inputs.blosc_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/f3d/.github/actions/vtk-install-dep
       with:
+        blosc_version: ${{inputs.blosc_version}}
+        tbb_version: ${{inputs.tbb_version}}
         vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Setup Build Directory
       shell: bash
diff -Nru f3d-3.2.0+dfsg/.github/actions/f3d-dependencies/action.yml f3d-3.4.1/.github/actions/f3d-dependencies/action.yml
--- f3d-3.2.0+dfsg/.github/actions/f3d-dependencies/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/f3d-dependencies/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -14,6 +14,9 @@
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
@@ -23,9 +26,18 @@
   pybind11_version:
     description: "Version of pybind11 to build"
     required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   usd_version:
     description: "Version of usd to build"
     required: false
+  webp_version:
+    description: "Version of webp to build"
+    required: false
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -36,6 +48,7 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.occt_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install Assimp dependency
       if: inputs.assimp_version != ''
@@ -43,6 +56,7 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.assimp_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install Draco dependency
       if: inputs.draco_version != ''
@@ -50,11 +64,15 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.draco_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install Imath dependency
+      if: inputs.imath_version != ''
       uses: ./source/.github/actions/imath-install-dep
       with:
         cpu: ${{inputs.cpu}}
+        version: ${{inputs.imath_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install OpenEXR dependency
       if: inputs.openexr_version != ''
@@ -62,6 +80,8 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.openexr_version}}
+        imath_version: ${{inputs.imath_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install Alembic dependency
       if: inputs.alembic_version != ''
@@ -69,12 +89,15 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.alembic_version}}
+        imath_version: ${{inputs.imath_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install pybind11 dependency
       if: inputs.pybind11_version != ''
       uses: ./source/.github/actions/pybind11-install-dep
       with:
         version: ${{inputs.pybind11_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install USD dependency
       if: inputs.usd_version != ''
@@ -83,3 +106,13 @@
         cpu: ${{inputs.cpu}}
         version: ${{inputs.usd_version}}
         openexr_version: ${{inputs.openexr_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
+
+    - name: Install webp dependency
+      if: inputs.webp_version != ''
+      uses: ./source/.github/actions/webp-install-dep
+      with:
+        cpu: ${{inputs.cpu}}
+        version: ${{inputs.webp_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
diff -Nru f3d-3.2.0+dfsg/.github/actions/generic-ci/action.yml f3d-3.4.1/.github/actions/generic-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/generic-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/generic-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -5,13 +5,6 @@
     description: "Name of the build"
     required: false
     default: "standard"
-  vtk_version:
-    description: "VTK version"
-    required: true
-  raytracing_label:
-    description: "Label to control raytracing"
-    required: false
-    default: "no-raytracing"
   optional_deps_label:
     description: "Label to optional dependencies"
     required: false
@@ -45,9 +38,18 @@
   assimp_version:
     description: "Version of assimp to build"
     required: false
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
+  java_version:
+    description: "Version of java to install"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
@@ -57,18 +59,33 @@
   openvdb_version:
     description: "Version of openvdb to build"
     required: false
+  ospray_version:
+    description: "Version of ospray to build"
+    required: false
   pybind11_version:
     description: "Version of pybind11 to build"
     required: false
   python_version:
     description: "Version of python to install"
     required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   usd_version:
     description: "Version of usd to build"
     required: false
-  java_version:
-    description: "Version of java to install"
-    required: false
+  vtk_version:
+    description: "VTK version"
+    required: true
+  webp_version:
+    description: "Version of webp to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -80,7 +97,7 @@
         [[ "${{ inputs.vtk_version }}" ]] || { echo "vtk_version input is empty" ; exit 1; }
 
     - name: Recover LFS Data
-      uses: f3d-app/lfs-data-cache-action@v1
+      uses: f3d-app/lfs-data-cache-action@v2
       with:
         type: "consumer"
         lfs_sha: ${{inputs.lfs_sha}}
@@ -90,23 +107,33 @@
       uses: ./source/.github/actions/generic-dependencies
       with:
         cpu: ${{inputs.cpu}}
-        raytracing_label: ${{inputs.raytracing_label}}
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
+        blosc_version: ${{inputs.blosc_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        ospray_version: ${{inputs.ospray_version}}
         pybind11_version: ${{inputs.pybind11_version}}
+        tbb_version: ${{inputs.tbb_version}}
         usd_version: ${{inputs.usd_version}}
+        webp_version: ${{inputs.webp_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/.github/actions/vtk-install-dep
       with:
-        vtk_version: ${{inputs.vtk_version}}
-        raytracing_label: ${{inputs.raytracing_label}}
         cpu: ${{inputs.cpu}}
+        blosc_version: ${{inputs.blosc_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        ospray_version: ${{inputs.ospray_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Set up Python
       if: |
@@ -149,6 +176,13 @@
       working-directory: ${{github.workspace}}
       run: echo "PATH=$env:PATH;$(pwd)\dependencies\install\bin\;$(pwd)\dependencies\install\lib\;$(pwd)\install\bin\;"| Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
 
+    - name: Set DYLD_LIBRARY_PATH on macOS for OSPRay dynamic loading
+      if: |
+        runner.os == 'macOS' &&
+        inputs.ospray_version != ''
+      shell: bash
+      run: echo "DYLD_LIBRARY_PATH=$(pwd)/dependencies/install/lib" >> $GITHUB_ENV
+
     # Works around for https://github.com/actions/runner-images/issues/10055
     - name: Remove conflicting libraries for Java bindings on Windows
       if: runner.os == 'Windows'
@@ -179,8 +213,9 @@
         -DCMAKE_INSTALL_PREFIX:PATH=../install
         -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON
         -DCMAKE_POLICY_DEFAULT_CMP0091=NEW
-        -DCMAKE_MODULE_PATH=${{ matrix.vtk_version != 'v9.2.6' && (runner.os != 'Windows' || matrix.vtk_version != 'v9.3.1') && inputs.optional_deps_label == 'optional-deps' && '$(pwd)/../dependencies/install/lib/cmake/OpenVDB/' || '' }}
+        -DCMAKE_MODULE_PATH=${{ (runner.os != 'Windows' || matrix.vtk_version != 'v9.3.0') && inputs.optional_deps_label == 'optional-deps' && '$(pwd)/../dependencies/install/lib/cmake/OpenVDB/' || '' }}
         -DCMAKE_PREFIX_PATH:PATH=$(pwd)/../dependencies/install/
+        -DF3D_BINDINGS_C=ON
         -DF3D_BINDINGS_JAVA=${{ (runner.os != 'macOS' || inputs.cpu == 'arm64') && inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
         -DF3D_BINDINGS_PYTHON=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
         -DF3D_BINDINGS_PYTHON_GENERATE_STUBS=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
@@ -189,7 +224,8 @@
         -DF3D_LINUX_INSTALL_DEFAULT_CONFIGURATION_FILE_IN_PREFIX=ON
         -DF3D_MACOS_BUNDLE=${{ inputs.bundle_label == 'bundle' && 'ON' || 'OFF' }}
         -DF3D_MODULE_EXR=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
-        -DF3D_MODULE_RAYTRACING=${{ inputs.raytracing_label == 'raytracing' && 'ON' || 'OFF' }}
+        -DF3D_MODULE_WEBP=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
+        -DF3D_MODULE_RAYTRACING=${{ inputs.ospray_version != '' && 'ON' || 'OFF' }}
         -DF3D_MODULE_UI=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
         -DF3D_MODULE_DMON=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
         -DF3D_MODULE_TINYFILEDIALOGS=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
@@ -200,7 +236,8 @@
         -DF3D_PLUGIN_BUILD_HDF=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
         -DF3D_PLUGIN_BUILD_OCCT=${{ inputs.optional_deps_label == 'optional-deps' && inputs.static_label == 'no-static' && 'ON' || 'OFF' }}
         -DF3D_PLUGIN_BUILD_USD=${{ inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
-        -DF3D_PLUGIN_BUILD_VDB=${{ matrix.vtk_version != 'v9.2.6' && (runner.os != 'Windows' || matrix.vtk_version != 'v9.3.1') && inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
+        -DF3D_PLUGIN_BUILD_VDB=${{ (runner.os != 'Windows' || matrix.vtk_version != 'v9.3.0') && inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
+        -DF3D_PLUGIN_OCCT_COLORING_SUPPORT=${{ inputs.occt_version != 'V7_6_3' && 'ON' || 'OFF' }}
         -DF3D_STRICT_BUILD=ON
         -DF3D_TESTING_ENABLE_EGL_TESTS=${{ runner.os == 'Linux' && 'ON' || 'OFF' }}
         -DF3D_TESTING_ENABLE_EXTERNAL_GLFW=${{ (runner.os == 'Linux' && inputs.rendering_backend == 'auto') && 'ON' || 'OFF' }}
@@ -265,12 +302,12 @@
         runner.os == 'macOS' &&
         inputs.cpu == 'arm64'
       shell: bash
-      run: echo "F3D_CTEST_EXCEPTIONS=(TestDepthPeelingToneMapping)|(TestDepthPeeling)|(TestTextureColor)|(TestDXF)|(TestScalarsCell)|(TestXCAFColors)|(TestGrid)|(TestConfig)|(TestGLTFDracoImporter)|(TestInteractionActors)|(TestInteractionFocalPointPickingPoints)|(TestInteractionCycleAnimation)|(TestInteractionAnimationSlow)|(TestInteractionConfigFileInteractions)|(TestGLTFRigArmature)|(TestInteractionRollCameraRotation)|(TestInteractionDragRotateVertical)|(TestInteractionCheatsheetConfigFile)" >> $GITHUB_ENV
+      run: echo "F3D_CTEST_EXCEPTIONS=(TestDepthPeelingToneMapping)|(TestDepthPeeling)|(TestTextureColor)|(TestDXF)|(TestScalarsCell)|(TestXCAFColors)|(TestGrid)|(TestConfig)|(TestGLTFDracoImporter)|(TestInteractionActors)|(TestInteractionFocalPointPickingPoints)|(TestInteractionCycleAnimation)|(TestInteractionAnimationSlow)|(TestInteractionConfigFileInteractions)|(TestGLTFRigArmature)|(TestInteractionRollCameraRotation)|(TestInteractionDragRotateVertical)|(TestInteractionCheatsheetConfigFile)|(TestInteractionCheatsheetWhiteBG)|(TestInteractionCheatsheetBlackBG)|(TestInteractionCheatsheetNoFile)|(TestInteractionCheatsheetOpacity)|(TestInteractionCheatsheetAnimationName)|(TestInteractionCheatsheetSSAA)|(TestMaxSizeAboveMultiFile)|(TestAxesCustomColor)" >> $GITHUB_ENV
 
     - name: Test
       shell: bash
       working-directory: ${{github.workspace}}/build
-      run: ctest -E "${{ env.F3D_CTEST_EXCEPTIONS }}" -C Release -j 2 --output-on-failure || ctest -E "${{ env.F3D_CTEST_EXCEPTIONS }}" -C Release -j 1 --rerun-failed -VV
+      run: ctest -E "${{ env.F3D_CTEST_EXCEPTIONS }}" -C Release -j 2 --output-on-failure --no-tests=error || ctest -E "${{ env.F3D_CTEST_EXCEPTIONS }}" -C Release -j 1 --rerun-failed --no-tests=error -VV
 
     - name: Upload Tests Artifact
       if: failure()
@@ -308,13 +345,26 @@
         cmake -Werror=dev -Werror=deprecated --warn-uninitialized -Df3d_DIR:PATH=$(pwd)/install/lib/cmake/f3d -DCMAKE_PREFIX_PATH:PATH=$(pwd)/dependencies/install/ -DBUILD_TESTING=ON -B build_python source/python
         cmake --build build_python --config Release
 
-    - name: Build and configure libf3d examples
+    - name: Configure libf3d examples (C++, C, Java)
       if: inputs.static_label == 'no-static'
       shell: bash
       working-directory: ${{github.workspace}}
-      run: |
-        cmake -Werror=dev -Werror=deprecated --warn-uninitialized -Df3d_DIR:PATH=$(pwd)/install/lib/cmake/f3d -DBUILD_TESTING=ON -B build_examples source/examples/libf3d
-        cmake --build build_examples --config Release
+      run: >
+        cmake -Werror=dev -Werror=deprecated --warn-uninitialized
+        -B build_examples -S source/examples/libf3d
+        -Df3d_DIR:PATH=$(pwd)/install/lib/cmake/f3d
+        -DBUILD_TESTING=ON
+        -DF3D_ENABLE_C_EXAMPLES=ON
+        -DF3D_ENABLE_JAVA_EXAMPLES=${{ (runner.os != 'macOS' || inputs.cpu == 'arm64') && inputs.optional_deps_label == 'optional-deps' && 'ON' || 'OFF' }}
+        -DF3D_EXAMPLES_EXTERNAL_GLFW=${{ (runner.os == 'Linux' && inputs.rendering_backend == 'auto' && inputs.vtk_version != 'v9.3.0') && 'ON' || 'OFF' }}
+        -DF3D_EXAMPLES_EXTERNAL_QT=${{ (runner.os == 'Linux' && inputs.rendering_backend == 'auto' && inputs.vtk_version != 'v9.3.0') && 'ON' || 'OFF' }}
+        -DF3D_EXAMPLES_EXTERNAL_FLTK=${{ (runner.os == 'Linux' && inputs.rendering_backend == 'auto' && inputs.vtk_version != 'v9.3.0') && 'ON' || 'OFF' }}
+
+    - name: Build libf3d examples
+      if: inputs.static_label == 'no-static'
+      shell: bash
+      working-directory: ${{github.workspace}}
+      run: cmake --build build_examples --config Release
 
     - name: Install Mesa Windows examples
       if: |
@@ -322,14 +372,14 @@
         inputs.static_label == 'no-static'
       uses: f3d-app/install-mesa-windows-action@v2
       with:
-        path: ${{github.workspace}}\build_examples\*\Release
+        path: ${{github.workspace}}\build_examples\*\*\Release
 
     # older VTK version create different renderings so they are not tested
     - name: Test libf3d examples
       if: inputs.static_label == 'no-static'
       shell: bash
       working-directory: ${{github.workspace}}
-      run: ctest --test-dir build_examples -C Release -VV
+      run: ctest --test-dir build_examples --no-tests=error -C Release -VV
 
     - name: Install optional component f3d plugins
       shell: bash
@@ -350,7 +400,7 @@
       if: inputs.static_label == 'no-static'
       shell: bash
       working-directory: ${{github.workspace}}
-      run: ctest --test-dir build_plugins -C Release -VV
+      run: ctest --test-dir build_plugins --no-tests=error -C Release -VV
 
     - name: Set F3D binary path Linux
       if: runner.os == 'Linux'
@@ -388,7 +438,7 @@
     # https://github.com/f3d-app/f3d/issues/1276
     - name: Check F3D_PLUGINS_PATH using plugin example
       if: |
-        inputs.vtk_version != 'v9.2.6' && inputs.vtk_version != 'v9.3.1' &&
+        inputs.vtk_version != 'v9.3.0' && inputs.vtk_version != 'v9.4.2' && inputs.vtk_version != 'v9.5.2' &&
         inputs.cpu != 'arm64' && inputs.static_label == 'no-static' && inputs.optional_deps_label == 'optional-deps'
       shell: bash
       working-directory: ${{github.workspace}}/install
@@ -406,7 +456,7 @@
     # https://github.com/f3d-app/f3d/issues/1276
     - name: Check Install
       if: |
-        inputs.vtk_version != 'v9.2.6' && inputs.vtk_version != 'v9.3.1'
+        inputs.vtk_version != 'v9.3.0' && inputs.vtk_version != 'v9.4.2' && inputs.vtk_version != 'v9.5.2'
         && inputs.cpu != 'arm64' && inputs.optional_deps_label == 'optional-deps'
       shell: bash
       working-directory: ${{github.workspace}}/install
@@ -415,8 +465,7 @@
 
     - name: Check Install plugins
       if: |
-        inputs.vtk_version != 'v9.2.6'
-        && inputs.vtk_version != 'v9.3.1' && inputs.cpu != 'arm64'
+        inputs.vtk_version != 'v9.3.0' && inputs.vtk_version != 'v9.4.2' && inputs.vtk_version != 'v9.5.2' && inputs.cpu != 'arm64'
         && inputs.static_label == 'no-static' && inputs.optional_deps_label == 'optional-deps'
       shell: bash
       working-directory: ${{github.workspace}}/install
@@ -425,7 +474,7 @@
 
     - name: Check Install Windows Console Executable
       if: |
-        inputs.vtk_version != 'v9.2.6' && inputs.vtk_version != 'v9.3.1'
+        inputs.vtk_version != 'v9.3.0' && inputs.vtk_version != 'v9.4.2' && inputs.vtk_version != 'v9.5.2' 
         && runner.os == 'Windows'
       shell: bash
       working-directory: ${{github.workspace}}/install
diff -Nru f3d-3.2.0+dfsg/.github/actions/generic-dependencies/action.yml f3d-3.4.1/.github/actions/generic-dependencies/action.yml
--- f3d-3.2.0+dfsg/.github/actions/generic-dependencies/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/generic-dependencies/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,10 +1,6 @@
 name: "Install F3D dependencies"
 description: "Install all F3D Dependencies but VTK"
 inputs:
-  raytracing_label:
-    description: "Label to control raytracing"
-    required: false
-    default: "no-raytracing"
   optional_deps_label:
     description: "Label to optional dependencies"
     required: false
@@ -19,9 +15,15 @@
   assimp_version:
     description: "Version of assimp to build"
     required: false
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
@@ -31,12 +33,27 @@
   openvdb_version:
     description: "Version of openvdb to build"
     required: false
+  ospray_version:
+    description: "Version of ospray to build"
+    required: false
   pybind11_version:
     description: "Version of pybind11 to build"
     required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   usd_version:
     description: "Version of usd to build"
     required: false
+  webp_version:
+    description: "Version of webp to build"
+    required: false
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -53,13 +70,19 @@
       uses: ./source/.github/actions/vtk-dependencies
       with:
         cpu: ${{inputs.cpu}}
+        blosc_version: ${{inputs.blosc_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install Raytracing Dependencies
-      if: inputs.raytracing_label == 'raytracing'
+      if: inputs.ospray_version != ''
       uses: ./source/.github/actions/ospray-sb-install-dep
       with:
+        version: ${{inputs.ospray_version}}
         cpu: ${{inputs.cpu}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install F3D dependencies
       if: inputs.optional_deps_label == 'optional-deps'
@@ -69,7 +92,11 @@
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
         pybind11_version: ${{inputs.pybind11_version}}
+        tbb_version: ${{inputs.tbb_version}}
         usd_version: ${{inputs.usd_version}}
+        webp_version: ${{inputs.webp_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
diff -Nru f3d-3.2.0+dfsg/.github/actions/imath-install-dep/action.yml f3d-3.4.1/.github/actions/imath-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/imath-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/imath-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -5,25 +5,37 @@
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  version:
+    description: "Version of imath to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache Imath
       id: cache-imath
       uses: actions/cache/restore@v4
       with:
         path: dependencies/imath_install
-        key: imath-v3.1.12-${{runner.os}}-${{inputs.cpu}}-0
+        key: imath-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
-    # Dependents: alembic
+    # Dependents: alembic openexr
     - name: Checkout Imath
       if: steps.cache-imath.outputs.cache-hit != 'true'
       uses: actions/checkout@v4
       with:
         repository: AcademySoftwareFoundation/Imath
         path: "./dependencies/imath"
-        ref: v3.1.12
+        ref: ${{inputs.version}}
 
     - name: Setup Imath
       if: steps.cache-imath.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/occt-install-dep/action.yml f3d-3.4.1/.github/actions/occt-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/occt-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/occt-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,9 @@
   version:
     description: "Version of occt to build"
     required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,13 +19,14 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache OCCT
       id: cache-occt
       uses: actions/cache/restore@v4
       with:
         path: dependencies/occt_install
-        key: occt-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-4
+        key: occt-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-1
 
     - name: Checkout OCCT
       if: steps.cache-occt.outputs.cache-hit != 'true'
@@ -74,6 +78,8 @@
         ${{ runner.os == 'macOS' && '-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0' || null }}
         ${{ runner.os == 'Windows' && '-Ax64 -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL' || null }}
 
+    # Build without XCAF (TKXDESTEP;TKXDEIGES;TKBinXCAF) in minver for better testing
+    # Drop this when V7_8_0 is the new minver for occt
     - name: Configure OCCT min version
       if: |
         steps.cache-occt.outputs.cache-hit != 'true' &&
@@ -82,7 +88,7 @@
       shell: bash
       run: >
         cmake ../occt
-        -DBUILD_ADDITIONAL_TOOLKITS="TKSTEP;TKIGES;TKMesh;TKXDESTEP;TKXDEIGES;TKBinXCAF"
+        -DBUILD_ADDITIONAL_TOOLKITS="TKSTEP;TKIGES;TKMesh"
         -DBUILD_DOC_Overview=OFF
         -DBUILD_LIBRARY_TYPE=Shared
         -DBUILD_MODULE_ApplicationFramework=OFF
diff -Nru f3d-3.2.0+dfsg/.github/actions/openexr-install-dep/action.yml f3d-3.4.1/.github/actions/openexr-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/openexr-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/openexr-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,12 @@
   version:
     description: "Version of openexr to build"
     required: true
+  imath_version:
+    description: "Version of imath to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,15 +22,16 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache OpenEXR
       id: cache-openexr
       uses: actions/cache/restore@v4
       with:
         path: dependencies/openexr_install
-        key: openexr-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-0
+        key: openexr-${{inputs.version}}-${{inputs.imath_version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
-    # Dependents (not version): usd
+    # Dependents: usd
     - name: Checkout OpenEXR
       if: steps.cache-openexr.outputs.cache-hit != 'true'
       uses: actions/checkout@v4
@@ -41,6 +48,13 @@
         mkdir openexr_build
         mkdir openexr_install
 
+    # OpenEXR 3.0.1 needs patching with recent libstd
+    - name: Patch OpenEXR v3.0.1
+      if: steps.cache-openexr.outputs.cache-hit != 'true' && inputs.version == 'v3.0.1'
+      working-directory: ${{github.workspace}}/dependencies/openexr
+      shell: bash
+      run: patch -p1 < $GITHUB_ACTION_PATH/include_cstdint.patch
+
     - name: Configure OpenEXR
       if: steps.cache-openexr.outputs.cache-hit != 'true'
       working-directory: ${{github.workspace}}/dependencies/openexr_build
diff -Nru f3d-3.2.0+dfsg/.github/actions/openexr-install-dep/include_cstdint.patch f3d-3.4.1/.github/actions/openexr-install-dep/include_cstdint.patch
--- f3d-3.2.0+dfsg/.github/actions/openexr-install-dep/include_cstdint.patch	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/actions/openexr-install-dep/include_cstdint.patch	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,39 @@
+diff --git a/src/lib/OpenEXR/ImfDeepTiledInputFile.h b/src/lib/OpenEXR/ImfDeepTiledInputFile.h
+index b01330f3..2f765fdb 100644
+--- a/src/lib/OpenEXR/ImfDeepTiledInputFile.h
++++ b/src/lib/OpenEXR/ImfDeepTiledInputFile.h
+@@ -21,6 +21,8 @@
+ 
+ #include <ImathBox.h>
+ 
++#include <cstdint>
++
+ OPENEXR_IMF_INTERNAL_NAMESPACE_HEADER_ENTER
+ 
+ class IMF_EXPORT_TYPE DeepTiledInputFile : public GenericInputFile
+diff --git a/src/lib/OpenEXR/ImfDeepTiledInputPart.h b/src/lib/OpenEXR/ImfDeepTiledInputPart.h
+index e5f950a9..a5533b49 100644
+--- a/src/lib/OpenEXR/ImfDeepTiledInputPart.h
++++ b/src/lib/OpenEXR/ImfDeepTiledInputPart.h
+@@ -12,6 +12,8 @@
+ 
+ #include <ImathBox.h>
+ 
++#include <cstdint>
++
+ OPENEXR_IMF_INTERNAL_NAMESPACE_HEADER_ENTER
+ 
+ class IMF_EXPORT_TYPE DeepTiledInputPart
+diff --git a/src/lib/OpenEXR/ImfTiledMisc.h b/src/lib/OpenEXR/ImfTiledMisc.h
+index 9059ae2c..9c016148 100644
+--- a/src/lib/OpenEXR/ImfTiledMisc.h
++++ b/src/lib/OpenEXR/ImfTiledMisc.h
+@@ -19,7 +19,7 @@
+ 
+ #include <stdio.h>
+ #include <vector>
+-
++#include <cstdint>
+ 
+ OPENEXR_IMF_INTERNAL_NAMESPACE_HEADER_ENTER
+ 
diff -Nru f3d-3.2.0+dfsg/.github/actions/openvdb-install-dep/action.yml f3d-3.4.1/.github/actions/openvdb-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/openvdb-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/openvdb-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,18 @@
   version:
     description: "Version of openvdb to build"
     required: true
+  blosc_version:
+    description: "Version of blosc to build against"
+    required: true
+  tbb_version:
+    description: "Version of tbb to build against"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -16,15 +28,19 @@
       shell: bash
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.blosc_version }}" ]] || { echo "blosc_version input is empty" ; exit 1; }
+        [[ "${{ inputs.tbb_version }}" ]] || { echo "tbb_version input is empty" ; exit 1; }
+        [[ "${{ inputs.zlib_version }}" ]] || { echo "zlib_version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache OpenVDB
       id: cache-openvdb
       uses: actions/cache/restore@v4
       with:
         path: dependencies/openvdb_install
-        key: openvdb-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-0
+        key: openvdb-${{inputs.version}}-${{inputs.zlib_version}}-${{inputs.blosc_version}}-${{inputs.tbb_version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
-    # Dependents (not version): vtk
+    # Dependents: vtk
     - name: Checkout OpenVDB
       if: steps.cache-openvdb.outputs.cache-hit != 'true'
       uses: actions/checkout@v4
diff -Nru f3d-3.2.0+dfsg/.github/actions/ospray-sb-install-dep/0001-fix-embree-for-modern-clang.patch f3d-3.4.1/.github/actions/ospray-sb-install-dep/0001-fix-embree-for-modern-clang.patch
--- f3d-3.2.0+dfsg/.github/actions/ospray-sb-install-dep/0001-fix-embree-for-modern-clang.patch	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/actions/ospray-sb-install-dep/0001-fix-embree-for-modern-clang.patch	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,74 @@
+From 61b6983c68e620710069a2c8c45c621e949fa3d1 Mon Sep 17 00:00:00 2001
+From: Mathieu Westphal <mathieu.westphal@kitware.com>
+Date: Sun, 26 Oct 2025 08:42:08 +0100
+Subject: [PATCH] fix embree for modern clang
+
+---
+ .../superbuild/dependencies/dep_embree.cmake  |  1 +
+ ...embree-fix-output-operator-issue-486.patch | 42 +++++++++++++++++++
+ 2 files changed, 43 insertions(+)
+ create mode 100644 scripts/superbuild/dependencies/embree-fix-output-operator-issue-486.patch
+
+diff --git a/scripts/superbuild/dependencies/dep_embree.cmake b/scripts/superbuild/dependencies/dep_embree.cmake
+index 6d442bf94..f0ebbaa32 100644
+--- a/scripts/superbuild/dependencies/dep_embree.cmake
++++ b/scripts/superbuild/dependencies/dep_embree.cmake
+@@ -31,6 +31,7 @@ if (BUILD_EMBREE_FROM_SOURCE)
+     LIST_SEPARATOR | # Use the alternate list separator
+     ${EMBREE_CLONE_URL}
+     ${EMBREE_URL_HASH}
++    PATCH_COMMAND git init -q . && git apply -v -p1 < ${CMAKE_CURRENT_SOURCE_DIR}/dependencies/embree-fix-output-operator-issue-486.patch
+     CMAKE_ARGS
+       -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}
+       -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
+diff --git a/scripts/superbuild/dependencies/embree-fix-output-operator-issue-486.patch b/scripts/superbuild/dependencies/embree-fix-output-operator-issue-486.patch
+new file mode 100644
+index 000000000..d5b652090
+--- /dev/null
++++ b/scripts/superbuild/dependencies/embree-fix-output-operator-issue-486.patch
+@@ -0,0 +1,42 @@
++From 5669d422255b15f4be33589c2b31564f319be529 Mon Sep 17 00:00:00 2001
++From: Daniel Opitz <daniel.opitz@intel.com>
++Date: Mon, 13 May 2024 10:17:51 +0200
++Subject: [PATCH] fix output operator, issue #486
++
++---
++ kernels/geometry/pointi.h     | 4 ++--
++ kernels/subdiv/bezier_curve.h | 2 +-
++ 2 files changed, 3 insertions(+), 3 deletions(-)
++
++diff --git a/kernels/geometry/pointi.h b/kernels/geometry/pointi.h
++index f81edb903..aba8ec4ab 100644
++--- a/kernels/geometry/pointi.h
+++++ b/kernels/geometry/pointi.h
++@@ -210,9 +210,9 @@ namespace embree
++     };
++ 
++     /*! output operator */
++-    friend __forceinline embree_ostream operator<<(embree_ostream cout, const PointMi& line)
+++    friend __forceinline embree_ostream operator<<(embree_ostream cout, const PointMi& point)
++     {
++-      return cout << "Line" << M << "i {" << line.v0 << ", " << line.geomID() << ", " << line.primID() << "}";
+++      return cout << "Point" << M << "i {" << point.geomID() << ", " << point.primID() << "}";
++     }
++ 
++    public:
++diff --git a/kernels/subdiv/bezier_curve.h b/kernels/subdiv/bezier_curve.h
++index 257e0afd4..5e3b5c83b 100644
++--- a/kernels/subdiv/bezier_curve.h
+++++ b/kernels/subdiv/bezier_curve.h
++@@ -135,7 +135,7 @@ namespace embree
++       }
++       
++       friend embree_ostream operator<<(embree_ostream cout, const QuadraticBezierCurve& a) {
++-        return cout << "QuadraticBezierCurve ( (" << a.u.lower << ", " << a.u.upper << "), " << a.v0 << ", " << a.v1 << ", " << a.v2 << ")";
+++        return cout << "QuadraticBezierCurve (" << a.v0 << ", " << a.v1 << ", " << a.v2 << ")";
++       }
++     };
++   
++-- 
++2.51.1
++
+-- 
+2.51.1
+
diff -Nru f3d-3.2.0+dfsg/.github/actions/ospray-sb-install-dep/action.yml f3d-3.4.1/.github/actions/ospray-sb-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/ospray-sb-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/ospray-sb-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,37 +1,56 @@
 name: "Install ospray sb Dependency"
 description: "Install ospray sb Dependency using cache when possible"
 inputs:
+  version:
+    description: "Version of ospray to build"
+    required: true
   cpu:
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache ospray
       id: cache-ospray
       uses: actions/cache/restore@v4
       with:
         path: dependencies/ospray_install
-        key: ospray-sb-v2.7.1-${{runner.os}}-${{inputs.cpu}}-3
+        key: ospray-sb-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
     # Dependents: vtk
     - name: Checkout ospray
       if: steps.cache-ospray.outputs.cache-hit != 'true'
       uses: actions/checkout@v4
       with:
-        repository: ospray/ospray
+        repository: RenderKit/ospray
         path: "./dependencies/ospray"
-        ref: v2.7.1
+        ref: ${{inputs.version}}
 
-      # https://github.com/ospray/ospray/pull/550
+    # https://github.com/ospray/ospray/pull/550
     - name: Patch ospray rkcommon superbuild
       if: steps.cache-ospray.outputs.cache-hit != 'true'
       working-directory: ${{github.workspace}}/dependencies/ospray
       shell: bash
       run: patch -p1 < $GITHUB_ACTION_PATH/0001-Fix-INSTALL_RPATH-in-rkcommon-superbuild.patch
 
+    # https://github.com/RenderKit/embree/issues/486
+    - name: Patch ospray embree superbuild
+      if: steps.cache-ospray.outputs.cache-hit != 'true'
+      working-directory: ${{github.workspace}}/dependencies/ospray
+      shell: bash
+      run: patch -p1 < $GITHUB_ACTION_PATH/0001-fix-embree-for-modern-clang.patch
+
     - name: Setup ospray
       if: steps.cache-ospray.outputs.cache-hit != 'true'
       working-directory: ${{github.workspace}}/dependencies
@@ -51,7 +70,7 @@
       shell: bash
       run: >
         cmake ../ospray/scripts/superbuild/
-        -DBUILD_EMBREE_FROM_SOURCE=ON
+        -DBUILD_EMBREE_FROM_SOURCE=${{ runner.os == 'Windows' && 'OFF' || 'ON' }}
         -DBUILD_GLFW=OFF
         -DBUILD_OIDN=ON
         -DBUILD_OIDN_FROM_SOURCE=ON
diff -Nru f3d-3.2.0+dfsg/.github/actions/pybind11-install-dep/action.yml f3d-3.4.1/.github/actions/pybind11-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/pybind11-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/pybind11-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -4,16 +4,25 @@
   version:
     description: "Version of pybind11 to build"
     required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache pybind11
       id: cache-pybind11
       uses: actions/cache/restore@v4
       with:
         path: dependencies/pybind11_install
-        key: pybind11-${{inputs.version}}-${{runner.os}}-0
+        key: pybind11-${{inputs.version}}-${{runner.os}}-${{inputs.global_cache_index}}-0
 
     - name: Checkout pybind11
       if: steps.cache-pybind11.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/python-ci/action.yml f3d-3.4.1/.github/actions/python-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/python-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/python-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -4,18 +4,21 @@
   lfs_sha:
     description: "Cache LFS sha"
     required: true
-  python_version:
-    description: "Python version"
-    required: true
   alembic_version:
     description: "Version of alembic to build"
     required: false
   assimp_version:
     description: "Version of assimp to build"
     required: false
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
@@ -28,12 +31,27 @@
   pybind11_version:
     description: "Version of pybind11 to build"
     required: false
+  python_version:
+    description: "Python version"
+    required: true
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   usd_version:
     description: "Version of usd to build"
     required: false
   vtk_version:
     description: "Version of vtk to build"
     required: true
+  webp_version:
+    description: "Version of webp to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -46,7 +64,7 @@
         [[ "${{ inputs.vtk_version }}" ]] || { echo "vtk_version input is empty" ; exit 1; }
 
     - name: Recover LFS Data
-      uses: f3d-app/lfs-data-cache-action@v1
+      uses: f3d-app/lfs-data-cache-action@v2
       with:
         type: "consumer"
         lfs_sha: ${{inputs.lfs_sha}}
@@ -57,18 +75,28 @@
       with:
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
+        blosc_version: ${{inputs.blosc_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
         openvdb_version: ${{inputs.openvdb_version}}
         pybind11_version: ${{inputs.pybind11_version}}
+        tbb_version: ${{inputs.tbb_version}}
         usd_version: ${{inputs.usd_version}}
+        webp_version: ${{inputs.webp_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/.github/actions/vtk-install-dep
       with:
-        vtk_version: ${{inputs.vtk_version}}
+        blosc_version: ${{inputs.blosc_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Set up Python
       uses: actions/setup-python@v5
@@ -80,6 +108,8 @@
       run: |
         python -m pip install --upgrade pip
         python -m pip install pytest==8.0.0
+        python -m pip install pyopengltk==0.0.4
+        python -m pip install PySide6==6.10.1
 
     - name: Set PATH windows
       if: runner.os == 'Windows'
@@ -118,6 +148,7 @@
         -DF3D_MACOS_BUNDLE=OFF;
         -DF3D_MODULE_EXR=ON;
         -DF3D_MODULE_UI=ON;
+        -DF3D_MODULE_WEBP=ON;
         -DF3D_BUILD_APPLICATION=OFF" >> $GITHUB_ENV
 
     - name: Build wheel
@@ -147,7 +178,8 @@
       with:
         path: ${{ env.Python_ROOT_DIR }}
 
+    # Qt6 tests only supported on windows for now https://github.com/f3d-app/f3d/issues/2670
     - name: Test wheel
       shell: bash
       working-directory: ${{github.workspace}}/source
-      run: python -m pytest
+      run: python -m pytest ${{ runner.os != 'Windows' && '-k "not test_minimal_qt6 and not test_minimal_qml"' || null }}
diff -Nru f3d-3.2.0+dfsg/.github/actions/sanitizer-ci/action.yml f3d-3.4.1/.github/actions/sanitizer-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/sanitizer-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/sanitizer-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -4,9 +4,6 @@
   sanitizer_type:
     description: "Type of sanitizer"
     required: true
-  vtk_version:
-    description: "VTK version"
-    required: true
   lfs_sha:
     description: "Cache LFS sha"
     required: true
@@ -16,15 +13,36 @@
   assimp_version:
     description: "Version of assimp to build"
     required: false
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: false
+  imath_version:
+    description: "Version of imath to build"
+    required: false
   occt_version:
     description: "Version of occt to build"
     required: false
   openexr_version:
     description: "Version of openexr to build"
     required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
+  vtk_version:
+    description: "VTK version"
+    required: true
+  webp_version:
+    description: "webP version"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -37,7 +55,7 @@
         [[ "${{ inputs.vtk_version }}" ]] || { echo "vtk_version input is empty" ; exit 1; }
 
     - name: Recover LFS Data
-      uses: f3d-app/lfs-data-cache-action@v1
+      uses: f3d-app/lfs-data-cache-action@v2
       with:
         type: "consumer"
         lfs_sha: ${{inputs.lfs_sha}}
@@ -48,14 +66,24 @@
       with:
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
+        blosc_version: ${{inputs.blosc_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        webp_version: ${{inputs.webp_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/.github/actions/vtk-install-dep
       with:
+        blosc_version: ${{inputs.blosc_version}}
+        tbb_version: ${{inputs.tbb_version}}
         vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Setup Build Directory
       shell: bash
@@ -78,6 +106,7 @@
         -DCMAKE_PREFIX_PATH:PATH=$(pwd)/../dependencies/install/
         -DF3D_MODULE_EXR=ON
         -DF3D_MODULE_UI=ON
+        -DF3D_MODULE_WEBP=ON
         -DF3D_PLUGIN_BUILD_ALEMBIC=ON
         -DF3D_PLUGIN_BUILD_ASSIMP=ON
         -DF3D_PLUGIN_BUILD_DRACO=ON
@@ -92,6 +121,7 @@
         -DF3D_TESTING_ENABLE_GLX_TESTS=ON
         -DF3D_TESTING_ENABLE_LONG_TIMEOUT_TESTS=ON
         -DF3D_TESTING_ENABLE_OSMESA_TESTS=ON
+        ${{ inputs.sanitizer_type == 'undefined' && '-DF3D_BINDINGS_C=OFF' || '' }}
 
     - name: Build
       shell: bash
diff -Nru f3d-3.2.0+dfsg/.github/actions/static-analysis-ci/action.yml f3d-3.4.1/.github/actions/static-analysis-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/static-analysis-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/static-analysis-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -10,9 +10,15 @@
   assimp_version:
     description: "Version of assimp to build"
     required: true
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   draco_version:
     description: "Version of draco to build"
     required: true
+  imath_version:
+    description: "Version of imath to build"
+    required: true
   occt_version:
     description: "Version of occt to build"
     required: true
@@ -25,9 +31,21 @@
   usd_version:
     description: "Version of usd to build"
     required: true
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
   vtk_version:
     description: "Version of vtk to build"
     required: true
+  webp_version:
+    description: "Version of webp to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -40,7 +58,7 @@
 
     # Needed for BUILD_TESTING
     - name: Recover LFS Data
-      uses: f3d-app/lfs-data-cache-action@v1
+      uses: f3d-app/lfs-data-cache-action@v2
       with:
         type: "consumer"
         lfs_sha: ${{inputs.lfs_sha}}
@@ -51,17 +69,27 @@
       with:
         alembic_version: ${{inputs.alembic_version}}
         assimp_version: ${{inputs.assimp_version}}
+        blosc_version: ${{inputs.blosc_version}}
         draco_version: ${{inputs.draco_version}}
+        imath_version: ${{inputs.imath_version}}
         occt_version: ${{inputs.occt_version}}
         openexr_version: ${{inputs.openexr_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        tbb_version: ${{inputs.tbb_version}}
         usd_version: ${{inputs.usd_version}}
+        webp_version: ${{inputs.webp_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install VTK dependency
       uses: ./source/.github/actions/vtk-install-dep
       with:
-        vtk_version: ${{inputs.vtk_version}}
+        blosc_version: ${{inputs.blosc_version}}
         openvdb_version: ${{inputs.openvdb_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        vtk_version: ${{inputs.vtk_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Setup Build Directory
       shell: bash
@@ -84,6 +112,7 @@
         -DCMAKE_PREFIX_PATH:PATH=$(pwd)/../dependencies/install/
         -DF3D_MODULE_EXR=ON
         -DF3D_MODULE_UI=ON
+        -DF3D_MODULE_WEBP=ON
         -DF3D_PLUGIN_BUILD_ALEMBIC=ON
         -DF3D_PLUGIN_BUILD_ASSIMP=ON
         -DF3D_PLUGIN_BUILD_DRACO=ON
diff -Nru f3d-3.2.0+dfsg/.github/actions/tbb-install-dep/action.yml f3d-3.4.1/.github/actions/tbb-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/tbb-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/tbb-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -5,16 +5,28 @@
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  version:
+    description: "Version of tbb to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache TBB
       id: cache-tbb
       uses: actions/cache/restore@v4
       with:
         path: dependencies/tbb_install
-        key: tbb-v2021.12.0-${{runner.os}}-${{inputs.cpu}}-2
+        key: tbb-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
     # Dependents: usd openvdb vtk
     # v2021.13.0 somehow cause memory issue in draco and alembic
@@ -25,7 +37,7 @@
       with:
         repository: oneapi-src/oneTBB
         path: "./dependencies/tbb"
-        ref: v2021.12.0
+        ref: ${{inputs.version}}
 
     - name: Setup TBB
       if: steps.cache-tbb.outputs.cache-hit != 'true'
diff -Nru f3d-3.2.0+dfsg/.github/actions/usd-install-dep/action.yml f3d-3.4.1/.github/actions/usd-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/usd-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/usd-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,12 @@
   openexr_version:
     description: "Version of openexr to build against"
     required: true
+  tbb_version:
+    description: "Version of tbb to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -20,13 +26,15 @@
       run: |
         [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
         [[ "${{ inputs.openexr_version }}" ]] || { echo "openexr_version input is empty" ; exit 1; }
+        [[ "${{ inputs.tbb_version }}" ]] || { echo "tbb_version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache USD
       id: cache-usd
       uses: actions/cache/restore@v4
       with:
         path: dependencies/usd_install
-        key: usd-${{inputs.version}}-${{inputs.openexr_version}}-${{runner.os}}-${{inputs.cpu}}-0
+        key: usd-${{inputs.version}}-${{inputs.openexr_version}}-${{inputs.tbb_version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
     - name: Checkout USD
       if: steps.cache-usd.outputs.cache-hit != 'true'
@@ -38,7 +46,14 @@
 
     # Upstream issue: https://github.com/PixarAnimationStudios/OpenUSD/issues/2490
     - name: Patch USD
-      if: steps.cache-usd.outputs.cache-hit != 'true'
+      if: steps.cache-usd.outputs.cache-hit != 'true' && inputs.version == 'v24.08'
+      working-directory: ${{github.workspace}}/dependencies/usd
+      shell: bash
+      run: patch -p1 < $GITHUB_ACTION_PATH/usd-install-bin-mindep.patch
+
+    # Upstream issue: https://github.com/PixarAnimationStudios/OpenUSD/issues/2490
+    - name: Patch USD
+      if: steps.cache-usd.outputs.cache-hit != 'true' && inputs.version != 'v24.08'
       working-directory: ${{github.workspace}}/dependencies/usd
       shell: bash
       run: patch -p1 < $GITHUB_ACTION_PATH/usd-install-bin.patch
diff -Nru f3d-3.2.0+dfsg/.github/actions/usd-install-dep/usd-install-bin-mindep.patch f3d-3.4.1/.github/actions/usd-install-dep/usd-install-bin-mindep.patch
--- f3d-3.2.0+dfsg/.github/actions/usd-install-dep/usd-install-bin-mindep.patch	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/actions/usd-install-dep/usd-install-bin-mindep.patch	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,77 @@
+diff --git a/cmake/macros/Private.cmake b/cmake/macros/Private.cmake
+index 23172745e..dddc5c2ee 100644
+--- a/cmake/macros/Private.cmake
++++ b/cmake/macros/Private.cmake
+@@ -1224,6 +1224,7 @@ function(_pxr_library NAME)
+     _get_install_dir("include" headerInstallDir)
+     _get_install_dir("include/${PXR_PREFIX}/${NAME}" headerInstallPrefix)
+     _get_install_dir("lib" libInstallPrefix)
++    _get_install_dir("bin" binInstallPrefix)
+     if(isPlugin)
+         if(NOT isObject)
+             # A plugin embedded in the monolithic library is found in
+@@ -1432,12 +1433,12 @@ function(_pxr_library NAME)
+                 TARGETS ${NAME}
+                 LIBRARY DESTINATION ${libInstallPrefix}
+                 ARCHIVE DESTINATION ${libInstallPrefix}
+-                RUNTIME DESTINATION ${libInstallPrefix}
++                RUNTIME DESTINATION ${binInstallPrefix}
+             )
+             if(WIN32)
+                 install(
+                     FILES $<TARGET_PDB_FILE:${NAME}>
+-                    DESTINATION ${libInstallPrefix}
++                    DESTINATION ${binInstallPrefix}
+                     OPTIONAL
+                 )
+             endif()
+@@ -1447,13 +1448,13 @@ function(_pxr_library NAME)
+                 EXPORT pxrTargets
+                 LIBRARY DESTINATION ${libInstallPrefix}
+                 ARCHIVE DESTINATION ${libInstallPrefix}
+-                RUNTIME DESTINATION ${libInstallPrefix}
++                RUNTIME DESTINATION ${binInstallPrefix}
+             )
+             if(WIN32)
+                 install(
+                     FILES $<TARGET_PDB_FILE:${NAME}>
+                     EXPORT pxrTargets
+-                    DESTINATION ${libInstallPrefix}
++                    DESTINATION ${binInstallPrefix}
+                     OPTIONAL
+                 )
+             endif()
+@@ -1463,7 +1464,7 @@ function(_pxr_library NAME)
+                 EXPORT pxrTargets
+                 LIBRARY DESTINATION ${libInstallPrefix}
+                 ARCHIVE DESTINATION ${libInstallPrefix}
+-                RUNTIME DESTINATION ${libInstallPrefix}
++                RUNTIME DESTINATION ${binInstallPrefix}
+             )
+         endif()
+     
+diff --git a/cmake/macros/Public.cmake b/cmake/macros/Public.cmake
+index a1734b6a2..397315e8e 100644
+--- a/cmake/macros/Public.cmake
++++ b/cmake/macros/Public.cmake
+@@ -1005,17 +1005,18 @@ function(pxr_toplevel_prologue)
+                     IMPORT_PREFIX "${libPrefix}"
+             )
+             _get_install_dir("lib" libInstallPrefix)
++            _get_install_dir("bin" binInstallPrefix)
+             install(
+                 TARGETS usd_ms
+                 EXPORT pxrTargets
+                 LIBRARY DESTINATION ${libInstallPrefix}
+                 ARCHIVE DESTINATION ${libInstallPrefix}
+-                RUNTIME DESTINATION ${libInstallPrefix}
++                RUNTIME DESTINATION ${binInstallPrefix}
+             )
+             if(WIN32)
+                 install(
+                     FILES $<TARGET_PDB_FILE:usd_ms>
+-                    DESTINATION ${libInstallPrefix}
++                    DESTINATION ${binInstallPrefix}
+                     OPTIONAL
+                 )
+             endif()
diff -Nru f3d-3.2.0+dfsg/.github/actions/usd-install-dep/usd-install-bin.patch f3d-3.4.1/.github/actions/usd-install-dep/usd-install-bin.patch
--- f3d-3.2.0+dfsg/.github/actions/usd-install-dep/usd-install-bin.patch	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/usd-install-dep/usd-install-bin.patch	2026-01-08 08:41:55.000000000 +0200
@@ -1,8 +1,18 @@
+From e70fcb8533e7ec780f94d96152506f0365f56c97 Mon Sep 17 00:00:00 2001
+From: Mathieu Westphal <mathieu.westphal@kitware.com>
+Date: Wed, 29 Oct 2025 17:21:46 +0100
+Subject: [PATCH] install-bin
+
+---
+ cmake/macros/Private.cmake | 11 ++++++-----
+ cmake/macros/Public.cmake  |  5 +++--
+ 2 files changed, 9 insertions(+), 7 deletions(-)
+
 diff --git a/cmake/macros/Private.cmake b/cmake/macros/Private.cmake
-index 23172745e..dddc5c2ee 100644
+index b90b743de..a08164afd 100644
 --- a/cmake/macros/Private.cmake
 +++ b/cmake/macros/Private.cmake
-@@ -1224,6 +1224,7 @@ function(_pxr_library NAME)
+@@ -1231,6 +1231,7 @@ function(_pxr_library NAME)
      _get_install_dir("include" headerInstallDir)
      _get_install_dir("include/${PXR_PREFIX}/${NAME}" headerInstallPrefix)
      _get_install_dir("lib" libInstallPrefix)
@@ -10,7 +20,7 @@
      if(isPlugin)
          if(NOT isObject)
              # A plugin embedded in the monolithic library is found in
-@@ -1432,12 +1433,12 @@ function(_pxr_library NAME)
+@@ -1423,12 +1424,12 @@ function(_pxr_library NAME)
                  TARGETS ${NAME}
                  LIBRARY DESTINATION ${libInstallPrefix}
                  ARCHIVE DESTINATION ${libInstallPrefix}
@@ -25,7 +35,7 @@
                      OPTIONAL
                  )
              endif()
-@@ -1447,13 +1448,13 @@ function(_pxr_library NAME)
+@@ -1438,13 +1439,13 @@ function(_pxr_library NAME)
                  EXPORT pxrTargets
                  LIBRARY DESTINATION ${libInstallPrefix}
                  ARCHIVE DESTINATION ${libInstallPrefix}
@@ -41,7 +51,7 @@
                      OPTIONAL
                  )
              endif()
-@@ -1463,7 +1464,7 @@ function(_pxr_library NAME)
+@@ -1454,7 +1455,7 @@ function(_pxr_library NAME)
                  EXPORT pxrTargets
                  LIBRARY DESTINATION ${libInstallPrefix}
                  ARCHIVE DESTINATION ${libInstallPrefix}
@@ -51,16 +61,16 @@
          endif()
      
 diff --git a/cmake/macros/Public.cmake b/cmake/macros/Public.cmake
-index a1734b6a2..397315e8e 100644
+index b8e588bf3..418234a5d 100644
 --- a/cmake/macros/Public.cmake
 +++ b/cmake/macros/Public.cmake
-@@ -1005,17 +1005,18 @@ function(pxr_toplevel_prologue)
-                     IMPORT_PREFIX "${libPrefix}"
+@@ -1154,17 +1154,18 @@ function(pxr_toplevel_prologue)
+                     OUTPUT_NAME ${libName}
              )
              _get_install_dir("lib" libInstallPrefix)
 +            _get_install_dir("bin" binInstallPrefix)
              install(
-                 TARGETS usd_ms
+                 TARGETS usd_m
                  EXPORT pxrTargets
                  LIBRARY DESTINATION ${libInstallPrefix}
                  ARCHIVE DESTINATION ${libInstallPrefix}
@@ -69,9 +79,12 @@
              )
              if(WIN32)
                  install(
-                     FILES $<TARGET_PDB_FILE:usd_ms>
+                     FILES $<TARGET_PDB_FILE:usd_m>
 -                    DESTINATION ${libInstallPrefix}
 +                    DESTINATION ${binInstallPrefix}
                      OPTIONAL
                  )
              endif()
+-- 
+2.51.1
+
diff -Nru f3d-3.2.0+dfsg/.github/actions/vtk-dependencies/action.yml f3d-3.4.1/.github/actions/vtk-dependencies/action.yml
--- f3d-3.2.0+dfsg/.github/actions/vtk-dependencies/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/vtk-dependencies/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -9,9 +9,21 @@
     description: "Source directory for actions"
     required: false
     default: "./source"
+  blosc_version:
+    description: "Version of blosc to build"
+    required: true
   openvdb_version:
     description: "Version of openvdb to build"
     required: false
+  tbb_version:
+    description: "Version of tbb to build"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -27,16 +39,23 @@
       uses: ./.actions/tbb-install-dep
       with:
         cpu: ${{inputs.cpu}}
+        version: ${{inputs.tbb_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install zlib
       uses: ./.actions/zlib-install-dep
       with:
         cpu: ${{inputs.cpu}}
+        version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install blosc
       uses: ./.actions/blosc-install-dep
       with:
         cpu: ${{inputs.cpu}}
+        version: ${{inputs.blosc_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
 
     - name: Install OpenVDB
       if: inputs.openvdb_version != ''
@@ -44,3 +63,7 @@
       with:
         cpu: ${{inputs.cpu}}
         version: ${{inputs.openvdb_version}}
+        blosc_version: ${{inputs.blosc_version}}
+        tbb_version: ${{inputs.tbb_version}}
+        zlib_version: ${{inputs.zlib_version}}
+        global_cache_index: ${{inputs.global_cache_index}}
diff -Nru f3d-3.2.0+dfsg/.github/actions/vtk-install-dep/action.yml f3d-3.4.1/.github/actions/vtk-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/vtk-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/vtk-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -4,17 +4,28 @@
   vtk_version:
     description: "VTK version"
     required: true
-  raytracing_label:
-    description: "Label to control raytracing"
-    required: false
-    default: "no-raytracing"
   cpu:
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  blosc_version:
+    description: "Version of blosc to build against"
+    required: true
   openvdb_version:
     description: "Version of openvdb to build against"
     required: false
+  ospray_version:
+    description: "Version of ospray to build against"
+    required: false
+  tbb_version:
+    description: "Version of tbb to build against"
+    required: true
+  zlib_version:
+    description: "Version of zlib to build against"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
@@ -22,14 +33,18 @@
     - name: Check required inputs
       shell: bash
       run: |
+        [[ "${{ inputs.blosc_version }}" ]] || { echo "blosc_version input is empty" ; exit 1; }
+        [[ "${{ inputs.tbb_version }}" ]] || { echo "tbb_version input is empty" ; exit 1; }
         [[ "${{ inputs.vtk_version }}" ]] || { echo "vtk_version input is empty" ; exit 1; }
+        [[ "${{ inputs.zlib_version }}" ]] || { echo "zlib_version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
 
     - name: Cache VTK
       id: cache-vtk
       uses: actions/cache/restore@v4
       with:
         path: dependencies/vtk_install
-        key: vtk-${{ inputs.vtk_version }}${{ inputs.openvdb_version != '' && format('-openvdb-{0}', inputs.openvdb_version) || '' }}-${{runner.os}}-${{inputs.raytracing_label}}-${{inputs.cpu}}-1
+        key: vtk-${{ inputs.vtk_version }}-${{inputs.zlib_version}}-${{inputs.blosc_version}}-${{inputs.tbb_version}}${{ inputs.openvdb_version != '' && format('-openvdb-{0}', inputs.openvdb_version) || '' }}${{ inputs.ospray_version != '' && format('-ospray-{0}', inputs.ospray_version) || '' }}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-1
 
     - name: Setup VTK
       if: steps.cache-vtk.outputs.cache-hit != 'true'
@@ -52,7 +67,7 @@
 
     # CMake 3.31 introduced a new policy producing a warning when using vtkModule.cmake
     # See https://gitlab.kitware.com/vtk/vtk/-/issues/19526
-    - name: Patch VTK
+    - name: Patch VTK CMake
       if: steps.cache-vtk.outputs.cache-hit != 'true'
       working-directory: ${{github.workspace}}/dependencies/vtk
       shell: bash
@@ -93,10 +108,10 @@
         -DVTK_MODULE_ENABLE_VTK_IOHDF=YES
         -DVTK_MODULE_ENABLE_VTK_IOImage=YES
         -DVTK_MODULE_ENABLE_VTK_IOImport=YES
+        -DVTK_MODULE_ENABLE_VTK_IOLegacy=YES
         -DVTK_MODULE_ENABLE_VTK_IONetCDF=YES
         -DVTK_MODULE_ENABLE_VTK_IOOpenVDB=${{ inputs.openvdb_version != '' && 'YES' || 'DEFAULT' }}
         -DVTK_MODULE_ENABLE_VTK_IOPLY=YES
-        -DVTK_MODULE_ENABLE_VTK_IOParallel=YES
         -DVTK_MODULE_ENABLE_VTK_IOXML=YES
         -DVTK_MODULE_ENABLE_VTK_ImagingCore=YES
         -DVTK_MODULE_ENABLE_VTK_ImagingHybrid=YES
@@ -104,11 +119,12 @@
         -DVTK_MODULE_ENABLE_VTK_InteractionWidgets=YES
         -DVTK_MODULE_ENABLE_VTK_RenderingAnnotation=YES
         -DVTK_MODULE_ENABLE_VTK_RenderingCore=YES
+        -DVTK_MODULE_ENABLE_VTK_RenderingGridAxes=YES
         -DVTK_MODULE_ENABLE_VTK_RenderingOpenGL2=YES
-        -DVTK_MODULE_ENABLE_VTK_RenderingRayTracing=${{ inputs.raytracing_label == 'raytracing' && 'YES' || 'DEFAULT' }}
+        -DVTK_MODULE_ENABLE_VTK_RenderingRayTracing=${{ inputs.ospray_version != '' && 'YES' || 'DEFAULT' }}
         -DVTK_MODULE_ENABLE_VTK_RenderingVolumeOpenGL2=YES
         -DVTK_MODULE_ENABLE_VTK_TestingCore=YES
-        -DVTK_OPENGL_HAS_EGL=${{ runner.os != 'macOS' && inputs.vtk_version != 'v9.2.6' && inputs.vtk_version != 'v9.3.1' && 'ON' || 'OFF' }}
+        -DVTK_OPENGL_HAS_EGL=${{ runner.os != 'macOS' && inputs.vtk_version != 'v9.3.0' && 'ON' || 'OFF' }}
         -DVTK_SMP_IMPLEMENTATION_TYPE=TBB
         -DVTK_VERSIONED_INSTALL=OFF
         ${{ runner.os == 'macOS' && '-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0' || null }}
diff -Nru f3d-3.2.0+dfsg/.github/actions/wasm-ci/action.yml f3d-3.4.1/.github/actions/wasm-ci/action.yml
--- f3d-3.2.0+dfsg/.github/actions/wasm-ci/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/wasm-ci/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,32 +1,37 @@
 name: "WebAssembly CI"
 description: "WebAssembly CI"
+inputs:
+  lfs_sha:
+    description: "Cache LFS sha"
+    required: true
 
 runs:
   using: "composite"
   steps:
-    - name: Configure
+    - name: Check required inputs
       shell: bash
-      working-directory: ${{github.workspace}}
-      run: >
-        cmake -S source -B build
-        -DBUILD_SHARED_LIBS=OFF
-        -DCMAKE_BUILD_TYPE=Release
-        -DCMAKE_FIND_ROOT_PATH:PATH=/depends
-        -DF3D_MODULE_UI=OFF
-        -DF3D_PLUGIN_BUILD_ASSIMP=ON
-        -DF3D_PLUGIN_BUILD_DRACO=ON
-        -DF3D_PLUGIN_BUILD_HDF=ON
-        -DF3D_PLUGIN_BUILD_OCCT=ON
-        -DF3D_STRICT_BUILD=ON
-        -DF3D_WASM_COPY_APP=ON
+      run: |
+        [[ "${{ inputs.lfs_sha }}" ]] || { echo "lfs_sha input is empty" ; exit 1; }
 
-    - name: Build
+    - name: Recover LFS Data
+      uses: f3d-app/lfs-data-cache-action@v2
+      with:
+        type: "consumer"
+        lfs_sha: ${{inputs.lfs_sha}}
+        cache_postfix: cache-0
+
+    - name: Build and pack
+      shell: bash
+      working-directory: ${{github.workspace}}/source
+      run: npm pack
+
+    - name: Test
       shell: bash
-      working-directory: ${{github.workspace}}
-      run: cmake --build build --parallel 2
+      working-directory: ${{github.workspace}}/source
+      run: npm test
 
-    - name: Upload wasm binaries
+    - name: Upload package
       uses: actions/upload-artifact@v4
       with:
-        path: ./build/bin/*
-        name: f3d-wasm
+        path: ./source/f3d-*.tgz
+        name: f3d-wasm-package
diff -Nru f3d-3.2.0+dfsg/.github/actions/webp-install-dep/action.yml f3d-3.4.1/.github/actions/webp-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/webp-install-dep/action.yml	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/actions/webp-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,86 @@
+name: "Install WebP Dependency"
+description: "Install WebP Dependency using cache when possible"
+inputs:
+  cpu:
+    description: "CPU architecture to build for"
+    required: false
+    default: "x86_64"
+  version:
+    description: "Version of WebP to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
+
+runs:
+  using: "composite"
+  steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
+    - name: Cache WebP
+      id: cache-webp
+      uses: actions/cache/restore@v4
+      with:
+        path: dependencies/webp_install
+        key: webp-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
+
+    - name: Checkout WebP
+      if: steps.cache-webp.outputs.cache-hit != 'true'
+      uses: actions/checkout@v4
+      with:
+        repository: webmproject/libwebp
+        path: "./dependencies/webp"
+        ref: ${{inputs.version}}
+
+    - name: Setup WebP
+      if: steps.cache-webp.outputs.cache-hit != 'true'
+      working-directory: ${{github.workspace}}/dependencies
+      shell: bash
+      run: |
+        mkdir webp_build
+        mkdir webp_install
+
+    - name: Configure WebP
+      if: steps.cache-webp.outputs.cache-hit != 'true'
+      working-directory: ${{github.workspace}}/dependencies/webp_build
+      shell: bash
+      run: >
+        cmake ../webp
+        -DBUILD_SHARED_LIBS=ON
+        -DCMAKE_BUILD_TYPE=Release
+        -DCMAKE_INSTALL_PREFIX:PATH=../webp_install
+        -DCMAKE_PREFIX_PATH:PATH=../install/
+        -DWEBP_BUILD_ANIM_UTILS=OFF
+        -DWEBP_BUILD_CWEBP=OFF
+        -DWEBP_BUILD_DWEBP=OFF
+        -DWEBP_BUILD_GIF2WEBP=OFF
+        -DWEBP_BUILD_IMG2WEBP=OFF
+        -DWEBP_BUILD_VWEBP=OFF
+        -DWEBP_BUILD_WEBPINFO=OFF
+        -DWEBP_BUILD_LIBWEBPMUX=OFF
+        -DWEBP_BUILD_WEBPMUX=OFF
+        -DWEBP_BUILD_EXTRAS=OFF
+        ${{ runner.os == 'macOS' && '-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 -DCMAKE_MACOSX_RPATH=ON' || null }}
+        ${{ runner.os == 'Windows' && '-Ax64 -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL' || null }}
+
+    - name: Build WebP
+      if: steps.cache-webp.outputs.cache-hit != 'true'
+      working-directory: ${{github.workspace}}/dependencies/webp_build
+      shell: bash
+      run: cmake --build . --parallel 2 --target install --config Release
+
+    - name: Copy to install
+      working-directory: ${{github.workspace}}/dependencies/webp_install
+      shell: bash
+      run: cp -r ./* ../install/
+
+    - name: Save cache
+      if: steps.cache-webp.outputs.cache-hit != 'true'
+      uses: actions/cache/save@v4
+      with:
+        key: ${{ steps.cache-webp.outputs.cache-primary-key }}
+        path: dependencies/webp_install
diff -Nru f3d-3.2.0+dfsg/.github/actions/zlib-install-dep/action.yml f3d-3.4.1/.github/actions/zlib-install-dep/action.yml
--- f3d-3.2.0+dfsg/.github/actions/zlib-install-dep/action.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/actions/zlib-install-dep/action.yml	2026-01-08 08:41:55.000000000 +0200
@@ -5,16 +5,28 @@
     description: "CPU architecture to build for"
     required: false
     default: "x86_64"
+  version:
+    description: "Version of zlib to build"
+    required: true
+  global_cache_index:
+    description: "Global cache index"
+    required: true
 
 runs:
   using: "composite"
   steps:
+    - name: Check required inputs
+      shell: bash
+      run: |
+        [[ "${{ inputs.version }}" ]] || { echo "version input is empty" ; exit 1; }
+        [[ "${{ inputs.global_cache_index }}" ]] || { echo "global_cache_index input is empty" ; exit 1; }
+
     - name: Cache zlib
       id: cache-zlib
       uses: actions/cache/restore@v4
       with:
         path: dependencies/zlib_install
-        key: zlib-v1.3.1-${{runner.os}}-${{inputs.cpu}}-4
+        key: zlib-${{inputs.version}}-${{runner.os}}-${{inputs.cpu}}-${{inputs.global_cache_index}}-0
 
     # Dependents: blosc openvdb vtk
     - name: Checkout zlib
@@ -23,7 +35,7 @@
       with:
         repository: madler/zlib
         path: "./dependencies/zlib"
-        ref: v1.3.1
+        ref: ${{inputs.version}}
 
     - name: Setup zlib
       if: steps.cache-zlib.outputs.cache-hit != 'true'
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/.github/baselines/install_example_plugin_output.png and /tmp/gJkK3JzSDT/f3d-3.4.1/.github/baselines/install_example_plugin_output.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/.github/baselines/install_output.png and /tmp/gJkK3JzSDT/f3d-3.4.1/.github/baselines/install_output.png differ
diff -Nru f3d-3.2.0+dfsg/.github/FUNDING.yml f3d-3.4.1/.github/FUNDING.yml
--- f3d-3.2.0+dfsg/.github/FUNDING.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/FUNDING.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,4 @@
 github: f3d-app
-patreon: f3d_app_foundation
 ko_fi: f3d_app
 liberapay: f3d-app
-buy_me_a_coffee: f3d.app
-custom: [fr.tipeee.com/f3d-app, donorbox.org/support-f3d]
+custom: f3d.app/thanks
diff -Nru f3d-3.2.0+dfsg/.github/ISSUE_TEMPLATE/new_release.md f3d-3.4.1/.github/ISSUE_TEMPLATE/new_release.md
--- f3d-3.2.0+dfsg/.github/ISSUE_TEMPLATE/new_release.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/ISSUE_TEMPLATE/new_release.md	2026-01-08 08:41:55.000000000 +0200
@@ -15,6 +15,7 @@
 
 - [ ] Force fetch origin remote tag with `git fetch origin --tags --force`
 - [ ] Write and format release notes from the pull requests since last release, including contributors and packagers
+- [ ] Create a new [NPM token](https://www.npmjs.com/package/f3d) and replace the existing `NPM_TOKEN` in https://github.com/f3d-app/f3d-superbuild/settings/secrets/actions
 
 Release Split :
 
@@ -22,7 +23,7 @@
 
 Release Candidates :
 
-- [ ] Commit, review and merge a `vX.Y.Z-RCN` version change in https://github.com/f3d-app/f3d `CMakeLists.txt` and `pyproject.toml` in the `release` branch
+- [ ] Commit, review and merge a `vX.Y.Z-RCN` version change in https://github.com/f3d-app/f3d `CMakeLists.txt`, `pyproject.toml` and `package.json` in the `release` branch
 - [ ] Tag and push to https://github.com/f3d-app/f3d: `git tag vX.Y.Z-RCN -m vX.Y.Z-RCN`
 - [ ] Trigger a release build using https://github.com/f3d-app/f3d-superbuild actions with `vX.Y.Z-RCN` F3D version, superbuild `main` branch and prerelease publish `true`
 - [ ] Download the prerelease from https://github.com/f3d-app/f3d/releases
@@ -33,23 +34,28 @@
 - [ ] Locally test the python wheels on Linux
 - [ ] Locally test the python wheels on macOS (Silicon)
 - [ ] Locally test the python wheels on Windows
+- [ ] Locally test the wasm package
 - If it fails,
   - [ ] Fix the issues in `release` branch,
-  - [ ] Edit this issuethis paragraph and increment `N`
+  - [ ] Edit this issue and increment `N`
   - [ ] Merge F3D release into master: https://github.com/f3d-app/f3d/compare/master...release
 - [ ] Else proceed to the next part
 
 Release :
 
-- [ ] Commit, review and merge `vX.Y.Z` version change in https://github.com/f3d-app/f3d `CMakeLists.txt`, `pyproject.toml`, `webassembly/package.json` and the multiple `doxygen\conf.py` in the `release` branch
+- [ ] Commit, review and merge `vX.Y.Z` version change in https://github.com/f3d-app/f3d `CMakeLists.txt`, `pyproject.toml` and `package.json` in the `release` branch
 - [ ] Tag `vX.Y.Z` and push it to https://github.com/f3d-app/f3d: `git tag vX.Y.Z -m vX.Y.Z`
 - [ ] Commit, review and merge adding `X.Y.Z` in https://github.com/f3d-app/f3d-superbuild `versions.cmake` in the `main` branch
 - [ ] Tag `vX.Y.Z` and push it to https://github.com/f3d-app/f3d-superbuild: `git tag vX.Y.Z -m vX.Y.Z`
 - [ ] Merge F3D release into master: https://github.com/f3d-app/f3d/compare/master...release
 - [ ] Trigger a release build using https://github.com/f3d-app/f3d-superbuild actions with `vX.Y.Z` F3D version, `vX.Y.Z` sb version and prerelease publish true
 - [ ] Finalize release note and add them to the release
+- [ ] Format the release note into a changelog and commit/review/merge them into https://github.com/f3d-app/f3d
+- [ ] Update dev and nightly docs as described [here](https://f3d.app/dev/TOOLING#how-to-update-the-doc-using-latest-master), commit/review/merge into https://github.com/f3d-app/f3d-website
+- [ ] Add a **new versioned doc** as described [here](https://f3d.app/dev/TOOLING#how-to-update-the-doc-for-a-new-release), commit/review/merge into https://github.com/f3d-app/f3d-website
+- [ ] Commit review and merge a bump of F3D version in `package.json` in https://github.com/f3d-app/f3d-website
+- [ ] Update **download links** using `GITHUB_TOKEN=$PAT npm run update-downloads`, commit/review/merge into https://github.com/f3d-app/f3d-website and then immediately
 - [ ] Release
-- [ ] Update **download links** and **changelog** in https://github.com/f3d-app/f3d `doc`
 - [ ] Communicate on discord
 - [ ] Communicate on reddit
 - [ ] Communicate on hackernews
@@ -57,7 +63,7 @@
 - [ ] Communicate on mastodon
 - [ ] Communicate on bluesky
 - [ ] Move all issue from current milestone to next milestone, close current roadmap issue and open a next roadmap issue
-- [ ] Update `doc/dev/ROADMAPS_AND_RELEASES.md` for next release
+- [ ] Update `dev/ROADMAPS_AND_RELEASES.md` for next release in https://github.com/f3d-app/f3d-website
 - [ ] Create an issue for updating dependencies in CI and superbuild
 - [ ] Update `.github/ISSUE_TEMPLATE/new_release.md` in https://github.com/f3d-app/f3d if needed
 
@@ -128,6 +134,18 @@
   - The name of the window is `f3d` at all time
   - Python provides suggestions whenever the "Tab" key is pressed
 
+Webassembly testing protocol:
+
+- Clone https://github.com/f3d-app/f3d-website
+- Replace the current `f3d` version by the last RC available and run the website locally to check the web viewer
+- Make sure to check for broken anchor in the npm output
+
+```bash
+npm uninstall f3d
+npm install f3d --tag next
+npm run start
+```
+
 Once a release cycle:
 
 - Check that completions are still working with `fish` and `zsh` when tab is pressed
diff -Nru f3d-3.2.0+dfsg/.github/PULL_REQUEST_TEMPLATE.md f3d-3.4.1/.github/PULL_REQUEST_TEMPLATE.md
--- f3d-3.2.0+dfsg/.github/PULL_REQUEST_TEMPLATE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/PULL_REQUEST_TEMPLATE.md	2026-01-08 08:41:55.000000000 +0200
@@ -4,24 +4,13 @@
 
 ### Checklist for finalizing the PR
 
-- [ ] I have performed a [self-review](https://f3d.app/doc/dev/CODING_STYLE.html) of my code
-- [ ] I have added [tests](https://f3d.app/doc/dev/TESTING.html) for new features and bugfixes
-- [ ] I have added [documentation](https://f3d.app/) for new features
+- [ ] I have performed a [self-review](https://f3d.app/dev/CODING_STYLE) of my code
+- [ ] I have added [tests](https://f3d.app/dev/TESTING) for new features and bugfixes
+- [ ] I have added [documentation](https://f3d.app/docs/next/user/QUICKSTART) for new features
 - [ ] If it is a modifying the libf3d API, I have updated bindings
-- [ ] If it is a modifying the `.github/workflows/versions.json`, I have updated `timestamp`
+- [ ] If it is a modifying the `.github/workflows/versions.json`, I have updated `docker_timestamp`
 
 ### Continuous integration
 
-Please check the checkbox of the CI you want to run, then push again on your branch.
-
-- [ ] Style checks
-- [ ] Fast CI
-- [ ] Coverage cached CI
-- [ ] Analysis cached CI
-- [ ] WASM docker CI
-- [ ] Android docker CI
-- [ ] macOS Intel cached CI
-- [ ] macOS ARM cached CI
-- [ ] Windows cached CI
-- [ ] Linux cached CI
-- [ ] Other cached CI
+Please write a comment to run CI, eg: `\ci fast`.
+See [here](https://f3d.app/dev/CONTRIBUTING#continuous-integration) for more info.
diff -Nru f3d-3.2.0+dfsg/.github/workflows/build-docker-images.yml f3d-3.4.1/.github/workflows/build-docker-images.yml
--- f3d-3.2.0+dfsg/.github/workflows/build-docker-images.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/build-docker-images.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,6 @@
 on:
   pull_request_target:
-    types: [assigned, opened, synchronize, reopened]
+    types: [assigned, opened, synchronize, reopened, labeled]
     branches:
       - master
       - release
@@ -11,40 +11,14 @@
 name: Build docker images
 jobs:
   #----------------------------------------------------------------------------
-  # Detect checkboxes are checked
-  #----------------------------------------------------------------------------
-  detect_checkboxes:
-    name: Detect Checkboxes
-    runs-on: ubuntu-latest
-    outputs:
-      checked: ${{ steps.detect.outputs.checked }}
-      unchecked: ${{ steps.detect.outputs.unchecked }}
-
-    permissions:
-      pull-requests: write
-
-    steps:
-      - name: Checkbox Trigger
-        id: detect
-        uses: AhmedBaset/checklist@3
-        with:
-          token: ${{ github.token }}
-
-      - name: list changes
-        run: |
-          echo "checked=${{ steps.detect.outputs.checked }}"
-          echo "unchecked=${{ steps.detect.outputs.unchecked }}"
-
-  #----------------------------------------------------------------------------
   # Default versions: Set default version for all dependencies
   #----------------------------------------------------------------------------
   default_versions:
     runs-on: ubuntu-22.04
     name: Set default versions
-    needs: detect_checkboxes
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'docker CI')) }}
+    if: contains(github.event.pull_request.labels.*.name, 'ci:full')
     outputs:
-      timestamp: ${{ steps.set_default_versions.outputs.timestamp }}
+      docker_timestamp: ${{ steps.set_default_versions.outputs.docker_timestamp }}
 
     steps:
       - uses: actions/checkout@v4
@@ -74,11 +48,11 @@
         working-directory: ${{github.workspace}}
         run: |
           res=0
-          docker manifest inspect ghcr.io/f3d-app/f3d-wasm:${{needs.default_versions.outputs.timestamp}} || res=$?
-          docker manifest inspect ghcr.io/f3d-app/f3d-android-armeabi-v7a:${{needs.default_versions.outputs.timestamp}} || res=$?
-          docker manifest inspect ghcr.io/f3d-app/f3d-android-arm64-v8a:${{needs.default_versions.outputs.timestamp}} || res=$?
-          docker manifest inspect ghcr.io/f3d-app/f3d-android-x86:${{needs.default_versions.outputs.timestamp}} || res=$?
-          docker manifest inspect ghcr.io/f3d-app/f3d-android-x86_64:${{needs.default_versions.outputs.timestamp}} || res=$?
+          docker manifest inspect ghcr.io/f3d-app/f3d-wasm:${{needs.default_versions.outputs.docker_timestamp}} || res=$?
+          docker manifest inspect ghcr.io/f3d-app/f3d-android-armeabi-v7a:${{needs.default_versions.outputs.docker_timestamp}} || res=$?
+          docker manifest inspect ghcr.io/f3d-app/f3d-android-arm64-v8a:${{needs.default_versions.outputs.docker_timestamp}} || res=$?
+          docker manifest inspect ghcr.io/f3d-app/f3d-android-x86:${{needs.default_versions.outputs.docker_timestamp}} || res=$?
+          docker manifest inspect ghcr.io/f3d-app/f3d-android-x86_64:${{needs.default_versions.outputs.docker_timestamp}} || res=$?
           echo "F3D_DOCKER_IMAGE_AVAILABLE=$res" >> $GITHUB_ENV
 
       # This require a F3D_DOCKER_CI_DISPATCH secret contain a PAT with read and write admin access
diff -Nru f3d-3.2.0+dfsg/.github/workflows/build-website.yml f3d-3.4.1/.github/workflows/build-website.yml
--- f3d-3.2.0+dfsg/.github/workflows/build-website.yml	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/workflows/build-website.yml	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,38 @@
+on:
+  pull_request_target:
+    types: [assigned, opened, synchronize, reopened, labeled]
+    branches:
+      - master
+  push:
+    branches:
+      - master
+
+name: Build website
+jobs:
+  #----------------------------------------------------------------------------
+  # Build website
+  #----------------------------------------------------------------------------
+  build_website:
+    name: Build website
+    if: github.event_name == 'push' ||
+      contains(github.event.pull_request.labels.*.name, 'ci:fast') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full')
+    runs-on: ubuntu-latest
+    steps:
+      - name: Set WEBSITE_SKIP_DEPLOY
+        shell: bash
+        run: echo "WEBSITE_SKIP_DEPLOY=${{ github.ref == 'refs/heads/master' && github.event_name == 'push' && 'false' || 'true' }}" >> $GITHUB_ENV
+
+      - name: Trigger docker images build
+        uses: convictional/trigger-workflow-and-wait@v1.6.5
+        with:
+          owner: f3d-app
+          repo: f3d-website
+          github_token: ${{ secrets.F3D_WEBSITE_CI_DISPATCH }}
+          workflow_file_name: static.yml
+          wait_interval: 60
+          client_payload: '{"skip_deploy": "${{ env.WEBSITE_SKIP_DEPLOY }}" , "docs_tag": "${{github.event.pull_request.head.ref}}", "docs_repository": "${{github.event.pull_request.head.repo.full_name}}" }'
+          propagate_failure: true
+          trigger_workflow: true
+          wait_workflow: true
diff -Nru f3d-3.2.0+dfsg/.github/workflows/ci.yml f3d-3.4.1/.github/workflows/ci.yml
--- f3d-3.2.0+dfsg/.github/workflows/ci.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/ci.yml	2026-01-08 08:41:55.000000000 +0200
@@ -2,7 +2,7 @@
 
 on:
   pull_request:
-    types: [opened, synchronize, reopened, ready_for_review]
+    types: [opened, synchronize, reopened, ready_for_review, labeled]
     paths-ignore:
       - "doc/**"
       - "_config.yml"
@@ -16,63 +16,52 @@
 
 jobs:
   #----------------------------------------------------------------------------
-  # Detect checkboxes are checked
-  #----------------------------------------------------------------------------
-  detect_checkboxes:
-    name: Detect Checkboxes
-    runs-on: ubuntu-latest
-    if: github.event.pull_request.draft == false
-    outputs:
-      checked: ${{ steps.detect.outputs.checked }}
-      unchecked: ${{ steps.detect.outputs.unchecked }}
-
-    permissions:
-      pull-requests: write
-
-    steps:
-      - name: Checkbox Trigger
-        id: detect
-        uses: AhmedBaset/checklist@3
-        with:
-          token: ${{ github.token }}
-
-      - name: list changes
-        run: |
-          echo "checked=${{ steps.detect.outputs.checked }}"
-          echo "unchecked=${{ steps.detect.outputs.unchecked }}"
-          echo "${{ github.ref }}"
-
-  #----------------------------------------------------------------------------
   # Default versions: Set default version for all dependencies
   #----------------------------------------------------------------------------
   default_versions:
     runs-on: ubuntu-22.04
     name: Set default versions
-    needs: detect_checkboxes
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'CI')) || (github.ref == 'refs/heads/master') }}
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:fast') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     outputs:
-      alembic_version: ${{ steps.set_default_versions.outputs.alembic_version }}
       alembic_min_version: ${{ steps.set_default_versions.outputs.alembic_min_version }}
-      assimp_version: ${{ steps.set_default_versions.outputs.assimp_version }}
+      alembic_version: ${{ steps.set_default_versions.outputs.alembic_version }}
       assimp_min_version: ${{ steps.set_default_versions.outputs.assimp_min_version }}
-      draco_version: ${{ steps.set_default_versions.outputs.draco_version }}
+      assimp_version: ${{ steps.set_default_versions.outputs.assimp_version }}
+      blosc_min_version: ${{ steps.set_default_versions.outputs.blosc_min_version }}
+      blosc_version: ${{ steps.set_default_versions.outputs.blosc_version }}
       draco_min_version: ${{ steps.set_default_versions.outputs.draco_min_version }}
-      occt_version: ${{ steps.set_default_versions.outputs.occt_version }}
+      draco_version: ${{ steps.set_default_versions.outputs.draco_version }}
+      imath_min_version: ${{ steps.set_default_versions.outputs.imath_min_version }}
+      imath_version: ${{ steps.set_default_versions.outputs.imath_version }}
+      java_min_version: ${{ steps.set_default_versions.outputs.java_min_version }}
+      java_version: ${{ steps.set_default_versions.outputs.java_version }}
       occt_min_version: ${{ steps.set_default_versions.outputs.occt_min_version }}
-      openexr_version: ${{ steps.set_default_versions.outputs.openexr_version }}
+      occt_version: ${{ steps.set_default_versions.outputs.occt_version }}
       openexr_min_version: ${{ steps.set_default_versions.outputs.openexr_min_version }}
-      openvdb_version: ${{ steps.set_default_versions.outputs.openvdb_version }}
+      openexr_version: ${{ steps.set_default_versions.outputs.openexr_version }}
       openvdb_min_version: ${{ steps.set_default_versions.outputs.openvdb_min_version }}
-      pybind11_version: ${{ steps.set_default_versions.outputs.pybind11_version }}
+      openvdb_version: ${{ steps.set_default_versions.outputs.openvdb_version }}
+      ospray_min_version: ${{ steps.set_default_versions.outputs.ospray_min_version }}
+      ospray_version: ${{ steps.set_default_versions.outputs.ospray_version }}
       pybind11_min_version: ${{ steps.set_default_versions.outputs.pybind11_min_version }}
-      python_version: ${{ steps.set_default_versions.outputs.python_version }}
+      pybind11_version: ${{ steps.set_default_versions.outputs.pybind11_version }}
       python_min_version: ${{ steps.set_default_versions.outputs.python_min_version }}
-      usd_version: ${{ steps.set_default_versions.outputs.usd_version }}
+      python_version: ${{ steps.set_default_versions.outputs.python_version }}
+      tbb_min_version: ${{ steps.set_default_versions.outputs.tbb_min_version }}
+      tbb_version: ${{ steps.set_default_versions.outputs.tbb_version }}
       usd_min_version: ${{ steps.set_default_versions.outputs.usd_min_version }}
-      java_version: ${{ steps.set_default_versions.outputs.java_version }}
-      java_min_version: ${{ steps.set_default_versions.outputs.java_min_version }}
+      usd_version: ${{ steps.set_default_versions.outputs.usd_version }}
+      webp_min_version: ${{ steps.set_default_versions.outputs.webp_min_version }}
+      webp_version: ${{ steps.set_default_versions.outputs.webp_version }}
+      zlib_min_version: ${{ steps.set_default_versions.outputs.zlib_min_version }}
+      zlib_version: ${{ steps.set_default_versions.outputs.zlib_version }}
       vtk_commit_sha: ${{ steps.set_default_versions.outputs.vtk_commit_sha }}
-      timestamp: ${{ steps.set_default_versions.outputs.timestamp }}
+      docker_timestamp: ${{ steps.set_default_versions.outputs.docker_timestamp }}
+      global_cache_index: ${{ steps.set_default_versions.outputs.global_cache_index }}
 
     steps:
       - name: Checkout
@@ -94,8 +83,11 @@
   cache_lfs:
     runs-on: ubuntu-22.04
     name: Update LFS data cache
-    needs: detect_checkboxes
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'CI')) || (github.ref == 'refs/heads/master') }}
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:fast') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     outputs:
       lfs_sha: ${{ steps.lfs_sha_recover.outputs.lfs_sha }}
     steps:
@@ -108,7 +100,7 @@
 
       - name: Cache LFS Data
         id: lfs_sha_recover
-        uses: f3d-app/lfs-data-cache-action@v1
+        uses: f3d-app/lfs-data-cache-action@v2
         with:
           cache_postfix: cache-0
 
@@ -117,39 +109,58 @@
   #----------------------------------------------------------------------------
   cache_dependencies:
     name: Cache dependencies
-    needs: [default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'cached CI')) || (github.ref == 'refs/heads/master') }}
+    needs: default_versions
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-22.04, windows-latest, macos-13, macos-14]
+        os: [ubuntu-22.04, windows-latest, macos-15-intel, macos-15]
         build_type: [standard]
         include:
           - cpu: x86_64
-          - os: macos-14
+          - os: macos-15
             cpu: arm64
           - alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           - assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          - blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           - draco_version: ${{needs.default_versions.outputs.draco_version}}
+          - imath_version: ${{needs.default_versions.outputs.imath_version}}
           - occt_version: ${{needs.default_versions.outputs.occt_version}}
           - openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           - openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          - ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           - pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
+          - tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           - usd_version: ${{needs.default_versions.outputs.usd_version}}
+          - webp_version: ${{needs.default_versions.outputs.webp_version}}
+          - zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: mindeps
             os: ubuntu-22.04
             cpu: x86_64
             alembic_version: ${{needs.default_versions.outputs.alembic_min_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_min_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_min_version}}
             draco_version: ${{needs.default_versions.outputs.draco_min_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_min_version}}
             occt_version: ${{needs.default_versions.outputs.occt_min_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_min_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_min_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_min_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_min_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_min_version}}
             usd_version: ${{needs.default_versions.outputs.usd_min_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_min_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_min_version}}
 
     runs-on: ${{matrix.os}}
     container: ${{ matrix.os == 'ubuntu-22.04' && 'ghcr.io/f3d-app/f3d-ci' || null }}
+
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -161,72 +172,90 @@
       - name: Generic Dependencies
         uses: ./source/.github/actions/generic-dependencies
         with:
-          raytracing_label: raytracing
           cpu: ${{matrix.cpu}}
           alembic_version: ${{matrix.alembic_version}}
           assimp_version: ${{matrix.assimp_version}}
+          blosc_version: ${{matrix.blosc_version}}
           draco_version: ${{matrix.draco_version}}
+          imath_version: ${{matrix.imath_version}}
           occt_version: ${{matrix.occt_version}}
           openexr_version: ${{matrix.openexr_version}}
           openvdb_version: ${{matrix.openvdb_version}}
+          ospray_version: ${{matrix.ospray_version}}
           pybind11_version: ${{matrix.pybind11_version}}
+          tbb_version: ${{matrix.tbb_version}}
           usd_version: ${{matrix.usd_version}}
+          webp_version: ${{matrix.webp_version}}
+          zlib_version: ${{matrix.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Cache VTK Dependency: Checkout and compile VTK if not cached
   #----------------------------------------------------------------------------
   cache_vtk_dependency:
     name: Cache VTK dependency
-    needs: [default_versions, detect_checkboxes, cache_dependencies]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'cached CI')) || (github.ref == 'refs/heads/master') }}
+    needs: [default_versions, cache_dependencies]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-22.04, windows-latest, macos-13, macos-14]
+        os: [ubuntu-22.04, windows-latest, macos-15-intel, macos-15]
         vtk_version:
           [
             "${{needs.default_versions.outputs.vtk_commit_sha}}",
-            v9.5.0,
+            v9.5.2,
             v9.4.2,
-            v9.3.1,
-            v9.2.6,
+            v9.3.0,
           ]
         build_type: [standard]
         exclude:
-          - os: macos-13
-            vtk_version: v9.3.1
-          - os: macos-13
-            vtk_version: v9.2.6
-          - os: macos-14
-            vtk_version: v9.3.1
-          - os: macos-14
-            vtk_version: v9.2.6
+          - os: macos-15-intel
+            vtk_version: v9.3.0
+          - os: macos-15
+            vtk_version: v9.3.0
         include:
           - cpu: x86_64
-          - raytracing_label: raytracing
+          - blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           - openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
-          - os: macos-14
+          - ospray_version: ${{needs.default_versions.outputs.ospray_version}}
+          - tbb_version: ${{needs.default_versions.outputs.tbb_version}}
+          - zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          - os: macos-15
             cpu: arm64
           - build_type: mindeps
             os: ubuntu-22.04
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
             cpu: x86_64
-            raytracing_label: raytracing
+            blosc_version: ${{needs.default_versions.outputs.blosc_min_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_min_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_min_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_min_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_min_version}}
           - build_type: no_optional_deps
             os: ubuntu-22.04
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
             cpu: x86_64
-            raytracing_label: no-raytracing
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: no_raytracing
             os: ubuntu-22.04
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
             cpu: x86_64
-            raytracing_label: no-raytracing
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
 
     runs-on: ${{matrix.os}}
     container: ${{ matrix.os == 'ubuntu-22.04' && 'ghcr.io/f3d-app/f3d-ci' || null }}
+
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -248,38 +277,49 @@
         with:
           cpu: ${{matrix.cpu}}
           openvdb_version: ${{matrix.openvdb_version}}
+          blosc_version: ${{matrix.blosc_version}}
+          tbb_version: ${{matrix.tbb_version}}
+          zlib_version: ${{matrix.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
       - name: Install Raytracing Dependencies
-        if: matrix.raytracing_label == 'raytracing'
+        if: matrix.ospray_version != ''
         uses: ./source/.github/actions/ospray-sb-install-dep
         with:
+          version: ${{matrix.ospray_version}}
           cpu: ${{matrix.cpu}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
       - name: VTK dependency
         uses: ./source/.github/actions/vtk-install-dep
         with:
           vtk_version: ${{matrix.vtk_version}}
-          raytracing_label: ${{matrix.raytracing_label}}
           cpu: ${{matrix.cpu}}
+          blosc_version: ${{matrix.blosc_version}}
           openvdb_version: ${{matrix.openvdb_version}}
+          ospray_version: ${{matrix.ospray_version}}
+          tbb_version: ${{matrix.tbb_version}}
+          zlib_version: ${{matrix.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Windows CI: Build and test, cross-vtk build matrix
   #----------------------------------------------------------------------------
   windows:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Windows cached CI')) || (github.ref == 'refs/heads/master') }}
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
         vtk_version:
           [
             "${{needs.default_versions.outputs.vtk_commit_sha}}",
-            v9.5.0,
+            v9.5.2,
             v9.4.2,
-            v9.3.1,
-            v9.2.6,
+            v9.3.0,
           ]
         static_label: [no-static]
         include:
@@ -288,6 +328,9 @@
 
     runs-on: windows-latest
 
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -300,31 +343,41 @@
         uses: ./source/.github/actions/generic-ci
         with:
           vtk_version: ${{matrix.vtk_version}}
-          raytracing_label: raytracing
           static_label: ${{matrix.static_label}}
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Linux fast CI: Build and test a single fast CI
   #----------------------------------------------------------------------------
   linux_fast:
-    needs: [cache_lfs, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Fast CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:fast') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     runs-on: ubuntu-22.04
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       DISPLAY: :0
 
     steps:
@@ -339,147 +392,182 @@
         uses: ./source/.github/actions/generic-ci
         with:
           build_type: fast
-          vtk_version: v9.5.0
-          raytracing_label: no-raytracing
           rendering_backend: auto
           optional_deps_label: no-optional-deps
           exclude_deprecated_label: exclude-deprecated
           static_label: no-static
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
+          vtk_version: v9.5.2
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Linux CI: Build and test, cross-vtk build matrix
   #----------------------------------------------------------------------------
   linux:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Linux cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
         vtk_version:
           [
             "${{needs.default_versions.outputs.vtk_commit_sha}}",
-            v9.5.0,
+            v9.5.2,
             v9.4.2,
-            v9.3.1,
-            v9.2.6,
+            v9.3.0,
           ]
         build_type: [standard]
         include:
-          - raytracing_label: raytracing
           - exclude_deprecated_label: no-exclude-deprecated
           - optional_deps_label: optional-deps
           - rendering_backend: auto
           - static_label: no-static
           - alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           - assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          - blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           - draco_version: ${{needs.default_versions.outputs.draco_version}}
+          - imath_version: ${{needs.default_versions.outputs.imath_version}}
+          - java_version: ${{needs.default_versions.outputs.java_version}}
           - occt_version: ${{needs.default_versions.outputs.occt_version}}
           - openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           - openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          - ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           - pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           - python_version: ${{needs.default_versions.outputs.python_version}}
+          - tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           - usd_version: ${{needs.default_versions.outputs.usd_version}}
-          - java_version: ${{needs.default_versions.outputs.java_version}}
+          - webp_version: ${{needs.default_versions.outputs.webp_version}}
+          - zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: egl
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: raytracing
             optional_deps_label: optional-deps
             exclude_deprecated_label: no-exclude-deprecated
             rendering_backend: egl
             static_label: no-static
             alembic_version: ${{needs.default_versions.outputs.alembic_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
             draco_version: ${{needs.default_versions.outputs.draco_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_version}}
+            java_version: ${{needs.default_versions.outputs.java_version}}
             occt_version: ${{needs.default_versions.outputs.occt_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
             python_version: ${{needs.default_versions.outputs.python_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
             usd_version: ${{needs.default_versions.outputs.usd_version}}
-            java_version: ${{needs.default_versions.outputs.java_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: osmesa
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: raytracing
             optional_deps_label: optional-deps
             exclude_deprecated_label: no-exclude-deprecated
             rendering_backend: osmesa
             static_label: no-static
             alembic_version: ${{needs.default_versions.outputs.alembic_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
             draco_version: ${{needs.default_versions.outputs.draco_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_version}}
+            java_version: ${{needs.default_versions.outputs.java_version}}
             occt_version: ${{needs.default_versions.outputs.occt_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
             python_version: ${{needs.default_versions.outputs.python_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
             usd_version: ${{needs.default_versions.outputs.usd_version}}
-            java_version: ${{needs.default_versions.outputs.java_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: exclude_deprecated
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: raytracing
             optional_deps_label: optional-deps
             exclude_deprecated_label: exclude-deprecated
             rendering_backend: auto
             static_label: no-static
             alembic_version: ${{needs.default_versions.outputs.alembic_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
             draco_version: ${{needs.default_versions.outputs.draco_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_version}}
+            java_version: ${{needs.default_versions.outputs.java_version}}
             occt_version: ${{needs.default_versions.outputs.occt_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
             python_version: ${{needs.default_versions.outputs.python_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
             usd_version: ${{needs.default_versions.outputs.usd_version}}
-            java_version: ${{needs.default_versions.outputs.java_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: no_optional_deps
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: no-raytracing
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
             optional_deps_label: no-optional-deps
             exclude_deprecated_label: no-exclude-deprecated
             rendering_backend: auto
             static_label: no-static
           - build_type: static_libs
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: no-raytracing
             optional_deps_label: optional-deps
             exclude_deprecated_label: no-exclude-deprecated
             rendering_backend: auto
             static_label: static
             alembic_version: ${{needs.default_versions.outputs.alembic_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_version}}
             draco_version: ${{needs.default_versions.outputs.draco_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_version}}
+            java_version: ${{needs.default_versions.outputs.java_version}}
             occt_version: ${{needs.default_versions.outputs.occt_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
             python_version: ${{needs.default_versions.outputs.python_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_version}}
             usd_version: ${{needs.default_versions.outputs.usd_version}}
-            java_version: ${{needs.default_versions.outputs.java_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_version}}
           - build_type: mindeps
             vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
-            raytracing_label: raytracing
             optional_deps_label: optional-deps
             exclude_deprecated_label: no-exclude-deprecated
             rendering_backend: auto
             static_label: no-static
             alembic_version: ${{needs.default_versions.outputs.alembic_min_version}}
             assimp_version: ${{needs.default_versions.outputs.assimp_min_version}}
+            blosc_version: ${{needs.default_versions.outputs.blosc_min_version}}
             draco_version: ${{needs.default_versions.outputs.draco_min_version}}
+            imath_version: ${{needs.default_versions.outputs.imath_min_version}}
+            java_version: ${{needs.default_versions.outputs.java_min_version}}
             occt_version: ${{needs.default_versions.outputs.occt_min_version}}
             openexr_version: ${{needs.default_versions.outputs.openexr_min_version}}
             openvdb_version: ${{needs.default_versions.outputs.openvdb_min_version}}
+            ospray_version: ${{needs.default_versions.outputs.ospray_min_version}}
             pybind11_version: ${{needs.default_versions.outputs.pybind11_min_version}}
             python_version: ${{needs.default_versions.outputs.python_min_version}}
+            tbb_version: ${{needs.default_versions.outputs.tbb_min_version}}
             usd_version: ${{needs.default_versions.outputs.usd_min_version}}
-            java_version: ${{needs.default_versions.outputs.java_min_version}}
+            webp_version: ${{needs.default_versions.outputs.webp_min_version}}
+            zlib_version: ${{needs.default_versions.outputs.zlib_min_version}}
 
     runs-on: ubuntu-22.04
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       DISPLAY: :0
 
     steps:
@@ -495,7 +583,6 @@
         with:
           build_type: ${{matrix.build_type}}
           vtk_version: ${{matrix.vtk_version}}
-          raytracing_label: ${{matrix.raytracing_label}}
           rendering_backend: ${{matrix.rendering_backend}}
           optional_deps_label: ${{matrix.optional_deps_label}}
           exclude_deprecated_label: ${{matrix.exclude_deprecated_label}}
@@ -503,36 +590,41 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{matrix.alembic_version}}
           assimp_version: ${{matrix.assimp_version}}
+          blosc_version: ${{matrix.blosc_version}}
           draco_version: ${{matrix.draco_version}}
+          imath_version: ${{matrix.imath_version}}
+          java_version: ${{matrix.java_version}}
           occt_version: ${{matrix.occt_version}}
           openexr_version: ${{matrix.openexr_version}}
           openvdb_version: ${{matrix.openvdb_version}}
+          ospray_version: ${{matrix.ospray_version}}
           pybind11_version: ${{matrix.pybind11_version}}
           python_version: ${{matrix.python_version}}
+          tbb_version: ${{matrix.tbb_version}}
           usd_version: ${{matrix.usd_version}}
-          java_version: ${{matrix.java_version}}
+          webp_version: ${{matrix.webp_version}}
+          zlib_version: ${{matrix.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # MacOS CI: Build and test, cross-vtk build matrix
   #----------------------------------------------------------------------------
   macos:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'macOS Intel cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
         vtk_version:
-          [
-            "${{needs.default_versions.outputs.vtk_commit_sha}}",
-            v9.5.0,
-            v9.4.2,
-            v9.3.1,
-            v9.2.6,
-          ]
+          ["${{needs.default_versions.outputs.vtk_commit_sha}}", v9.5.2, v9.4.2]
+
+    runs-on: macos-15-intel
 
-    runs-on: macos-13
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
 
     steps:
       - name: Checkout
@@ -546,36 +638,39 @@
         uses: ./source/.github/actions/generic-ci
         with:
           vtk_version: ${{matrix.vtk_version}}
-          raytracing_label: raytracing
           cpu: x86_64
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # MacOS ARM CI: Build and test, cross-vtk build matrix with a few optional builds
   #----------------------------------------------------------------------------
   macos_arm:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'macOS ARM cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
-        vtk_version: [
-            "${{needs.default_versions.outputs.vtk_commit_sha}}",
-            v9.5.0,
-            v9.4.2,
-            v9.3.1,
-          ] # VTK 9.2.6 not compatible with recent clang version on macOS
+        vtk_version:
+          ["${{needs.default_versions.outputs.vtk_commit_sha}}", v9.5.2, v9.4.2]
         bundle_label: [no-bundle]
         static_label: [no-static]
         include:
@@ -586,7 +681,10 @@
             bundle_label: no-bundle
             static_label: static
 
-    runs-on: macos-14
+    runs-on: macos-15
+
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
 
     steps:
       - name: Checkout
@@ -600,38 +698,42 @@
         uses: ./source/.github/actions/generic-ci
         with:
           vtk_version: ${{matrix.vtk_version}}
-          raytracing_label: raytracing
           bundle_label: ${{matrix.bundle_label}}
           static_label: ${{matrix.static_label}}
           cpu: arm64
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Python packaging: Build and test the Python wheel
   #----------------------------------------------------------------------------
   python-packaging:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Other cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-22.04, windows-latest, macos-13]
-        python_version: ["3.11"]
+        os: [ubuntu-22.04, windows-latest, macos-15-intel]
+        python_version: ["3.14"]
         include:
           - os: ubuntu-22.04
-            python_version: "3.9"
-          - os: ubuntu-22.04
             python_version: "3.10"
           - os: ubuntu-22.04
             python_version: "3.11"
@@ -639,12 +741,15 @@
             python_version: "3.12"
           - os: ubuntu-22.04
             python_version: "3.13"
+          - os: ubuntu-22.04
+            python_version: "3.14"
 
     runs-on: ${{matrix.os}}
     container: ${{ matrix.os == 'ubuntu-22.04' && 'ghcr.io/f3d-app/f3d-ci' || null }}
 
     env:
       DISPLAY: :0
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
 
     steps:
       - name: Checkout
@@ -661,22 +766,29 @@
           python_version: ${{matrix.python_version}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
           vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Coverage: Build and test on linux with last VTK with coverage option
   #----------------------------------------------------------------------------
   coverage:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Coverage cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     runs-on: ubuntu-22.04
     container: ghcr.io/f3d-app/f3d-ci
 
@@ -700,12 +812,19 @@
           codecov_token: ${{secrets.CODECOV_TOKEN}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
           vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Sanitizer: Build and test on linux with last VTK with sanitizer options
@@ -714,10 +833,10 @@
   # "memory" returns false positives in VTK:
   # https://stackoverflow.com/questions/60097307/memory-sanitizer-reports-use-of-uninitialized-value-in-global-object-constructio
   sanitizer:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Other cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
@@ -727,6 +846,7 @@
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       DISPLAY: :0
       CC: clang
       CXX: clang++
@@ -747,19 +867,25 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # static-analysis: Run static analysis on linux
   #----------------------------------------------------------------------------
   static-analysis:
-    needs:
-      [cache_lfs, cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Analysis cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
 
@@ -767,6 +893,7 @@
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       CC: clang
       CXX: clang++
 
@@ -784,26 +911,36 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
           vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
+          webp_version: ${{needs.default_versions.outputs.webp_version}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # external-build: Check build of F3D as sub-project
   #----------------------------------------------------------------------------
   external-build:
-    needs: [cache_vtk_dependency, default_versions, detect_checkboxes]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Other cached CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_vtk_dependency, default_versions]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
 
     runs-on: ubuntu-22.04
     container: ghcr.io/f3d-app/f3d-ci
 
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -815,39 +952,55 @@
       - name: External build CI
         uses: ./source/f3d/.github/actions/external-build-ci
         with:
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           vtk_version: ${{needs.default_versions.outputs.vtk_commit_sha}}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Check android/wasm docker images
   #----------------------------------------------------------------------------
   check_docker_images:
     needs: default_versions
-
     runs-on: ubuntu-22.04
 
     # Pull request can require a rebuild of docker image, wait for it to be done
+    # This job does not do anything in master branch but is not skipped for simpler logic
     steps:
-      - name: Wait on build-docker-images workflow
-        if: ${{ github.event_name == 'pull_request' }}
-        uses: ArcticLampyrid/action-wait-for-workflow@v1.2.0
-        with:
-          workflow: build-docker-images.yml
-          allowed-conclusions: success
+      - name: Extract branch name
+        if: contains(github.event.pull_request.labels.*.name, 'ci:full')
+        shell: bash
+        run: echo "F3D_BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
+
+      - name: Wait for docker images
+        if: contains(github.event.pull_request.labels.*.name, 'ci:full')
+        run: |
+          gh run list -w build-docker-images.yml --repo ${{ github.repository }} -L 1 -b ${{ env.F3D_BRANCH_NAME }}
+          until gh run list -w build-docker-images.yml --repo ${{ github.repository }} -L 1 -b ${{ env.F3D_BRANCH_NAME }} --json status | jq '.[]| .status' | grep -q "completed";
+          do
+            echo "Waiting 1s"
+            sleep 1;
+          done
+          gh run list -w build-docker-images.yml --repo ${{ github.repository }} -L 1 -b ${{ env.F3D_BRANCH_NAME }} --json conclusion | jq '.[]| .conclusion' | grep -q "success"
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
   #----------------------------------------------------------------------------
   # android: Check build of F3D for android
   #----------------------------------------------------------------------------
   android:
-    needs: [default_versions, detect_checkboxes, check_docker_images]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Android docker CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [default_versions, check_docker_images]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
       matrix:
         arch: [armeabi-v7a, arm64-v8a, x86, x86_64]
 
     runs-on: ubuntu-22.04
-    container: ghcr.io/f3d-app/f3d-android-${{ matrix.arch }}:${{needs.default_versions.outputs.timestamp}}
+    container: ghcr.io/f3d-app/f3d-android-${{ matrix.arch }}:${{needs.default_versions.outputs.docker_timestamp}}
 
     steps:
       - name: Checkout
@@ -866,64 +1019,52 @@
   # webassembly: Build webassembly artifacts
   #----------------------------------------------------------------------------
   webassembly:
-    needs: [default_versions, detect_checkboxes, check_docker_images]
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'WASM docker CI')) || (github.ref == 'refs/heads/master') }}
-
+    needs: [cache_lfs, default_versions, check_docker_images]
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:full') ||
+      github.ref == 'refs/heads/master'
     strategy:
       fail-fast: false
 
     runs-on: ubuntu-22.04
-    container: ghcr.io/f3d-app/f3d-wasm:${{needs.default_versions.outputs.timestamp}}
+
+    env:
+      CMAKE_BUILD_PARALLEL_LEVEL: 4
+      F3D_DOCKER_TIMESTAMP: ${{needs.default_versions.outputs.docker_timestamp}}
 
     steps:
-      # Note that LFS is needed in order to include the default model
       - name: Checkout
         uses: actions/checkout@v4
         with:
           path: "source"
           fetch-depth: 0
-          lfs: true
+          lfs: false
 
       - name: WebAssembly CI
         uses: ./source/.github/actions/wasm-ci
+        with:
+          lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
 
-  #----------------------------------------------------------------------------
-  # generate website
-  #----------------------------------------------------------------------------
-  generate-website:
-    needs: webassembly
-    name: Generate Website
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-
-      - name: Build Jekyll
-        uses: actions/jekyll-build-pages@v1
-
-      - name: Install doxygen and build doxygen doc
-        working-directory: ${{github.workspace}}
-        run: |
-          pip install "sphinx==5.3.0" "pydata-sphinx-theme==0.13.1" sphinx-book-theme exhale
-          sudo apt install doxygen
-          sphinx-build -M html ./library/doxygen ./library/doxygen/build/
-          sudo mv library/doxygen/build/html _site/doc/libf3d/doxygen
-          sphinx-build -M html ./vtkext/public/doxygen ./vtkext/public/doxygen/build/
-          sudo mv vtkext/public/doxygen/build/html _site/doc/libf3d/vtkext_doxygen
+      # TODO: it's currently NOT using NPM, let's fix it when 3.3 is available
+      - name: Install dependencies
+        working-directory: ${{github.workspace}}/source/examples/libf3d/web
+        run: npm install
+
+      - name: Build F3D web app with npm F3D package
+        working-directory: ${{github.workspace}}/source/examples/libf3d/web
+        run: npm run build
 
-      - name: Retrieve web assembly binaries
+      - name: Retrieve f3d package
         uses: actions/download-artifact@v4
         with:
-          name: f3d-wasm
-          path: ./web
-
-      - name: Copy web assembly binaries
-        run: sudo mv web _site/
+          name: f3d-wasm-package
+          path: ${{github.workspace}}/source/examples/libf3d/web
 
-      - name: Deploy
-        if: github.ref == 'refs/heads/master'
-        uses: JamesIves/github-pages-deploy-action@v4
-        with:
-          folder: _site
-          token: ${{ secrets.GITHUB_TOKEN }}
-          clean: true
+      # Override with the locally built package
+      - name: Install f3d package
+        working-directory: ${{github.workspace}}/source/examples/libf3d/web
+        run: npm install f3d-*.tgz
+
+      - name: Build F3D web app with package just built from branch
+        working-directory: ${{github.workspace}}/source/examples/libf3d/web
+        run: npm run build
diff -Nru f3d-3.2.0+dfsg/.github/workflows/labelize.yml f3d-3.4.1/.github/workflows/labelize.yml
--- f3d-3.2.0+dfsg/.github/workflows/labelize.yml	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/.github/workflows/labelize.yml	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,52 @@
+on: issue_comment
+name: Labelize
+jobs:
+  #----------------------------------------------------------------------------
+  # Labelize
+  #----------------------------------------------------------------------------
+  labelize:
+    name: Labelize
+    runs-on: ubuntu-latest
+    if: |
+      github.event.issue.pull_request &&
+      contains(github.event.comment.body, '\ci')
+    env:
+      GH_REPO: ${{ github.repository }}
+      GH_TOKEN: ${{ secrets.F3D_CI_LABEL }}
+
+    steps:
+      - name: Analyze comment
+        shell: bash
+        run: |
+          label="${{github.event.comment.body}}"
+          label=${label#* }
+          label=${label%% *}
+          echo "F3D_LABEL_COMMENT=$label" >> $GITHUB_ENV
+
+      - name: Add ack reaction
+        uses: GrantBirki/comment@v2.1.1
+        with:
+          comment-id: ${{ github.event.comment.id }}
+          reactions: eyes
+
+      - name: Add label on PR
+        if: ${{!contains(env.F3D_LABEL_COMMENT, '-')}}
+        shell: bash
+        run: gh issue edit ${{ github.event.issue.number }} --add-label "ci:${{env.F3D_LABEL_COMMENT}}"
+
+      - name: Remove label on PR
+        if: contains(env.F3D_LABEL_COMMENT, '-')
+        shell: bash
+        run: |
+          label="${{env.F3D_LABEL_COMMENT}}"
+          label=${label#*-}
+          gh issue edit ${{ github.event.issue.number }} --remove-label "ci:$label"
+
+      - name: Create comment for invalid command
+        if: failure()
+        uses: GrantBirki/comment@v2.1.1
+        with:
+          body: |
+            > ${{github.event.comment.body}}
+
+            ❌ Invalid CI command "${{env.F3D_LABEL_COMMENT}}"
diff -Nru f3d-3.2.0+dfsg/.github/workflows/nightly_vtk_master.yml f3d-3.4.1/.github/workflows/nightly_vtk_master.yml
--- f3d-3.2.0+dfsg/.github/workflows/nightly_vtk_master.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/nightly_vtk_master.yml	2026-01-08 08:41:55.000000000 +0200
@@ -41,26 +41,23 @@
     name: Set default versions
     outputs:
       alembic_version: ${{ steps.set_default_versions.outputs.alembic_version }}
-      alembic_min_version: ${{ steps.set_default_versions.outputs.alembic_min_version }}
       assimp_version: ${{ steps.set_default_versions.outputs.assimp_version }}
-      assimp_min_version: ${{ steps.set_default_versions.outputs.assimp_min_version }}
+      blosc_version: ${{ steps.set_default_versions.outputs.blosc_version }}
       draco_version: ${{ steps.set_default_versions.outputs.draco_version }}
-      draco_min_version: ${{ steps.set_default_versions.outputs.draco_min_version }}
+      imath_version: ${{ steps.set_default_versions.outputs.imath_version }}
+      java_version: ${{ steps.set_default_versions.outputs.java_version }}
       occt_version: ${{ steps.set_default_versions.outputs.occt_version }}
-      occt_min_version: ${{ steps.set_default_versions.outputs.occt_min_version }}
       openexr_version: ${{ steps.set_default_versions.outputs.openexr_version }}
-      openexr_min_version: ${{ steps.set_default_versions.outputs.openexr_min_version }}
       openvdb_version: ${{ steps.set_default_versions.outputs.openvdb_version }}
-      openvdb_min_version: ${{ steps.set_default_versions.outputs.openvdb_min_version }}
+      ospray_version: ${{ steps.set_default_versions.outputs.ospray_version }}
       pybind11_version: ${{ steps.set_default_versions.outputs.pybind11_version }}
-      pybind11_min_version: ${{ steps.set_default_versions.outputs.pybind11_min_version }}
       python_version: ${{ steps.set_default_versions.outputs.python_version }}
-      python_min_version: ${{ steps.set_default_versions.outputs.python_min_version }}
+      tbb_version: ${{ steps.set_default_versions.outputs.tbb_version }}
       usd_version: ${{ steps.set_default_versions.outputs.usd_version }}
-      usd_min_version: ${{ steps.set_default_versions.outputs.usd_min_version }}
-      java_version: ${{ steps.set_default_versions.outputs.java_version }}
-      java_min_version: ${{ steps.set_default_versions.outputs.java_min_version }}
-      timestamp: ${{ steps.set_default_versions.outputs.timestamp }}
+      webp_version: ${{ steps.set_default_versions.outputs.webp_version }}
+      zlib_version: ${{ steps.set_default_versions.outputs.zlib_version }}
+      docker_timestamp: ${{ steps.set_default_versions.outputs.docker_timestamp }}
+      global_cache_index: ${{ steps.set_default_versions.outputs.global_cache_index }}
 
     steps:
       - name: Checkout
@@ -97,7 +94,7 @@
 
       - name: Cache LFS Data
         id: lfs_sha_recover
-        uses: f3d-app/lfs-data-cache-action@v1
+        uses: f3d-app/lfs-data-cache-action@v2
         with:
           cache_postfix: cache-0
 
@@ -111,6 +108,9 @@
       fail-fast: false
     runs-on: windows-latest
 
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -123,18 +123,24 @@
         uses: ./source/.github/actions/generic-ci
         with:
           vtk_version: ${{needs.check_nightly.outputs.vtk_sha}}
-          raytracing_label: raytracing
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{ needs.default_versions.outputs.webp_version }}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Linux CI: Build and test
@@ -156,6 +162,7 @@
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       DISPLAY: :0
 
     steps:
@@ -171,19 +178,25 @@
         with:
           build_type: ${{matrix.build_type}}
           vtk_version: ${{needs.check_nightly.outputs.vtk_sha}}
-          raytracing_label: raytracing
           rendering_backend: ${{matrix.rendering_backend}}
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{ needs.default_versions.outputs.webp_version }}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # MacOS CI: Build and test
@@ -194,7 +207,10 @@
     strategy:
       fail-fast: false
 
-    runs-on: macos-13
+    runs-on: macos-15-intel
+
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
 
     steps:
       - name: Checkout
@@ -212,14 +228,21 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{ needs.default_versions.outputs.webp_version }}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # MacOS arm64 CI: Build and test
@@ -230,7 +253,10 @@
     strategy:
       fail-fast: false
 
-    runs-on: macos-14
+    runs-on: macos-15
+
+    env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
 
     steps:
       - name: Checkout
@@ -248,14 +274,21 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
+          ospray_version: ${{needs.default_versions.outputs.ospray_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{ needs.default_versions.outputs.webp_version }}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Sanitizer: Build and test on linux with last VTK with sanitizer options
@@ -276,6 +309,7 @@
     container: ghcr.io/f3d-app/f3d-ci
 
     env:
+      CMAKE_POLICY_VERSION_MINIMUM: 3.5
       DISPLAY: :0
       CC: clang
       CXX: clang++
@@ -297,14 +331,20 @@
           lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
           alembic_version: ${{needs.default_versions.outputs.alembic_version}}
           assimp_version: ${{needs.default_versions.outputs.assimp_version}}
+          blosc_version: ${{needs.default_versions.outputs.blosc_version}}
           draco_version: ${{needs.default_versions.outputs.draco_version}}
+          imath_version: ${{needs.default_versions.outputs.imath_version}}
+          java_version: ${{needs.default_versions.outputs.java_version}}
           occt_version: ${{needs.default_versions.outputs.occt_version}}
           openexr_version: ${{needs.default_versions.outputs.openexr_version}}
           openvdb_version: ${{needs.default_versions.outputs.openvdb_version}}
           pybind11_version: ${{needs.default_versions.outputs.pybind11_version}}
           python_version: ${{needs.default_versions.outputs.python_version}}
+          tbb_version: ${{needs.default_versions.outputs.tbb_version}}
           usd_version: ${{needs.default_versions.outputs.usd_version}}
-          java_version: ${{needs.default_versions.outputs.java_version}}
+          webp_version: ${{ needs.default_versions.outputs.webp_version }}
+          zlib_version: ${{needs.default_versions.outputs.zlib_version}}
+          global_cache_index: ${{needs.default_versions.outputs.global_cache_index}}
 
   #----------------------------------------------------------------------------
   # Build android/wasm docker images
@@ -339,7 +379,7 @@
         arch: [armeabi-v7a, arm64-v8a, x86, x86_64]
 
     runs-on: ubuntu-22.04
-    container: ghcr.io/f3d-app/f3d-android-${{ matrix.arch }}:${{needs.default_versions.outputs.timestamp}}_${{needs.check_nightly.outputs.vtk_sha}}
+    container: ghcr.io/f3d-app/f3d-android-${{ matrix.arch }}:${{needs.default_versions.outputs.docker_timestamp}}_${{needs.check_nightly.outputs.vtk_sha}}
 
     steps:
       - name: Checkout
@@ -358,15 +398,15 @@
   # webassembly: Build webassembly artifacts
   #----------------------------------------------------------------------------
   webassembly:
-    needs: [check_nightly, default_versions, build_docker_images]
+    needs: [cache_lfs, check_nightly, default_versions, build_docker_images]
     strategy:
       fail-fast: false
 
     runs-on: ubuntu-22.04
-    container: ghcr.io/f3d-app/f3d-wasm:${{needs.default_versions.outputs.timestamp}}
-    # Will be fixed once VTK fixes wasm
-    # https://github.com/f3d-app/f3d/issues/2289
-    # container: ghcr.io/f3d-app/f3d-wasm:${{needs.default_versions.outputs.timestamp}}_${{needs.check_nightly.outputs.vtk_sha}}
+
+    env:
+      CMAKE_BUILD_PARALLEL_LEVEL: 4
+      F3D_DOCKER_TIMESTAMP: ${{needs.default_versions.outputs.docker_timestamp}}
 
     steps:
       - name: Checkout
@@ -378,3 +418,5 @@
 
       - name: WebAssembly CI
         uses: ./source/.github/actions/wasm-ci
+        with:
+          lfs_sha: ${{ needs.cache_lfs.outputs.lfs_sha}}
diff -Nru f3d-3.2.0+dfsg/.github/workflows/public-api-warn.yml f3d-3.4.1/.github/workflows/public-api-warn.yml
--- f3d-3.2.0+dfsg/.github/workflows/public-api-warn.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/public-api-warn.yml	2026-01-08 08:41:55.000000000 +0200
@@ -16,4 +16,4 @@
         with:
           message: |
             You are modifying libf3d public API! :warning:Please update bindings accordingly:warning:!
-            You can find them in their respective directories: `python`, `java`, `webassembly`.
+            You can find them in their respective directories: `c`, `python`, `java`, `webassembly`.
diff -Nru f3d-3.2.0+dfsg/.github/workflows/style-checks.yml f3d-3.4.1/.github/workflows/style-checks.yml
--- f3d-3.2.0+dfsg/.github/workflows/style-checks.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/style-checks.yml	2026-01-08 08:41:55.000000000 +0200
@@ -1,44 +1,21 @@
 on:
   pull_request_target:
-    types: [assigned, opened, synchronize, reopened]
+    types: [assigned, opened, synchronize, reopened, labeled]
     branches:
       - master
       - release
 name: Style Checks
 jobs:
   #----------------------------------------------------------------------------
-  # Detect checkboxes are checked
-  #----------------------------------------------------------------------------
-  detect_checkboxes:
-    name: Detect Checkboxes
-    runs-on: ubuntu-latest
-    outputs:
-      checked: ${{ steps.detect.outputs.checked }}
-      unchecked: ${{ steps.detect.outputs.unchecked }}
-
-    permissions:
-      pull-requests: write
-
-    steps:
-      - name: Checkbox Trigger
-        id: detect
-        uses: AhmedBaset/checklist@3
-        with:
-          token: ${{ github.token }}
-
-      - name: list changes
-        run: |
-          echo "checked=${{ steps.detect.outputs.checked }}"
-          echo "unchecked=${{ steps.detect.outputs.unchecked }}"
-
-  #----------------------------------------------------------------------------
   # Style checks
   #----------------------------------------------------------------------------
   style_checks:
     name: Formatting Check
     runs-on: ubuntu-latest
-    needs: detect_checkboxes
-    if: ${{ (contains(needs.detect_checkboxes.outputs.checked, 'Style checks')) }}
+    if: |
+      contains(github.event.pull_request.labels.*.name, 'ci:fast') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:main') ||
+      contains(github.event.pull_request.labels.*.name, 'ci:full')
     env:
       ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
@@ -78,12 +55,42 @@
           dry: True
           prettier_options: "-w **/*.{js,json,md,html,yml}"
 
-      - uses: googleapis/code-suggester@v5
-        with:
-          command: review
-          pull_number: ${{ github.event.pull_request.number }}
-          git_dir: "."
-
-      - name: Diff output
+      - name: Echo Diff
         shell: bash
         run: git diff --color=always --exit-code
+
+      - name: Generate Diff
+        if: failure()
+        shell: bash
+        run: |
+          diff=$(git diff)
+          echo "$diff" > ./f3d_style_checks_diff.patch
+          {
+            echo 'F3D_STYLE_DIFF<<EOF'
+            echo "$diff"
+            echo EOF
+          } >> "$GITHUB_ENV"
+
+      - name: Upload diff as an artifact
+        if: failure()
+        uses: actions/upload-artifact@v4
+        with:
+          path: ./f3d_style_diff.patch
+          name: f3d-style-checks-diff-${{ github.event.pull_request.head.ref }}
+
+      - name: Delete comment
+        uses: marocchino/sticky-pull-request-comment@v2
+        with:
+          header: style
+          delete: true
+
+      - name: Create comment
+        if: failure()
+        uses: marocchino/sticky-pull-request-comment@v2
+        with:
+          header: style
+          message: |
+            Style Checks CI failed:
+            ```diff
+            ${{env.F3D_STYLE_DIFF}}
+            ```
diff -Nru f3d-3.2.0+dfsg/.github/workflows/versions.json f3d-3.4.1/.github/workflows/versions.json
--- f3d-3.2.0+dfsg/.github/workflows/versions.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/versions.json	2026-01-08 08:41:55.000000000 +0200
@@ -4,32 +4,47 @@
     "OpenEXR is part of VFX reference platform (CY2025: 3.3.x)",
     "OpenVDB is part of VFX reference platform (CY2025: 12.x)",
     "OpenVDB min version is not enforced by F3D code and missing from VTK code",
-    "Python is part of VFX reference platform (CY2025: 3.11)"
+    "Python is part of VFX reference platform (CY2025: 3.11)",
+    "OSPRay is fixed at 2.12.0 until full support for OSPRay 3.0 is added in VTK",
+    "TBB is set at v2021.13.1 because OpenUSD seems to not support more revent versions: https://github.com/PixarAnimationStudios/OpenUSD/issues/3878"
   ],
   "versions": {
-    "alembic": "1.8.8",
-    "assimp": "v6.0.1",
+    "alembic": "1.8.9",
+    "assimp": "v6.0.2",
+    "blosc": "v1.21.6",
     "draco": "1.5.7",
-    "occt": "V7_9_0",
-    "openexr": "v3.3.3",
-    "openvdb": "v12.0.1",
-    "pybind11": "v2.13.6",
-    "python": "3.13",
-    "usd": "v25.02a",
-    "java": "21"
+    "imath": "v3.2.2",
+    "java": "25",
+    "occt": "V7_9_2",
+    "openexr": "v3.4.2",
+    "openvdb": "v12.1.1",
+    "ospray": "v2.12.0",
+    "pybind11": "v3.0.1",
+    "python": "3.14",
+    "tbb": "v2021.13.1",
+    "usd": "v25.11",
+    "webp": "v1.6.0",
+    "zlib": "v1.3.1"
   },
   "min_versions": {
     "alembic": "1.8.5",
     "assimp": "v5.4.0",
+    "blosc": "v1.17.0",
     "draco": "1.5.6",
+    "imath": "v3.0.1",
+    "java": "17",
     "occt": "V7_6_3",
     "openexr": "v3.0.1",
     "openvdb": "v12.0.0",
+    "ospray": "v2.12.0",
     "pybind11": "v2.9.2",
-    "python": "3.9",
+    "python": "3.10",
+    "tbb": "v2021.10.0",
     "usd": "v24.08",
-    "java": "17"
+    "webp": "v1.2.4",
+    "zlib": "v1.2.13"
   },
-  "vtk_commit_sha": "6ad1aadbfd484044f33b5d89b0eec56c9a5d8178",
-  "timestamp": "20250703_0"
+  "vtk_commit_sha": "0ee0de4ac6c33cf3939cc7edbd69bead532d5d5f",
+  "docker_timestamp": "20251223_0",
+  "global_cache_index": "1"
 }
diff -Nru f3d-3.2.0+dfsg/.github/workflows/version-timestamp-warn.yml f3d-3.4.1/.github/workflows/version-timestamp-warn.yml
--- f3d-3.2.0+dfsg/.github/workflows/version-timestamp-warn.yml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.github/workflows/version-timestamp-warn.yml	2026-01-08 08:41:55.000000000 +0200
@@ -13,4 +13,4 @@
     steps:
       - uses: marocchino/sticky-pull-request-comment@v2
         with:
-          message: You are modifying versions.json, please update the timestamp as well, this will generate new docker images and caches needed for CI.
+          message: You are modifying versions.json, please update the docker timestamp as well, this will generate new docker images and caches needed for CI.
diff -Nru f3d-3.2.0+dfsg/.gitignore f3d-3.4.1/.gitignore
--- f3d-3.2.0+dfsg/.gitignore	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.gitignore	2026-01-08 08:41:55.000000000 +0200
@@ -17,6 +17,7 @@
 
 # wasm process related
 node_modules
+_wasm_build
 
 # testing related
 .cache
diff -Nru f3d-3.2.0+dfsg/_includes/head_custom.html f3d-3.4.1/_includes/head_custom.html
--- f3d-3.2.0+dfsg/_includes/head_custom.html	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/_includes/head_custom.html	1970-01-01 02:00:00.000000000 +0200
@@ -1,9 +0,0 @@
-<link
-  rel="shortcut icon"
-  href="https://raw.githubusercontent.com/f3d-app/f3d/master/resources/logo.ico"
-  type="image/x-icon"
-/>
-<script
-  type="application/javascript"
-  src="{{ site.baseurl }}/assets/main.js"
-></script>
diff -Nru f3d-3.2.0+dfsg/java/Camera.java f3d-3.4.1/java/Camera.java
--- f3d-3.2.0+dfsg/java/Camera.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/Camera.java	2026-01-08 08:41:55.000000000 +0200
@@ -2,23 +2,244 @@
 
 public class Camera {
 
+    /**
+     * A class containing all information to configure a camera.
+     */
+    public static class CameraState {
+        /**
+         * Camera position [x, y, z].
+         */
+        public double[] position;
+
+        /**
+         * Camera focal point [x, y, z].
+         */
+        public double[] focalPoint;
+
+        /**
+         * Camera view up vector [x, y, z].
+         */
+        public double[] viewUp;
+
+        /**
+         * Camera view angle in degrees.
+         */
+        public double viewAngle;
+
+        /**
+         * Create a camera state with default values.
+         */
+        public CameraState() {
+            this.position = new double[] { 0.0, 0.0, 1.0 };
+            this.focalPoint = new double[] { 0.0, 0.0, 0.0 };
+            this.viewUp = new double[] { 0.0, 1.0, 0.0 };
+            this.viewAngle = 30.0;
+        }
+
+        /**
+         * Create a camera state with specified values.
+         *
+         * @param position camera position [x, y, z]
+         * @param focalPoint camera focal point [x, y, z]
+         * @param viewUp camera view up vector [x, y, z]
+         * @param viewAngle camera view angle in degrees
+         */
+        public CameraState(double[] position, double[] focalPoint, double[] viewUp, double viewAngle) {
+            this.position = position;
+            this.focalPoint = focalPoint;
+            this.viewUp = viewUp;
+            this.viewAngle = viewAngle;
+        }
+    }
+
     public Camera(long nativeAddress) {
         mNativeAddress = nativeAddress;
     }
 
-    public native void dolly(double val);
-    public native void roll(double angle);
-    public native void azimuth(double angle);
-    public native void yaw(double angle);
-    public native void elevation(double angle);
-    public native void pitch(double angle);
+    /**
+     * Set the position of the camera.
+     *
+     * @param pos array of 3 doubles [x, y, z]
+     * @return this camera for method chaining
+     */
+    public native Camera setPosition(double[] pos);
+
+    /**
+     * Get the position of the camera.
+     *
+     * @return array of 3 doubles [x, y, z]
+     */
+    public native double[] getPosition();
 
+    /**
+     * Set the focal point of the camera.
+     *
+     * @param foc array of 3 doubles [x, y, z]
+     * @return this camera for method chaining
+     */
+    public native Camera setFocalPoint(double[] foc);
+
+    /**
+     * Get the focal point of the camera.
+     *
+     * @return array of 3 doubles [x, y, z]
+     */
     public native double[] getFocalPoint();
-    public native void setFocalPoint(double[] pt);
-    public native double[] getPosition();
-    public native void setPosition(double[] pt);
 
-    public native void resetToBounds();
+    /**
+     * Set the view up vector of the camera.
+     *
+     * @param up array of 3 doubles [x, y, z]
+     * @return this camera for method chaining
+     */
+    public native Camera setViewUp(double[] up);
+
+    /**
+     * Get the view up vector of the camera.
+     *
+     * @return array of 3 doubles [x, y, z]
+     */
+    public native double[] getViewUp();
+
+    /**
+     * Set the view angle in degrees of the camera.
+     *
+     * @param angle view angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera setViewAngle(double angle);
+
+    /**
+     * Get the view angle in degrees of the camera.
+     *
+     * @return view angle in degrees
+     */
+    public native double getViewAngle();
+
+    /**
+     * Set the complete state of the camera.
+     *
+     * @param state camera state to set
+     * @return this camera for method chaining
+     */
+    public native Camera setState(CameraState state);
+
+    /**
+     * Get the complete state of the camera.
+     *
+     * @return current camera state
+     */
+    public native CameraState getState();
+
+    /**
+     * Divide the camera's distance from the focal point by the given value.
+     *
+     * @param val dolly factor
+     * @return this camera for method chaining
+     */
+    public native Camera dolly(double val);
+
+    /**
+     * Move the camera along its horizontal, vertical, and forward axes.
+     *
+     * @param right distance to move right
+     * @param up distance to move up
+     * @param forward distance to move forward
+     * @return this camera for method chaining
+     */
+    public native Camera pan(double right, double up, double forward);
+
+    /**
+     * Move the camera along its horizontal and vertical axes.
+     *
+     * @param right distance to move right
+     * @param up distance to move up
+     * @return this camera for method chaining
+     */
+    public Camera pan(double right, double up) {
+        return pan(right, up, 0.0);
+    }
+
+    /**
+     * Decrease the view angle (or the parallel scale in parallel mode) by the specified factor.
+     *
+     * @param factor zoom factor
+     * @return this camera for method chaining
+     */
+    public native Camera zoom(double factor);
+
+    /**
+     * Rotate the camera about its forward axis.
+     *
+     * @param angle rotation angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera roll(double angle);
+
+    /**
+     * Rotate the camera about its vertical axis, centered at the focal point.
+     *
+     * @param angle rotation angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera azimuth(double angle);
+
+    /**
+     * Rotate the camera about its vertical axis, centered at the camera's position.
+     *
+     * @param angle rotation angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera yaw(double angle);
+
+    /**
+     * Rotate the camera about its horizontal axis, centered at the focal point.
+     *
+     * @param angle rotation angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera elevation(double angle);
+
+    /**
+     * Rotate the camera about its horizontal axis, centered at the camera's position.
+     *
+     * @param angle rotation angle in degrees
+     * @return this camera for method chaining
+     */
+    public native Camera pitch(double angle);
+
+    /**
+     * Store the current camera configuration as default.
+     *
+     * @return this camera for method chaining
+     */
+    public native Camera setCurrentAsDefault();
+
+    /**
+     * Reset the camera to the stored default camera configuration.
+     *
+     * @return this camera for method chaining
+     */
+    public native Camera resetToDefault();
+
+    /**
+     * Reset the camera using the bounds of actors in the scene.
+     * Provided zoomFactor will be used to position the camera.
+     * A value of 1 corresponds to the bounds roughly aligned to the edges of the window.
+     *
+     * @param zoomFactor zoom factor (default 0.9)
+     * @return this camera for method chaining
+     */
+    public native Camera resetToBounds(double zoomFactor);
+
+    /**
+     * Reset the camera using the bounds of actors in the scene with default zoom factor.
+     *
+     * @return this camera for method chaining
+     */
+    public Camera resetToBounds() {
+        return resetToBounds(0.9);
+    }
 
     private long mNativeAddress;
 }
diff -Nru f3d-3.2.0+dfsg/java/CMakeLists.txt f3d-3.4.1/java/CMakeLists.txt
--- f3d-3.2.0+dfsg/java/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -10,9 +10,16 @@
 
 set(F3D_JAVA_SOURCES
   Camera.java
+  Context.java
   Engine.java
+  Image.java
+  Interactor.java
+  Log.java
   Options.java
   Scene.java
+  Transform2D.java
+  Types.java
+  Utils.java
   Window.java)
 
 # Generate JNI headers, and builds a JAR package
@@ -21,7 +28,20 @@
   GENERATE_NATIVE_HEADERS f3d-jni-headers
   OUTPUT_NAME f3d)
 
-add_library(javaf3d SHARED F3DJavaBindings.cxx)
+set(F3D_JAVA_BINDINGS_SOURCES
+  F3DCameraBindings.cxx
+  F3DContextBindings.cxx
+  F3DEngineBindings.cxx
+  F3DImageBindings.cxx
+  F3DInteractorBindings.cxx
+  F3DLogBindings.cxx
+  F3DOptionsBindings.cxx
+  F3DSceneBindings.cxx
+  F3DTypesBindings.cxx
+  F3DUtilsBindings.cxx
+  F3DWindowBindings.cxx)
+
+add_library(javaf3d SHARED ${F3D_JAVA_BINDINGS_SOURCES})
 target_link_libraries(javaf3d PRIVATE f3d-jni-headers)
 
 target_include_directories(javaf3d PRIVATE ${JNI_INCLUDE_DIRS})
diff -Nru f3d-3.2.0+dfsg/java/Context.java f3d-3.4.1/java/Context.java
--- f3d-3.2.0+dfsg/java/Context.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Context.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,60 @@
+package app.f3d.F3D;
+
+public class Context {
+
+    // Used to load the 'f3d' library on application startup.
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    /**
+     * Create a GLX context.
+     *
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long glx();
+
+    /**
+     * Create a WGL context.
+     *
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long wgl();
+
+    /**
+     * Create a COCOA context.
+     *
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long cocoa();
+
+    /**
+     * Create an EGL context.
+     *
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long egl();
+
+    /**
+     * Create an OSMesa context.
+     *
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long osmesa();
+
+    /**
+     * Create a context from a library name and a function name.
+     *
+     * @param lib the library name (without prefix or extension)
+     * @param func the function name to resolve
+     * @return context handle (must be deleted with {@link #delete(long)})
+     */
+    public static native long getSymbol(String lib, String func);
+
+    /**
+     * Delete a context and free associated resources.
+     *
+     * @param contextHandle the context handle to delete
+     */
+    public static native void delete(long contextHandle);
+}
diff -Nru f3d-3.2.0+dfsg/java/Engine.java f3d-3.4.1/java/Engine.java
--- f3d-3.2.0+dfsg/java/Engine.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/Engine.java	2026-01-08 08:41:55.000000000 +0200
@@ -1,41 +1,311 @@
 package app.f3d.F3D;
 
+import java.util.List;
+import java.util.Map;
+
 public class Engine implements AutoCloseable {
+    @FunctionalInterface
+    public interface ContextFunction {
+        /**
+         * Get the address of an OpenGL function by name
+         * @param name the function name (e.g., "glClear")
+         * @return the function pointer address as a long
+         */
+        long getProcAddress(String name);
+    }
+
+    public static class LibInfo {
+        public String version;
+        public String versionFull;
+        public String buildDate;
+        public String buildSystem;
+        public String compiler;
+        public Map<String, Boolean> modules;
+        public String vtkVersion;
+        public List<String> copyrights;
+        public String license;
+
+        public LibInfo(String version, String versionFull, String buildDate, String buildSystem,
+                       String compiler, Map<String, Boolean> modules, String vtkVersion,
+                       List<String> copyrights, String license) {
+            this.version = version;
+            this.versionFull = versionFull;
+            this.buildDate = buildDate;
+            this.buildSystem = buildSystem;
+            this.compiler = compiler;
+            this.modules = modules;
+            this.vtkVersion = vtkVersion;
+            this.copyrights = copyrights;
+            this.license = license;
+        }
+    }
+
+    public static class ReaderInfo {
+        public String name;
+        public String description;
+        public List<String> extensions;
+        public List<String> mimeTypes;
+        public String pluginName;
+        public boolean hasSceneReader;
+        public boolean hasGeometryReader;
+
+        public ReaderInfo(String name, String description, List<String> extensions,
+                          List<String> mimeTypes, String pluginName,
+                          boolean hasSceneReader, boolean hasGeometryReader) {
+            this.name = name;
+            this.description = description;
+            this.extensions = extensions;
+            this.mimeTypes = mimeTypes;
+            this.pluginName = pluginName;
+            this.hasSceneReader = hasSceneReader;
+            this.hasGeometryReader = hasGeometryReader;
+        }
+    }
+
     // Used to load the 'f3d' library on application startup.
     static {
         System.loadLibrary("f3d-java");
     }
 
-    public Engine() {
-        mNativeAddress = construct(); // instantiate the native engine
+    private Engine(long nativeAddress) {
+        mNativeAddress = nativeAddress;
         mScene = new Scene(mNativeAddress);
         mOptions = new Options(mNativeAddress);
         mWindow = new Window(mNativeAddress);
     }
 
-    // The engine class is automatically released using the Java
-    // try-with-resources idiom
-    // see https://www.baeldung.com/java-try-with-resources
+    /**
+     * Create an engine with automatic window selection
+     * @param offscreen if true, window will be hidden
+     * @return new Engine instance
+     */
+    public static Engine create(boolean offscreen) {
+        return new Engine(nativeCreate(offscreen));
+    }
+
+    /**
+     * Create an engine with automatic window selection (non-offscreen)
+     * @return new Engine instance
+     */
+    public static Engine create() {
+        return create(false);
+    }
+
+    /**
+     * Create an engine with no window
+     * @return new Engine instance
+     */
+    public static Engine createNone() {
+        return new Engine(nativeCreateNone());
+    }
+
+    /**
+     * Create an engine with a GLX window (Linux only)
+     * @param offscreen if true, window will be hidden
+     * @return new Engine instance
+     */
+    public static Engine createGLX(boolean offscreen) {
+        return new Engine(nativeCreateGLX(offscreen));
+    }
+
+    /**
+     * Create an engine with a GLX window (Linux only, non-offscreen)
+     * @return new Engine instance
+     */
+    public static Engine createGLX() {
+        return createGLX(false);
+    }
+
+    /**
+     * Create an engine with a WGL window (Windows only)
+     * @param offscreen if true, window will be hidden
+     * @return new Engine instance
+     */
+    public static Engine createWGL(boolean offscreen) {
+        return new Engine(nativeCreateWGL(offscreen));
+    }
+
+    /**
+     * Create an engine with a WGL window (Windows only, non-offscreen)
+     * @return new Engine instance
+     */
+    public static Engine createWGL() {
+        return createWGL(false);
+    }
+
+    /**
+     * Create an engine with an offscreen EGL window
+     * @return new Engine instance
+     */
+    public static Engine createEGL() {
+        return new Engine(nativeCreateEGL());
+    }
+
+    /**
+     * Create an engine with an offscreen OSMesa window
+     * @return new Engine instance
+     */
+    public static Engine createOSMesa() {
+        return new Engine(nativeCreateOSMesa());
+    }
+
+    /**
+     * Create an engine with an external context using a custom getProcAddress function
+     * @param getProcAddress callback function to resolve OpenGL function addresses
+     * @return new Engine instance
+     */
+    public static Engine createExternal(ContextFunction getProcAddress) {
+        return new Engine(nativeCreateExternal(getProcAddress));
+    }
+
+    /**
+     * Create an engine with an external GLX context (Linux only)
+     * @return new Engine instance
+     */
+    public static Engine createExternalGLX() {
+        return new Engine(nativeCreateExternalGLX());
+    }
+
+    /**
+     * Create an engine with an external WGL context (Windows only)
+     * @return new Engine instance
+     */
+    public static Engine createExternalWGL() {
+        return new Engine(nativeCreateExternalWGL());
+    }
+
+    /**
+     * Create an engine with an external COCOA context (macOS only)
+     * @return new Engine instance
+     */
+    public static Engine createExternalCOCOA() {
+        return new Engine(nativeCreateExternalCOCOA());
+    }
+
+    /**
+     * Create an engine with an external EGL context
+     * @return new Engine instance
+     */
+    public static Engine createExternalEGL() {
+        return new Engine(nativeCreateExternalEGL());
+    }
+
+    /**
+     * Create an engine with an external OSMesa context
+     * @return new Engine instance
+     */
+    public static Engine createExternalOSMesa() {
+        return new Engine(nativeCreateExternalOSMesa());
+    }
+
     @Override
     public void close() {
-        destroy(mNativeAddress);
+        nativeDestroy(mNativeAddress);
     }
 
+    /**
+     * Set the cache path directory
+     * @param cachePath path to cache directory
+     */
     public native void setCachePath(String cachePath);
 
-    static public native void loadPlugin(String plugin);
-    static public native void autoloadPlugins();
+    /**
+     * Set options for this engine
+     * @param options the options to set
+     */
+    public native void setOptions(Options options);
 
-    public Scene getScene() { return mScene; }
+    /**
+     * Get the options
+     * @return Options instance
+     */
     public Options getOptions() { return mOptions; }
+
+    /**
+     * Get the scene
+     * @return Scene instance
+     */
+    public Scene getScene() { return mScene; }
+
+    /**
+     * Get the window
+     * @return Window instance
+     */
     public Window getWindow() { return mWindow; }
 
-    private native long construct();
-    private native void destroy(long nativeAddress);
+    /**
+     * Get the interactor
+     * @return Interactor instance
+     */
+    public native Interactor getInteractor();
+
+    /**
+     * Load a plugin by path or name
+     * @param plugin path or name of the plugin
+     */
+    public static native void loadPlugin(String plugin);
+
+    /**
+     * Automatically load all static plugins
+     */
+    public static native void autoloadPlugins();
+
+    /**
+     * List plugins available in the given directory
+     * @param pluginPath path to search for plugins
+     * @return list of available plugin names
+     */
+    public static native List<String> getPluginsList(String pluginPath);
+
+    /**
+     * Get information about libf3d
+     * @return LibInfo object containing library information
+     */
+    public static native LibInfo getLibInfo();
+
+    /**
+     * Get information about supported readers
+     * @return list of ReaderInfo objects
+     */
+    public static native List<ReaderInfo> getReadersInfo();
+
+    /**
+     * Get list of rendering backends supported by libf3d
+     * @return map of backend names to availability boolean
+     */
+    public static native Map<String, Boolean> getRenderingBackendList();
 
+    /**
+     * Set a reader option value
+     * @param name option name
+     * @param value option value
+     */
+    public static native void setReaderOption(String name, String value);
+
+    /**
+     * Get all reader option names
+     * @return list of option names
+     */
+    public static native List<String> getAllReaderOptionNames();
+
+    // Native methods
+    private static native long nativeCreate(boolean offscreen);
+    private static native long nativeCreateNone();
+    private static native long nativeCreateGLX(boolean offscreen);
+    private static native long nativeCreateWGL(boolean offscreen);
+    private static native long nativeCreateEGL();
+    private static native long nativeCreateOSMesa();
+    private static native long nativeCreateExternal(ContextFunction getProcAddress);
+    private static native long nativeCreateExternalGLX();
+    private static native long nativeCreateExternalWGL();
+    private static native long nativeCreateExternalCOCOA();
+    private static native long nativeCreateExternalEGL();
+    private static native long nativeCreateExternalOSMesa();
+    private static native void nativeDestroy(long nativeAddress);
+
+    // Member variables
+    private long mNativeAddress;
     private Scene mScene;
     private Options mOptions;
     private Window mWindow;
-
-    private long mNativeAddress;
 }
diff -Nru f3d-3.2.0+dfsg/java/F3DCameraBindings.cxx f3d-3.4.1/java/F3DCameraBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DCameraBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DCameraBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,196 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Camera.h>
+
+#include <camera.h>
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Camera, setPosition)(JNIEnv* env, jobject self, jdoubleArray pos)
+  {
+    double* arr = env->GetDoubleArrayElements(pos, nullptr);
+    GetEngine(env, self)->getWindow().getCamera().setPosition({ arr[0], arr[1], arr[2] });
+    env->ReleaseDoubleArrayElements(pos, arr, 0);
+    return self;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Camera, getPosition)(JNIEnv* env, jobject self)
+  {
+    f3d::point3_t pt = GetEngine(env, self)->getWindow().getCamera().getPosition();
+    jdoubleArray ret = env->NewDoubleArray(3);
+    env->SetDoubleArrayRegion(ret, 0, 3, pt.data());
+    return ret;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, setFocalPoint)(JNIEnv* env, jobject self, jdoubleArray foc)
+  {
+    double* arr = env->GetDoubleArrayElements(foc, nullptr);
+    GetEngine(env, self)->getWindow().getCamera().setFocalPoint({ arr[0], arr[1], arr[2] });
+    env->ReleaseDoubleArrayElements(foc, arr, 0);
+    return self;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Camera, getFocalPoint)(JNIEnv* env, jobject self)
+  {
+    f3d::point3_t pt = GetEngine(env, self)->getWindow().getCamera().getFocalPoint();
+    jdoubleArray ret = env->NewDoubleArray(3);
+    env->SetDoubleArrayRegion(ret, 0, 3, pt.data());
+    return ret;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, setViewUp)(JNIEnv* env, jobject self, jdoubleArray up)
+  {
+    double* arr = env->GetDoubleArrayElements(up, nullptr);
+    GetEngine(env, self)->getWindow().getCamera().setViewUp({ arr[0], arr[1], arr[2] });
+    env->ReleaseDoubleArrayElements(up, arr, 0);
+    return self;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Camera, getViewUp)(JNIEnv* env, jobject self)
+  {
+    f3d::vector3_t vec = GetEngine(env, self)->getWindow().getCamera().getViewUp();
+    jdoubleArray ret = env->NewDoubleArray(3);
+    env->SetDoubleArrayRegion(ret, 0, 3, vec.data());
+    return ret;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, setViewAngle)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().setViewAngle(angle);
+    return self;
+  }
+
+  JNIEXPORT jdouble JAVA_BIND(Camera, getViewAngle)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getWindow().getCamera().getViewAngle();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, setState)(JNIEnv* env, jobject self, jobject state)
+  {
+    jclass stateClass = env->GetObjectClass(state);
+
+    jfieldID positionField = env->GetFieldID(stateClass, "position", "[D");
+    jfieldID focalPointField = env->GetFieldID(stateClass, "focalPoint", "[D");
+    jfieldID viewUpField = env->GetFieldID(stateClass, "viewUp", "[D");
+    jfieldID viewAngleField = env->GetFieldID(stateClass, "viewAngle", "D");
+
+    jdoubleArray posArray = static_cast<jdoubleArray>(env->GetObjectField(state, positionField));
+    jdoubleArray focArray = static_cast<jdoubleArray>(env->GetObjectField(state, focalPointField));
+    jdoubleArray upArray = static_cast<jdoubleArray>(env->GetObjectField(state, viewUpField));
+    jdouble angle = env->GetDoubleField(state, viewAngleField);
+
+    double* pos = env->GetDoubleArrayElements(posArray, nullptr);
+    double* foc = env->GetDoubleArrayElements(focArray, nullptr);
+    double* up = env->GetDoubleArrayElements(upArray, nullptr);
+
+    f3d::camera_state_t cppState;
+    cppState.position = { pos[0], pos[1], pos[2] };
+    cppState.focalPoint = { foc[0], foc[1], foc[2] };
+    cppState.viewUp = { up[0], up[1], up[2] };
+    cppState.viewAngle = angle;
+
+    GetEngine(env, self)->getWindow().getCamera().setState(cppState);
+
+    env->ReleaseDoubleArrayElements(posArray, pos, 0);
+    env->ReleaseDoubleArrayElements(focArray, foc, 0);
+    env->ReleaseDoubleArrayElements(upArray, up, 0);
+
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, getState)(JNIEnv* env, jobject self)
+  {
+    f3d::camera_state_t cppState = GetEngine(env, self)->getWindow().getCamera().getState();
+
+    jclass stateClass = env->FindClass("app/f3d/F3D/Camera$CameraState");
+    jmethodID constructor = env->GetMethodID(stateClass, "<init>", "()V");
+    jobject state = env->NewObject(stateClass, constructor);
+
+    jfieldID positionField = env->GetFieldID(stateClass, "position", "[D");
+    jfieldID focalPointField = env->GetFieldID(stateClass, "focalPoint", "[D");
+    jfieldID viewUpField = env->GetFieldID(stateClass, "viewUp", "[D");
+    jfieldID viewAngleField = env->GetFieldID(stateClass, "viewAngle", "D");
+
+    jdoubleArray posArray = env->NewDoubleArray(3);
+    jdoubleArray focArray = env->NewDoubleArray(3);
+    jdoubleArray upArray = env->NewDoubleArray(3);
+
+    env->SetDoubleArrayRegion(posArray, 0, 3, cppState.position.data());
+    env->SetDoubleArrayRegion(focArray, 0, 3, cppState.focalPoint.data());
+    env->SetDoubleArrayRegion(upArray, 0, 3, cppState.viewUp.data());
+
+    env->SetObjectField(state, positionField, posArray);
+    env->SetObjectField(state, focalPointField, focArray);
+    env->SetObjectField(state, viewUpField, upArray);
+    env->SetDoubleField(state, viewAngleField, cppState.viewAngle);
+
+    return state;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, dolly)(JNIEnv* env, jobject self, jdouble val)
+  {
+    GetEngine(env, self)->getWindow().getCamera().dolly(val);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, pan)(
+    JNIEnv* env, jobject self, jdouble right, jdouble up, jdouble forward)
+  {
+    GetEngine(env, self)->getWindow().getCamera().pan(right, up, forward);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, zoom)(JNIEnv* env, jobject self, jdouble factor)
+  {
+    GetEngine(env, self)->getWindow().getCamera().zoom(factor);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, roll)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().roll(angle);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, azimuth)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().azimuth(angle);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, yaw)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().yaw(angle);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, elevation)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().elevation(angle);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, pitch)(JNIEnv* env, jobject self, jdouble angle)
+  {
+    GetEngine(env, self)->getWindow().getCamera().pitch(angle);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, setCurrentAsDefault)(JNIEnv* env, jobject self)
+  {
+    GetEngine(env, self)->getWindow().getCamera().setCurrentAsDefault();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, resetToDefault)(JNIEnv* env, jobject self)
+  {
+    GetEngine(env, self)->getWindow().getCamera().resetToDefault();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Camera, resetToBounds)(JNIEnv* env, jobject self, jdouble zoomFactor)
+  {
+    GetEngine(env, self)->getWindow().getCamera().resetToBounds(zoomFactor);
+    return self;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DContextBindings.cxx f3d-3.4.1/java/F3DContextBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DContextBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DContextBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,61 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Context.h>
+
+#include <context.h>
+
+struct f3d_java_context
+{
+  f3d::context::function func;
+
+  explicit f3d_java_context(f3d::context::function&& f)
+    : func(std::move(f))
+  {
+  }
+};
+
+extern "C"
+{
+  JNIEXPORT jlong JAVA_BIND(Context, glx)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d_java_context(f3d::context::glx()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Context, wgl)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d_java_context(f3d::context::wgl()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Context, cocoa)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d_java_context(f3d::context::cocoa()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Context, egl)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d_java_context(f3d::context::egl()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Context, osmesa)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d_java_context(f3d::context::osmesa()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Context, getSymbol)(JNIEnv* env, jclass, jstring lib, jstring func)
+  {
+    const char* libStr = env->GetStringUTFChars(lib, nullptr);
+    const char* funcStr = env->GetStringUTFChars(func, nullptr);
+
+    f3d_java_context* ctx = new f3d_java_context(f3d::context::getSymbol(libStr, funcStr));
+
+    env->ReleaseStringUTFChars(lib, libStr);
+    env->ReleaseStringUTFChars(func, funcStr);
+
+    return reinterpret_cast<jlong>(ctx);
+  }
+
+  JNIEXPORT void JAVA_BIND(Context, delete)(JNIEnv*, jclass, jlong contextHandle)
+  {
+    delete reinterpret_cast<f3d_java_context*>(contextHandle);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DEngineBindings.cxx f3d-3.4.1/java/F3DEngineBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DEngineBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DEngineBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,252 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Engine.h>
+
+#include <context.h>
+
+extern "C"
+{
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreate)(JNIEnv*, jclass, jboolean offscreen)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::create(offscreen)));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateNone)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createNone()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateGLX)(JNIEnv*, jclass, jboolean offscreen)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createGLX(offscreen)));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateWGL)(JNIEnv*, jclass, jboolean offscreen)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createWGL(offscreen)));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateEGL)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createEGL()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateOSMesa)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createOSMesa()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternal)(
+    JNIEnv* env, jclass, jobject getProcAddress)
+  {
+    if (!getProcAddress)
+    {
+      return 0;
+    }
+
+    jobject globalRef = env->NewGlobalRef(getProcAddress);
+
+    jclass contextFunctionClass = env->GetObjectClass(getProcAddress);
+    jmethodID methodID =
+      env->GetMethodID(contextFunctionClass, "getProcAddress", "(Ljava/lang/String;)J");
+
+    f3d::context::function func = [env, globalRef, methodID](const char* name) -> f3d::context::fptr
+    {
+      jstring jname = env->NewStringUTF(name);
+      jlong addr = env->CallLongMethod(globalRef, methodID, jname);
+      env->DeleteLocalRef(jname);
+      return reinterpret_cast<f3d::context::fptr>(addr);
+    };
+
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternal(func)));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternalGLX)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternalGLX()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternalWGL)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternalWGL()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternalCOCOA)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternalCOCOA()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternalEGL)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternalEGL()));
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Engine, nativeCreateExternalOSMesa)(JNIEnv*, jclass)
+  {
+    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::createExternalOSMesa()));
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, nativeDestroy)(JNIEnv*, jclass, jlong ptr)
+  {
+    delete reinterpret_cast<f3d::engine*>(ptr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, loadPlugin)(JNIEnv* env, jclass, jstring str)
+  {
+    const char* plugin = env->GetStringUTFChars(str, nullptr);
+    f3d::engine::loadPlugin(plugin);
+    env->ReleaseStringUTFChars(str, plugin);
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, autoloadPlugins)(JNIEnv*, jclass)
+  {
+    f3d::engine::autoloadPlugins();
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, setCachePath)(JNIEnv* env, jobject self, jstring path)
+  {
+    const char* str = env->GetStringUTFChars(path, nullptr);
+    GetEngine(env, self)->setCachePath(fs::path(str));
+    env->ReleaseStringUTFChars(path, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, setOptions)(JNIEnv* env, jobject self, jobject options)
+  {
+    jclass optionsClass = env->GetObjectClass(options);
+    jfieldID fid = env->GetFieldID(optionsClass, "mNativeAddress", "J");
+    jlong optionsPtr = env->GetLongField(options, fid);
+
+    GetEngine(env, self)->setOptions(*reinterpret_cast<f3d::options*>(optionsPtr));
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getInteractor)(JNIEnv* env, jobject self)
+  {
+    f3d::interactor& interactor = GetEngine(env, self)->getInteractor();
+
+    jclass interactorClass = env->FindClass("app/f3d/F3D/Interactor");
+    jmethodID constructor = env->GetMethodID(interactorClass, "<init>", "(J)V");
+
+    return env->NewObject(interactorClass, constructor, reinterpret_cast<jlong>(&interactor));
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getPluginsList)(JNIEnv* env, jclass, jstring path)
+  {
+    const char* str = env->GetStringUTFChars(path, nullptr);
+    std::vector<std::string> plugins = f3d::engine::getPluginsList(fs::path(str));
+    env->ReleaseStringUTFChars(path, str);
+
+    return CreateStringList(env, plugins);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getLibInfo)(JNIEnv* env, jclass)
+  {
+    const f3d::engine::libInformation& info = f3d::engine::getLibInfo();
+
+    jclass libInfoClass = env->FindClass("app/f3d/F3D/Engine$LibInfo");
+    jmethodID constructor = env->GetMethodID(libInfoClass, "<init>",
+      "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;"
+      "Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V");
+
+    jstring version = env->NewStringUTF(info.Version.c_str());
+    jstring versionFull = env->NewStringUTF(info.VersionFull.c_str());
+    jstring buildDate = env->NewStringUTF(info.BuildDate.c_str());
+    jstring buildSystem = env->NewStringUTF(info.BuildSystem.c_str());
+    jstring compiler = env->NewStringUTF(info.Compiler.c_str());
+    jobject modules = CreateStringBooleanMap(env, info.Modules);
+    jstring vtkVersion = env->NewStringUTF(info.VTKVersion.c_str());
+    jobject copyrights = CreateStringList(env, info.Copyrights);
+    jstring license = env->NewStringUTF(info.License.c_str());
+
+    jobject libInfo = env->NewObject(libInfoClass, constructor, version, versionFull, buildDate,
+      buildSystem, compiler, modules, vtkVersion, copyrights, license);
+
+    env->DeleteLocalRef(version);
+    env->DeleteLocalRef(versionFull);
+    env->DeleteLocalRef(buildDate);
+    env->DeleteLocalRef(buildSystem);
+    env->DeleteLocalRef(compiler);
+    env->DeleteLocalRef(modules);
+    env->DeleteLocalRef(vtkVersion);
+    env->DeleteLocalRef(copyrights);
+    env->DeleteLocalRef(license);
+
+    return libInfo;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getReadersInfo)(JNIEnv* env, jclass)
+  {
+    const std::vector<f3d::engine::readerInformation>& readers = f3d::engine::getReadersInfo();
+
+    jclass arrayListClass = env->FindClass("java/util/ArrayList");
+    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+    jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+    jobject list = env->NewObject(arrayListClass, arrayListConstructor);
+
+    jclass readerInfoClass = env->FindClass("app/f3d/F3D/Engine$ReaderInfo");
+    jmethodID readerInfoConstructor = env->GetMethodID(readerInfoClass, "<init>",
+      "(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/"
+      "String;ZZ)V");
+
+    for (const auto& reader : readers)
+    {
+      jstring name = env->NewStringUTF(reader.Name.c_str());
+      jstring description = env->NewStringUTF(reader.Description.c_str());
+      jobject extensions = CreateStringList(env, reader.Extensions);
+      jobject mimeTypes = CreateStringList(env, reader.MimeTypes);
+      jstring pluginName = env->NewStringUTF(reader.PluginName.c_str());
+      jboolean hasSceneReader = reader.HasSceneReader;
+      jboolean hasGeometryReader = reader.HasGeometryReader;
+
+      jobject readerInfo = env->NewObject(readerInfoClass, readerInfoConstructor, name, description,
+        extensions, mimeTypes, pluginName, hasSceneReader, hasGeometryReader);
+
+      env->CallBooleanMethod(list, addMethod, readerInfo);
+
+      env->DeleteLocalRef(name);
+      env->DeleteLocalRef(description);
+      env->DeleteLocalRef(extensions);
+      env->DeleteLocalRef(mimeTypes);
+      env->DeleteLocalRef(pluginName);
+      env->DeleteLocalRef(readerInfo);
+    }
+
+    return list;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getRenderingBackendList)(JNIEnv* env, jclass)
+  {
+    std::map<std::string, bool> backends = f3d::engine::getRenderingBackendList();
+    return CreateStringBooleanMap(env, backends);
+  }
+
+  JNIEXPORT void JAVA_BIND(Engine, setReaderOption)(
+    JNIEnv* env, jclass, jstring name, jstring value)
+  {
+    const char* nameStr = env->GetStringUTFChars(name, nullptr);
+    const char* valueStr = env->GetStringUTFChars(value, nullptr);
+
+    try
+    {
+      f3d::engine::setReaderOption(nameStr, valueStr);
+    }
+    catch (const f3d::options::inexistent_exception& e)
+    {
+      env->ReleaseStringUTFChars(name, nameStr);
+      env->ReleaseStringUTFChars(value, valueStr);
+
+      jclass exceptionClass = env->FindClass("java/lang/IllegalArgumentException");
+      env->ThrowNew(exceptionClass, e.what());
+      return;
+    }
+
+    env->ReleaseStringUTFChars(name, nameStr);
+    env->ReleaseStringUTFChars(value, valueStr);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Engine, getAllReaderOptionNames)(JNIEnv* env, jclass)
+  {
+    std::vector<std::string> names = f3d::engine::getAllReaderOptionNames();
+    return CreateStringList(env, names);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DImageBindings.cxx f3d-3.4.1/java/F3DImageBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DImageBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DImageBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,259 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Image.h>
+
+#include <image.h>
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Image, getSupportedFormats)(JNIEnv* env, jclass)
+  {
+    return CreateStringList(env, f3d::image::getSupportedFormats());
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Image, nativeCreateFromFile)(JNIEnv* env, jclass, jstring filePath)
+  {
+    const char* path = env->GetStringUTFChars(filePath, nullptr);
+    f3d::image* img = new f3d::image(path);
+    env->ReleaseStringUTFChars(filePath, path);
+    return reinterpret_cast<jlong>(img);
+  }
+
+  JNIEXPORT jlong JAVA_BIND(Image, nativeCreate)(
+    JNIEnv* env, jclass, jint width, jint height, jint channelCount, jint type)
+  {
+    f3d::image::ChannelType channelType = static_cast<f3d::image::ChannelType>(type);
+    f3d::image* img = new f3d::image(width, height, channelCount, channelType);
+    return reinterpret_cast<jlong>(img);
+  }
+
+  JNIEXPORT void JAVA_BIND(Image, nativeDestroy)(JNIEnv* env, jclass, jlong nativeAddress)
+  {
+    f3d::image* img = reinterpret_cast<f3d::image*>(nativeAddress);
+    delete img;
+  }
+
+  JNIEXPORT jint JAVA_BIND(Image, getWidth)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+    return img->getWidth();
+  }
+
+  JNIEXPORT jint JAVA_BIND(Image, getHeight)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+    return img->getHeight();
+  }
+
+  JNIEXPORT jint JAVA_BIND(Image, getChannelCount)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+    return img->getChannelCount();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Image, getChannelType)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    f3d::image::ChannelType type = img->getChannelType();
+
+    jclass enumClass = env->FindClass("app/f3d/F3D/Image$ChannelType");
+    jfieldID fieldID;
+
+    switch (type)
+    {
+      case f3d::image::ChannelType::BYTE:
+        fieldID = env->GetStaticFieldID(enumClass, "BYTE", "Lapp/f3d/F3D/Image$ChannelType;");
+        break;
+      case f3d::image::ChannelType::SHORT:
+        fieldID = env->GetStaticFieldID(enumClass, "SHORT", "Lapp/f3d/F3D/Image$ChannelType;");
+        break;
+      case f3d::image::ChannelType::FLOAT:
+        fieldID = env->GetStaticFieldID(enumClass, "FLOAT", "Lapp/f3d/F3D/Image$ChannelType;");
+        break;
+      default:
+        fieldID = env->GetStaticFieldID(enumClass, "BYTE", "Lapp/f3d/F3D/Image$ChannelType;");
+        break;
+    }
+
+    return env->GetStaticObjectField(enumClass, fieldID);
+  }
+
+  JNIEXPORT jint JAVA_BIND(Image, getChannelTypeSize)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+    return img->getChannelTypeSize();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Image, setContent)(JNIEnv* env, jobject self, jbyteArray buffer)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    jbyte* bufferData = env->GetByteArrayElements(buffer, nullptr);
+    img->setContent(bufferData);
+
+    env->ReleaseByteArrayElements(buffer, bufferData, 0);
+    return self;
+  }
+
+  JNIEXPORT jbyteArray JAVA_BIND(Image, getContent)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    void* content = img->getContent();
+    unsigned int size =
+      img->getWidth() * img->getHeight() * img->getChannelCount() * img->getChannelTypeSize();
+
+    jbyteArray result = env->NewByteArray(size);
+    env->SetByteArrayRegion(result, 0, size, static_cast<jbyte*>(content));
+
+    return result;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Image, getNormalizedPixel)(
+    JNIEnv* env, jobject self, jint x, jint y)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    std::vector<double> pixel = img->getNormalizedPixel({ x, y });
+
+    jdoubleArray result = env->NewDoubleArray(pixel.size());
+    env->SetDoubleArrayRegion(result, 0, pixel.size(), pixel.data());
+
+    return result;
+  }
+
+  JNIEXPORT jdouble JAVA_BIND(Image, compare)(JNIEnv* env, jobject self, jobject reference)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    jclass refCls = env->GetObjectClass(reference);
+    jfieldID refFid = env->GetFieldID(refCls, "mNativeAddress", "J");
+    jlong refPtr = env->GetLongField(reference, refFid);
+    f3d::image* refImg = reinterpret_cast<f3d::image*>(refPtr);
+
+    return img->compare(*refImg);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Image, save)(
+    JNIEnv* env, jobject self, jstring filePath, jobject format)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    const char* path = env->GetStringUTFChars(filePath, nullptr);
+
+    jclass formatEnum = env->GetObjectClass(format);
+    jmethodID ordinalMethod = env->GetMethodID(formatEnum, "ordinal", "()I");
+    jint formatOrdinal = env->CallIntMethod(format, ordinalMethod);
+
+    f3d::image::SaveFormat saveFormat = static_cast<f3d::image::SaveFormat>(formatOrdinal);
+    img->save(path, saveFormat);
+
+    env->ReleaseStringUTFChars(filePath, path);
+    return self;
+  }
+
+  JNIEXPORT jbyteArray JAVA_BIND(Image, saveBuffer)(JNIEnv* env, jobject self, jobject format)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    jclass formatEnum = env->GetObjectClass(format);
+    jmethodID ordinalMethod = env->GetMethodID(formatEnum, "ordinal", "()I");
+    jint formatOrdinal = env->CallIntMethod(format, ordinalMethod);
+
+    f3d::image::SaveFormat saveFormat = static_cast<f3d::image::SaveFormat>(formatOrdinal);
+    std::vector<unsigned char> buffer = img->saveBuffer(saveFormat);
+
+    jbyteArray result = env->NewByteArray(buffer.size());
+    env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast<jbyte*>(buffer.data()));
+
+    return result;
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Image, toTerminalText)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    std::string text = img->toTerminalText();
+    return env->NewStringUTF(text.c_str());
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Image, setMetadata)(
+    JNIEnv* env, jobject self, jstring key, jstring value)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    const char* keyStr = env->GetStringUTFChars(key, nullptr);
+    const char* valueStr = env->GetStringUTFChars(value, nullptr);
+
+    img->setMetadata(keyStr, valueStr);
+
+    env->ReleaseStringUTFChars(key, keyStr);
+    env->ReleaseStringUTFChars(value, valueStr);
+
+    return self;
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Image, getMetadata)(JNIEnv* env, jobject self, jstring key)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    const char* keyStr = env->GetStringUTFChars(key, nullptr);
+    std::string value = img->getMetadata(keyStr);
+    env->ReleaseStringUTFChars(key, keyStr);
+
+    return env->NewStringUTF(value.c_str());
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Image, allMetadata)(JNIEnv* env, jobject self)
+  {
+    jclass cls = env->GetObjectClass(self);
+    jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+    jlong ptr = env->GetLongField(self, fid);
+    f3d::image* img = reinterpret_cast<f3d::image*>(ptr);
+
+    return CreateStringList(env, img->allMetadata());
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DInteractorBindings.cxx f3d-3.4.1/java/F3DInteractorBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DInteractorBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DInteractorBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,645 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Interactor.h>
+
+#include <interactor.h>
+
+#include <map>
+
+namespace
+{
+std::map<std::string, jobject> g_commandCallbacks;
+JavaVM* g_jvm = nullptr;
+
+f3d::interactor& GetInteractor(JNIEnv* env, jobject self)
+{
+  jclass cls = env->GetObjectClass(self);
+  jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+  jlong ptr = env->GetLongField(self, fid);
+
+  if (g_jvm == nullptr)
+  {
+    env->GetJavaVM(&g_jvm);
+  }
+
+  return *reinterpret_cast<f3d::interactor*>(ptr);
+}
+
+f3d::interaction_bind_t::ModifierKeys JavaModToNative(jint mod)
+{
+  switch (mod)
+  {
+    case 0:
+      return f3d::interaction_bind_t::ModifierKeys::ANY;
+    case 1:
+      return f3d::interaction_bind_t::ModifierKeys::NONE;
+    case 2:
+      return f3d::interaction_bind_t::ModifierKeys::CTRL;
+    case 3:
+      return f3d::interaction_bind_t::ModifierKeys::SHIFT;
+    case 4:
+      return f3d::interaction_bind_t::ModifierKeys::CTRL_SHIFT;
+    default:
+      return f3d::interaction_bind_t::ModifierKeys::NONE;
+  }
+}
+
+jint NativeModToJava(f3d::interaction_bind_t::ModifierKeys mod)
+{
+  switch (mod)
+  {
+    case f3d::interaction_bind_t::ModifierKeys::ANY:
+      return 0;
+    case f3d::interaction_bind_t::ModifierKeys::NONE:
+      return 1;
+    case f3d::interaction_bind_t::ModifierKeys::CTRL:
+      return 2;
+    case f3d::interaction_bind_t::ModifierKeys::SHIFT:
+      return 3;
+    case f3d::interaction_bind_t::ModifierKeys::CTRL_SHIFT:
+      return 4;
+    default:
+      return 1;
+  }
+}
+
+f3d::interaction_bind_t JavaBindToNative(JNIEnv* env, jobject bind)
+{
+  f3d::interaction_bind_t nativeBind;
+  jclass bindClass = env->GetObjectClass(bind);
+
+  jfieldID modField = env->GetFieldID(bindClass, "mod", "Lapp/f3d/F3D/Interactor$ModifierKeys;");
+  jobject modObj = env->GetObjectField(bind, modField);
+  jclass modEnum = env->GetObjectClass(modObj);
+  jmethodID ordinalMethod = env->GetMethodID(modEnum, "ordinal", "()I");
+  jint modOrdinal = env->CallIntMethod(modObj, ordinalMethod);
+  nativeBind.mod = JavaModToNative(modOrdinal);
+
+  jfieldID interField = env->GetFieldID(bindClass, "inter", "Ljava/lang/String;");
+  jstring interStr = static_cast<jstring>(env->GetObjectField(bind, interField));
+  const char* interCStr = env->GetStringUTFChars(interStr, nullptr);
+  nativeBind.inter = interCStr;
+  env->ReleaseStringUTFChars(interStr, interCStr);
+
+  return nativeBind;
+}
+
+jobject NativeBindToJava(JNIEnv* env, const f3d::interaction_bind_t& bind)
+{
+  jclass bindClass = env->FindClass("app/f3d/F3D/Interactor$InteractionBind");
+  jmethodID constructor = env->GetMethodID(bindClass, "<init>", "()V");
+  jobject bindObj = env->NewObject(bindClass, constructor);
+
+  jclass modEnum = env->FindClass("app/f3d/F3D/Interactor$ModifierKeys");
+  jmethodID valuesMethod =
+    env->GetStaticMethodID(modEnum, "values", "()[Lapp/f3d/F3D/Interactor$ModifierKeys;");
+  jobjectArray modsArray =
+    static_cast<jobjectArray>(env->CallStaticObjectMethod(modEnum, valuesMethod));
+  jobject modObj = env->GetObjectArrayElement(modsArray, NativeModToJava(bind.mod));
+
+  jfieldID modField = env->GetFieldID(bindClass, "mod", "Lapp/f3d/F3D/Interactor$ModifierKeys;");
+  env->SetObjectField(bindObj, modField, modObj);
+
+  jfieldID interField = env->GetFieldID(bindClass, "inter", "Ljava/lang/String;");
+  jstring interStr = env->NewStringUTF(bind.inter.c_str());
+  env->SetObjectField(bindObj, interField, interStr);
+
+  return bindObj;
+}
+}
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Interactor, initCommands)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).initCommands();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, addCommand)(
+    JNIEnv* env, jobject self, jstring action, jobject callback)
+  {
+    const char* actionStr = env->GetStringUTFChars(action, nullptr);
+    std::string actionCpp = actionStr;
+    env->ReleaseStringUTFChars(action, actionStr);
+
+    g_commandCallbacks[actionCpp] = env->NewGlobalRef(callback);
+
+    auto cppCallback = [actionCpp](const std::vector<std::string>& args)
+    {
+      JNIEnv* env = nullptr;
+
+#ifdef __ANDROID__
+      if (g_jvm->AttachCurrentThread(&env, nullptr) != JNI_OK)
+#else
+      if (g_jvm->AttachCurrentThread(reinterpret_cast<void**>(&env), nullptr) != JNI_OK)
+#endif
+      {
+        return;
+      }
+
+      jobject callback = g_commandCallbacks[actionCpp];
+      if (callback == nullptr)
+      {
+        g_jvm->DetachCurrentThread();
+        return;
+      }
+
+      jclass arrayListClass = env->FindClass("java/util/ArrayList");
+      jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+      jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+      jobject argsList = env->NewObject(arrayListClass, arrayListConstructor);
+
+      for (const auto& arg : args)
+      {
+        jstring jstr = env->NewStringUTF(arg.c_str());
+        env->CallBooleanMethod(argsList, addMethod, jstr);
+        env->DeleteLocalRef(jstr);
+      }
+
+      jclass callbackClass = env->GetObjectClass(callback);
+      jmethodID executeMethod = env->GetMethodID(callbackClass, "execute", "(Ljava/util/List;)V");
+      env->CallVoidMethod(callback, executeMethod, argsList);
+
+      env->DeleteLocalRef(argsList);
+
+      g_jvm->DetachCurrentThread();
+    };
+
+    GetInteractor(env, self).addCommand(actionCpp, cppCallback);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, removeCommand)(JNIEnv* env, jobject self, jstring action)
+  {
+    const char* actionStr = env->GetStringUTFChars(action, nullptr);
+    std::string actionCpp = actionStr;
+    env->ReleaseStringUTFChars(action, actionStr);
+
+    auto it = g_commandCallbacks.find(actionCpp);
+    if (it != g_commandCallbacks.end())
+    {
+      env->DeleteGlobalRef(it->second);
+      g_commandCallbacks.erase(it);
+    }
+
+    GetInteractor(env, self).removeCommand(actionCpp);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getCommandActions)(JNIEnv* env, jobject self)
+  {
+    return CreateStringList(env, GetInteractor(env, self).getCommandActions());
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Interactor, triggerCommand)(
+    JNIEnv* env, jobject self, jstring command, jboolean keepComments)
+  {
+    const char* commandStr = env->GetStringUTFChars(command, nullptr);
+    bool result = GetInteractor(env, self).triggerCommand(commandStr, keepComments);
+    env->ReleaseStringUTFChars(command, commandStr);
+    return result;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, initBindings)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).initBindings();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, addBindingCommands)(
+    JNIEnv* env, jobject self, jobject bind, jobject commands, jstring group, jobject type)
+  {
+    f3d::interaction_bind_t nativeBind = JavaBindToNative(env, bind);
+
+    jclass listClass = env->GetObjectClass(commands);
+    jmethodID sizeMethod = env->GetMethodID(listClass, "size", "()I");
+    jmethodID getMethod = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
+    jint size = env->CallIntMethod(commands, sizeMethod);
+
+    std::vector<std::string> commandsVec;
+    for (jint i = 0; i < size; i++)
+    {
+      jstring cmdStr = static_cast<jstring>(env->CallObjectMethod(commands, getMethod, i));
+      const char* cmdCStr = env->GetStringUTFChars(cmdStr, nullptr);
+      commandsVec.push_back(cmdCStr);
+      env->ReleaseStringUTFChars(cmdStr, cmdCStr);
+    }
+
+    const char* groupStr = env->GetStringUTFChars(group, nullptr);
+    std::string groupCpp = groupStr;
+    env->ReleaseStringUTFChars(group, groupStr);
+
+    jclass typeEnum = env->GetObjectClass(type);
+    jmethodID ordinalMethod = env->GetMethodID(typeEnum, "ordinal", "()I");
+    jint typeOrdinal = env->CallIntMethod(type, ordinalMethod);
+
+    f3d::interactor::BindingType nativeType;
+    switch (typeOrdinal)
+    {
+      case 0:
+        nativeType = f3d::interactor::BindingType::CYCLIC;
+        break;
+      case 1:
+        nativeType = f3d::interactor::BindingType::NUMERICAL;
+        break;
+      case 2:
+        nativeType = f3d::interactor::BindingType::TOGGLE;
+        break;
+      case 3:
+      default:
+        nativeType = f3d::interactor::BindingType::OTHER;
+        break;
+    }
+
+    GetInteractor(env, self).addBinding(nativeBind, commandsVec, groupCpp, nullptr, nativeType);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, addBindingCommand)(
+    JNIEnv* env, jobject self, jobject bind, jstring command, jstring group, jobject type)
+  {
+    f3d::interaction_bind_t nativeBind = JavaBindToNative(env, bind);
+
+    const char* commandStr = env->GetStringUTFChars(command, nullptr);
+    std::string commandCpp = commandStr;
+    env->ReleaseStringUTFChars(command, commandStr);
+
+    const char* groupStr = env->GetStringUTFChars(group, nullptr);
+    std::string groupCpp = groupStr;
+    env->ReleaseStringUTFChars(group, groupStr);
+
+    jclass typeEnum = env->GetObjectClass(type);
+    jmethodID ordinalMethod = env->GetMethodID(typeEnum, "ordinal", "()I");
+    jint typeOrdinal = env->CallIntMethod(type, ordinalMethod);
+
+    f3d::interactor::BindingType nativeType;
+    switch (typeOrdinal)
+    {
+      case 0:
+        nativeType = f3d::interactor::BindingType::CYCLIC;
+        break;
+      case 1:
+        nativeType = f3d::interactor::BindingType::NUMERICAL;
+        break;
+      case 2:
+        nativeType = f3d::interactor::BindingType::TOGGLE;
+        break;
+      case 3:
+      default:
+        nativeType = f3d::interactor::BindingType::OTHER;
+        break;
+    }
+
+    GetInteractor(env, self).addBinding(nativeBind, commandCpp, groupCpp, nullptr, nativeType);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, removeBinding)(JNIEnv* env, jobject self, jobject bind)
+  {
+    f3d::interaction_bind_t nativeBind = JavaBindToNative(env, bind);
+    GetInteractor(env, self).removeBinding(nativeBind);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getBindGroups)(JNIEnv* env, jobject self)
+  {
+    return CreateStringList(env, GetInteractor(env, self).getBindGroups());
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getBindsForGroup)(
+    JNIEnv* env, jobject self, jstring group)
+  {
+    const char* groupStr = env->GetStringUTFChars(group, nullptr);
+    std::vector<f3d::interaction_bind_t> binds =
+      GetInteractor(env, self).getBindsForGroup(groupStr);
+    env->ReleaseStringUTFChars(group, groupStr);
+
+    jclass arrayListClass = env->FindClass("java/util/ArrayList");
+    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+    jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+    jobject list = env->NewObject(arrayListClass, arrayListConstructor);
+
+    for (const auto& bind : binds)
+    {
+      jobject bindObj = NativeBindToJava(env, bind);
+      env->CallBooleanMethod(list, addMethod, bindObj);
+      env->DeleteLocalRef(bindObj);
+    }
+
+    return list;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getBinds)(JNIEnv* env, jobject self)
+  {
+    std::vector<f3d::interaction_bind_t> binds = GetInteractor(env, self).getBinds();
+
+    jclass arrayListClass = env->FindClass("java/util/ArrayList");
+    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+    jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+    jobject list = env->NewObject(arrayListClass, arrayListConstructor);
+
+    for (const auto& bind : binds)
+    {
+      jobject bindObj = NativeBindToJava(env, bind);
+      env->CallBooleanMethod(list, addMethod, bindObj);
+      env->DeleteLocalRef(bindObj);
+    }
+
+    return list;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getBindingDocumentation)(
+    JNIEnv* env, jobject self, jobject bind)
+  {
+    f3d::interaction_bind_t nativeBind = JavaBindToNative(env, bind);
+    auto doc = GetInteractor(env, self).getBindingDocumentation(nativeBind);
+
+    jclass docClass = env->FindClass("app/f3d/F3D/Interactor$BindingDocumentation");
+    jmethodID constructor =
+      env->GetMethodID(docClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
+
+    jstring docStr = env->NewStringUTF(doc.first.c_str());
+    jstring valueStr = env->NewStringUTF(doc.second.c_str());
+
+    jobject docObj = env->NewObject(docClass, constructor, docStr, valueStr);
+
+    env->DeleteLocalRef(docStr);
+    env->DeleteLocalRef(valueStr);
+
+    return docObj;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getBindingType)(JNIEnv* env, jobject self, jobject bind)
+  {
+    f3d::interaction_bind_t nativeBind = JavaBindToNative(env, bind);
+    f3d::interactor::BindingType type = GetInteractor(env, self).getBindingType(nativeBind);
+
+    jclass enumClass = env->FindClass("app/f3d/F3D/Interactor$BindingType");
+    jfieldID fieldID;
+
+    switch (type)
+    {
+      case f3d::interactor::BindingType::CYCLIC:
+        fieldID =
+          env->GetStaticFieldID(enumClass, "CYCLIC", "Lapp/f3d/F3D/Interactor$BindingType;");
+        break;
+      case f3d::interactor::BindingType::NUMERICAL:
+        fieldID =
+          env->GetStaticFieldID(enumClass, "NUMERICAL", "Lapp/f3d/F3D/Interactor$BindingType;");
+        break;
+      case f3d::interactor::BindingType::TOGGLE:
+        fieldID =
+          env->GetStaticFieldID(enumClass, "TOGGLE", "Lapp/f3d/F3D/Interactor$BindingType;");
+        break;
+      case f3d::interactor::BindingType::OTHER:
+      default:
+        fieldID = env->GetStaticFieldID(enumClass, "OTHER", "Lapp/f3d/F3D/Interactor$BindingType;");
+        break;
+    }
+
+    return env->GetStaticObjectField(enumClass, fieldID);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, toggleAnimation)(
+    JNIEnv* env, jobject self, jobject direction)
+  {
+    jclass directionEnum = env->GetObjectClass(direction);
+    jmethodID getValueMethod = env->GetMethodID(directionEnum, "getValue", "()I");
+    jint directionValue = env->CallIntMethod(direction, getValueMethod);
+
+    f3d::interactor::AnimationDirection nativeDirection =
+      static_cast<f3d::interactor::AnimationDirection>(directionValue);
+
+    GetInteractor(env, self).toggleAnimation(nativeDirection);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, startAnimation)(
+    JNIEnv* env, jobject self, jobject direction)
+  {
+    jclass directionEnum = env->GetObjectClass(direction);
+    jmethodID getValueMethod = env->GetMethodID(directionEnum, "getValue", "()I");
+    jint directionValue = env->CallIntMethod(direction, getValueMethod);
+
+    f3d::interactor::AnimationDirection nativeDirection =
+      static_cast<f3d::interactor::AnimationDirection>(directionValue);
+
+    GetInteractor(env, self).startAnimation(nativeDirection);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, stopAnimation)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).stopAnimation();
+    return self;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Interactor, isPlayingAnimation)(JNIEnv* env, jobject self)
+  {
+    return GetInteractor(env, self).isPlayingAnimation();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, getAnimationDirection)(JNIEnv* env, jobject self)
+  {
+    f3d::interactor::AnimationDirection nativeDirection =
+      GetInteractor(env, self).getAnimationDirection();
+
+    jclass enumClass = env->FindClass("app/f3d/F3D/Interactor$AnimationDirection");
+    jmethodID fromValueMethod = env->GetStaticMethodID(
+      enumClass, "fromValue", "(I)Lapp/f3d/F3D/Interactor$AnimationDirection;");
+
+    return env->CallStaticObjectMethod(
+      enumClass, fromValueMethod, static_cast<int>(nativeDirection));
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, enableCameraMovement)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).enableCameraMovement();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, disableCameraMovement)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).disableCameraMovement();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerModUpdate)(JNIEnv* env, jobject self, jobject mod)
+  {
+    jclass modEnum = env->GetObjectClass(mod);
+    jmethodID ordinalMethod = env->GetMethodID(modEnum, "ordinal", "()I");
+    jint modOrdinal = env->CallIntMethod(mod, ordinalMethod);
+
+    f3d::interactor::InputModifier nativeMod;
+    switch (modOrdinal)
+    {
+      case 0:
+        nativeMod = f3d::interactor::InputModifier::NONE;
+        break;
+      case 1:
+        nativeMod = f3d::interactor::InputModifier::CTRL;
+        break;
+      case 2:
+        nativeMod = f3d::interactor::InputModifier::SHIFT;
+        break;
+      case 3:
+        nativeMod = f3d::interactor::InputModifier::CTRL_SHIFT;
+        break;
+      default:
+        nativeMod = f3d::interactor::InputModifier::NONE;
+        break;
+    }
+
+    GetInteractor(env, self).triggerModUpdate(nativeMod);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerMouseButton)(
+    JNIEnv* env, jobject self, jobject action, jobject button)
+  {
+    jclass actionEnum = env->GetObjectClass(action);
+    jmethodID actionOrdinalMethod = env->GetMethodID(actionEnum, "ordinal", "()I");
+    jint actionOrdinal = env->CallIntMethod(action, actionOrdinalMethod);
+
+    jclass buttonEnum = env->GetObjectClass(button);
+    jmethodID buttonOrdinalMethod = env->GetMethodID(buttonEnum, "ordinal", "()I");
+    jint buttonOrdinal = env->CallIntMethod(button, buttonOrdinalMethod);
+
+    f3d::interactor::InputAction nativeAction = actionOrdinal == 0
+      ? f3d::interactor::InputAction::PRESS
+      : f3d::interactor::InputAction::RELEASE;
+
+    f3d::interactor::MouseButton nativeButton;
+    switch (buttonOrdinal)
+    {
+      case 0:
+        nativeButton = f3d::interactor::MouseButton::LEFT;
+        break;
+      case 1:
+        nativeButton = f3d::interactor::MouseButton::RIGHT;
+        break;
+      case 2:
+        nativeButton = f3d::interactor::MouseButton::MIDDLE;
+        break;
+      default:
+        nativeButton = f3d::interactor::MouseButton::LEFT;
+        break;
+    }
+
+    GetInteractor(env, self).triggerMouseButton(nativeAction, nativeButton);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerMousePosition)(
+    JNIEnv* env, jobject self, jdouble xpos, jdouble ypos)
+  {
+    GetInteractor(env, self).triggerMousePosition(xpos, ypos);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerMouseWheel)(
+    JNIEnv* env, jobject self, jobject direction)
+  {
+    jclass directionEnum = env->GetObjectClass(direction);
+    jmethodID ordinalMethod = env->GetMethodID(directionEnum, "ordinal", "()I");
+    jint directionOrdinal = env->CallIntMethod(direction, ordinalMethod);
+
+    f3d::interactor::WheelDirection nativeDirection;
+    switch (directionOrdinal)
+    {
+      case 0:
+        nativeDirection = f3d::interactor::WheelDirection::FORWARD;
+        break;
+      case 1:
+        nativeDirection = f3d::interactor::WheelDirection::BACKWARD;
+        break;
+      case 2:
+        nativeDirection = f3d::interactor::WheelDirection::LEFT;
+        break;
+      case 3:
+        nativeDirection = f3d::interactor::WheelDirection::RIGHT;
+        break;
+      default:
+        nativeDirection = f3d::interactor::WheelDirection::FORWARD;
+        break;
+    }
+
+    GetInteractor(env, self).triggerMouseWheel(nativeDirection);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerKeyboardKey)(
+    JNIEnv* env, jobject self, jobject action, jstring keySym)
+  {
+    jclass actionEnum = env->GetObjectClass(action);
+    jmethodID ordinalMethod = env->GetMethodID(actionEnum, "ordinal", "()I");
+    jint actionOrdinal = env->CallIntMethod(action, ordinalMethod);
+
+    f3d::interactor::InputAction nativeAction = actionOrdinal == 0
+      ? f3d::interactor::InputAction::PRESS
+      : f3d::interactor::InputAction::RELEASE;
+
+    const char* keySymStr = env->GetStringUTFChars(keySym, nullptr);
+    GetInteractor(env, self).triggerKeyboardKey(nativeAction, keySymStr);
+    env->ReleaseStringUTFChars(keySym, keySymStr);
+
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerTextCharacter)(
+    JNIEnv* env, jobject self, jint codepoint)
+  {
+    GetInteractor(env, self).triggerTextCharacter(codepoint);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, triggerEventLoop)(
+    JNIEnv* env, jobject self, jdouble deltaTime)
+  {
+    GetInteractor(env, self).triggerEventLoop(deltaTime);
+    return self;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Interactor, playInteraction)(
+    JNIEnv* env, jobject self, jstring file, jdouble deltaTime)
+  {
+    const char* fileStr = env->GetStringUTFChars(file, nullptr);
+    bool result = GetInteractor(env, self).playInteraction(fileStr, deltaTime);
+    env->ReleaseStringUTFChars(file, fileStr);
+    return result;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Interactor, recordInteraction)(
+    JNIEnv* env, jobject self, jstring file)
+  {
+    const char* fileStr = env->GetStringUTFChars(file, nullptr);
+    bool result = GetInteractor(env, self).recordInteraction(fileStr);
+    env->ReleaseStringUTFChars(file, fileStr);
+    return result;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, start)(JNIEnv* env, jobject self, jdouble deltaTime)
+  {
+    GetInteractor(env, self).start(deltaTime);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, stop)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).stop();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, requestRender)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).requestRender();
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Interactor, requestStop)(JNIEnv* env, jobject self)
+  {
+    GetInteractor(env, self).requestStop();
+    return self;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DJavaBindings.cxx f3d-3.4.1/java/F3DJavaBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DJavaBindings.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/F3DJavaBindings.cxx	1970-01-01 02:00:00.000000000 +0200
@@ -1,201 +0,0 @@
-// Automatically generated headers
-#include <app_f3d_F3D_Camera.h>
-#include <app_f3d_F3D_Engine.h>
-#include <app_f3d_F3D_Options.h>
-#include <app_f3d_F3D_Scene.h>
-#include <app_f3d_F3D_Window.h>
-
-#include <engine.h>
-#include <log.h>
-
-#include <cassert>
-
-#define JAVA_BIND(Cls, Func) JNICALL Java_app_f3d_F3D_##Cls##_##Func
-
-namespace fs = std::filesystem;
-
-inline f3d::engine* GetEngine(JNIEnv* env, jobject self)
-{
-  jclass cls = env->GetObjectClass(self);
-  jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
-  jlong ptr = env->GetLongField(self, fid);
-
-  return reinterpret_cast<f3d::engine*>(ptr);
-}
-
-extern "C"
-{
-  // Engine
-  JNIEXPORT void JAVA_BIND(Engine, loadPlugin)(JNIEnv* env, jclass, jstring str)
-  {
-    const char* plugin = env->GetStringUTFChars(str, nullptr);
-    f3d::engine::loadPlugin(plugin);
-    env->ReleaseStringUTFChars(str, plugin);
-  }
-
-  JNIEXPORT void JAVA_BIND(Engine, autoloadPlugins)(JNIEnv*, jclass)
-  {
-    f3d::engine::autoloadPlugins();
-  }
-
-  JNIEXPORT void JAVA_BIND(Engine, setCachePath)(JNIEnv* env, jobject self, jstring path)
-  {
-    const char* str = env->GetStringUTFChars(path, nullptr);
-    GetEngine(env, self)->setCachePath(fs::path(str));
-    env->ReleaseStringUTFChars(path, str);
-  }
-
-  JNIEXPORT jlong JAVA_BIND(Engine, construct)(JNIEnv*, jobject)
-  {
-    f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
-    return reinterpret_cast<jlong>(new f3d::engine(f3d::engine::create()));
-  }
-
-  JNIEXPORT void JAVA_BIND(Engine, destroy)(JNIEnv*, jobject, jlong ptr)
-  {
-    delete reinterpret_cast<f3d::engine*>(ptr);
-  }
-
-  // Scene
-  JNIEXPORT void JAVA_BIND(Scene, add)(JNIEnv* env, jobject self, jstring path)
-  {
-    const char* str = env->GetStringUTFChars(path, nullptr);
-    GetEngine(env, self)->getScene().add(str);
-    env->ReleaseStringUTFChars(path, str);
-  }
-  JNIEXPORT void JAVA_BIND(Scene, clear)(JNIEnv* env, jobject self)
-  {
-    GetEngine(env, self)->getScene().clear();
-  }
-
-  // Window
-  JNIEXPORT void JAVA_BIND(Window, render)(JNIEnv* env, jobject self)
-  {
-    GetEngine(env, self)->getWindow().render();
-  }
-
-  JNIEXPORT void JAVA_BIND(Window, setSize)(JNIEnv* env, jobject self, jint w, jint h)
-  {
-    GetEngine(env, self)->getWindow().setSize(w, h);
-  }
-
-  JNIEXPORT jint JAVA_BIND(Window, getWidth)(JNIEnv* env, jobject self)
-  {
-    return GetEngine(env, self)->getWindow().getWidth();
-  }
-
-  JNIEXPORT jint JAVA_BIND(Window, getHeight)(JNIEnv* env, jobject self)
-  {
-    return GetEngine(env, self)->getWindow().getHeight();
-  }
-
-  JNIEXPORT jdoubleArray JAVA_BIND(Window, getDisplayFromWorld)(
-    JNIEnv* env, jobject self, jdoubleArray pt)
-  {
-    double* arr = env->GetDoubleArrayElements(pt, nullptr);
-    f3d::point3_t newPt =
-      GetEngine(env, self)->getWindow().getDisplayFromWorld({ arr[0], arr[1], arr[2] });
-    env->ReleaseDoubleArrayElements(pt, arr, 0);
-
-    jdoubleArray ret = env->NewDoubleArray(3);
-    env->SetDoubleArrayRegion(ret, 0, 3, newPt.data());
-
-    return ret;
-  }
-
-  JNIEXPORT jdoubleArray JAVA_BIND(Window, getWorldFromDisplay)(
-    JNIEnv* env, jobject self, jdoubleArray pt)
-  {
-    double* arr = env->GetDoubleArrayElements(pt, nullptr);
-    f3d::point3_t newPt =
-      GetEngine(env, self)->getWindow().getWorldFromDisplay({ arr[0], arr[1], arr[2] });
-    env->ReleaseDoubleArrayElements(pt, arr, 0);
-
-    jdoubleArray ret = env->NewDoubleArray(3);
-    env->SetDoubleArrayRegion(ret, 0, 3, newPt.data());
-
-    return ret;
-  }
-
-  // Options
-  JNIEXPORT void JAVA_BIND(Options, toggle)(JNIEnv* env, jobject self, jstring name)
-  {
-    const char* str = env->GetStringUTFChars(name, nullptr);
-    GetEngine(env, self)->getOptions().toggle(str);
-    env->ReleaseStringUTFChars(name, str);
-  }
-
-  // Camera
-  JNIEXPORT void JAVA_BIND(Camera, dolly)(JNIEnv* env, jobject self, jdouble factor)
-  {
-    GetEngine(env, self)->getWindow().getCamera().dolly(factor);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, roll)(JNIEnv* env, jobject self, jdouble angle)
-  {
-    GetEngine(env, self)->getWindow().getCamera().roll(angle);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, azimuth)(JNIEnv* env, jobject self, jdouble angle)
-  {
-    GetEngine(env, self)->getWindow().getCamera().azimuth(angle);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, yaw)(JNIEnv* env, jobject self, jdouble angle)
-  {
-    GetEngine(env, self)->getWindow().getCamera().yaw(angle);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, elevation)(JNIEnv* env, jobject self, jdouble angle)
-  {
-    GetEngine(env, self)->getWindow().getCamera().elevation(angle);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, pitch)(JNIEnv* env, jobject self, jdouble angle)
-  {
-    GetEngine(env, self)->getWindow().getCamera().pitch(angle);
-  }
-
-  JNIEXPORT jdoubleArray JAVA_BIND(Camera, getFocalPoint)(JNIEnv* env, jobject self)
-  {
-    f3d::point3_t pt = GetEngine(env, self)->getWindow().getCamera().getFocalPoint();
-
-    jdoubleArray ret = env->NewDoubleArray(3);
-    env->SetDoubleArrayRegion(ret, 0, 3, pt.data());
-
-    return ret;
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, setFocalPoint)(JNIEnv* env, jobject self, jdoubleArray pt)
-  {
-    assert(env->GetArrayLength(pt) == 3);
-
-    double* arr = env->GetDoubleArrayElements(pt, nullptr);
-    GetEngine(env, self)->getWindow().getCamera().setFocalPoint({ arr[0], arr[1], arr[2] });
-    env->ReleaseDoubleArrayElements(pt, arr, 0);
-  }
-
-  JNIEXPORT jdoubleArray JAVA_BIND(Camera, getPosition)(JNIEnv* env, jobject self)
-  {
-    f3d::point3_t pt = GetEngine(env, self)->getWindow().getCamera().getPosition();
-
-    jdoubleArray ret = env->NewDoubleArray(3);
-    env->SetDoubleArrayRegion(ret, 0, 3, pt.data());
-
-    return ret;
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, setPosition)(JNIEnv* env, jobject self, jdoubleArray pt)
-  {
-    assert(env->GetArrayLength(pt) == 3);
-
-    double* arr = env->GetDoubleArrayElements(pt, nullptr);
-    GetEngine(env, self)->getWindow().getCamera().setPosition({ arr[0], arr[1], arr[2] });
-    env->ReleaseDoubleArrayElements(pt, arr, 0);
-  }
-
-  JNIEXPORT void JAVA_BIND(Camera, resetToBounds)(JNIEnv* env, jobject self)
-  {
-    GetEngine(env, self)->getWindow().getCamera().resetToBounds();
-  }
-}
diff -Nru f3d-3.2.0+dfsg/java/F3DJavaBindings.h f3d-3.4.1/java/F3DJavaBindings.h
--- f3d-3.2.0+dfsg/java/F3DJavaBindings.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DJavaBindings.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,70 @@
+#ifndef F3D_JAVA_BINDINGS_H
+#define F3D_JAVA_BINDINGS_H
+
+#include <jni.h>
+
+#include <engine.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+#define JAVA_BIND(Cls, Func) JNICALL Java_app_f3d_F3D_##Cls##_##Func
+
+namespace fs = std::filesystem;
+
+// Helper function to get the f3d::engine pointer from a Java object
+inline f3d::engine* GetEngine(JNIEnv* env, jobject self)
+{
+  jclass cls = env->GetObjectClass(self);
+  jfieldID fid = env->GetFieldID(cls, "mNativeAddress", "J");
+  jlong ptr = env->GetLongField(self, fid);
+
+  return reinterpret_cast<f3d::engine*>(ptr);
+}
+
+// Helper function to convert std::vector<std::string> to Java List
+inline jobject CreateStringList(JNIEnv* env, const std::vector<std::string>& vec)
+{
+  jclass arrayListClass = env->FindClass("java/util/ArrayList");
+  jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+  jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+  jobject list = env->NewObject(arrayListClass, arrayListConstructor);
+
+  for (const auto& str : vec)
+  {
+    jstring jstr = env->NewStringUTF(str.c_str());
+    env->CallBooleanMethod(list, addMethod, jstr);
+    env->DeleteLocalRef(jstr);
+  }
+
+  return list;
+}
+
+// Helper function to convert std::map<std::string, bool> to Java Map
+inline jobject CreateStringBooleanMap(JNIEnv* env, const std::map<std::string, bool>& map)
+{
+  jclass hashMapClass = env->FindClass("java/util/HashMap");
+  jmethodID hashMapConstructor = env->GetMethodID(hashMapClass, "<init>", "()V");
+  jmethodID putMethod = env->GetMethodID(
+    hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+
+  jclass booleanClass = env->FindClass("java/lang/Boolean");
+  jmethodID booleanConstructor = env->GetMethodID(booleanClass, "<init>", "(Z)V");
+
+  jobject jmap = env->NewObject(hashMapClass, hashMapConstructor);
+
+  for (const auto& [key, value] : map)
+  {
+    jstring jkey = env->NewStringUTF(key.c_str());
+    jobject jvalue = env->NewObject(booleanClass, booleanConstructor, value);
+    env->CallObjectMethod(jmap, putMethod, jkey, jvalue);
+    env->DeleteLocalRef(jkey);
+    env->DeleteLocalRef(jvalue);
+  }
+
+  return jmap;
+}
+
+#endif // F3D_JAVA_BINDINGS_H
diff -Nru f3d-3.2.0+dfsg/java/F3DLogBindings.cxx f3d-3.4.1/java/F3DLogBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DLogBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DLogBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,172 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Log.h>
+
+#include <log.h>
+
+static jobject g_callback_ref = nullptr;
+static JavaVM* g_jvm = nullptr;
+
+static void cpp_log_forwarder(f3d::log::VerboseLevel level, const std::string& message)
+{
+  if (g_callback_ref == nullptr || g_jvm == nullptr)
+  {
+    return;
+  }
+
+  JNIEnv* env;
+#ifdef __ANDROID__
+  if (g_jvm->AttachCurrentThread(&env, nullptr) != 0)
+#else
+  if (g_jvm->AttachCurrentThread(reinterpret_cast<void**>(&env), nullptr) != 0)
+#endif
+  {
+    return;
+  }
+
+  jclass enumClass = env->FindClass("app/f3d/F3D/Log$VerboseLevel");
+  jmethodID fromValueMethod =
+    env->GetStaticMethodID(enumClass, "fromValue", "(I)Lapp/f3d/F3D/Log$VerboseLevel;");
+  jobject jLevel = env->CallStaticObjectMethod(enumClass, fromValueMethod, static_cast<int>(level));
+
+  jstring jMessage = env->NewStringUTF(message.c_str());
+
+  jclass callbackClass = env->GetObjectClass(g_callback_ref);
+  jmethodID onLogMessageMethod = env->GetMethodID(
+    callbackClass, "onLogMessage", "(Lapp/f3d/F3D/Log$VerboseLevel;Ljava/lang/String;)V");
+
+  env->CallVoidMethod(g_callback_ref, onLogMessageMethod, jLevel, jMessage);
+
+  env->DeleteLocalRef(jLevel);
+  env->DeleteLocalRef(jMessage);
+
+  g_jvm->DetachCurrentThread();
+}
+
+extern "C"
+{
+  JNIEXPORT void JAVA_BIND(Log, print)(JNIEnv* env, jclass, jobject level, jstring message)
+  {
+    if (!level || !message)
+    {
+      return;
+    }
+
+    jclass enumClass = env->GetObjectClass(level);
+    jmethodID getValueMethod = env->GetMethodID(enumClass, "getValue", "()I");
+    jint levelValue = env->CallIntMethod(level, getValueMethod);
+
+    const char* messageStr = env->GetStringUTFChars(message, nullptr);
+
+    f3d::log::VerboseLevel cppLevel = static_cast<f3d::log::VerboseLevel>(levelValue);
+    f3d::log::print(cppLevel, messageStr);
+
+    env->ReleaseStringUTFChars(message, messageStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, debug)(JNIEnv* env, jclass, jstring message)
+  {
+    if (!message)
+    {
+      return;
+    }
+
+    const char* messageStr = env->GetStringUTFChars(message, nullptr);
+    f3d::log::debug(messageStr);
+    env->ReleaseStringUTFChars(message, messageStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, info)(JNIEnv* env, jclass, jstring message)
+  {
+    if (!message)
+    {
+      return;
+    }
+
+    const char* messageStr = env->GetStringUTFChars(message, nullptr);
+    f3d::log::info(messageStr);
+    env->ReleaseStringUTFChars(message, messageStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, warn)(JNIEnv* env, jclass, jstring message)
+  {
+    if (!message)
+    {
+      return;
+    }
+
+    const char* messageStr = env->GetStringUTFChars(message, nullptr);
+    f3d::log::warn(messageStr);
+    env->ReleaseStringUTFChars(message, messageStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, error)(JNIEnv* env, jclass, jstring message)
+  {
+    if (!message)
+    {
+      return;
+    }
+
+    const char* messageStr = env->GetStringUTFChars(message, nullptr);
+    f3d::log::error(messageStr);
+    env->ReleaseStringUTFChars(message, messageStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, setUseColoring)(JNIEnv*, jclass, jboolean use)
+  {
+    f3d::log::setUseColoring(use != 0);
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, setVerboseLevel)(
+    JNIEnv* env, jclass, jobject level, jboolean forceStdErr)
+  {
+    if (!level)
+    {
+      return;
+    }
+
+    jclass enumClass = env->GetObjectClass(level);
+    jmethodID getValueMethod = env->GetMethodID(enumClass, "getValue", "()I");
+    jint levelValue = env->CallIntMethod(level, getValueMethod);
+
+    f3d::log::VerboseLevel cppLevel = static_cast<f3d::log::VerboseLevel>(levelValue);
+    f3d::log::setVerboseLevel(cppLevel, forceStdErr != 0);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Log, getVerboseLevel)(JNIEnv* env, jclass)
+  {
+    f3d::log::VerboseLevel cppLevel = f3d::log::getVerboseLevel();
+
+    jclass enumClass = env->FindClass("app/f3d/F3D/Log$VerboseLevel");
+    jmethodID fromValueMethod =
+      env->GetStaticMethodID(enumClass, "fromValue", "(I)Lapp/f3d/F3D/Log$VerboseLevel;");
+
+    return env->CallStaticObjectMethod(enumClass, fromValueMethod, static_cast<int>(cppLevel));
+  }
+
+  JNIEXPORT void JAVA_BIND(Log, forward)(JNIEnv* env, jclass, jobject callback)
+  {
+    if (g_callback_ref != nullptr)
+    {
+      env->DeleteGlobalRef(g_callback_ref);
+      g_callback_ref = nullptr;
+    }
+
+    if (callback != nullptr)
+    {
+      if (g_jvm == nullptr)
+      {
+        env->GetJavaVM(&g_jvm);
+      }
+
+      g_callback_ref = env->NewGlobalRef(callback);
+
+      f3d::log::forward(cpp_log_forwarder);
+    }
+    else
+    {
+      f3d::log::forward(nullptr);
+      g_jvm = nullptr;
+    }
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DOptionsBindings.cxx f3d-3.4.1/java/F3DOptionsBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DOptionsBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DOptionsBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,244 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Options.h>
+
+#include <options.h>
+
+namespace
+{
+f3d::options& GetOptionsFromEngine(JNIEnv* env, jobject self)
+{
+  return GetEngine(env, self)->getOptions();
+}
+}
+
+extern "C"
+{
+  JNIEXPORT void JAVA_BIND(Options, setAsBool)(
+    JNIEnv* env, jobject self, jstring name, jboolean value)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).set(str, static_cast<bool>(value));
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsInt)(JNIEnv* env, jobject self, jstring name, jint value)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).set(str, static_cast<int>(value));
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsDouble)(
+    JNIEnv* env, jobject self, jstring name, jdouble value)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).set(str, static_cast<double>(value));
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsString)(
+    JNIEnv* env, jobject self, jstring name, jstring value)
+  {
+    const char* nameStr = env->GetStringUTFChars(name, nullptr);
+    const char* valueStr = env->GetStringUTFChars(value, nullptr);
+    GetOptionsFromEngine(env, self).set(nameStr, std::string(valueStr));
+    env->ReleaseStringUTFChars(name, nameStr);
+    env->ReleaseStringUTFChars(value, valueStr);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsDoubleVector)(
+    JNIEnv* env, jobject self, jstring name, jdoubleArray values)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    jsize len = env->GetArrayLength(values);
+    double* arr = env->GetDoubleArrayElements(values, nullptr);
+
+    std::vector<double> vec(arr, arr + len);
+    GetOptionsFromEngine(env, self).set(str, vec);
+
+    env->ReleaseDoubleArrayElements(values, arr, 0);
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsIntVector)(
+    JNIEnv* env, jobject self, jstring name, jintArray values)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    jsize len = env->GetArrayLength(values);
+    int* arr = env->GetIntArrayElements(values, nullptr);
+
+    std::vector<int> vec(arr, arr + len);
+    GetOptionsFromEngine(env, self).set(str, vec);
+
+    env->ReleaseIntArrayElements(values, arr, 0);
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Options, getAsBool)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    bool value = std::get<bool>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+    return value;
+  }
+
+  JNIEXPORT jint JAVA_BIND(Options, getAsInt)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    int value = std::get<int>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+    return value;
+  }
+
+  JNIEXPORT jdouble JAVA_BIND(Options, getAsDouble)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    double value = std::get<double>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+    return value;
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Options, getAsString)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    std::string value = std::get<std::string>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+    return env->NewStringUTF(value.c_str());
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Options, getAsStringRepresentation)(
+    JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    std::string value = GetOptionsFromEngine(env, self).getAsString(str);
+    env->ReleaseStringUTFChars(name, str);
+    return env->NewStringUTF(value.c_str());
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, setAsStringRepresentation)(
+    JNIEnv* env, jobject self, jstring name, jstring str)
+  {
+    const char* nameStr = env->GetStringUTFChars(name, nullptr);
+    const char* valueStr = env->GetStringUTFChars(str, nullptr);
+    GetOptionsFromEngine(env, self).setAsString(nameStr, valueStr);
+    env->ReleaseStringUTFChars(name, nameStr);
+    env->ReleaseStringUTFChars(str, valueStr);
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Options, getAsDoubleVector)(
+    JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    std::vector<double> vec =
+      std::get<std::vector<double>>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+
+    jdoubleArray result = env->NewDoubleArray(vec.size());
+    env->SetDoubleArrayRegion(result, 0, vec.size(), vec.data());
+    return result;
+  }
+
+  JNIEXPORT jintArray JAVA_BIND(Options, getAsIntVector)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    std::vector<int> vec = std::get<std::vector<int>>(GetOptionsFromEngine(env, self).get(str));
+    env->ReleaseStringUTFChars(name, str);
+
+    jintArray result = env->NewIntArray(vec.size());
+    env->SetIntArrayRegion(result, 0, vec.size(), vec.data());
+    return result;
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, toggle)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).toggle(str);
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Options, isSame)(
+    JNIEnv* env, jobject self, jobject other, jstring name)
+  {
+    jclass otherClass = env->GetObjectClass(other);
+    jfieldID fid = env->GetFieldID(otherClass, "mNativeAddress", "J");
+    jlong otherPtr = env->GetLongField(other, fid);
+
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    bool result = GetOptionsFromEngine(env, self).isSame(
+      reinterpret_cast<f3d::engine*>(otherPtr)->getOptions(), str);
+    env->ReleaseStringUTFChars(name, str);
+    return result;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Options, hasValue)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    bool result = GetOptionsFromEngine(env, self).hasValue(str);
+    env->ReleaseStringUTFChars(name, str);
+    return result;
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, copy)(JNIEnv* env, jobject self, jobject other, jstring name)
+  {
+    jclass otherClass = env->GetObjectClass(other);
+    jfieldID fid = env->GetFieldID(otherClass, "mNativeAddress", "J");
+    jlong otherPtr = env->GetLongField(other, fid);
+
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).copy(
+      reinterpret_cast<f3d::engine*>(otherPtr)->getOptions(), str);
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Options, getAllNames)(JNIEnv* env, jclass)
+  {
+    std::vector<std::string> names = f3d::options::getAllNames();
+    return CreateStringList(env, names);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Options, getNames)(JNIEnv* env, jobject self)
+  {
+    f3d::options& opts = GetOptionsFromEngine(env, self);
+    std::vector<std::string> names = opts.getNames();
+    return CreateStringList(env, names);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Options, getClosestOption)(JNIEnv* env, jobject self, jstring option)
+  {
+    const char* str = env->GetStringUTFChars(option, nullptr);
+    auto [name, distance] = GetOptionsFromEngine(env, self).getClosestOption(str);
+    env->ReleaseStringUTFChars(option, str);
+
+    jclass resultClass = env->FindClass("app/f3d/F3D/Options$ClosestOptionResult");
+    jmethodID constructor = env->GetMethodID(resultClass, "<init>", "(Ljava/lang/String;I)V");
+
+    jstring jname = env->NewStringUTF(name.c_str());
+    jobject result = env->NewObject(resultClass, constructor, jname, static_cast<jint>(distance));
+    env->DeleteLocalRef(jname);
+
+    return result;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Options, isOptional)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    bool result = GetOptionsFromEngine(env, self).isOptional(str);
+    env->ReleaseStringUTFChars(name, str);
+    return result;
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, reset)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).reset(str);
+    env->ReleaseStringUTFChars(name, str);
+  }
+
+  JNIEXPORT void JAVA_BIND(Options, removeValue)(JNIEnv* env, jobject self, jstring name)
+  {
+    const char* str = env->GetStringUTFChars(name, nullptr);
+    GetOptionsFromEngine(env, self).removeValue(str);
+    env->ReleaseStringUTFChars(name, str);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DSceneBindings.cxx f3d-3.4.1/java/F3DSceneBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DSceneBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DSceneBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,382 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Scene.h>
+
+#include <scene.h>
+#include <types.h>
+
+static std::vector<std::string> JavaListToStringVector(JNIEnv* env, jobject list)
+{
+  std::vector<std::string> vec;
+
+  jclass listClass = env->GetObjectClass(list);
+  jmethodID sizeMethod = env->GetMethodID(listClass, "size", "()I");
+  jmethodID getMethod = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
+
+  jint size = env->CallIntMethod(list, sizeMethod);
+
+  for (jint i = 0; i < size; i++)
+  {
+    if (jstring jstr = static_cast<jstring>(env->CallObjectMethod(list, getMethod, i)))
+    {
+      const char* str = env->GetStringUTFChars(jstr, nullptr);
+      vec.push_back(str);
+      env->ReleaseStringUTFChars(jstr, str);
+      env->DeleteLocalRef(jstr);
+    }
+  }
+
+  return vec;
+}
+
+static f3d::mesh_t JavaMeshToCppMesh(JNIEnv* env, jobject jmesh)
+{
+  f3d::mesh_t cppMesh;
+
+  jclass meshClass = env->GetObjectClass(jmesh);
+
+  jfieldID pointsField = env->GetFieldID(meshClass, "points", "[F");
+  jfieldID normalsField = env->GetFieldID(meshClass, "normals", "[F");
+  jfieldID textureCoordinatesField = env->GetFieldID(meshClass, "textureCoordinates", "[F");
+  jfieldID faceSidesField = env->GetFieldID(meshClass, "faceSides", "[I");
+  jfieldID faceIndicesField = env->GetFieldID(meshClass, "faceIndices", "[I");
+
+  jfloatArray pointsArray = static_cast<jfloatArray>(env->GetObjectField(jmesh, pointsField));
+  jfloatArray normalsArray = static_cast<jfloatArray>(env->GetObjectField(jmesh, normalsField));
+  jfloatArray textureCoordinatesArray =
+    static_cast<jfloatArray>(env->GetObjectField(jmesh, textureCoordinatesField));
+  jintArray faceSidesArray = static_cast<jintArray>(env->GetObjectField(jmesh, faceSidesField));
+  jintArray faceIndicesArray = static_cast<jintArray>(env->GetObjectField(jmesh, faceIndicesField));
+
+  if (pointsArray)
+  {
+    jsize pointsLen = env->GetArrayLength(pointsArray);
+    float* pointsData = env->GetFloatArrayElements(pointsArray, nullptr);
+    cppMesh.points.assign(pointsData, pointsData + pointsLen);
+    env->ReleaseFloatArrayElements(pointsArray, pointsData, 0);
+  }
+
+  if (normalsArray)
+  {
+    jsize normalsLen = env->GetArrayLength(normalsArray);
+    float* normalsData = env->GetFloatArrayElements(normalsArray, nullptr);
+    cppMesh.normals.assign(normalsData, normalsData + normalsLen);
+    env->ReleaseFloatArrayElements(normalsArray, normalsData, 0);
+  }
+
+  if (textureCoordinatesArray)
+  {
+    jsize texCoordsLen = env->GetArrayLength(textureCoordinatesArray);
+    float* texCoordsData = env->GetFloatArrayElements(textureCoordinatesArray, nullptr);
+    cppMesh.texture_coordinates.assign(texCoordsData, texCoordsData + texCoordsLen);
+    env->ReleaseFloatArrayElements(textureCoordinatesArray, texCoordsData, 0);
+  }
+
+  if (faceSidesArray)
+  {
+    jsize faceSidesLen = env->GetArrayLength(faceSidesArray);
+    int* faceSidesData = env->GetIntArrayElements(faceSidesArray, nullptr);
+    cppMesh.face_sides.assign(faceSidesData, faceSidesData + faceSidesLen);
+    env->ReleaseIntArrayElements(faceSidesArray, faceSidesData, 0);
+  }
+
+  if (faceIndicesArray)
+  {
+    jsize faceIndicesLen = env->GetArrayLength(faceIndicesArray);
+    int* faceIndicesData = env->GetIntArrayElements(faceIndicesArray, nullptr);
+    cppMesh.face_indices.assign(faceIndicesData, faceIndicesData + faceIndicesLen);
+    env->ReleaseIntArrayElements(faceIndicesArray, faceIndicesData, 0);
+  }
+
+  return cppMesh;
+}
+
+static f3d::light_state_t JavaLightStateToCppLightState(JNIEnv* env, jobject jlightState)
+{
+  f3d::light_state_t cppLightState;
+
+  jclass lightStateClass = env->GetObjectClass(jlightState);
+
+  jfieldID typeField = env->GetFieldID(lightStateClass, "type", "Lapp/f3d/F3D/Types$LightType;");
+  jfieldID positionField = env->GetFieldID(lightStateClass, "position", "[D");
+  jfieldID colorField = env->GetFieldID(lightStateClass, "color", "[D");
+  jfieldID directionField = env->GetFieldID(lightStateClass, "direction", "[D");
+  jfieldID positionalLightField = env->GetFieldID(lightStateClass, "positionalLight", "Z");
+  jfieldID intensityField = env->GetFieldID(lightStateClass, "intensity", "D");
+  jfieldID switchStateField = env->GetFieldID(lightStateClass, "switchState", "Z");
+
+  jobject jtype = env->GetObjectField(jlightState, typeField);
+  jclass typeEnumClass = env->GetObjectClass(jtype);
+  jmethodID getValueMethod = env->GetMethodID(typeEnumClass, "getValue", "()I");
+  jint typeValue = env->CallIntMethod(jtype, getValueMethod);
+  cppLightState.type = static_cast<f3d::light_type>(typeValue);
+
+  if (jdoubleArray jposition =
+        static_cast<jdoubleArray>(env->GetObjectField(jlightState, positionField)))
+  {
+    double* posData = env->GetDoubleArrayElements(jposition, nullptr);
+    cppLightState.position = { posData[0], posData[1], posData[2] };
+    env->ReleaseDoubleArrayElements(jposition, posData, 0);
+  }
+
+  if (jdoubleArray jcolor = static_cast<jdoubleArray>(env->GetObjectField(jlightState, colorField)))
+  {
+    double* colorData = env->GetDoubleArrayElements(jcolor, nullptr);
+    cppLightState.color = { colorData[0], colorData[1], colorData[2] };
+    env->ReleaseDoubleArrayElements(jcolor, colorData, 0);
+  }
+
+  if (jdoubleArray jdirection =
+        static_cast<jdoubleArray>(env->GetObjectField(jlightState, directionField)))
+  {
+    double* dirData = env->GetDoubleArrayElements(jdirection, nullptr);
+    cppLightState.direction = { dirData[0], dirData[1], dirData[2] };
+    env->ReleaseDoubleArrayElements(jdirection, dirData, 0);
+  }
+
+  cppLightState.positionalLight = env->GetBooleanField(jlightState, positionalLightField);
+  cppLightState.intensity = env->GetDoubleField(jlightState, intensityField);
+  cppLightState.switchState = env->GetBooleanField(jlightState, switchStateField);
+
+  return cppLightState;
+}
+
+// Helper function to convert C++ f3d::light_state_t to Java Types.LightState
+static jobject CppLightStateToJavaLightState(JNIEnv* env, const f3d::light_state_t& cppLightState)
+{
+  jclass lightStateClass = env->FindClass("app/f3d/F3D/Types$LightState");
+  jmethodID constructor = env->GetMethodID(lightStateClass, "<init>", "()V");
+  jobject jlightState = env->NewObject(lightStateClass, constructor);
+
+  jclass typeEnumClass = env->FindClass("app/f3d/F3D/Types$LightType");
+  jmethodID fromValueMethod =
+    env->GetStaticMethodID(typeEnumClass, "fromValue", "(I)Lapp/f3d/F3D/Types$LightType;");
+  jobject jtype = env->CallStaticObjectMethod(
+    typeEnumClass, fromValueMethod, static_cast<int>(cppLightState.type));
+  jfieldID typeField = env->GetFieldID(lightStateClass, "type", "Lapp/f3d/F3D/Types$LightType;");
+  env->SetObjectField(jlightState, typeField, jtype);
+
+  jdoubleArray jposition = env->NewDoubleArray(3);
+  double posData[] = { cppLightState.position[0], cppLightState.position[1],
+    cppLightState.position[2] };
+  env->SetDoubleArrayRegion(jposition, 0, 3, posData);
+  jfieldID positionField = env->GetFieldID(lightStateClass, "position", "[D");
+  env->SetObjectField(jlightState, positionField, jposition);
+
+  jdoubleArray jcolor = env->NewDoubleArray(3);
+  double colorData[] = { cppLightState.color[0], cppLightState.color[1], cppLightState.color[2] };
+  env->SetDoubleArrayRegion(jcolor, 0, 3, colorData);
+  jfieldID colorField = env->GetFieldID(lightStateClass, "color", "[D");
+  env->SetObjectField(jlightState, colorField, jcolor);
+
+  jdoubleArray jdirection = env->NewDoubleArray(3);
+  double dirData[] = { cppLightState.direction[0], cppLightState.direction[1],
+    cppLightState.direction[2] };
+  env->SetDoubleArrayRegion(jdirection, 0, 3, dirData);
+  jfieldID directionField = env->GetFieldID(lightStateClass, "direction", "[D");
+  env->SetObjectField(jlightState, directionField, jdirection);
+
+  jfieldID positionalLightField = env->GetFieldID(lightStateClass, "positionalLight", "Z");
+  env->SetBooleanField(jlightState, positionalLightField, cppLightState.positionalLight);
+
+  jfieldID intensityField = env->GetFieldID(lightStateClass, "intensity", "D");
+  env->SetDoubleField(jlightState, intensityField, cppLightState.intensity);
+
+  jfieldID switchStateField = env->GetFieldID(lightStateClass, "switchState", "Z");
+  env->SetBooleanField(jlightState, switchStateField, cppLightState.switchState);
+
+  return jlightState;
+}
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Scene, add)(JNIEnv* env, jobject self, jstring path)
+  {
+    if (!path)
+    {
+      return self;
+    }
+
+    const char* str = env->GetStringUTFChars(path, nullptr);
+    try
+    {
+      GetEngine(env, self)->getScene().add(str);
+    }
+    catch (const std::exception& e)
+    {
+      env->ReleaseStringUTFChars(path, str);
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+    env->ReleaseStringUTFChars(path, str);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, addAll)(JNIEnv* env, jobject self, jobject paths)
+  {
+    if (!paths)
+    {
+      return self;
+    }
+
+    std::vector<std::string> vec = JavaListToStringVector(env, paths);
+    try
+    {
+      GetEngine(env, self)->getScene().add(vec);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, addMesh)(JNIEnv* env, jobject self, jobject mesh)
+  {
+    if (!mesh)
+    {
+      return self;
+    }
+
+    f3d::mesh_t cppMesh = JavaMeshToCppMesh(env, mesh);
+    try
+    {
+      GetEngine(env, self)->getScene().add(cppMesh);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, clear)(JNIEnv* env, jobject self)
+  {
+    GetEngine(env, self)->getScene().clear();
+    return self;
+  }
+
+  JNIEXPORT jint JAVA_BIND(Scene, addLight)(JNIEnv* env, jobject self, jobject lightState)
+  {
+    if (!lightState)
+    {
+      return -1;
+    }
+
+    f3d::light_state_t cppLightState = JavaLightStateToCppLightState(env, lightState);
+    try
+    {
+      return GetEngine(env, self)->getScene().addLight(cppLightState);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return -1;
+    }
+  }
+
+  JNIEXPORT jint JAVA_BIND(Scene, getLightCount)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getScene().getLightCount();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, getLight)(JNIEnv* env, jobject self, jint index)
+  {
+    try
+    {
+      f3d::light_state_t cppLightState = GetEngine(env, self)->getScene().getLight(index);
+      return CppLightStateToJavaLightState(env, cppLightState);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, updateLight)(
+    JNIEnv* env, jobject self, jint index, jobject lightState)
+  {
+    if (!lightState)
+    {
+      return self;
+    }
+
+    f3d::light_state_t cppLightState = JavaLightStateToCppLightState(env, lightState);
+    try
+    {
+      GetEngine(env, self)->getScene().updateLight(index, cppLightState);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, removeLight)(JNIEnv* env, jobject self, jint index)
+  {
+    try
+    {
+      GetEngine(env, self)->getScene().removeLight(index);
+    }
+    catch (const std::exception& e)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, removeAllLights)(JNIEnv* env, jobject self)
+  {
+    GetEngine(env, self)->getScene().removeAllLights();
+    return self;
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Scene, supports)(JNIEnv* env, jobject self, jstring filePath)
+  {
+    if (!filePath)
+    {
+      return false;
+    }
+
+    const char* str = env->GetStringUTFChars(filePath, nullptr);
+    bool result = GetEngine(env, self)->getScene().supports(str);
+    env->ReleaseStringUTFChars(filePath, str);
+    return result;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Scene, loadAnimationTime)(
+    JNIEnv* env, jobject self, jdouble timeValue)
+  {
+    GetEngine(env, self)->getScene().loadAnimationTime(timeValue);
+    return self;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Scene, animationTimeRange)(JNIEnv* env, jobject self)
+  {
+    auto [minTime, maxTime] = GetEngine(env, self)->getScene().animationTimeRange();
+
+    jdoubleArray result = env->NewDoubleArray(2);
+    double timeRange[] = { minTime, maxTime };
+    env->SetDoubleArrayRegion(result, 0, 2, timeRange);
+
+    return result;
+  }
+
+  JNIEXPORT jint JAVA_BIND(Scene, availableAnimations)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getScene().availableAnimations();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DTypesBindings.cxx f3d-3.4.1/java/F3DTypesBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DTypesBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DTypesBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,134 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Transform2D.h>
+#include <app_f3d_F3D_Types_Mesh.h>
+
+#include <types.h>
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Transform2D, create)(
+    JNIEnv* env, jclass, jdoubleArray scale, jdoubleArray translate, jdouble angleDeg)
+  {
+    if (!scale || !translate)
+    {
+      return nullptr;
+    }
+
+    jsize scaleLen = env->GetArrayLength(scale);
+    jsize translateLen = env->GetArrayLength(translate);
+
+    if (scaleLen != 2 || translateLen != 2)
+    {
+      jclass exceptionClass = env->FindClass("java/lang/IllegalArgumentException");
+      env->ThrowNew(exceptionClass, "Scale and translate arrays must have exactly 2 elements");
+      return nullptr;
+    }
+
+    double* scaleData = env->GetDoubleArrayElements(scale, nullptr);
+    double* translateData = env->GetDoubleArrayElements(translate, nullptr);
+
+    f3d::double_array_t<2> cppScale({ scaleData[0], scaleData[1] });
+    f3d::double_array_t<2> cppTranslate({ translateData[0], translateData[1] });
+    f3d::transform2d_t cppTransform(cppScale, cppTranslate, angleDeg);
+
+    env->ReleaseDoubleArrayElements(scale, scaleData, 0);
+    env->ReleaseDoubleArrayElements(translate, translateData, 0);
+
+    jclass transform2DClass = env->FindClass("app/f3d/F3D/Transform2D");
+    jmethodID constructor = env->GetMethodID(transform2DClass, "<init>", "()V");
+    jobject result = env->NewObject(transform2DClass, constructor);
+
+    jfieldID dataField = env->GetFieldID(transform2DClass, "data", "[D");
+    jdoubleArray dataArray = env->NewDoubleArray(9);
+    std::vector<double> vec = cppTransform;
+    env->SetDoubleArrayRegion(dataArray, 0, 9, vec.data());
+    env->SetObjectField(result, dataField, dataArray);
+
+    return result;
+  }
+  JNIEXPORT jobject JAVA_BIND(Types_00024Mesh, isValid)(JNIEnv* env, jobject self)
+  {
+    jclass meshClass = env->GetObjectClass(self);
+
+    jfieldID pointsField = env->GetFieldID(meshClass, "points", "[F");
+    jfieldID normalsField = env->GetFieldID(meshClass, "normals", "[F");
+    jfieldID textureCoordinatesField = env->GetFieldID(meshClass, "textureCoordinates", "[F");
+    jfieldID faceSidesField = env->GetFieldID(meshClass, "faceSides", "[I");
+    jfieldID faceIndicesField = env->GetFieldID(meshClass, "faceIndices", "[I");
+
+    jfloatArray pointsArray = (jfloatArray)env->GetObjectField(self, pointsField);
+    jfloatArray normalsArray = (jfloatArray)env->GetObjectField(self, normalsField);
+    jfloatArray textureCoordinatesArray =
+      (jfloatArray)env->GetObjectField(self, textureCoordinatesField);
+    jintArray faceSidesArray = (jintArray)env->GetObjectField(self, faceSidesField);
+    jintArray faceIndicesArray = (jintArray)env->GetObjectField(self, faceIndicesField);
+
+    f3d::mesh_t cppMesh;
+
+    if (pointsArray)
+    {
+      jsize pointsLen = env->GetArrayLength(pointsArray);
+      float* pointsData = env->GetFloatArrayElements(pointsArray, nullptr);
+      cppMesh.points.assign(pointsData, pointsData + pointsLen);
+      env->ReleaseFloatArrayElements(pointsArray, pointsData, 0);
+    }
+
+    if (normalsArray)
+    {
+      jsize normalsLen = env->GetArrayLength(normalsArray);
+      if (normalsLen > 0)
+      {
+        float* normalsData = env->GetFloatArrayElements(normalsArray, nullptr);
+        cppMesh.normals.assign(normalsData, normalsData + normalsLen);
+        env->ReleaseFloatArrayElements(normalsArray, normalsData, 0);
+      }
+    }
+
+    if (textureCoordinatesArray)
+    {
+      jsize texCoordsLen = env->GetArrayLength(textureCoordinatesArray);
+      if (texCoordsLen > 0)
+      {
+        float* texCoordsData = env->GetFloatArrayElements(textureCoordinatesArray, nullptr);
+        cppMesh.texture_coordinates.assign(texCoordsData, texCoordsData + texCoordsLen);
+        env->ReleaseFloatArrayElements(textureCoordinatesArray, texCoordsData, 0);
+      }
+    }
+
+    if (faceSidesArray)
+    {
+      jsize faceSidesLen = env->GetArrayLength(faceSidesArray);
+      if (faceSidesLen > 0)
+      {
+        int* faceSidesData = env->GetIntArrayElements(faceSidesArray, nullptr);
+        cppMesh.face_sides.assign(faceSidesData, faceSidesData + faceSidesLen);
+        env->ReleaseIntArrayElements(faceSidesArray, faceSidesData, 0);
+      }
+    }
+
+    if (faceIndicesArray)
+    {
+      jsize faceIndicesLen = env->GetArrayLength(faceIndicesArray);
+      if (faceIndicesLen > 0)
+      {
+        int* faceIndicesData = env->GetIntArrayElements(faceIndicesArray, nullptr);
+        cppMesh.face_indices.assign(faceIndicesData, faceIndicesData + faceIndicesLen);
+        env->ReleaseIntArrayElements(faceIndicesArray, faceIndicesData, 0);
+      }
+    }
+
+    auto [valid, errorMessage] = cppMesh.isValid();
+
+    jclass validationResultClass = env->FindClass("app/f3d/F3D/Types$Mesh$ValidationResult");
+    jmethodID constructor =
+      env->GetMethodID(validationResultClass, "<init>", "(ZLjava/lang/String;)V");
+
+    jstring jErrorMessage = env->NewStringUTF(errorMessage.c_str());
+    jobject result = env->NewObject(validationResultClass, constructor, valid, jErrorMessage);
+
+    env->DeleteLocalRef(jErrorMessage);
+
+    return result;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DUtilsBindings.cxx f3d-3.4.1/java/F3DUtilsBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DUtilsBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DUtilsBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,131 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Utils.h>
+
+#include <utils.h>
+
+extern "C"
+{
+  JNIEXPORT jint JAVA_BIND(Utils, textDistance)(JNIEnv* env, jclass, jstring strA, jstring strB)
+  {
+    if (!strA || !strB)
+    {
+      return 0;
+    }
+
+    const char* strAChars = env->GetStringUTFChars(strA, nullptr);
+    const char* strBChars = env->GetStringUTFChars(strB, nullptr);
+
+    unsigned int distance = f3d::utils::textDistance(strAChars, strBChars);
+
+    env->ReleaseStringUTFChars(strA, strAChars);
+    env->ReleaseStringUTFChars(strB, strBChars);
+
+    return static_cast<jint>(distance);
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Utils, tokenize)(
+    JNIEnv* env, jclass, jstring str, jboolean keepComments)
+  {
+    if (!str)
+    {
+      return CreateStringList(env, std::vector<std::string>());
+    }
+
+    const char* strChars = env->GetStringUTFChars(str, nullptr);
+
+    try
+    {
+      std::vector<std::string> tokens = f3d::utils::tokenize(strChars, keepComments != 0);
+      env->ReleaseStringUTFChars(str, strChars);
+      return CreateStringList(env, tokens);
+    }
+    catch (const std::exception& e)
+    {
+      env->ReleaseStringUTFChars(str, strChars);
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Utils, collapsePath)(
+    JNIEnv* env, jclass, jstring path, jstring baseDirectory)
+  {
+    if (!path)
+    {
+      return env->NewStringUTF("");
+    }
+
+    const char* pathChars = env->GetStringUTFChars(path, nullptr);
+    std::string pathStr = pathChars;
+    env->ReleaseStringUTFChars(path, pathChars);
+
+    std::filesystem::path basePath;
+    if (baseDirectory)
+    {
+      const char* baseDirChars = env->GetStringUTFChars(baseDirectory, nullptr);
+      basePath = baseDirChars;
+      env->ReleaseStringUTFChars(baseDirectory, baseDirChars);
+    }
+
+    std::filesystem::path result = f3d::utils::collapsePath(pathStr, basePath);
+    return env->NewStringUTF(result.string().c_str());
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Utils, globToRegex)(
+    JNIEnv* env, jclass, jstring glob, jchar pathSeparator)
+  {
+    if (!glob)
+    {
+      return env->NewStringUTF("");
+    }
+
+    const char* globChars = env->GetStringUTFChars(glob, nullptr);
+
+    try
+    {
+      std::string result = f3d::utils::globToRegex(globChars, static_cast<char>(pathSeparator));
+      env->ReleaseStringUTFChars(glob, globChars);
+      return env->NewStringUTF(result.c_str());
+    }
+    catch (const std::exception& e)
+    {
+      env->ReleaseStringUTFChars(glob, globChars);
+      jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
+      env->ThrowNew(exceptionClass, e.what());
+      return nullptr;
+    }
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Utils, getEnv)(JNIEnv* env, jclass, jstring envVar)
+  {
+    if (!envVar)
+    {
+      return nullptr;
+    }
+
+    const char* envVarChars = env->GetStringUTFChars(envVar, nullptr);
+    std::optional<std::string> result = f3d::utils::getEnv(envVarChars);
+    env->ReleaseStringUTFChars(envVar, envVarChars);
+
+    return result.has_value() ? env->NewStringUTF(result.value().c_str()) : nullptr;
+  }
+
+  JNIEXPORT jstring JAVA_BIND(Utils, getKnownFolder)(JNIEnv* env, jclass, jobject knownFolder)
+  {
+    if (!knownFolder)
+    {
+      return nullptr;
+    }
+
+    jclass enumClass = env->GetObjectClass(knownFolder);
+    jmethodID getValueMethod = env->GetMethodID(enumClass, "getValue", "()I");
+    jint folderValue = env->CallIntMethod(knownFolder, getValueMethod);
+
+    f3d::utils::KnownFolder folder = static_cast<f3d::utils::KnownFolder>(folderValue);
+    std::optional<std::string> result = f3d::utils::getKnownFolder(folder);
+
+    return result.has_value() ? env->NewStringUTF(result.value().c_str()) : nullptr;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/F3DWindowBindings.cxx f3d-3.4.1/java/F3DWindowBindings.cxx
--- f3d-3.2.0+dfsg/java/F3DWindowBindings.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/F3DWindowBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,140 @@
+#include "F3DJavaBindings.h"
+
+#include <app_f3d_F3D_Window.h>
+
+#include <types.h>
+#include <window.h>
+
+extern "C"
+{
+  JNIEXPORT jobject JAVA_BIND(Window, getType)(JNIEnv* env, jobject self)
+  {
+    f3d::window::Type type = GetEngine(env, self)->getWindow().getType();
+
+    jclass enumClass = env->FindClass("app/f3d/F3D/Window$Type");
+    jfieldID fieldID;
+
+    switch (type)
+    {
+      case f3d::window::Type::NONE:
+        fieldID = env->GetStaticFieldID(enumClass, "NONE", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::EXTERNAL:
+        fieldID = env->GetStaticFieldID(enumClass, "EXTERNAL", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::GLX:
+        fieldID = env->GetStaticFieldID(enumClass, "GLX", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::WGL:
+        fieldID = env->GetStaticFieldID(enumClass, "WGL", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::COCOA:
+        fieldID = env->GetStaticFieldID(enumClass, "COCOA", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::EGL:
+        fieldID = env->GetStaticFieldID(enumClass, "EGL", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::OSMESA:
+        fieldID = env->GetStaticFieldID(enumClass, "OSMESA", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      case f3d::window::Type::WASM:
+        fieldID = env->GetStaticFieldID(enumClass, "WASM", "Lapp/f3d/F3D/Window$Type;");
+        break;
+      default:
+        fieldID = env->GetStaticFieldID(enumClass, "UNKNOWN", "Lapp/f3d/F3D/Window$Type;");
+        break;
+    }
+
+    return env->GetStaticObjectField(enumClass, fieldID);
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Window, isOffscreen)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getWindow().isOffscreen();
+  }
+
+  JNIEXPORT jboolean JAVA_BIND(Window, render)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getWindow().render();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Window, renderToImage)(
+    JNIEnv* env, jobject self, jboolean noBackground)
+  {
+    f3d::image* img = new f3d::image(GetEngine(env, self)->getWindow().renderToImage(noBackground));
+
+    jclass imageClass = env->FindClass("app/f3d/F3D/Image");
+    jmethodID constructor = env->GetMethodID(imageClass, "<init>", "(J)V");
+
+    jobject result = env->NewObject(imageClass, constructor, reinterpret_cast<jlong>(img));
+
+    return result;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Window, setSize)(JNIEnv* env, jobject self, jint width, jint height)
+  {
+    GetEngine(env, self)->getWindow().setSize(width, height);
+    return self;
+  }
+
+  JNIEXPORT jint JAVA_BIND(Window, getWidth)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getWindow().getWidth();
+  }
+
+  JNIEXPORT jint JAVA_BIND(Window, getHeight)(JNIEnv* env, jobject self)
+  {
+    return GetEngine(env, self)->getWindow().getHeight();
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Window, setPosition)(JNIEnv* env, jobject self, jint x, jint y)
+  {
+    GetEngine(env, self)->getWindow().setPosition(x, y);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Window, setIcon)(JNIEnv* env, jobject self, jbyteArray icon)
+  {
+    jsize iconSize = env->GetArrayLength(icon);
+    jbyte* iconData = env->GetByteArrayElements(icon, nullptr);
+
+    GetEngine(env, self)->getWindow().setIcon(reinterpret_cast<unsigned char*>(iconData), iconSize);
+
+    env->ReleaseByteArrayElements(icon, iconData, 0);
+    return self;
+  }
+
+  JNIEXPORT jobject JAVA_BIND(Window, setWindowName)(JNIEnv* env, jobject self, jstring windowName)
+  {
+    const char* name = env->GetStringUTFChars(windowName, nullptr);
+    GetEngine(env, self)->getWindow().setWindowName(name);
+    env->ReleaseStringUTFChars(windowName, name);
+    return self;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Window, getWorldFromDisplay)(
+    JNIEnv* env, jobject self, jdoubleArray displayPoint)
+  {
+    double* arr = env->GetDoubleArrayElements(displayPoint, nullptr);
+    f3d::point3_t worldPoint =
+      GetEngine(env, self)->getWindow().getWorldFromDisplay({ arr[0], arr[1], arr[2] });
+    env->ReleaseDoubleArrayElements(displayPoint, arr, 0);
+
+    jdoubleArray ret = env->NewDoubleArray(3);
+    env->SetDoubleArrayRegion(ret, 0, 3, worldPoint.data());
+    return ret;
+  }
+
+  JNIEXPORT jdoubleArray JAVA_BIND(Window, getDisplayFromWorld)(
+    JNIEnv* env, jobject self, jdoubleArray worldPoint)
+  {
+    double* arr = env->GetDoubleArrayElements(worldPoint, nullptr);
+    f3d::point3_t displayPoint =
+      GetEngine(env, self)->getWindow().getDisplayFromWorld({ arr[0], arr[1], arr[2] });
+    env->ReleaseDoubleArrayElements(worldPoint, arr, 0);
+
+    jdoubleArray ret = env->NewDoubleArray(3);
+    env->SetDoubleArrayRegion(ret, 0, 3, displayPoint.data());
+    return ret;
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/Image.java f3d-3.4.1/java/Image.java
--- f3d-3.2.0+dfsg/java/Image.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Image.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,237 @@
+package app.f3d.F3D;
+
+import java.util.List;
+
+public class Image {
+
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    public enum SaveFormat {
+        PNG,
+        JPG,
+        TIF,
+        BMP
+    }
+
+    public enum ChannelType {
+        BYTE,
+        SHORT,
+        FLOAT
+    }
+
+    private long mNativeAddress;
+
+    /**
+     * Read provided file path into a new image instance.
+     *
+     * @param filePath path to image file
+     */
+    public Image(String filePath) {
+        mNativeAddress = nativeCreateFromFile(filePath);
+    }
+
+    /**
+     * Create an image from given width, height, and channel count.
+     *
+     * @param width image width
+     * @param height image height
+     * @param channelCount number of channels
+     * @param type channel type (default BYTE)
+     */
+    public Image(int width, int height, int channelCount, ChannelType type) {
+        mNativeAddress = nativeCreate(width, height, channelCount, type.ordinal());
+    }
+
+    /**
+     * Create an image from given width, height, and channel count with BYTE type.
+     *
+     * @param width image width
+     * @param height image height
+     * @param channelCount number of channels
+     */
+    public Image(int width, int height, int channelCount) {
+        this(width, height, channelCount, ChannelType.BYTE);
+    }
+
+    Image(long nativeAddress) {
+        mNativeAddress = nativeAddress;
+    }
+
+    /**
+     * Read one specific pixel and return all channel normalized values.
+     *
+     * @param x x coordinate
+     * @param y y coordinate
+     * @return normalized pixel values [0, 1]
+     */
+    public native double[] getNormalizedPixel(int x, int y);
+
+    /**
+     * Get the list of supported image format extensions.
+     *
+     * @return list of supported formats
+     */
+    public static native List<String> getSupportedFormats();
+
+    /**
+     * Get image width.
+     *
+     * @return width
+     */
+    public native int getWidth();
+
+    /**
+     * Get image height.
+     *
+     * @return height
+     */
+    public native int getHeight();
+
+    /**
+     * Get image channel count.
+     *
+     * @return channel count
+     */
+    public native int getChannelCount();
+
+    /**
+     * Get image channel type.
+     *
+     * @return channel type
+     */
+    public native ChannelType getChannelType();
+
+    /**
+     * Get image channel type size in bytes.
+     *
+     * @return channel type size
+     */
+    public native int getChannelTypeSize();
+
+    /**
+     * Set image buffer data.
+     *
+     * @param buffer byte array containing image data
+     * @return this image for method chaining
+     */
+    public native Image setContent(byte[] buffer);
+
+    /**
+     * Get image buffer data.
+     *
+     * @return byte array containing image data
+     */
+    public native byte[] getContent();
+
+    /**
+     * Compare current image to a reference.
+     *
+     * @param reference reference image
+     * @return error value (0.0 = identical, 1.0 = completely different)
+     */
+    public native double compare(Image reference);
+
+    /**
+     * Compare images for equality using a threshold of 1e-14.
+     *
+     * @param reference reference image
+     * @return true if images are equal within threshold, false otherwise
+     */
+    public boolean equals(Image reference) {
+        return compare(reference) < 1e-14;
+    }
+
+    /**
+     * Compare images for inequality using a threshold of 1e-14.
+     *
+     * @param reference reference image
+     * @return true if images are not equal within threshold, false otherwise
+     */
+    public boolean notEquals(Image reference) {
+        return compare(reference) >= 1e-14;
+    }
+
+    /**
+     * Save an image to the provided file path in the specified format.
+     *
+     * @param filePath path to save image
+     * @param format save format
+     * @return this image for method chaining
+     */
+    public native Image save(String filePath, SaveFormat format);
+
+    /**
+     * Save an image to the provided file path in PNG format.
+     *
+     * @param filePath path to save image
+     * @return this image for method chaining
+     */
+    public Image save(String filePath) {
+        return save(filePath, SaveFormat.PNG);
+    }
+
+    /**
+     * Save an image to a memory buffer in the specified format.
+     *
+     * @param format save format
+     * @return byte array containing saved image
+     */
+    public native byte[] saveBuffer(SaveFormat format);
+
+    /**
+     * Save an image to a memory buffer in PNG format.
+     *
+     * @return byte array containing saved image
+     */
+    public byte[] saveBuffer() {
+        return saveBuffer(SaveFormat.PNG);
+    }
+
+    /**
+     * Convert to colored text using ANSI escape sequences.
+     *
+     * @return ANSI colored text representation
+     */
+    public native String toTerminalText();
+
+    /**
+     * Set the value for a metadata key.
+     *
+     * @param key metadata key
+     * @param value metadata value
+     * @return this image for method chaining
+     */
+    public native Image setMetadata(String key, String value);
+
+    /**
+     * Get the value for a metadata key.
+     *
+     * @param key metadata key
+     * @return metadata value
+     */
+    public native String getMetadata(String key);
+
+    /**
+     * List all metadata keys which have a value set.
+     *
+     * @return list of metadata keys
+     */
+    public native List<String> allMetadata();
+
+    /**
+     * Delete the image and free native resources.
+     * After calling this method, the image should not be used anymore.
+     */
+    public void delete() {
+        if (mNativeAddress != 0) {
+            nativeDestroy(mNativeAddress);
+            mNativeAddress = 0;
+        }
+    }
+
+    private static native long nativeCreateFromFile(String filePath);
+    private static native long nativeCreate(int width, int height, int channelCount, int type);
+    private static native void nativeDestroy(long nativeAddress);
+}
diff -Nru f3d-3.2.0+dfsg/java/Interactor.java f3d-3.4.1/java/Interactor.java
--- f3d-3.2.0+dfsg/java/Interactor.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Interactor.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,525 @@
+package app.f3d.F3D;
+
+import java.util.List;
+
+public class Interactor {
+
+    private long mNativeAddress;
+
+    public enum ModifierKeys {
+        ANY,
+        NONE,
+        CTRL,
+        SHIFT,
+        CTRL_SHIFT
+    }
+
+    public enum BindingType {
+        CYCLIC,
+        NUMERICAL,
+        TOGGLE,
+        OTHER
+    }
+
+    public enum MouseButton {
+        LEFT,
+        RIGHT,
+        MIDDLE
+    }
+
+    public enum WheelDirection {
+        FORWARD,
+        BACKWARD,
+        LEFT,
+        RIGHT
+    }
+
+    public enum InputAction {
+        PRESS,
+        RELEASE
+    }
+
+    public enum InputModifier {
+        NONE,
+        CTRL,
+        SHIFT,
+        CTRL_SHIFT
+    }
+
+    public enum AnimationDirection {
+        FORWARD(0),
+        BACKWARD(1);
+
+        private final int value;
+
+        AnimationDirection(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public static AnimationDirection fromValue(int value) {
+            for (AnimationDirection dir : AnimationDirection.values()) {
+                if (dir.value == value) {
+                    return dir;
+                }
+            }
+            throw new IllegalArgumentException("Invalid AnimationDirection value: " + value);
+        }
+    }
+
+    public static class InteractionBind implements Comparable<InteractionBind> {
+        public ModifierKeys mod;
+        public String inter;
+
+        public InteractionBind() {
+            this.mod = ModifierKeys.NONE;
+            this.inter = "";
+        }
+
+        public InteractionBind(ModifierKeys mod, String inter) {
+            this.mod = mod;
+            this.inter = inter;
+        }
+
+        public String format() {
+            switch (mod) {
+                case CTRL_SHIFT:
+                    return "Ctrl+Shift+" + inter;
+                case CTRL:
+                    return "Ctrl+" + inter;
+                case SHIFT:
+                    return "Shift+" + inter;
+                case ANY:
+                    return "Any+" + inter;
+                default:
+                    return inter;
+            }
+        }
+
+        public static InteractionBind parse(String str) {
+            InteractionBind bind = new InteractionBind();
+            int plusIndex = str.lastIndexOf('+');
+            if (plusIndex == -1) {
+                bind.inter = str;
+            } else {
+                bind.inter = str.substring(plusIndex + 1);
+                String modStr = str.substring(0, plusIndex);
+                if (modStr.equals("Ctrl+Shift")) {
+                    bind.mod = ModifierKeys.CTRL_SHIFT;
+                } else if (modStr.equals("Shift")) {
+                    bind.mod = ModifierKeys.SHIFT;
+                } else if (modStr.equals("Ctrl")) {
+                    bind.mod = ModifierKeys.CTRL;
+                } else if (modStr.equals("Any")) {
+                    bind.mod = ModifierKeys.ANY;
+                } else if (modStr.equals("None")) {
+                    bind.mod = ModifierKeys.NONE;
+                }
+            }
+            return bind;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null || getClass() != obj.getClass()) return false;
+            InteractionBind other = (InteractionBind) obj;
+            return mod == other.mod && inter.equals(other.inter);
+        }
+
+        @Override
+        public int compareTo(InteractionBind other) {
+            if (mod != other.mod) {
+                return mod.compareTo(other.mod);
+            }
+            return inter.compareTo(other.inter);
+        }
+    }
+
+    public static class BindingDocumentation {
+        public String documentation;
+        public String currentValue;
+
+        public BindingDocumentation(String documentation, String currentValue) {
+            this.documentation = documentation;
+            this.currentValue = currentValue;
+        }
+    }
+
+    public interface CommandCallback {
+        void execute(List<String> args);
+    }
+
+    Interactor(long nativeAddress) {
+        mNativeAddress = nativeAddress;
+    }
+
+    /**
+     * Remove all existing commands and add all default commands.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor initCommands();
+
+    /**
+     * Add a command to be called using triggerCommand.
+     *
+     * @param action action name for the command
+     * @param callback callback to execute when command is triggered
+     * @return this interactor for method chaining
+     */
+    public native Interactor addCommand(String action, CommandCallback callback);
+
+    /**
+     * Remove a command for provided action.
+     *
+     * @param action action name to remove
+     * @return this interactor for method chaining
+     */
+    public native Interactor removeCommand(String action);
+
+    /**
+     * Return a list containing all currently defined actions of commands.
+     *
+     * @return list of command actions
+     */
+    public native List<String> getCommandActions();
+
+    /**
+     * Trigger provided command.
+     *
+     * @param command command string to execute
+     * @param keepComments if true, comments with # are supported
+     * @return true if command succeeded, false otherwise
+     */
+    public native boolean triggerCommand(String command, boolean keepComments);
+
+    /**
+     * Trigger provided command with keepComments enabled.
+     *
+     * @param command command string to execute
+     * @return true if command succeeded, false otherwise
+     */
+    public boolean triggerCommand(String command) {
+        return triggerCommand(command, true);
+    }
+
+    /**
+     * Remove all existing interaction commands and add all default bindings.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor initBindings();
+
+    /**
+     * Add binding to trigger commands for a specific bind.
+     *
+     * @param bind interaction bind (key combination)
+     * @param commands list of commands to trigger
+     * @param group optional group name for organization
+     * @param type optional binding type
+     * @return this interactor for method chaining
+     */
+    private native Interactor addBindingCommands(InteractionBind bind, List<String> commands, String group, BindingType type);
+
+    /**
+     * Add binding to trigger commands for a specific bind.
+     *
+     * @param bind interaction bind (key combination)
+     * @param commands list of commands to trigger
+     * @param group optional group name for organization
+     * @param type optional binding type
+     * @return this interactor for method chaining
+     */
+    public Interactor addBinding(InteractionBind bind, List<String> commands, String group, BindingType type) {
+        return addBindingCommands(bind, commands, group, type);
+    }
+
+    /**
+     * Add binding to trigger commands for a specific bind with default group and type.
+     *
+     * @param bind interaction bind (key combination)
+     * @param commands list of commands to trigger
+     * @return this interactor for method chaining
+     */
+    public Interactor addBinding(InteractionBind bind, List<String> commands) {
+        return addBindingCommands(bind, commands, "", BindingType.OTHER);
+    }
+
+    /**
+     * Add binding to trigger a single command for a specific bind.
+     *
+     * @param bind interaction bind (key combination)
+     * @param command command to trigger
+     * @param group optional group name for organization
+     * @param type optional binding type
+     * @return this interactor for method chaining
+     */
+    private native Interactor addBindingCommand(InteractionBind bind, String command, String group, BindingType type);
+
+    /**
+     * Add binding to trigger a single command for a specific bind.
+     *
+     * @param bind interaction bind (key combination)
+     * @param command command to trigger
+     * @param group optional group name for organization
+     * @param type optional binding type
+     * @return this interactor for method chaining
+     */
+    public Interactor addBinding(InteractionBind bind, String command, String group, BindingType type) {
+        return addBindingCommand(bind, command, group, type);
+    }
+
+    /**
+     * Add binding to trigger a single command for a specific bind with default group and type.
+     *
+     * @param bind interaction bind (key combination)
+     * @param command command to trigger
+     * @return this interactor for method chaining
+     */
+    public Interactor addBinding(InteractionBind bind, String command) {
+        return addBindingCommand(bind, command, "", BindingType.OTHER);
+    }
+
+    /**
+     * Remove binding corresponding to provided bind.
+     *
+     * @param bind binding to remove
+     * @return this interactor for method chaining
+     */
+    public native Interactor removeBinding(InteractionBind bind);
+
+    /**
+     * Return a vector of available bind groups, in order of addition.
+     *
+     * @return list of bind group names
+     */
+    public native List<String> getBindGroups();
+
+    /**
+     * Return a vector of bind for the specified group, in order of addition.
+     *
+     * @param group group name
+     * @return list of binds for the group
+     */
+    public native List<InteractionBind> getBindsForGroup(String group);
+
+    /**
+     * Return a vector of all binds, in order of addition.
+     *
+     * @return list of all binds
+     */
+    public native List<InteractionBind> getBinds();
+
+    /**
+     * Get documentation for a binding.
+     *
+     * @param bind binding to get documentation for
+     * @return documentation pair (description, current value)
+     */
+    public native BindingDocumentation getBindingDocumentation(InteractionBind bind);
+
+    /**
+     * Get the type of a binding.
+     *
+     * @param bind binding to get type for
+     * @return binding type
+     */
+    public native BindingType getBindingType(InteractionBind bind);
+
+    /**
+     * Toggle animation state.
+     *
+     * @param direction animation direction
+     * @return this interactor for method chaining
+     */
+    public native Interactor toggleAnimation(AnimationDirection direction);
+
+    /**
+     * Toggle animation state with default forward direction.
+     *
+     * @return this interactor for method chaining
+     */
+    public Interactor toggleAnimation() {
+        return toggleAnimation(AnimationDirection.FORWARD);
+    }
+
+    /**
+     * Start animation.
+     *
+     * @param direction animation direction
+     * @return this interactor for method chaining
+     */
+    public native Interactor startAnimation(AnimationDirection direction);
+
+    /**
+     * Start animation with default forward direction.
+     *
+     * @return this interactor for method chaining
+     */
+    public Interactor startAnimation() {
+        return startAnimation(AnimationDirection.FORWARD);
+    }
+
+    /**
+     * Stop animation.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor stopAnimation();
+
+    /**
+     * Check if animation is playing.
+     *
+     * @return true if playing, false otherwise
+     */
+    public native boolean isPlayingAnimation();
+
+    /**
+     * Get the current animation direction.
+     *
+     * @return animation direction
+     */
+    public native AnimationDirection getAnimationDirection();
+
+    /**
+     * Enable camera movement.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor enableCameraMovement();
+
+    /**
+     * Disable camera movement.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor disableCameraMovement();
+
+    /**
+     * Trigger a modifier update.
+     *
+     * @param mod modifier state
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerModUpdate(InputModifier mod);
+
+    /**
+     * Trigger a mouse button event.
+     *
+     * @param action press or release action
+     * @param button mouse button
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerMouseButton(InputAction action, MouseButton button);
+
+    /**
+     * Trigger a mouse new position event.
+     *
+     * @param xpos x position in pixels
+     * @param ypos y position in pixels
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerMousePosition(double xpos, double ypos);
+
+    /**
+     * Trigger a mouse wheel event.
+     *
+     * @param direction wheel direction
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerMouseWheel(WheelDirection direction);
+
+    /**
+     * Trigger a keyboard key event.
+     *
+     * @param action press or release action
+     * @param keySym key symbol (e.g., "A", "Left", "Space")
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerKeyboardKey(InputAction action, String keySym);
+
+    /**
+     * Trigger a text character input event.
+     *
+     * @param codepoint Unicode codepoint
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerTextCharacter(int codepoint);
+
+    /**
+     * Manually trigger the event loop.
+     *
+     * @param deltaTime time delta in seconds (must be positive)
+     * @return this interactor for method chaining
+     */
+    public native Interactor triggerEventLoop(double deltaTime);
+
+    /**
+     * Play a VTK interaction file.
+     *
+     * @param file path to interaction file
+     * @param deltaTime time delta in seconds
+     * @return true on success, false otherwise
+     */
+    public native boolean playInteraction(String file, double deltaTime);
+
+    /**
+     * Play a VTK interaction file with default deltaTime.
+     *
+     * @param file path to interaction file
+     * @return true on success, false otherwise
+     */
+    public boolean playInteraction(String file) {
+        return playInteraction(file, 1.0 / 30);
+    }
+
+    /**
+     * Start interaction and record it in a VTK interaction file.
+     *
+     * @param file path to save interaction file
+     * @return true on success, false otherwise
+     */
+    public native boolean recordInteraction(String file);
+
+    /**
+     * Start the interactor event loop.
+     *
+     * @param deltaTime time delta in seconds (must be positive)
+     * @return this interactor for method chaining
+     */
+    public native Interactor start(double deltaTime);
+
+    /**
+     * Start the interactor event loop with default deltaTime.
+     *
+     * @return this interactor for method chaining
+     */
+    public Interactor start() {
+        return start(1.0 / 30);
+    }
+
+    /**
+     * Stop the interactor.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor stop();
+
+    /**
+     * Request a render to be done on the next event loop.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor requestRender();
+
+    /**
+     * Request the interactor to stop on the next event loop.
+     *
+     * @return this interactor for method chaining
+     */
+    public native Interactor requestStop();
+}
diff -Nru f3d-3.2.0+dfsg/java/Log.java f3d-3.4.1/java/Log.java
--- f3d-3.2.0+dfsg/java/Log.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Log.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,129 @@
+package app.f3d.F3D;
+
+public class Log {
+    /**
+     * Enumeration of verbose levels.
+     */
+    public enum VerboseLevel {
+        DEBUG(0),
+        INFO(1),
+        WARN(2),
+        ERROR(3),
+        QUIET(4);
+
+        private final int value;
+
+        VerboseLevel(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public static VerboseLevel fromValue(int value) {
+            for (VerboseLevel level : VerboseLevel.values()) {
+                if (level.value == value) {
+                    return level;
+                }
+            }
+            throw new IllegalArgumentException("Invalid VerboseLevel value: " + value);
+        }
+    }
+
+    /**
+     * Callback interface for log forwarding.
+     */
+    @FunctionalInterface
+    public interface ForwardCallback {
+        /**
+         * Called when a log message is forwarded.
+         *
+         * @param level verbose level of the message
+         * @param message log message
+         */
+        void onLogMessage(VerboseLevel level, String message);
+    }
+
+    // Load the native library
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    private Log() {
+    }
+
+    /**
+     * Log a message at the specified verbose level.
+     *
+     * @param level verbose level for the message
+     * @param message message string
+     */
+    public static native void print(VerboseLevel level, String message);
+
+    /**
+     * Log a debug message.
+     *
+     * @param message message string
+     */
+    public static native void debug(String message);
+
+    /**
+     * Log an info message.
+     *
+     * @param message message string
+     */
+    public static native void info(String message);
+
+    /**
+     * Log a warning message.
+     *
+     * @param message message string
+     */
+    public static native void warn(String message);
+
+    /**
+     * Log an error message.
+     *
+     * @param message message string
+     */
+    public static native void error(String message);
+
+    /**
+     * Set the coloring usage.
+     *
+     * @param use if true, coloring will be used
+     */
+    public static native void setUseColoring(boolean use);
+
+    /**
+     * Set the verbose level.
+     *
+     * @param level verbose level to set
+     * @param forceStdErr if true, all messages are written to stderr
+     */
+    public static native void setVerboseLevel(VerboseLevel level, boolean forceStdErr);
+
+    /**
+     * Set the verbose level.
+     *
+     * @param level verbose level to set
+     */
+    public static void setVerboseLevel(VerboseLevel level) {
+        setVerboseLevel(level, false);
+    }
+
+    /**
+     * Get the current verbose level.
+     *
+     * @return current verbose level
+     */
+    public static native VerboseLevel getVerboseLevel();
+
+    /**
+     * Set a callback function to forward log messages.
+     *
+     * @param callback callback function, or null to disable forwarding
+     */
+    public static native void forward(ForwardCallback callback);
+}
diff -Nru f3d-3.2.0+dfsg/java/Options.java f3d-3.4.1/java/Options.java
--- f3d-3.2.0+dfsg/java/Options.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/Options.java	2026-01-08 08:41:55.000000000 +0200
@@ -1,12 +1,255 @@
 package app.f3d.F3D;
 
+import java.util.List;
+
 public class Options {
 
-    public Options(long nativeAddress) {
+    /**
+     * Package-private constructor for creating an Options instance from a native pointer.
+     *
+     * @param nativeAddress the native memory address of the underlying f3d::options object
+     */
+    Options(long nativeAddress) {
         mNativeAddress = nativeAddress;
     }
 
+    /**
+     * Sets a boolean option value.
+     *
+     * @param name the name of the option to set
+     * @param value the boolean value to set
+     * @throws IllegalArgumentException if the option name does not exist or is not a boolean type
+     */
+    public native void setAsBool(String name, boolean value);
+
+    /**
+     * Sets an integer option value.
+     *
+     * @param name the name of the option to set
+     * @param value the integer value to set
+     * @throws IllegalArgumentException if the option name does not exist or is not an integer type
+     */
+    public native void setAsInt(String name, int value);
+
+    /**
+     * Sets a double option value.
+     *
+     * @param name the name of the option to set
+     * @param value the double value to set
+     * @throws IllegalArgumentException if the option name does not exist or is not a double type
+     */
+    public native void setAsDouble(String name, double value);
+
+    /**
+     * Sets a string option value.
+     *
+     * @param name the name of the option to set
+     * @param value the string value to set
+     * @throws IllegalArgumentException if the option name does not exist or is not a string type
+     */
+    public native void setAsString(String name, String value);
+
+    /**
+     * Sets a double vector option value.
+     *
+     * @param name the name of the option to set
+     * @param values the array of double values to set
+     * @throws IllegalArgumentException if the option name does not exist or is not a double vector type
+     */
+    public native void setAsDoubleVector(String name, double[] values);
+
+    /**
+     * Sets an integer vector option value.
+     *
+     * @param name the name of the option to set
+     * @param values the array of integer values to set
+     * @throws IllegalArgumentException if the option name does not exist or is not an integer vector type
+     */
+    public native void setAsIntVector(String name, int[] values);
+
+    /**
+     * Gets a boolean option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the boolean value of the option
+     * @throws IllegalArgumentException if the option name does not exist or is not a boolean type
+     */
+    public native boolean getAsBool(String name);
+
+    /**
+     * Gets an integer option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the integer value of the option
+     * @throws IllegalArgumentException if the option name does not exist or is not an integer type
+     */
+    public native int getAsInt(String name);
+
+    /**
+     * Gets a double option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the double value of the option
+     * @throws IllegalArgumentException if the option name does not exist or is not a double type
+     */
+    public native double getAsDouble(String name);
+
+    /**
+     * Gets a string option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the string value of the option
+     * @throws IllegalArgumentException if the option name does not exist or is not a string type
+     */
+    public native String getAsString(String name);
+
+    /**
+     * Gets the string representation of an option value regardless of its underlying type.
+     *
+     * @param name the name of the option to retrieve
+     * @return the string representation of the option value
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native String getAsStringRepresentation(String name);
+
+    /**
+     * Sets an option value from its string representation.
+     *
+     * @param name the name of the option to set
+     * @param str the string representation to parse and set
+     * @throws IllegalArgumentException if the option name does not exist or the string cannot be parsed
+     */
+    public native void setAsStringRepresentation(String name, String str);
+
+    /**
+     * Gets a double vector option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the array of double values
+     * @throws IllegalArgumentException if the option name does not exist or is not a double vector type
+     */
+    public native double[] getAsDoubleVector(String name);
+
+    /**
+     * Gets an integer vector option value.
+     *
+     * @param name the name of the option to retrieve
+     * @return the array of integer values
+     * @throws IllegalArgumentException if the option name does not exist or is not an integer vector type
+     */
+    public native int[] getAsIntVector(String name);
+
+    /**
+     * Toggles a boolean option value.
+     *
+     * @param name the name of the boolean option to toggle
+     * @throws IllegalArgumentException if the option name does not exist or is not a boolean type
+     */
     public native void toggle(String name);
 
+    /**
+     * Checks if the specified option has the same value in this instance and another Options instance.
+     *
+     * @param other the other Options instance to compare with
+     * @param name the name of the option to compare
+     * @return true if both options have the same value, false otherwise
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native boolean isSame(Options other, String name);
+
+    /**
+     * Checks if the specified option has a value set.
+     *
+     * @param name the name of the option to check
+     * @return true if the option has a value, false otherwise
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native boolean hasValue(String name);
+
+    /**
+     * Copies the value of an option from another Options instance to this instance.
+     *
+     * @param other the Options instance to copy from
+     * @param name the name of the option to copy
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native void copy(Options other, String name);
+
+    /**
+     * Gets a list of all available option names in F3D.
+     *
+     * @return a list containing all option names
+     */
+    public static native List<String> getAllNames();
+
+    /**
+     * Gets a list of option names that have values set in this Options instance.
+     *
+     * @return a list containing names of options with values
+     */
+    public native List<String> getNames();
+
+    /**
+     * Finds the closest matching option name to the provided string.
+     *
+     * @param option the option name to find a match for
+     * @return a ClosestOptionResult containing the closest match name and edit distance
+     */
+    public native ClosestOptionResult getClosestOption(String option);
+
+    /**
+     * Checks if the specified option is optional.
+     *
+     * @param name the name of the option to check
+     * @return true if the option is optional, false if it is required
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native boolean isOptional(String name);
+
+    /**
+     * Resets the specified option to its default value.
+     *
+     * @param name the name of the option to reset
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native void reset(String name);
+
+    /**
+     * Removes the value of the specified option, making it unset.
+     *
+     * @param name the name of the option to remove the value from
+     * @throws IllegalArgumentException if the option name does not exist
+     */
+    public native void removeValue(String name);
+
+    /**
+     * Result class for the closest option search operation.
+     */
+    public static class ClosestOptionResult {
+        /**
+         * The name of the closest matching option.
+         */
+        public String name;
+
+        /**
+         * The edit distance to the match.
+         */
+        public int distance;
+
+        /**
+         * Constructs a ClosestOptionResult.
+         *
+         * @param name the name of the closest matching option
+         * @param distance the edit distance to the match
+         */
+        public ClosestOptionResult(String name, int distance) {
+            this.name = name;
+            this.distance = distance;
+        }
+    }
+
+    /**
+     * Native pointer to the underlying f3d::options object.
+     */
     private long mNativeAddress;
 }
diff -Nru f3d-3.2.0+dfsg/java/Scene.java f3d-3.4.1/java/Scene.java
--- f3d-3.2.0+dfsg/java/Scene.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/Scene.java	2026-01-08 08:41:55.000000000 +0200
@@ -1,13 +1,120 @@
 package app.f3d.F3D;
 
+import java.util.List;
+
 public class Scene {
 
     public Scene(long nativeAddress) {
         mNativeAddress = nativeAddress;
     }
 
-    public native void add(String file);
-    public native void clear();
+    /**
+     * Add and load a file into the scene.
+     *
+     * @param filePath file path to add
+     * @return this scene for method chaining
+     */
+    public native Scene add(String filePath);
+
+    /**
+     * Add and load multiple files into the scene.
+     *
+     * @param filePaths list of file paths to add
+     * @return this scene for method chaining
+     */
+    public native Scene addAll(List<String> filePaths);
+
+    /**
+     * Add and load a mesh into the scene.
+     *
+     * @param mesh mesh to add
+     * @return this scene for method chaining
+     */
+    public native Scene addMesh(Types.Mesh mesh);
+
+    /**
+     * Clear the scene of all added files.
+     *
+     * @return this scene for method chaining
+     */
+    public native Scene clear();
+
+    /**
+     * Add a light based on a light state.
+     *
+     * @param lightState light state
+     * @return index of the added light
+     */
+    public native int addLight(Types.LightState lightState);
+
+    /**
+     * Get the number of lights.
+     *
+     * @return number of lights in the scene
+     */
+    public native int getLightCount();
+
+    /**
+     * Get the light state at provided index.
+     *
+     * @param index index of the light
+     * @return light state
+     */
+    public native Types.LightState getLight(int index);
+
+    /**
+     * Update a light at provided index with the provided light state.
+     *
+     * @param index index of the light to update
+     * @param lightState new light state
+     * @return this scene for method chaining
+     */
+    public native Scene updateLight(int index, Types.LightState lightState);
+
+    /**
+     * Remove a light at provided index.
+     *
+     * @param index index of the light to remove
+     * @return this scene for method chaining
+     */
+    public native Scene removeLight(int index);
+
+    /**
+     * Remove all lights from the scene.
+     *
+     * @return this scene for method chaining
+     */
+    public native Scene removeAllLights();
+
+    /**
+     * Check if a file path is supported by the scene.
+     *
+     * @param filePath file path to check
+     * @return true if supported, false otherwise
+     */
+    public native boolean supports(String filePath);
+
+    /**
+     * Load added files at provided time value if they contain any animation.
+     *
+     * @param timeValue time value to load
+     * @return this scene for method chaining
+     */
+    public native Scene loadAnimationTime(double timeValue);
+
+    /**
+     * Get animation time range of currently added files.
+     *
+     * @return array of 2 doubles [min_time, max_time]
+     */
+    public native double[] animationTimeRange();
+
+    /**
+     * Return the number of animations available in the currently loaded files.
+     *
+     * @return number of available animations
+     */
+    public native int availableAnimations();
 
     private long mNativeAddress;
 }
diff -Nru f3d-3.2.0+dfsg/java/testing/CMakeLists.txt f3d-3.4.1/java/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/java/testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,14 @@
 list(APPEND javaf3dTests_list
-     ${CMAKE_CURRENT_SOURCE_DIR}/TestJavaBindings.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestCamera.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestEngine.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestImage.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestInteractor.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestLog.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestOptions.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestScene.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestTypes.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestUtils.java
+  ${CMAKE_CURRENT_SOURCE_DIR}/TestWindow.java
     )
 
 set(java_test_args "-ea") # enable assertions
diff -Nru f3d-3.2.0+dfsg/java/testing/TestCamera.java f3d-3.4.1/java/testing/TestCamera.java
--- f3d-3.2.0+dfsg/java/testing/TestCamera.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestCamera.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,64 @@
+import app.f3d.F3D.*;
+
+public class TestCamera {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine engine = Engine.create(true);
+    Window window = engine.getWindow();
+    Camera camera = window.getCamera();
+
+    camera.setPosition(new double[]{1.0, 2.0, 3.0});
+    camera.getPosition();
+
+    camera.setFocalPoint(new double[]{0.5, 0.6, 0.7});
+    camera.getFocalPoint();
+
+    camera.setViewUp(new double[]{0.0, 1.0, 0.0});
+    camera.getViewUp();
+
+    camera.setViewAngle(45.0);
+    camera.getViewAngle();
+
+    Camera.CameraState state = new Camera.CameraState(
+      new double[]{2.0, 3.0, 4.0},
+      new double[]{1.0, 1.0, 1.0},
+      new double[]{0.0, 1.0, 0.0},
+      60.0
+    );
+    camera.setState(state);
+    camera.getState();
+
+    camera.dolly(1.5);
+    camera.pan(0.1, 0.2, 0.3);
+    camera.pan(0.1, 0.2);
+    camera.zoom(1.2);
+    camera.roll(10.0);
+    camera.azimuth(15.0);
+    camera.yaw(20.0);
+    camera.elevation(25.0);
+    camera.pitch(30.0);
+
+    camera.setCurrentAsDefault();
+    camera.resetToDefault();
+    camera.resetToBounds(0.8);
+    camera.resetToBounds();
+
+    camera.setPosition(new double[]{1.0, 1.0, 1.0})
+          .setFocalPoint(new double[]{0.0, 0.0, 0.0})
+          .setViewUp(new double[]{0.0, 1.0, 0.0})
+          .setViewAngle(30.0)
+          .dolly(1.0)
+          .roll(5.0);
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestEngine.java f3d-3.4.1/java/testing/TestEngine.java
--- f3d-3.2.0+dfsg/java/testing/TestEngine.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestEngine.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,50 @@
+import app.f3d.F3D.*;
+
+public class TestEngine {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine eng1 = Engine.create(true);
+    eng1.close();
+
+    Engine eng2 = Engine.create();
+    eng2.close();
+
+    Engine eng3 = Engine.createNone();
+    eng3.close();
+
+    Engine.loadPlugin("native");
+
+    Engine.getPluginsList(".");
+
+    Engine.getAllReaderOptionNames();
+
+    Engine.getLibInfo();
+
+    Engine.getReadersInfo();
+
+    Engine.getRenderingBackendList();
+
+    Engine engine = Engine.create(true);
+
+    engine.setCachePath("/tmp/f3d_test");
+
+    engine.getOptions();
+
+    engine.getScene();
+
+    engine.getWindow();
+
+    engine.getInteractor();
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestImage.java f3d-3.4.1/java/testing/TestImage.java
--- f3d-3.2.0+dfsg/java/testing/TestImage.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestImage.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,61 @@
+import app.f3d.F3D.*;
+
+public class TestImage {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    String testDataPath = args.length > 0 ? args[0] : ".";
+    String tmpPath = args.length > 1 ? args[1] : "/tmp/";
+
+    Image.getSupportedFormats();
+
+    Image img1 = new Image(300, 200, 3);
+    img1.getWidth();
+    img1.getHeight();
+    img1.getChannelCount();
+    img1.getChannelType();
+    img1.getChannelTypeSize();
+
+    byte[] buffer = new byte[300 * 200 * 3];
+    img1.setContent(buffer);
+    img1.getContent();
+
+    img1.getNormalizedPixel(10, 10);
+
+    img1.save(tmpPath + "test.png");
+    img1.save(tmpPath + "test.jpg", Image.SaveFormat.JPG);
+
+    img1.saveBuffer();
+    img1.saveBuffer(Image.SaveFormat.PNG);
+
+    img1.setMetadata("key1", "value1");
+    img1.getMetadata("key1");
+    img1.allMetadata();
+
+    Image img2 = new Image(300, 200, 3, Image.ChannelType.BYTE);
+
+    img1.compare(img2);
+    img1.equals(img2);
+    img1.notEquals(img2);
+
+    Image img3 = new Image(testDataPath + "data/world.png");
+    img3.getWidth();
+    img3.toTerminalText();
+
+    img1.setContent(buffer)
+        .setMetadata("author", "F3D")
+        .save(tmpPath + "chained.png");
+
+    img1.delete();
+    img2.delete();
+    img3.delete();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestInteractor.java f3d-3.4.1/java/testing/TestInteractor.java
--- f3d-3.2.0+dfsg/java/testing/TestInteractor.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestInteractor.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,63 @@
+import app.f3d.F3D.*;
+
+public class TestInteractor {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine engine = Engine.create(true);
+    Interactor interactor = engine.getInteractor();
+
+    interactor.addCommand("test::hello", cmdArgs -> {
+      System.out.println("Hello command called with " + cmdArgs.size() + " args");
+    });
+
+    interactor.getCommandActions();
+    interactor.removeCommand("test::hello");
+
+    Interactor.InteractionBind bind = new Interactor.InteractionBind();
+    bind.mod = Interactor.ModifierKeys.CTRL;
+    bind.inter = "A";
+
+    bind.format();
+    Interactor.InteractionBind parsed = Interactor.InteractionBind.parse("Ctrl+A");
+    Interactor.InteractionBind parsed2 = Interactor.InteractionBind.parse("Shift+B");
+    Interactor.InteractionBind parsed3 = Interactor.InteractionBind.parse("C");
+
+    bind.equals(parsed);
+    parsed.compareTo(parsed2);
+    parsed2.hashCode();
+
+    interactor.toggleAnimation();
+    interactor.toggleAnimation(Interactor.AnimationDirection.FORWARD);
+    interactor.toggleAnimation(Interactor.AnimationDirection.BACKWARD);
+    interactor.startAnimation();
+    interactor.startAnimation(Interactor.AnimationDirection.FORWARD);
+    interactor.startAnimation(Interactor.AnimationDirection.BACKWARD);
+    interactor.stopAnimation();
+    interactor.isPlayingAnimation();
+    interactor.getAnimationDirection();
+
+    Interactor.AnimationDirection.FORWARD.getValue();
+    Interactor.AnimationDirection.BACKWARD.getValue();
+    Interactor.AnimationDirection.fromValue(0);
+    Interactor.AnimationDirection.fromValue(1);
+
+    interactor.enableCameraMovement();
+    interactor.disableCameraMovement();
+
+    interactor.requestRender();
+
+    interactor.toggleAnimation()
+             .enableCameraMovement();
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestJavaBindings.java f3d-3.4.1/java/testing/TestJavaBindings.java
--- f3d-3.2.0+dfsg/java/testing/TestJavaBindings.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/testing/TestJavaBindings.java	1970-01-01 02:00:00.000000000 +0200
@@ -1,35 +0,0 @@
-import app.f3d.F3D.*;
-
-public class TestJavaBindings {
-
-  static {
-    if (System.getProperty("os.name").startsWith("Windows"))
-    {
-      // On Windows, preload the OpenGL library
-      // This ensures the OpenGL used is the one in the working directory if any.
-      // In practice, it is used in F3D CI to run the test using Mesa, it's not required for production.
-      System.loadLibrary("opengl32");
-    }
-  }
-
-  public static void main(String[] args) {
-
-    Engine.autoloadPlugins();
-
-    // Always use try-with-resources idiom to ensure the native engine is released
-    try (Engine engine = new Engine()) {
-
-      Camera camera = engine.getWindow().getCamera();
-
-      camera.setPosition(new double[] { 0, 1, 2 });
-      double[] pos = camera.getPosition();
-
-      assert pos[0] == 0.0 : "Position X is not valid";
-      assert pos[1] == 1.0 : "Position Y is not valid";
-      assert pos[2] == 2.0 : "Position Z is not valid";
-
-      Scene scene = engine.getScene();
-      scene.add(args[0] + "data/cow.vtp");
-    }
-  }
-}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestLog.java f3d-3.4.1/java/testing/TestLog.java
--- f3d-3.2.0+dfsg/java/testing/TestLog.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestLog.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,71 @@
+import app.f3d.F3D.*;
+
+public class TestLog {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  static class TestCallback implements Log.ForwardCallback {
+    @Override
+    public void onLogMessage(Log.VerboseLevel level, String message) {
+    }
+  }
+
+  public static void main(String[] args) {
+    Log.VerboseLevel level = Log.getVerboseLevel();
+
+    Log.setVerboseLevel(Log.VerboseLevel.DEBUG);
+    Log.getVerboseLevel();
+
+    Log.setVerboseLevel(Log.VerboseLevel.QUIET);
+    Log.getVerboseLevel();
+
+    Log.setVerboseLevel(level);
+
+    Log.setUseColoring(true);
+    Log.setUseColoring(false);
+
+    Log.VerboseLevel savedLevel = Log.getVerboseLevel();
+    Log.setVerboseLevel(Log.VerboseLevel.QUIET);
+
+    Log.print(Log.VerboseLevel.DEBUG, "Test debug message");
+    Log.print(Log.VerboseLevel.INFO, "Test info message");
+    Log.print(Log.VerboseLevel.WARN, "Test warning message");
+    Log.print(Log.VerboseLevel.ERROR, "Test error message");
+
+    Log.debug("Test debug");
+    Log.info("Test info");
+    Log.warn("Test warning");
+    Log.error("Test error");
+
+    Log.setVerboseLevel(Log.VerboseLevel.INFO, true);
+    Log.setVerboseLevel(Log.VerboseLevel.INFO, false);
+
+    Log.setVerboseLevel(savedLevel);
+
+    TestCallback callback = new TestCallback();
+    Log.forward(callback);
+
+    Log.print(Log.VerboseLevel.WARN, "Test callback message");
+    Log.info("First message");
+    Log.warn("Second message");
+
+    Log.forward(null);
+
+    Log.VerboseLevel.DEBUG.getValue();
+    Log.VerboseLevel.INFO.getValue();
+    Log.VerboseLevel.WARN.getValue();
+    Log.VerboseLevel.ERROR.getValue();
+    Log.VerboseLevel.QUIET.getValue();
+
+    Log.VerboseLevel.fromValue(0);
+    Log.VerboseLevel.fromValue(1);
+    Log.VerboseLevel.fromValue(2);
+    Log.VerboseLevel.fromValue(3);
+    Log.VerboseLevel.fromValue(4);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestOptions.java f3d-3.4.1/java/testing/TestOptions.java
--- f3d-3.2.0+dfsg/java/testing/TestOptions.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestOptions.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,57 @@
+import app.f3d.F3D.*;
+
+public class TestOptions {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine engine = Engine.createNone();
+    Options options = engine.getOptions();
+
+    options.setAsBool("ui.scalar_bar", true);
+    options.getAsBool("ui.scalar_bar");
+    options.toggle("ui.scalar_bar");
+
+    options.setAsInt("scene.animation.index", 5);
+    options.getAsInt("scene.animation.index");
+
+    options.setAsDouble("render.line_width", 2.0);
+    options.getAsDouble("render.line_width");
+
+    options.setAsString("model.color.texture", "test.png");
+    options.getAsString("model.color.texture");
+
+    options.setAsDoubleVector("render.background.color", new double[]{1.0, 2.0, 3.0});
+    options.getAsDoubleVector("render.background.color");
+
+    options.setAsIntVector("scene.animation.indices", new int[]{1, 2});
+    options.getAsIntVector("scene.animation.indices");
+
+    options.setAsStringRepresentation("render.point_size", "5.0");
+    options.getAsStringRepresentation("render.point_size");
+
+    options.hasValue("ui.scalar_bar");
+    options.getNames();
+    options.getAllNames();
+    options.getClosestOption("ui.scalar");
+    options.isOptional("ui.scalar_bar");
+
+    Engine engine2 = Engine.create(true);
+    Options options2 = engine2.getOptions();
+    options2.copy(options, "ui.scalar_bar");
+    options.isSame(options2, "ui.scalar_bar");
+    engine2.close();
+
+    options.reset("render.line_width");
+    options.removeValue("model.color.texture");
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestScene.java f3d-3.4.1/java/testing/TestScene.java
--- f3d-3.2.0+dfsg/java/testing/TestScene.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestScene.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,73 @@
+import app.f3d.F3D.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestScene {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine engine = Engine.createNone();
+    Scene scene = engine.getScene();
+
+    scene.supports("test.obj");
+
+    scene.clear();
+
+    float[] points = new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.0f };
+    int[] faceSides = new int[] { 3 };
+    int[] faceIndices = new int[] { 0, 1, 2 };
+    Types.Mesh mesh = new Types.Mesh(points, new float[0], new float[0], faceSides, faceIndices);
+
+    scene.addMesh(mesh);
+
+    scene.loadAnimationTime(0.5);
+    scene.animationTimeRange();
+    scene.availableAnimations();
+
+    Types.LightState lightState = new Types.LightState();
+    lightState.type = Types.LightType.HEADLIGHT;
+    lightState.intensity = 1.0;
+    lightState.switchState = true;
+
+    int lightIdx = scene.addLight(lightState);
+
+    scene.getLightCount();
+
+    if (lightIdx >= 0)
+    {
+      scene.getLight(lightIdx);
+
+      Types.LightState updateLight = new Types.LightState();
+      updateLight.type = Types.LightType.HEADLIGHT;
+      updateLight.intensity = 2.0;
+      updateLight.switchState = true;
+
+      scene.updateLight(lightIdx, updateLight);
+
+      scene.removeLight(lightIdx);
+    }
+
+    Types.LightState light1 = new Types.LightState();
+    light1.type = Types.LightType.HEADLIGHT;
+    light1.intensity = 1.0;
+
+    Types.LightState light2 = new Types.LightState();
+    light2.type = Types.LightType.CAMERA_LIGHT;
+    light2.intensity = 1.0;
+
+    light1.equals(light2);
+
+    scene.removeAllLights();
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestTypes.java f3d-3.4.1/java/testing/TestTypes.java
--- f3d-3.2.0+dfsg/java/testing/TestTypes.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestTypes.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,63 @@
+import app.f3d.F3D.*;
+
+public class TestTypes {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Types.LightType lightType = Types.LightType.SCENE_LIGHT;
+    lightType.getValue();
+    Types.LightType.fromValue(1);
+    Types.LightType.fromValue(2);
+    Types.LightType.fromValue(3);
+
+    Types.LightState lightState = new Types.LightState();
+    Types.LightState customLight = new Types.LightState(
+      Types.LightType.CAMERA_LIGHT,
+      new double[] { 1.0, 2.0, 3.0 },
+      new double[] { 0.8, 0.8, 1.0 },
+      new double[] { 0.0, 0.0, -1.0 },
+      true,
+      0.5,
+      false
+    );
+    customLight.equals(lightState);
+
+    Types.Colormap colormap = new Types.Colormap();
+    Types.Colormap colormap2 = new Types.Colormap(new double[] { 0.0, 1.0, 0.0, 0.0 });
+
+    Transform2D transform = new Transform2D();
+    Transform2D transform2 = new Transform2D(new double[] { 1, 0, 0, 0, 1, 0, 0, 0, 1 });
+    Transform2D.create(new double[] { 1.0, 1.0 }, new double[] { 0.0, 0.0 }, 45.0);
+
+    Types.Mesh emptyMesh = new Types.Mesh();
+    emptyMesh.isValid();
+
+    float[] points = new float[] {
+      0.0f, 0.0f, 0.0f,
+      1.0f, 0.0f, 0.0f,
+      0.0f, 1.0f, 0.0f
+    };
+    int[] faceSides = new int[] { 3 };
+    int[] faceIndices = new int[] { 0, 1, 2 };
+
+    Types.Mesh triangleMesh = new Types.Mesh(points, new float[0], new float[0], faceSides, faceIndices);
+    triangleMesh.isValid();
+
+    float[] normals = new float[] {
+      0.0f, 0.0f, 1.0f,
+      0.0f, 0.0f, 1.0f,
+      0.0f, 0.0f, 1.0f
+    };
+    Types.Mesh meshWithNormals = new Types.Mesh(points, normals, new float[0], faceSides, faceIndices);
+    meshWithNormals.isValid();
+
+    Types.Mesh pointCloud = new Types.Mesh(points, new float[0], new float[0], new int[0], new int[0]);
+    pointCloud.isValid();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestUtils.java f3d-3.4.1/java/testing/TestUtils.java
--- f3d-3.2.0+dfsg/java/testing/TestUtils.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestUtils.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,44 @@
+import app.f3d.F3D.*;
+
+import java.util.List;
+
+public class TestUtils {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    int distance1 = Utils.textDistance("kitten", "sitting");
+    int distance2 = Utils.textDistance("same", "same");
+
+    List<String> tokens1 = Utils.tokenize("one two \"three four\" # comment");
+    List<String> tokens2 = Utils.tokenize("one two \"three four\" # comment", true);
+    List<String> tokens3 = Utils.tokenize("one two \"three four\" # comment", false);
+
+    String collapsed1 = Utils.collapsePath(".");
+    String collapsed2 = Utils.collapsePath(".", "");
+    String collapsed3 = Utils.collapsePath("test.txt", "/tmp");
+
+    String regex1 = Utils.globToRegex("*.txt");
+    String regex2 = Utils.globToRegex("*.txt", '/');
+    String regex3 = Utils.globToRegex("test/**/*.txt", '/');
+
+    String envPath = Utils.getEnv("PATH");
+    String envNonExistent = Utils.getEnv("F3D_NONEXISTENT_VAR_12345");
+
+    Utils.KnownFolder.ROAMINGAPPDATA.getValue();
+    Utils.KnownFolder.LOCALAPPDATA.getValue();
+    Utils.KnownFolder.PICTURES.getValue();
+    Utils.KnownFolder.fromValue(0);
+    Utils.KnownFolder.fromValue(1);
+    Utils.KnownFolder.fromValue(2);
+
+    String knownFolder1 = Utils.getKnownFolder(Utils.KnownFolder.ROAMINGAPPDATA);
+    String knownFolder2 = Utils.getKnownFolder(Utils.KnownFolder.LOCALAPPDATA);
+    String knownFolder3 = Utils.getKnownFolder(Utils.KnownFolder.PICTURES);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/testing/TestWindow.java f3d-3.4.1/java/testing/TestWindow.java
--- f3d-3.2.0+dfsg/java/testing/TestWindow.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/testing/TestWindow.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,57 @@
+import app.f3d.F3D.*;
+
+public class TestWindow {
+
+  static {
+    if (System.getProperty("os.name").startsWith("Windows"))
+    {
+      System.loadLibrary("opengl32");
+    }
+  }
+
+  public static void main(String[] args) {
+    Engine.autoloadPlugins();
+
+    Engine engine = Engine.create(true);
+    Window window = engine.getWindow();
+
+    window.getType();
+    window.isOffscreen();
+    window.getCamera();
+
+    window.render();
+
+    Image img = window.renderToImage(true);
+    img.getWidth();
+    img.getHeight();
+    img.getChannelCount();
+    img.getContent();
+    img.delete();
+
+    Image img2 = window.renderToImage();
+    img2.delete();
+
+    window.setSize(800, 600);
+    window.getWidth();
+    window.getHeight();
+
+    window.setPosition(100, 100);
+
+    byte[] icon = new byte[]{1, 2, 3, 4};
+    window.setIcon(icon);
+
+    window.setWindowName("F3D Test Window");
+
+    double[] worldPoint = new double[]{0.0, 0.0, 0.0};
+    window.getDisplayFromWorld(worldPoint);
+
+    double[] displayPoint = new double[]{100.0, 100.0, 0.0};
+    window.getWorldFromDisplay(displayPoint);
+
+    window.setSize(1024, 768)
+          .setPosition(50, 50)
+          .setWindowName("Chained");
+
+    engine.close();
+  }
+}
diff -Nru f3d-3.2.0+dfsg/java/Transform2D.java f3d-3.4.1/java/Transform2D.java
--- f3d-3.2.0+dfsg/java/Transform2D.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Transform2D.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,31 @@
+package app.f3d.F3D;
+
+public class Transform2D {
+    // Load the native library
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    public double[] data;
+
+    public Transform2D() {
+        data = new double[] { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
+    }
+
+    public Transform2D(double[] matrix) {
+        if (matrix.length != 9) {
+            throw new IllegalArgumentException("Matrix array must have exactly 9 elements");
+        }
+        this.data = matrix;
+    }
+
+    /**
+     * Create a 2D transform from scale, translate and angle.
+     *
+     * @param scale array of 2 doubles for x and y scale factors
+     * @param translate array of 2 doubles for x and y translation
+     * @param angleDeg rotation angle in degrees
+     * @return Transform2D object with computed matrix
+     */
+    public static native Transform2D create(double[] scale, double[] translate, double angleDeg);
+}
diff -Nru f3d-3.2.0+dfsg/java/Types.java f3d-3.4.1/java/Types.java
--- f3d-3.2.0+dfsg/java/Types.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Types.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,136 @@
+package app.f3d.F3D;
+
+public class Types {
+    // Load the native library
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    /**
+     * Describe a colormap, which is a vector of repeated: val,r,g,b
+     */
+    public static class Colormap {
+        public double[] data;
+
+        public Colormap() {
+            data = new double[0];
+        }
+
+        public Colormap(double[] data) {
+            this.data = data;
+        }
+    }
+
+    /**
+     * Light type enumeration.
+     */
+    public enum LightType {
+        HEADLIGHT(1),
+        CAMERA_LIGHT(2),
+        SCENE_LIGHT(3);
+
+        private final int value;
+
+        LightType(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public static LightType fromValue(int value) {
+            for (LightType type : values()) {
+                if (type.value == value) {
+                    return type;
+                }
+            }
+            throw new IllegalArgumentException("Unknown light type: " + value);
+        }
+    }
+
+    /**
+     * Structure describing the state of a light.
+     */
+    public static class LightState {
+        public LightType type = LightType.SCENE_LIGHT;
+        public double[] position = new double[] { 0.0, 0.0, 0.0 };
+        public double[] color = new double[] { 1.0, 1.0, 1.0 };
+        public double[] direction = new double[] { 1.0, 0.0, 0.0 };
+        public boolean positionalLight = false;
+        public double intensity = 1.0;
+        public boolean switchState = true;
+
+        public LightState() {
+        }
+
+        public LightState(LightType type, double[] position, double[] color,
+                         double[] direction, boolean positionalLight,
+                         double intensity, boolean switchState) {
+            this.type = type;
+            this.position = position;
+            this.color = color;
+            this.direction = direction;
+            this.positionalLight = positionalLight;
+            this.intensity = intensity;
+            this.switchState = switchState;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null || getClass() != obj.getClass()) return false;
+            LightState other = (LightState) obj;
+            return type == other.type &&
+                   java.util.Arrays.equals(position, other.position) &&
+                   java.util.Arrays.equals(color, other.color) &&
+                   java.util.Arrays.equals(direction, other.direction) &&
+                   positionalLight == other.positionalLight &&
+                   intensity == other.intensity &&
+                   switchState == other.switchState;
+        }
+    }
+
+    /**
+     * Describes a 3D surfacic mesh.
+     */
+    public static class Mesh {
+        public float[] points = new float[0];
+        public float[] normals = new float[0];
+        public float[] textureCoordinates = new float[0];
+        public int[] faceSides = new int[0];
+        public int[] faceIndices = new int[0];
+
+        public Mesh() {
+        }
+
+        public Mesh(float[] points, float[] normals, float[] textureCoordinates,
+                   int[] faceSides, int[] faceIndices) {
+            this.points = points != null ? points : new float[0];
+            this.normals = normals != null ? normals : new float[0];
+            this.textureCoordinates = textureCoordinates != null ? textureCoordinates : new float[0];
+            this.faceSides = faceSides != null ? faceSides : new int[0];
+            this.faceIndices = faceIndices != null ? faceIndices : new int[0];
+        }
+
+        /**
+         * Validates the mesh.
+         *
+         * @return ValidationResult containing whether mesh is valid and error message if invalid
+         */
+        public native ValidationResult isValid();
+
+        /**
+         * Result of mesh validation.
+         */
+        public static class ValidationResult {
+            public boolean valid;
+            public String errorMessage;
+
+            public ValidationResult(boolean valid, String errorMessage) {
+                this.valid = valid;
+                this.errorMessage = errorMessage;
+            }
+        }
+    }
+}
diff -Nru f3d-3.2.0+dfsg/java/Utils.java f3d-3.4.1/java/Utils.java
--- f3d-3.2.0+dfsg/java/Utils.java	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/java/Utils.java	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,124 @@
+package app.f3d.F3D;
+
+import java.util.List;
+
+public class Utils {
+
+    /**
+     * Enumeration of supported Windows known folders.
+     */
+    public enum KnownFolder {
+        ROAMINGAPPDATA(0),
+        LOCALAPPDATA(1),
+        PICTURES(2);
+
+        private final int value;
+
+        KnownFolder(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        public static KnownFolder fromValue(int value) {
+            for (KnownFolder folder : KnownFolder.values()) {
+                if (folder.value == value) {
+                    return folder;
+                }
+            }
+            throw new IllegalArgumentException("Invalid KnownFolder value: " + value);
+        }
+    }
+
+    // Load the native library
+    static {
+        System.loadLibrary("f3d-java");
+    }
+
+    private Utils() {
+    }
+
+    /**
+     * Compute the Levenshtein distance between two strings.
+     *
+     * @param strA first string
+     * @param strB second string
+     * @return Levenshtein distance between the two strings
+     */
+    public static native int textDistance(String strA, String strB);
+
+    /**
+     * Tokenize a string using the same logic as bash.
+     *
+     * @param str input string to tokenize
+     * @param keepComments if true, keep comments with '#'
+     * @return list of tokens
+     */
+    public static native List<String> tokenize(String str, boolean keepComments);
+
+    /**
+     * Tokenize a string with keepComments enabled.
+     *
+     * @param str input string to tokenize
+     * @return list of tokens
+     */
+    public static List<String> tokenize(String str) {
+        return tokenize(str, true);
+    }
+
+    /**
+     * Collapse a filesystem path.
+     *
+     * @param path input path
+     * @param baseDirectory base directory for relative paths
+     * @return collapsed absolute path string
+     */
+    public static native String collapsePath(String path, String baseDirectory);
+
+    /**
+     * Collapse a filesystem path using current directory as base.
+     *
+     * @param path input path
+     * @return collapsed absolute path string
+     */
+    public static String collapsePath(String path) {
+        return collapsePath(path, "");
+    }
+
+    /**
+     * Converts a glob expression to a regular expression.
+     *
+     * @param glob glob expression
+     * @param pathSeparator path separator character
+     * @return regular expression string
+     */
+    public static native String globToRegex(String glob, char pathSeparator);
+
+    /**
+     * Converts a glob expression to a regular expression with default separator '/'.
+     *
+     * @param glob glob expression
+     * @return regular expression string
+     */
+    public static String globToRegex(String glob) {
+        return globToRegex(glob, '/');
+    }
+
+    /**
+     * Get the value of an environment variable.
+     *
+     * @param env environment variable name
+     * @return value of the environment variable, or null if not set
+     */
+    public static native String getEnv(String env);
+
+    /**
+     * Get a Windows known folder.
+     *
+     * @param knownFolder known folder identifier
+     * @return folder path, or null on non-Windows platforms or error
+     */
+    public static native String getKnownFolder(KnownFolder knownFolder);
+}
diff -Nru f3d-3.2.0+dfsg/java/Window.java f3d-3.4.1/java/Window.java
--- f3d-3.2.0+dfsg/java/Window.java	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/java/Window.java	2026-01-08 08:41:55.000000000 +0200
@@ -2,23 +2,136 @@
 
 public class Window {
 
-    public Window(long nativeAddress) {
+    public enum Type {
+        NONE,
+        EXTERNAL,
+        GLX,
+        WGL,
+        COCOA,
+        EGL,
+        OSMESA,
+        WASM,
+        UNKNOWN
+    }
+
+    Window(long nativeAddress) {
         mNativeAddress = nativeAddress;
         mCamera = new Camera(nativeAddress);
     }
 
-    public Camera getCamera() { return mCamera; }
+    /**
+     * Get the type of the window.
+     *
+     * @return window type
+     */
+    public native Type getType();
+
+    /**
+     * Check if the window is offscreen.
+     *
+     * @return true if offscreen, false otherwise
+     */
+    public native boolean isOffscreen();
+
+    /**
+     * Get the camera provided by the window.
+     *
+     * @return camera instance
+     */
+    public Camera getCamera() {
+        return mCamera;
+    }
 
-    public native void render();
-    public native void setSize(int width, int height);
+    /**
+     * Perform a render of the window to the screen.
+     * All dynamic options are updated if needed.
+     *
+     * @return true on success, false otherwise
+     */
+    public native boolean render();
+
+    /**
+     * Perform a render of the window to the screen and save the result in an image.
+     * Set noBackground to true to have a transparent background.
+     *
+     * @param noBackground if true, background will be transparent
+     * @return resulting image
+     */
+    public native Image renderToImage(boolean noBackground);
+
+    /**
+     * Perform a render of the window to the screen and save the result in an image.
+     *
+     * @return resulting image
+     */
+    public Image renderToImage() {
+        return renderToImage(false);
+    }
 
+    /**
+     * Set the size of the window.
+     *
+     * @param width window width
+     * @param height window height
+     * @return this window for method chaining
+     */
+    public native Window setSize(int width, int height);
+
+    /**
+     * Get the width of the window.
+     *
+     * @return window width
+     */
     public native int getWidth();
+
+    /**
+     * Get the height of the window.
+     *
+     * @return window height
+     */
     public native int getHeight();
 
-    public native double[] getDisplayFromWorld(double[] pt);
-    public native double[] getWorldFromDisplay(double[] pt);
+    /**
+     * Set the position of the window.
+     *
+     * @param x x position
+     * @param y y position
+     * @return this window for method chaining
+     */
+    public native Window setPosition(int x, int y);
+
+    /**
+     * Set the icon to be shown by a window manager.
+     *
+     * @param icon icon data as byte array
+     * @return this window for method chaining
+     */
+    public native Window setIcon(byte[] icon);
+
+    /**
+     * Set the window name to be shown by a window manager.
+     *
+     * @param windowName window name
+     * @return this window for method chaining
+     */
+    public native Window setWindowName(String windowName);
+
+    /**
+     * Convert a point in display coordinate to world coordinate.
+     *
+     * @param displayPoint array of 3 doubles [x, y, z] in display coordinates
+     * @return array of 3 doubles [x, y, z] in world coordinates
+     */
+    public native double[] getWorldFromDisplay(double[] displayPoint);
+
+    /**
+     * Convert a point in world coordinate to display coordinate.
+     *
+     * @param worldPoint array of 3 doubles [x, y, z] in world coordinates
+     * @return array of 3 doubles [x, y, z] in display coordinates
+     */
+    public native double[] getDisplayFromWorld(double[] worldPoint);
 
     private long mNativeAddress;
-
     private Camera mCamera;
 }
diff -Nru f3d-3.2.0+dfsg/library/CMakeLists.txt f3d-3.4.1/library/CMakeLists.txt
--- f3d-3.2.0+dfsg/library/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,15 @@
 )
 
 # Check modules
+
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250513)
+  if(TARGET VTK::RenderingGridAxes)
+    set(F3D_VTK_MODULES ${F3D_VTK_MODULES} VTK::RenderingGridAxes)
+  else()
+    message(FATAL_ERROR "VTK::RenderingGridAxes is not found, please enable it")
+  endif()
+endif()
+
 if(F3D_MODULE_RAYTRACING)
   if(TARGET VTK::RenderingRayTracing)
     message(STATUS "VTK::RenderingRayTracing module found")
@@ -177,6 +186,11 @@
   target_compile_definitions(libf3d PRIVATE F3D_MODULE_EXR)
 endif ()
 
+# webp
+if (F3D_MODULE_WEBP)
+  target_compile_definitions(libf3d PRIVATE F3D_MODULE_WEBP)
+endif ()
+
 # ui
 if (F3D_MODULE_UI)
   target_compile_definitions(libf3d PRIVATE F3D_MODULE_UI)
diff -Nru f3d-3.2.0+dfsg/library/doxygen/conf.py f3d-3.4.1/library/doxygen/conf.py
--- f3d-3.2.0+dfsg/library/doxygen/conf.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/doxygen/conf.py	1970-01-01 02:00:00.000000000 +0200
@@ -1,80 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Path setup --------------------------------------------------------------
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
-
-
-# -- Project information -----------------------------------------------------
-
-project = "libf3d"
-copyright = "2025, Michael Migliore and Mathieu Westphal"
-author = "Michael Migliore and Mathieu Westphal"
-
-# The full version, including alpha/beta/rc tags
-release = "3.2.0"
-
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = ["breathe", "exhale"]
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
-
-
-# -- Options for HTML output -------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = "sphinx_book_theme"
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
-
-# Setup the breathe extension
-breathe_projects = {"libf3d": "./_doxygen/xml"}
-breathe_default_project = "libf3d"
-
-# Setup the exhale extension
-exhale_args = {
-    # These arguments are required
-    "containmentFolder": "./api",
-    "rootFileName": "library_root.rst",
-    "doxygenStripFromPath": "..",
-    # Heavily encouraged optional argument (see docs)
-    "rootFileTitle": "Library API",
-    # Suggested optional arguments
-    "createTreeView": True,
-    # TIP: if using the sphinx-bootstrap-theme, you need
-    # "treeViewIsBootstrap": True,
-    "exhaleExecutesDoxygen": True,
-    "exhaleDoxygenStdin": "INPUT = ../public\nDISTRIBUTE_GROUP_DOC = YES\nEXCLUDE_SYMBOLS = std",
-}
-
-# Tell sphinx what the primary language being documented is.
-primary_domain = "cpp"
-
-# Tell sphinx what the pygments highlight language should be.
-highlight_language = "cpp"
diff -Nru f3d-3.2.0+dfsg/library/doxygen/index.rst f3d-3.4.1/library/doxygen/index.rst
--- f3d-3.2.0+dfsg/library/doxygen/index.rst	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/doxygen/index.rst	1970-01-01 02:00:00.000000000 +0200
@@ -1,26 +0,0 @@
-.. libf3d documentation master file, created by
-   sphinx-quickstart on Sat Nov 19 12:29:28 2022.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-libf3d doxygen documentation
-============================
-
-This is the automatically generated doxygen documentation for the libf3d_,
-which is part of F3D_.
-
-.. _libf3d: https://f3d.app/doc/libf3d/OVERVIEW.html
-.. _F3D: https://f3d.app
-
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Contents:
-
-   api/library_root
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`search`
diff -Nru f3d-3.2.0+dfsg/library/options.json f3d-3.4.1/library/options.json
--- f3d-3.2.0+dfsg/library/options.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/options.json	2026-01-08 08:41:55.000000000 +0200
@@ -69,6 +69,12 @@
         "default_value": "0.0, 0.0, 0.0"
       }
     },
+    "axes_grid": {
+      "enable": {
+        "type": "bool",
+        "default_value": "false"
+      }
+    },
     "raytracing": {
       "enable": {
         "type": "bool",
@@ -86,7 +92,18 @@
     "effect": {
       "translucency_support": {
         "type": "bool",
-        "default_value": "false"
+        "default_value": "false",
+        "deprecated": "use render.effect.blending.enable instead"
+      },
+      "blending": {
+        "enable": {
+          "type": "bool",
+          "default_value": "false"
+        },
+        "mode": {
+          "type": "string",
+          "default_value": "ddp"
+        }
       },
       "anti_aliasing": {
         "type": "bool",
@@ -158,10 +175,28 @@
     }
   },
   "ui": {
+    "backdrop": {
+      "opacity": {
+        "type": "double",
+        "default_value": "0.9"
+      }
+    },
     "axis": {
       "type": "bool",
       "default_value": "false"
     },
+    "x_color": {
+      "type": "color",
+      "default_value": "0.9764705882352941,0.2627450980392157,0.023529411764705882"
+    },
+    "y_color": {
+      "type": "color",
+      "default_value": "0.47843137254901963,1.0,0.47843137254901963"
+    },
+    "z_color": {
+      "type": "color",
+      "default_value": "0.47058823529411764,0.5450980392156862,1.0"
+    },
     "scalar_bar": {
       "type": "bool",
       "default_value": "false"
@@ -192,16 +227,41 @@
     },
     "dropzone": {
       "type": "bool",
-      "default_value": "false"
+      "default_value": "false",
+      "deprecated": "use ui.drop_zone.enable instead"
     },
     "dropzone_info": {
       "type": "string",
-      "default_value": ""
+      "default_value": "",
+      "deprecated": "use ui.drop_zone.custom_binds instead"
+    },
+    "drop_zone": {
+      "enable": {
+        "type": "bool",
+        "default_value": "false"
+      },
+      "info": {
+        "type": "string",
+        "default_value": "",
+        "deprecated": "use ui.drop_zone.custom_binds instead"
+      },
+      "show_logo": {
+        "type": "bool",
+        "default_value": "false"
+      },
+      "custom_binds": {
+        "type": "string",
+        "default_value": ""
+      }
     },
     "metadata": {
       "type": "bool",
       "default_value": "false"
     },
+    "hdri_filename": {
+      "type": "bool",
+      "default_value": "false"
+    },
     "font_file": {
       "type": "path"
     },
@@ -213,9 +273,15 @@
       "type": "bool",
       "default_value": "false"
     },
+    "loader_progress_color": {
+      "type": "color"
+    },
     "animation_progress": {
       "type": "bool",
       "default_value": "false"
+    },
+    "animation_progress_color": {
+      "type": "color"
     }
   },
   "model": {
@@ -307,6 +373,10 @@
       "size": {
         "type": "double",
         "default_value": 10.0
+      },
+      "absolute_size": {
+        "type": "bool",
+        "default_value": "false"
       }
     },
     "volume": {
diff -Nru f3d-3.2.0+dfsg/library/private/animationManager.h f3d-3.4.1/library/private/animationManager.h
--- f3d-3.2.0+dfsg/library/private/animationManager.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/animationManager.h	2026-01-08 08:41:55.000000000 +0200
@@ -15,7 +15,7 @@
 #include <set>
 
 class vtkF3DRenderer;
-class vtkImporter;
+class vtkF3DMetaImporter;
 class vtkRenderWindow;
 
 namespace f3d
@@ -41,7 +41,15 @@
   /**
    * Set the importer to use in the animation_manager, must be set before initializing
    */
-  void SetImporter(vtkImporter* importer);
+  void SetImporter(vtkF3DMetaImporter* importer);
+
+  /**
+   * Set animation direction,
+   * Only following values are correct :
+   * 1 for forward animation
+   * -1 for backward animation
+   */
+  void SetAnimationDirection(int direction);
 
   /**
    * Initialize the animation manager, required before playing the animation.
@@ -54,6 +62,7 @@
 
   /**
    * Start/Stop playing the animation
+   * Direction must always be equal to 1 (forward) or -1 (backward)
    */
   void ToggleAnimation();
   void StartAnimation();
@@ -72,6 +81,16 @@
   std::string GetAnimationName();
 
   /**
+   * Return animation direction
+   * 1 for forward animation
+   * -1 for backward animation
+   */
+  int GetAnimationDirection() const
+  {
+    return AnimationDirection;
+  }
+
+  /**
    * Return true if the animation manager is playing the animation
    */
   bool IsPlaying() const
@@ -80,12 +99,12 @@
   }
 
   /**
-   * Set the animation in delta time in seconds
+   *Set the animation in delta time in seconds
    */
   void SetDeltaTime(double deltaTime);
 
   /**
-   * Advance animationTime of DeltaTime and call loadAtTime accordingly
+   * Advance animationTime of deltaTime and call loadAtTime accordingly
    * Do nothing if IsPlaying is false
    */
   void Tick();
@@ -96,6 +115,12 @@
   bool LoadAtTime(double timeValue);
 
   /**
+   * Load animation at provided frmae value
+   * When relative is false frame -1 is equal to last frame
+   */
+  void JumpToFrame(int frame, bool relative);
+
+  /**
    * Return a pair containing the current time range values
    */
   std::pair<double, double> GetTimeRange();
@@ -105,6 +130,11 @@
    */
   unsigned int GetNumberOfAvailableAnimations() const;
 
+  /**
+   * Update the dynamic options value to trigger cheatsheet update if needed.
+   */
+  void UpdateDynamicOptions();
+
   animationManager(animationManager const&) = delete;
   void operator=(animationManager const&) = delete;
 
@@ -115,12 +145,28 @@
    */
   void PrepareForAnimationIndices();
 
+  /**
+   * Internal setter for Autoplay.
+   */
+  void SetAutoplay(bool enable);
+
+  /**
+   * Internal setter for SpeedFactor.
+   */
+  void SetSpeedFactor(double speedFactor);
+
+  /**
+   * Helper method to call the homonymous method from vtkF3DRenderer.
+   */
+  void SetCheatSheetConfigured(bool configured);
+
   options& Options;
   window_impl& Window;
-  vtkImporter* Importer = nullptr;
+  vtkF3DMetaImporter* Importer = nullptr;
   interactor_impl* Interactor = nullptr;
 
   int AvailAnimations = 0;
+  int AnimationDirection = 1;
 
   std::optional<std::vector<int>> PreparedAnimationIndices;
   double TimeRange[2] = { 0.0, 0.0 };
@@ -129,6 +175,10 @@
   double DeltaTime = 0;
   bool CurrentTimeSet = false;
 
+  // Dynamic options
+  bool Autoplay = false;
+  double SpeedFactor = 1.0;
+
   vtkSmartPointer<vtkProgressBarWidget> ProgressWidget;
 };
 }
diff -Nru f3d-3.2.0+dfsg/library/private/camera_impl.h f3d-3.4.1/library/private/camera_impl.h
--- f3d-3.2.0+dfsg/library/private/camera_impl.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/camera_impl.h	2026-01-08 08:41:55.000000000 +0200
@@ -36,20 +36,20 @@
    * Documented public API
    */
   camera& setPosition(const point3_t& pos) override;
-  point3_t getPosition() override;
-  void getPosition(point3_t& pos) override;
+  point3_t getPosition() const override;
+  void getPosition(point3_t& pos) const override;
   camera& setFocalPoint(const point3_t& foc) override;
-  point3_t getFocalPoint() override;
-  void getFocalPoint(point3_t& foc) override;
+  point3_t getFocalPoint() const override;
+  void getFocalPoint(point3_t& foc) const override;
   camera& setViewUp(const vector3_t& up) override;
-  vector3_t getViewUp() override;
-  void getViewUp(vector3_t& up) override;
+  vector3_t getViewUp() const override;
+  void getViewUp(vector3_t& up) const override;
   camera& setViewAngle(const angle_deg_t& angle) override;
-  angle_deg_t getViewAngle() override;
-  void getViewAngle(angle_deg_t& angle) override;
+  angle_deg_t getViewAngle() const override;
+  void getViewAngle(angle_deg_t& angle) const override;
   camera& setState(const camera_state_t& state) override;
-  camera_state_t getState() override;
-  void getState(camera_state_t& state) override;
+  camera_state_t getState() const override;
+  void getState(camera_state_t& state) const override;
 
   camera& dolly(double val) override;
   camera& pan(double right, double up, double forward) override;
@@ -77,7 +77,12 @@
    * This should only be called after the renderer have been set and initialized
    * so that a camera is available.
    */
-  vtkCamera* GetVTKCamera();
+  vtkCamera* GetVTKCamera() const;
+
+  /**
+   * Check if camera was successfully reset with resetToBounds.
+   */
+  bool GetSuccessfullyReset() const;
 
 private:
   class internals;
diff -Nru f3d-3.2.0+dfsg/library/private/interactor_impl.h f3d-3.4.1/library/private/interactor_impl.h
--- f3d-3.2.0+dfsg/library/private/interactor_impl.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/interactor_impl.h	2026-01-08 08:41:55.000000000 +0200
@@ -36,30 +36,43 @@
   ~interactor_impl() override;
 
   interactor& initCommands() override;
-  interactor& addCommand(
-    std::string action, std::function<void(const std::vector<std::string>&)> callback) override;
+  interactor& addCommand(const std::string& action,
+    std::function<void(const std::vector<std::string>&)> callback,
+    std::optional<command_documentation_t> doc = std::nullopt,
+    std::function<std::vector<std::string>(const std::vector<std::string>&)> completionCallback =
+      nullptr) override;
   interactor& removeCommand(const std::string& action) override;
   std::vector<std::string> getCommandActions() const override;
-  bool triggerCommand(std::string_view command) override;
+  bool triggerCommand(std::string_view command, bool keepComments = true) override;
 
   interactor& initBindings() override;
   interactor& addBinding(const interaction_bind_t& bind, std::vector<std::string> commands,
-    std::string group = std::string(),
-    documentation_callback_t documentationCallback = nullptr) override;
+    std::string group = std::string(), documentation_callback_t documentationCallback = nullptr,
+    BindingType type = BindingType::OTHER) override;
   interactor& addBinding(const interaction_bind_t& bind, std::string command,
-    std::string group = std::string(),
-    documentation_callback_t documentationCallback = nullptr) override;
+    std::string group = std::string(), documentation_callback_t documentationCallback = nullptr,
+    BindingType type = BindingType::OTHER) override;
   interactor& removeBinding(const interaction_bind_t& bind) override;
   std::vector<std::string> getBindGroups() const override;
   std::vector<interaction_bind_t> getBindsForGroup(std::string group) const override;
   std::vector<interaction_bind_t> getBinds() const override;
   std::pair<std::string, std::string> getBindingDocumentation(
     const interaction_bind_t& bind) const override;
+  BindingType getBindingType(const interaction_bind_t& bind) const override;
 
-  interactor& toggleAnimation() override;
-  interactor& startAnimation() override;
+  interactor& triggerEventLoop(double deltaTime) override;
+  interactor& triggerModUpdate(InputModifier mod) override;
+  interactor& triggerMouseButton(InputAction action, MouseButton button) override;
+  interactor& triggerMousePosition(double xpos, double ypos) override;
+  interactor& triggerMouseWheel(WheelDirection direction) override;
+  interactor& triggerKeyboardKey(InputAction action, std::string_view keySym) override;
+  interactor& triggerTextCharacter(unsigned int codepoint) override;
+
+  interactor& toggleAnimation(AnimationDirection direction = AnimationDirection::FORWARD) override;
+  interactor& startAnimation(AnimationDirection direction = AnimationDirection::FORWARD) override;
   interactor& stopAnimation() override;
   bool isPlayingAnimation() override;
+  interactor::AnimationDirection getAnimationDirection() override;
 
   interactor& enableCameraMovement() override;
   interactor& disableCameraMovement() override;
@@ -71,6 +84,7 @@
   interactor& start(double deltaTime, std::function<void()> userCallBack) override;
   interactor& stop() override;
   interactor& requestRender() override;
+  interactor& requestStop() override;
   ///@}
 
   /**
diff -Nru f3d-3.2.0+dfsg/library/private/options_tools.h f3d-3.4.1/library/private/options_tools.h
--- f3d-3.2.0+dfsg/library/private/options_tools.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/options_tools.h	2026-01-08 08:41:55.000000000 +0200
@@ -493,15 +493,208 @@
 //----------------------------------------------------------------------------
 /**
  *  Parse provided string into a transform2d_t
- *  Supported format: double, double, double, ... as a sequence of 9 values
+ *  Supported formats:
+ *  - "double, double, double, ..." as a sequence of 9 values
+ *  - "scale,translation,angle" in any order, default values used if one or two options are omitted
+ *    "scale: double", "scale: double,double", "translation: double, double", "angle: double"
  *  Can throw options::parsing_exception in case of failure to parse
  */
 template<>
 transform2d_t parse(const std::string& str)
 {
-  std::vector<double> input;
-  input = parse<std::vector<double>>(str);
-  return transform2d_t(input);
+  const std::string strCompact = std::regex_replace(str, std::regex("\\s"), "");
+
+  if (strCompact[0] >= '0' && strCompact[0] <= '9')
+  {
+    std::vector<double> input;
+    // since the input starts with a numeric char, we try to read as a double vector
+    try
+    {
+      input = parse<std::vector<double>>(strCompact);
+    }
+    catch (const options::parsing_exception&)
+    {
+      throw options::parsing_exception("Cannot convert input into a double vector: " + str);
+    }
+
+    if (input.size() > 9)
+    {
+      throw options::parsing_exception(
+        "Input vector too large to read as transform2d, size 9 required: " + str);
+    }
+    else if (input.size() < 9)
+    {
+      throw options::parsing_exception(
+        "Input vector too small to read as transform2d, size 9 required: " + str);
+    }
+
+    return transform2d_t(input);
+  }
+
+  // try to read with scale/translate/angle notation
+  //  read as a scale/translation/angle series
+  const std::regex settingCheck(
+    "^((scale\\:)|(translation\\:)|(angle\\:)).*", std::regex_constants::icase);
+  const std::regex keywordCheck("^((scale)|(translation)|(angle))", std::regex_constants::icase);
+  std::smatch matcher;
+  std::istringstream split(strCompact);
+  std::string each;
+  bool setScale = false;
+  std::vector<double> scaleVec;
+  std::vector<double> translationVec;
+  bool angleNext = false;
+  std::vector<angle_deg_t> angleVec;
+  bool hasScale = false;
+  bool hasTranslation = false;
+  bool hasAngle = false;
+
+  // split order for parsing: (example "scale:0.1;translation:0.51,2.1;angle:60.0")
+  //  - split substrings by ';' ("scale:0.1" "translation:0.51,2.1" "angle:60.0")
+  //  - split each substring by ':' (["scale" "0.1"]["translation" "0.51,2.1"]["angle" "60.0"])
+  //  - read remaining substrings as keyword or series of double values split by ','
+
+  while (std::getline(split, each, ';'))
+  {
+    if (std::regex_match(each, matcher, settingCheck))
+    {
+      std::istringstream subSplit(each);
+      std::string subStr;
+      while (std::getline(subSplit, subStr, ':'))
+      {
+        if (std::regex_match(subStr, matcher, keywordCheck))
+        {
+          // read and apply keyword
+          if (subStr == "scale")
+          {
+            if (hasScale)
+            {
+              throw options::parsing_exception(
+                "Input cannot have multiple scale transforms: " + str);
+            }
+            setScale = true;
+            hasScale = true;
+          }
+          else if (subStr == "translation")
+          {
+            if (hasTranslation)
+            {
+              throw options::parsing_exception(
+                "Input cannot have multiple translation transforms: " + str);
+            }
+            setScale = false;
+            hasTranslation = true;
+          }
+          else if (subStr == "angle")
+          {
+            if (hasAngle)
+            {
+              throw options::parsing_exception(
+                "Input cannot have multiple angle transforms: " + str);
+            }
+            angleNext = true;
+            hasAngle = true;
+          }
+        }
+        else
+        {
+          // read as series of double values
+          std::istringstream dblSeries(subStr);
+          std::string dblStr;
+          while (std::getline(dblSeries, dblStr, ','))
+          {
+            double val;
+            try
+            {
+              val = parse<double>(dblStr);
+            }
+            catch (const options::parsing_exception&)
+            {
+              throw options::parsing_exception(
+                "Cannot parse input substring " + subStr + " as double series from input: " + str);
+            }
+
+            if (angleNext)
+            {
+              angleVec.emplace_back(val);
+            }
+            else if (setScale)
+            {
+              scaleVec.emplace_back(val);
+            }
+            else
+            {
+              translationVec.emplace_back(val);
+            }
+          }
+        }
+      }
+    }
+    else
+    {
+      // option passed without scale/translation/angle keyword
+      throw options::parsing_exception(
+        "Cannot parse input substring " + each + " as option from input: " + str);
+    }
+  }
+
+  // input completed, validate vectors and apply
+  if (scaleVec.size() > 2)
+  {
+    throw options::parsing_exception(
+      "Too many scale values (" + std::to_string(scaleVec.size()) + ") in input: " + str);
+  }
+  else if (hasScale && scaleVec.size() == 0)
+  {
+    throw options::parsing_exception("Scale called without value in input: " + str);
+  }
+
+  if (translationVec.size() == 1)
+  {
+    throw options::parsing_exception("Translation requires two values, single translation value " +
+      std::to_string(translationVec[0]) + " in input: " + str);
+  }
+  else if (translationVec.size() > 2)
+  {
+    throw options::parsing_exception("Too many translation values (" +
+      std::to_string(translationVec.size()) + ") in input: " + str);
+  }
+  else if (hasTranslation && translationVec.size() == 0)
+  {
+    throw options::parsing_exception("Translation called without value in input: " + str);
+  }
+
+  if (angleVec.size() > 1)
+  {
+    throw options::parsing_exception(
+      "Multiple angle values (" + std::to_string(angleVec.size()) + ") in input: " + str);
+  }
+  else if (hasAngle && angleVec.size() == 0)
+  {
+    throw options::parsing_exception("Angle called without value in input: " + str);
+  }
+
+  // Clean up vectors to fit with constructor
+  if (scaleVec.size() == 0)
+  {
+    scaleVec = { 1, 1 };
+  }
+  else if (scaleVec.size() == 1)
+  {
+    scaleVec.emplace_back(scaleVec.front());
+  }
+  if (translationVec.size() == 0)
+  {
+    translationVec = { 0, 0 };
+  }
+  if (angleVec.size() == 0)
+  {
+    angleVec.emplace_back(0);
+  }
+
+  double_array_t<2> scaleVecOut = double_array_t<2>(scaleVec);
+  double_array_t<2> translationVecOut = double_array_t<2>(translationVec);
+
+  return transform2d_t(scaleVecOut, translationVecOut, angleVec[0]);
 }
 
 //----------------------------------------------------------------------------
@@ -589,7 +782,7 @@
  * Formats in hex color string "#RRGGBB" if values are multiple of 1/255.
  * Otherwise rely on format(std::vector<double>&)
  */
-std::string format(color_t var)
+std::string format(const color_t& var)
 {
   const std::vector<double> colors = { var.r(), var.g(), var.b() };
   if (std::all_of(colors.begin(), colors.end(),
@@ -617,7 +810,7 @@
  * Format provided var into a string from provided direction_t.
  * Format as `+X`/`+X-Y`/... if possible, otherwise rely on `format(std::vector<double>&)`
  */
-std::string format(direction_t var)
+std::string format(const direction_t& var)
 {
   const auto isZero = [](double a) { return std::abs(a) < 1e-12; };
   const auto absDiff = [](double a, double b) { return std::abs(a) - std::abs(b); };
@@ -686,18 +879,11 @@
 //----------------------------------------------------------------------------
 /**
  * Format provided var into a string from provided transform2d_t.
- * Rely on `format(double)` for each item in value array`
+ * Rely on format(std::vector<double>&)
  */
 std::string format(const transform2d_t& var)
 {
-  std::ostringstream stream;
-  std::vector<double> vec(var);
-  size_t size = vec.size();
-  for (unsigned int i = 0; i < size; i++)
-  {
-    stream << ((i > 0) ? "," : "") << options_tools::format(vec[i]);
-  }
-  return stream.str();
+  return options_tools::format(static_cast<std::vector<double>>(var));
 }
 
 } // option_tools
diff -Nru f3d-3.2.0+dfsg/library/private/scene_impl.h f3d-3.4.1/library/private/scene_impl.h
--- f3d-3.2.0+dfsg/library/private/scene_impl.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/scene_impl.h	2026-01-08 08:41:55.000000000 +0200
@@ -36,6 +36,12 @@
   scene& add(const std::vector<std::string>& filePathStrings) override;
   scene& add(const mesh_t& mesh) override;
   scene& clear() override;
+  int addLight(const light_state_t& lightState) const override;
+  int getLightCount() const override;
+  light_state_t getLight(int index) const override;
+  scene& updateLight(int index, const light_state_t& lightState) override;
+  scene& removeLight(int index) override;
+  scene& removeAllLights() override;
   bool supports(const std::filesystem::path& filePath) override;
   scene& loadAnimationTime(double timeValue) override;
   std::pair<double, double> animationTimeRange() override;
diff -Nru f3d-3.2.0+dfsg/library/private/window_impl.h f3d-3.4.1/library/private/window_impl.h
--- f3d-3.2.0+dfsg/library/private/window_impl.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/private/window_impl.h	2026-01-08 08:41:55.000000000 +0200
@@ -21,6 +21,7 @@
 
 class vtkRenderWindow;
 class vtkF3DMetaImporter;
+class vtkF3DRenderer;
 namespace f3d
 {
 class options;
@@ -68,9 +69,9 @@
 
   /**
    * Implementation only API.
-   * Initialize the up vector on the renderer using the Up string option
+   * Initialize the camera to a default position and set the up direction.
    */
-  void InitializeUpVector();
+  void InitializeUpDirection();
 
   /**
    * Implementation only API.
@@ -123,6 +124,12 @@
    */
   void RenderUIOnly();
 
+  /**
+   * Implementation only API.
+   * Get the internal renderer
+   */
+  [[nodiscard]] vtkF3DRenderer* GetRenderer() const;
+
 private:
   class internals;
   std::unique_ptr<internals> Internals;
diff -Nru f3d-3.2.0+dfsg/library/public/camera.h f3d-3.4.1/library/public/camera.h
--- f3d-3.2.0+dfsg/library/public/camera.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/camera.h	2026-01-08 08:41:55.000000000 +0200
@@ -4,11 +4,17 @@
 #include "export.h"
 #include "types.h"
 
+/// @cond
 #include <array>
 #include <string>
+/// @endcond
 
 namespace f3d
 {
+/**
+ * @struct camera_state_t
+ * @brief a struct containing all information to configure a camera
+ */
 struct F3D_EXPORT camera_state_t
 {
   point3_t position = { 0., 0., 1. };
@@ -30,25 +36,39 @@
 {
 public:
   ///@{ @name Parameters
-  /**
-   * Set/Get the camera parameters.
-   * Angles are in degrees.
-   */
+  /// Set/Get the camera parameters.
+
+  /** Set the position of the camera to the provided arg */
   virtual camera& setPosition(const point3_t& pos) = 0;
-  [[nodiscard]] virtual point3_t getPosition() = 0;
-  virtual void getPosition(point3_t& pos) = 0;
+  /** Return the position of the camera */
+  [[nodiscard]] virtual point3_t getPosition() const = 0;
+  /** Get the position of the camera into the provided arg */
+  virtual void getPosition(point3_t& pos) const = 0;
+  /** Set the focal point of the camera to the provided arg */
   virtual camera& setFocalPoint(const point3_t& foc) = 0;
-  [[nodiscard]] virtual point3_t getFocalPoint() = 0;
-  virtual void getFocalPoint(point3_t& foc) = 0;
+  /** Return the focal point of the camera */
+  [[nodiscard]] virtual point3_t getFocalPoint() const = 0;
+  /** Get the focal point of the camera into the provided arg */
+  virtual void getFocalPoint(point3_t& foc) const = 0;
+  /** Set the view up of the camera to the provided arg */
   virtual camera& setViewUp(const vector3_t& up) = 0;
-  [[nodiscard]] virtual vector3_t getViewUp() = 0;
-  virtual void getViewUp(vector3_t& up) = 0;
+  /** Return the view up of the camera */
+  [[nodiscard]] virtual vector3_t getViewUp() const = 0;
+  /** Get the view up of the camera into the provided arg */
+  virtual void getViewUp(vector3_t& up) const = 0;
+  /** Set the view angle in degrees of the camera to the provided arg */
   virtual camera& setViewAngle(const angle_deg_t& angle) = 0;
-  [[nodiscard]] virtual angle_deg_t getViewAngle() = 0;
-  virtual void getViewAngle(angle_deg_t& angle) = 0;
+  /** Return the view angle in degrees of the camera */
+  [[nodiscard]] virtual angle_deg_t getViewAngle() const = 0;
+  /** Get the view angle in degrees of the camera into the provided arg */
+  virtual void getViewAngle(angle_deg_t& angle) const = 0;
+  /** Set the complete state of the provided arg */
   virtual camera& setState(const camera_state_t& state) = 0;
-  [[nodiscard]] virtual camera_state_t getState() = 0;
-  virtual void getState(camera_state_t& state) = 0;
+  /** Return the complete state of the camera */
+  [[nodiscard]] virtual camera_state_t getState() const = 0;
+  /** Get the complete state of the camera into the provided arg */
+  virtual void getState(camera_state_t& state) const = 0;
+
   ///@}
 
   ///@{ @name Manipulation
diff -Nru f3d-3.2.0+dfsg/library/public/context.h f3d-3.4.1/library/public/context.h
--- f3d-3.2.0+dfsg/library/public/context.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/context.h	2026-01-08 08:41:55.000000000 +0200
@@ -4,8 +4,10 @@
 #include "exception.h"
 #include "export.h"
 
+/// @cond
 #include <functional>
 #include <string>
+/// @endcond
 
 namespace f3d
 {
@@ -15,7 +17,7 @@
  *
  * These functions should be used only when the rendering context (the OpenGL window) isn't
  * managed by libf3d, but by the user directly.
- * Therefore, the engine is created using `f3d::engine::createExternal` and the functions below
+ * Therefore, the engine is created using f3d::engine::createExternal and the functions below
  * can be used as argument of this factory.
  * One can also create a custom functor returning a `f3d::context::function`.
  */
diff -Nru f3d-3.2.0+dfsg/library/public/engine.h f3d-3.4.1/library/public/engine.h
--- f3d-3.2.0+dfsg/library/public/engine.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/engine.h	2026-01-08 08:41:55.000000000 +0200
@@ -9,9 +9,11 @@
 #include "scene.h"
 #include "window.h"
 
+/// @cond
 #include <map>
 #include <string>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -39,22 +41,24 @@
   /**
    * Create an engine with an automatic window.
    * Optionally, the window can be hidden by setting offscreen to true.
-   * For VTK < 9.3, the window type will depend on the VTK build options
+   * For VTK < 9.3, the window type will depend on the VTK build options.
    * For VTK >= 9.4:
-   * Linux: Try GLX, then EGL, then OSMesa
-   * Windows: Try Win32, then EGL, then OSMesa
-   * macOS: Always use Cocoa
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   * Linux: Try GLX, then EGL, then OSMesa.
+   * Windows: Try Win32, then EGL, then OSMesa.
+   * macOS: Always use Cocoa.
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine create(bool offscreen = false);
 
   /**
    * Create an engine with no window.
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createNone();
 
@@ -63,10 +67,11 @@
    * Works on Linux only.
    * VTK >= 9.4 required.
    * Optionally, the window can be hidden by setting offscreen to true.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createGLX(bool offscreen = false);
 
@@ -75,10 +80,11 @@
    * Works on Windows only.
    * VTK >= 9.4 required.
    * Optionally, the window can be hidden by setting offscreen to true.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createWGL(bool offscreen = false);
 
@@ -87,33 +93,35 @@
    * VTK >= 9.4 required.
    * If several GPU are available, the environment variable
    * `VTK_DEFAULT_EGL_DEVICE_INDEX` allows its selection.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createEGL();
 
   /**
    * Create an engine with an offscreen OSMesa window.
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createOSMesa();
 
   /**
    * Create an engine with an external window.
    * A context to retrieve OpenGL symbols is required.
-   * The context can be nullptr for an external Cocoa window.
    * Here's an example if a GLFW window is used:
    * \code{.cpp}
    *  f3d::engine eng = f3d::engine::createExternal(glfwGetProcAddress);
    * \endcode
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a engine::no_window_exception if the window cannot be created.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternal(const context::function& getProcAddress);
 
@@ -121,10 +129,11 @@
    * Create an engine with an external GLX context.
    * Equivalent to createExternal(f3d::context::glx());
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternalGLX();
 
@@ -132,10 +141,11 @@
    * Create an engine with an external WGL context.
    * Equivalent to createExternal(f3d::context::wgl());
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternalWGL();
 
@@ -143,10 +153,11 @@
    * Create an engine with an external COCOA context.
    * Equivalent to createExternal(f3d::context::cocoa());
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternalCOCOA();
 
@@ -154,10 +165,11 @@
    * Create an engine with an external EGL context.
    * Equivalent to createExternal(f3d::context::egl());
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternalEGL();
 
@@ -165,10 +177,11 @@
    * Create an engine with an external OSMesa context.
    * Equivalent to createExternal(f3d::context::osmesa());
    * VTK >= 9.4 required.
-   * Throws a context::loading_exception if a needed graphic library cannot be loaded
-   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library
-   * Throws a engine::no_window_exception if the window cannot be created for another reason
-   * Throws a engine::cache_exception if the default cache directory cannot be used
+   *
+   * Throws a context::loading_exception if a needed graphic library cannot be loaded.
+   * Throws a context::symbol_exception if a needed symbol cannot be found in graphic library.
+   * Throws a engine::no_window_exception if the window cannot be created for another reason.
+   * Throws a engine::cache_exception if the default cache directory cannot be used.
    */
   [[nodiscard]] static engine createExternalOSMesa();
 
@@ -194,6 +207,7 @@
    * - Windows: %LOCALAPPDATA%\f3d
    * - Linux: ~/.cache/f3d
    * - macOS: ~/Library/Caches/f3d
+   *
    * Throws a engine::cache_exception if the provided cachePath cannot be used.
    */
   engine& setCachePath(const std::filesystem::path& cachePath);
@@ -247,9 +261,11 @@
    * Then try to load the path provided as if it is a full path to a plugin.
    * Then try to load a plugin by its name looking into the provided plugin search paths (used as
    * is). Then try to load a plugin by its name relying on internal system (eg: LD_LIBRARY_PATH).
+   *
    * The plugin "native" is always available and includes native VTK readers.
    * If built and available in your build, F3D is providing 6 additional plugins:
    * "alembic", "assimp", "draco", "hdf", "occt", "usd", "vdb".
+   *
    * Custom plugins can also be available that F3D is not supporting officially.
    * Throws a engine::plugin_exception if the plugin can't be loaded for some reason.
    */
@@ -266,6 +282,7 @@
    * List plugins based on associated json files located in the given directory, used as is.
    * Listed plugins can be loaded using engine::loadPlugin function.
    * Note that the listed plugins may fail to load if the library is not found or incompatible.
+   *
    * Return available plugins if any, or an empty vector if there are none or the provided path does
    * not exist.
    */
diff -Nru f3d-3.2.0+dfsg/library/public/exception.h f3d-3.4.1/library/public/exception.h
--- f3d-3.2.0+dfsg/library/public/exception.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/exception.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,8 +1,10 @@
 #ifndef f3d_exception_h
 #define f3d_exception_h
 
+/// @cond
 #include <stdexcept>
 #include <string>
+/// @endcond
 
 namespace f3d
 {
diff -Nru f3d-3.2.0+dfsg/library/public/image.h f3d-3.4.1/library/public/image.h
--- f3d-3.2.0+dfsg/library/public/image.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/image.h	2026-01-08 08:41:55.000000000 +0200
@@ -4,9 +4,11 @@
 #include "exception.h"
 #include "export.h"
 
+/// @cond
 #include <filesystem>
 #include <string>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -33,10 +35,9 @@
 
   /**
    * Enumeration of supported channel types
-   * ======================================
-   * BYTE: 8-bit integer in range [0,255]
-   * SHORT: 16-bit integer in range [0,65535]
-   * FLOAT: 32-bit floating point in range [-inf,+inf]
+   *  - BYTE: 8-bit integer in range [0,255]
+   *  - SHORT: 16-bit integer in range [0,65535]
+   *  - FLOAT: 32-bit floating point in range [-inf,+inf]
    */
   enum class ChannelType : unsigned char
   {
@@ -48,8 +49,10 @@
   /**
    * Read provided file path (used as is) into a new image instance, the following formats are
    * supported: PNG, PNM, TIFF, BMP, HDR, JPEG, GESigna, MetaImage, TGA. EXR files are also
-   * supported if the associated module is built. The complete list can be retrieve at runtime by
-   * calling `getSupportedFormats()`. Throw an `image::read_exception` in case of failure.
+   * supported if the associated module is built. The complete list can be retrieved at runtime by
+   * calling getSupportedFormats().
+   *
+   * Throws an image::read_exception in case of failure.
    */
   explicit image(const std::filesystem::path& filePath);
 
@@ -83,7 +86,8 @@
   /**
    * Read one specific pixel and return all channel normalized values.
    * If the channel type is BYTE or SHORT, the values are normalized to [0, 1] range.
-   * \warning Because of the normalization, this function can be slow, prefer getContent when
+   *
+   * Because of the normalization, this function can be slow, prefer getContent() when
    * reading several pixels and normalization is not needed.
    */
   [[nodiscard]] std::vector<double> getNormalizedPixel(const std::pair<int, int>& xy) const;
@@ -110,7 +114,7 @@
 
   /**
    * Get image channel type.
-   * throw an `image::read_exception` if the type is unknown.
+   * Throws an image::read_exception if the type is unknown.
    */
   [[nodiscard]] ChannelType getChannelType() const;
 
@@ -136,22 +140,23 @@
    * a non-zero value can be returned with identical images.
    * Depending on the VTK version, another comparison algorithm may be used.
    * Error value meaning is described below:
-   * 1e-14: Pixel perfect comparison.
-   * 0.04: Visually indistinguishable.
-   * 0.1: Small visible difference.
-   * 0.5: Comparable images.
-   * 1.0: Different type, size or number of components
+   *  - 1e-14: Pixel perfect comparison.
+   *  - 0.04: Visually indistinguishable.
+   *  - 0.1: Small visible difference.
+   *  - 0.5: Comparable images.
+   *  - 1.0: Different type, size or number of components
    */
   double compare(const image& reference) const;
 
   /**
    * Save an image to the provided file path, used as is, in the specified format.
    * Default format is PNG if not specified.
-   * PNG: Supports channel type BYTE and SHORT with channel count of 1 to 4
-   * JPG: Supports channel type BYTE with channel count of 1 or 3
-   * TIF: Supports channel type BYTE, SHORT and FLOAT with channel count of 1 to 4
-   * BMP: Supports channel type BYTE with channel count of 1 to 4
-   * Throw an `image::write_exception` if the format is incompatible with with image channel type or
+   *  - PNG: Supports channel type BYTE and SHORT with channel count of 1 to 4
+   *  - JPG: Supports channel type BYTE with channel count of 1 or 3
+   *  - TIF: Supports channel type BYTE, SHORT and FLOAT with channel count of 1 to 4
+   *  - BMP: Supports channel type BYTE with channel count of 1 to 4
+   *
+   * Throws an image::write_exception if the format is incompatible with with image channel type or
    * channel count or if the image cannot be written for any other reason.
    */
   const image& save(
@@ -160,11 +165,12 @@
   /**
    * Save an image to a memory buffer in the specified format and returns it.
    * Default format is PNG if not specified.
-   * PNG: Supports channel type BYTE and SHORT with channel count of 1 to 4
-   * JPG: Supports channel type BYTE with channel count of 1 or 3
-   * BMP: Supports channel type BYTE with channel count of 1 to 4
-   * TIF format is not supported yet.
-   * Throw an `image::write_exception` if the type is TIF or
+   * - PNG: Supports channel type BYTE and SHORT with channel count of 1 to 4
+   * - JPG: Supports channel type BYTE with channel count of 1 or 3
+   * - BMP: Supports channel type BYTE with channel count of 1 to 4
+   * - TIF format is not supported yet.
+   *
+   * Throws an image::write_exception if the type is TIF or
    * if the format is incompatible with with image channel type or channel count.
    */
   [[nodiscard]] std::vector<unsigned char> saveBuffer(SaveFormat format = SaveFormat::PNG) const;
@@ -178,14 +184,16 @@
    * - unicode block characters (`U+2580`, `U+2584`, `U+2588`)
    * - SGR escape codes (`ESC[0m`, `ESC[49m`)
    * - 24-bit escape codes (`ESC[38;2;{r};{g};{b}m`, `ESC[48;2;{r};{g};{b}m`)
-   * Throw a `image::write_exception` if the type is not byte RGB or RGBA.
+   *
+   * Throws a image::write_exception if the type is not byte RGB or RGBA.
    */
   const image& toTerminalText(std::ostream& stream) const;
 
   /**
    * Convert to colored text using ANSI escape sequences for printing in a terminal.
-   * See `toTerminalText(std::ostream& stream)`.
-   * Throw a `image::write_exception` if the type is not byte RGB or RGBA.
+   * See toTerminalText(std::ostream& stream).
+   *
+   * Throws a image::write_exception if the type is not byte RGB or RGBA.
    */
   [[nodiscard]] std::string toTerminalText() const;
 
@@ -196,7 +204,7 @@
 
   /**
    * Get the value for a metadata key.
-   * Throw a `image::read_exception` if key does not exist.
+   * Throws a image::read_exception if key does not exist.
    */
   [[nodiscard]] std::string getMetadata(const std::string& key) const;
 
diff -Nru f3d-3.2.0+dfsg/library/public/interactor.h f3d-3.4.1/library/public/interactor.h
--- f3d-3.2.0+dfsg/library/public/interactor.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/interactor.h	2026-01-08 08:41:55.000000000 +0200
@@ -7,13 +7,16 @@
 #include "options.h"
 #include "window.h"
 
+/// @cond
 #include <functional>
 #include <string>
 #include <utility>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
+
 struct interaction_bind_t
 {
   /**
@@ -66,6 +69,8 @@
 {
 public:
   ///@{ @name Command
+  using command_documentation_t = std::pair<std::string, std::string>;
+
   /**
    * Remove all existing commands and add all default commands,
    * see COMMANDS.md for details.
@@ -76,11 +81,15 @@
    * Use this method to add a command to be called using triggerCommand.
    * Adding a command with an already existing action throw a
    * interactor::already_exists_exception.
+   *
    * Considering namespacing dedicated action to avoid conflicts with default action,
    * eg: `my_app::action`
    */
-  virtual interactor& addCommand(
-    std::string action, std::function<void(const std::vector<std::string>&)> callback) = 0;
+  virtual interactor& addCommand(const std::string& action,
+    std::function<void(const std::vector<std::string>&)> callback,
+    std::optional<command_documentation_t> doc = std::nullopt,
+    std::function<std::vector<std::string>(const std::vector<std::string>&)> completionCallback =
+      nullptr) = 0;
 
   /**
    * Remove a command for provided action, does not do anything if it does not exists.
@@ -98,19 +107,44 @@
    *
    * If the command fails, it prints a debug log explaining why.
    *
+   * When keepComments argument is true, comments are supported with `#`, any characters after are
+   * ignored otherwise `#` and any characters after will be handled as standard character
+   *
    * Return true if the command succeeded, false otherwise.
    * Throw an interactor::command_runtime_exception if the command callback
    * throw an unrecognized exception.
+   *
    * Note that default commands will never throw this exception, but adding commands
    * without exception catching may trigger this behavior.
    */
-  virtual bool triggerCommand(std::string_view command) = 0;
+  virtual bool triggerCommand(std::string_view command, bool keepComments = true) = 0;
   ///@}
 
   ///@{ @name Bindings
   using documentation_callback_t = std::function<std::pair<std::string, std::string>()>;
 
   /**
+   * Enumeration of binding types.
+   * Duplication present in vtkext/private/module/vtkF3DUIActor.h.
+   */
+  enum class BindingType : std::uint8_t
+  {
+    CYCLIC = 0,
+    NUMERICAL = 1,
+    TOGGLE = 2,
+    OTHER = 3,
+  };
+
+  /**
+   * Enumeration of animation direction.
+   */
+  enum class AnimationDirection : std::uint8_t
+  {
+    FORWARD,
+    BACKWARD
+  };
+
+  /**
    * Remove all existing interaction commands and add all default bindings
    * see INTERACTIONS.md for details.
    */
@@ -125,12 +159,15 @@
    * "Left", "Right", "Up", "Down", "Space", "Enter", "Escape", "Question".
    *
    * group is an optional arg to group bindings together for better display of the documentation.
-   * Groups are kept in order of addition when recovered using `getBindGroups`.
-   * Bindings are kept in order of addition when recovered using `getBindsForGroup`.
+   * Groups are kept in order of addition when recovered using getBindGroups().
+   * Bindings are kept in order of addition when recovered using getBindsForGroup().
    *
    * documentationCallback is an optional function that returns a pair of string,
    * the first is the doc itself, the second is the current value as a string, if any.
-   * Use `getBindingDocumentation` to access this doc.
+   * Use getBindingDocumentation() to access this doc.
+   *
+   * type is an optional type of binding to provide, it can be used for presenting the
+   * binding in a coherent way in logs and cheatsheet.
    *
    * When the corresponding bind happens, the provided commands will be triggered using
    * triggerCommand. Considering checking if an interaction exists or removing it before adding it
@@ -142,26 +179,29 @@
    * Adding commands for an existing bind will throw a interactor::already_exists_exception.
    */
   virtual interactor& addBinding(const interaction_bind_t& bind, std::vector<std::string> commands,
-    std::string group = {}, documentation_callback_t documentationCallback = nullptr) = 0;
+    std::string group = {}, documentation_callback_t documentationCallback = nullptr,
+    BindingType type = BindingType::OTHER) = 0;
 
   /**
    * See addBinding
    * Convenience method to add a single command for an interaction,
-   * similar as `addBinding(bind, {command})`
+   * similar as `addBinding(bind, {command})`.
    *
    * Adding command for an existing bind will throw a interactor::already_exists_exception.
    */
   virtual interactor& addBinding(const interaction_bind_t& bind, std::string command,
-    std::string group = {}, documentation_callback_t documentationCallback = nullptr) = 0;
+    std::string group = {}, documentation_callback_t documentationCallback = nullptr,
+    BindingType type = BindingType::OTHER) = 0;
 
   /**
    * Convenience initializer list signature for add binding method
    */
   interactor& addBinding(const interaction_bind_t& bind, std::initializer_list<std::string> list,
-    std::string group = {}, documentation_callback_t documentationCallback = nullptr)
+    std::string group = {}, documentation_callback_t documentationCallback = nullptr,
+    BindingType type = BindingType::OTHER)
   {
-    return this->addBinding(
-      bind, std::vector<std::string>(list), std::move(group), std::move(documentationCallback));
+    return this->addBinding(bind, std::vector<std::string>(list), std::move(group),
+      std::move(documentationCallback), type);
   }
 
   /**
@@ -192,8 +232,10 @@
    * Get a pair of string documenting a binding.
    * The first string is the documentation of the binding,
    * eg: "Toggle anti aliasing", "Print scene descr to terminal", "Decrease light intensity"
+   *
    * The second string is the current value of the binding,
    * eg: "OFF", "" if there is no value or "1.12".
+   *
    * If a binding was not documented on addition, the provided strings will be empty.
    * The possible string can depends on the bindings but boolean value are expected to be
    * "ON", "OFF", "N/A" (for optional values).
@@ -202,16 +244,26 @@
    */
   [[nodiscard]] virtual std::pair<std::string, std::string> getBindingDocumentation(
     const interaction_bind_t& bind) const = 0;
+
+  /**
+   * Get the type of a binding.
+   *
+   * Getting type for a bind that does not exists will throw a does_not_exists_exception.
+   */
+  [[nodiscard]] virtual BindingType getBindingType(const interaction_bind_t& bind) const = 0;
   ///@}
 
   ///@{ @name Animation
   /**
    * Control the animation.
    */
-  virtual interactor& toggleAnimation() = 0;
-  virtual interactor& startAnimation() = 0;
+  virtual interactor& toggleAnimation(
+    AnimationDirection direction = AnimationDirection::FORWARD) = 0;
+  virtual interactor& startAnimation(
+    AnimationDirection direction = AnimationDirection::FORWARD) = 0;
   virtual interactor& stopAnimation() = 0;
   [[nodiscard]] virtual bool isPlayingAnimation() = 0;
+  [[nodiscard]] virtual interactor::AnimationDirection getAnimationDirection() = 0;
   ///@}
 
   ///@{ @name Movement
@@ -222,41 +274,146 @@
   virtual interactor& disableCameraMovement() = 0;
   ///@}
 
+  ///@{ @name Forwarding input events
+  /**
+   * Enumeration of supported mouse buttons.
+   */
+  enum class MouseButton : unsigned char
+  {
+    LEFT,
+    RIGHT,
+    MIDDLE
+  };
+
+  /**
+   * Enumeration of supported mouse wheel directions.
+   */
+  enum class WheelDirection : unsigned char
+  {
+    FORWARD,
+    BACKWARD,
+    LEFT,
+    RIGHT
+  };
+
+  /**
+   * Enumeration of supported input actions.
+   */
+  enum class InputAction : unsigned char
+  {
+    PRESS,
+    RELEASE
+  };
+
+  /**
+   * Enumeration of supported input modifiers.
+   */
+  enum class InputModifier : unsigned char
+  {
+    NONE,
+    CTRL,
+    SHIFT,
+    CTRL_SHIFT
+  };
+
+  /**
+   * Trigger a modifier update.
+   * This will update the internal modifier state of the interactor to match the provided one.
+   */
+  virtual interactor& triggerModUpdate(InputModifier mod) = 0;
+
+  /**
+   * Trigger a mouse button event.
+   * This will trigger the corresponding mouse button press or release event.
+   */
+  virtual interactor& triggerMouseButton(InputAction action, MouseButton button) = 0;
+
+  /**
+   * Trigger a mouse new position event.
+   * Positions are in window coordinates, with (0, 0) being the top-left corner.
+   * The coordinates are expressed in pixels.
+   */
+  virtual interactor& triggerMousePosition(double xpos, double ypos) = 0;
+
+  /**
+   * Trigger a mouse wheel event.
+   * At the moment, only vertical wheel events are observed, but it can change in the future.
+   */
+  virtual interactor& triggerMouseWheel(WheelDirection direction) = 0;
+
+  /**
+   * Trigger a keyboard key event.
+   * This is based on X11 key symbols, it's hard to list all of them, but here are a few:
+   * - "A", "B", "C", ..., "Z" for letters
+   * - "0", "1", "2", ..., "9" for numbers
+   * - "Left", "Right", "Up", "Down" for arrow keys
+   * - "Space", "Return", "Escape", "Tab", "BackSpace" for common keys
+   * - "F1", "F2", ..., "F25" for function keys
+   * - "KP_0", "KP_1", ..., "KP_9" for numpad keys
+   *
+   * It's possible to run F3D application in verbose mode and press keys to print their symbols.
+   */
+  virtual interactor& triggerKeyboardKey(InputAction action, std::string_view keySym) = 0;
+
+  /**
+   * Trigger a text character input event.
+   * This will trigger the corresponding character input event, with the codepoint being the Unicode
+   * codepoint of the character.
+   * It's used for text input, like when typing in a the console input field.
+   */
+  virtual interactor& triggerTextCharacter(unsigned int codepoint) = 0;
+  ///@}
+
+  /**
+   * Manually trigger the event loop.
+   * Advances the internal timers of `deltaTime` seconds.
+   * deltaTime should be strictly positive.
+   */
+  virtual interactor& triggerEventLoop(double deltaTime) = 0;
+
   /**
    * Play a VTK interaction file.
    * Provided file path is used as is and file existence will be checked.
-   * The event loop will be triggered every deltaTime in seconds, and userCallBack will be called at
-   * the start of the event loop
+   * If the event loop is not already running, it will be triggered every deltaTime in seconds,
+   * and userCallBack will be called at the start of the event loop.
    */
   virtual bool playInteraction(const std::filesystem::path& file, double deltaTime = 1.0 / 30,
     std::function<void()> userCallBack = nullptr) = 0;
 
   /**
    * Start interaction and record it all in a VTK interaction file.
-   * Provided file path will be used as is and the parent directories of the file will be created
+   * Provided file path will be used as is and the parent directories of the file will be created.
    */
   virtual bool recordInteraction(const std::filesystem::path& file) = 0;
 
   /**
    * Start the interactor event loop.
    * The event loop will be triggered every deltaTime in seconds, and userCallBack will be called at
-   * the start of the event loop
+   * the start of the event loop, deltaTime should be strictly positive.
+   * Safe to call multiple times but will log an info in that case.
    */
   virtual interactor& start(
     double deltaTime = 1.0 / 30, std::function<void()> userCallBack = nullptr) = 0;
 
   /**
    * Stop the interactor.
+   * If interactor has not been started, this logs an info and returns without doing anything.
    */
   virtual interactor& stop() = 0;
 
   /**
-   * Request a render to be done on the next event loop
-   * Safe to call in a multithreaded environment
+   * Request a render to be done on the next event loop.
+   * Safe to call in a multithreaded environment.
    */
   virtual interactor& requestRender() = 0;
 
   /**
+   * Request the interactor to stop on the next event loop.
+   * Safe to call in a multithreaded environment.
+   */
+  virtual interactor& requestStop() = 0;
+
+  /**
    * An exception that can be thrown by the interactor
    * when adding something that already exists internally
    */
diff -Nru f3d-3.2.0+dfsg/library/public/log.h f3d-3.4.1/library/public/log.h
--- f3d-3.2.0+dfsg/library/public/log.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/log.h	2026-01-08 08:41:55.000000000 +0200
@@ -3,8 +3,11 @@
 
 #include "export.h"
 
+/// @cond
+#include <functional>
 #include <sstream>
 #include <string>
+/// @endcond
 
 namespace f3d
 {
@@ -22,12 +25,11 @@
 public:
   /**
    * Enumeration of verbose levels
-   * =============================
-   * DEBUG: All logs are displayed, including from third parties.
-   * INFO: Standard logging level, the default.
-   * WARN: Only warnings and errors are displayed.
-   * ERROR: Only errors are displayed.
-   * QUIET: Logging is fully disabled.
+   * - DEBUG: All logs are displayed, including from third parties.
+   * - INFO: Standard logging level, the default.
+   * - WARN: Only warnings and errors are displayed.
+   * - ERROR: Only errors are displayed.
+   * - QUIET: Logging is fully disabled.
    */
   enum class VerboseLevel : unsigned char
   {
@@ -110,6 +112,19 @@
    */
   static VerboseLevel getVerboseLevel();
 
+  /**
+   * Alias for log forwarder callback.
+   */
+  using forward_fn_t = std::function<void(VerboseLevel, const std::string&)>;
+
+  /**
+   * Set a callback function to forward log messages.
+   * The callback will be invoked with the level and the message string whenever a message is
+   * logged, regardless of the verbose level.
+   * Set to nullptr to disable forwarding.
+   */
+  static void forward(const forward_fn_t& callback);
+
 protected:
   //! @cond
   static void appendArg(std::stringstream&)
diff -Nru f3d-3.2.0+dfsg/library/public/options.h.in f3d-3.4.1/library/public/options.h.in
--- f3d-3.2.0+dfsg/library/public/options.h.in	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/options.h.in	2026-01-08 08:41:55.000000000 +0200
@@ -5,12 +5,14 @@
 #include "export.h"
 #include "types.h"
 
+/// @cond
 #include <array>
 #include <filesystem>
 #include <optional>
 #include <string>
 #include <variant>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -23,8 +25,7 @@
  * @brief   Class used to control the different options
  *
  * A class to control the different options of f3d.
- * See the README_libf3d.md for the full listing of options.
- * The struct below can also be consulted.
+ *
  */
 class F3D_EXPORT options
 {
@@ -45,58 +46,66 @@
    * Set an option as a variant based on its name
    * Please note that, on Windows, using an explicit std::string may be required:
    * https://stackoverflow.com/questions/60681710/prefer-stdstring-in-stdvariantbool-stdstring-for-const-char
-   * Throw an options::inexistent_exception if option does not exist.
-   * Throw an options::incompatible_exception if value is not compatible with option.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
+   * Throws an options::incompatible_exception if value is not compatible with option.
    */
   options& set(std::string_view name, const option_variant_t& value);
 
   /**
    * Get an option as a variant based on its name
-   * Throw an options::inexistent_exception if option does not exist.
-   * Throw an options::no_value_exception if option has not been set.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
+   * Throws an options::no_value_exception if option has not been set.
    */
   [[nodiscard]] option_variant_t get(std::string_view name) const;
 
   /**
    * Set an option as a string based on its name
    * Use dedicated parsing code, see the related doc in PARSING.md.
-   * Throw an options::inexistent_exception if option does not exist.
-   * Throw an options::parsing_exception if parsing failed.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
+   * Throws an options::parsing_exception if parsing failed.
    */
   options& setAsString(std::string_view name, const std::string& str);
 
   /**
    * Get an option as a string based on its name
-   * Throw an options::inexistent_exception if option does not exist.
-   * Throw an options::no_value_exception if option has not been set.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
+   * Throws an options::no_value_exception if option has not been set.
    */
   [[nodiscard]] std::string getAsString(std::string_view name) const;
 
   /**
    * A boolean option specific method to toggle it.
    * If the option has not been set yet, set it to true.
-   * Throw an options::inexistent_exception if option does not exist.
-   * Throw an options::incompatible_exception if option is not boolean.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
+   * Throws an options::incompatible_exception if option is not boolean.
    */
   options& toggle(std::string_view name);
 
   /**
    * Compare an option between this and a provided other.
    * Return true if they are the same value, false otherwise.
-   * Throw an options::inexistent_exception if option does not exist.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
    */
   [[nodiscard]] bool isSame(const options& other, std::string_view name) const;
 
   /**
    * Return true if an option has a value, false otherwise
    * Always returns true for non-optional options.
-   * Throw an options::inexistent_exception if option does not exist.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
    */
   [[nodiscard]] bool hasValue(std::string_view name) const;
 
   /**
    * Copy the value of an option from this to the provided other.
-   * Throw an options::inexistent_exception if option does not exist.
+   *
+   * Throws an options::inexistent_exception if option does not exist.
    */
   options& copy(const options& other, std::string_view name);
 
@@ -118,18 +127,21 @@
 
   /**
    *  Returns true if the option is optional else returns false.
+   *
    *  Throws an options::inexistent_exception if option does not exist.
    */
   [[nodiscard]] bool isOptional(std::string_view option) const;
 
   /**
    *  Resets the option to default value.
+   *
    *  Throws an options::inexistent_exception if option does not exist.
    */
   options& reset(std::string_view name);
 
   /**
    *  Unset the option if it is optional else throws options::incompatible_exception.
+   *
    *  Throws an options::inexistent_exception if option does not exist.
    */
   options& removeValue(std::string_view name);
@@ -138,7 +150,8 @@
    * Templated parsing method used internally to parse strings.
    * Implemented for the different supported types,
    * see PARSING.md for more info.
-   * Throw an options::parsing_exception if parsing failed.
+   *
+   * Throws an options::parsing_exception if parsing failed.
    */
   template<typename T>
   [[nodiscard]] static T parse(const std::string& str);
@@ -146,6 +159,7 @@
   /**
    * Templated parsing method used internally to format var into strings.
    * Implemented for the different supported types,
+   *
    * see PARSING.md for more info.
    */
   template<typename T>
diff -Nru f3d-3.2.0+dfsg/library/public/scene.h f3d-3.4.1/library/public/scene.h
--- f3d-3.2.0+dfsg/library/public/scene.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/scene.h	2026-01-08 08:41:55.000000000 +0200
@@ -5,9 +5,11 @@
 #include "export.h"
 #include "types.h"
 
+/// @cond
 #include <filesystem>
 #include <string>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -80,6 +82,49 @@
   virtual scene& clear() = 0;
 
   /**
+   * An exception that can be thrown by the scene
+   * when it fails to index the light
+   */
+  struct light_exception : public exception
+  {
+    explicit light_exception(const std::string& what = "")
+      : f3d::exception(what) {};
+  };
+
+  /**
+   * Add a light based on a light state, returns the index of the added light.
+   */
+  virtual int addLight(const light_state_t& lightState) const = 0;
+
+  /**
+   * Get the number of lights.
+   */
+  [[nodiscard]] virtual int getLightCount() const = 0;
+
+  /**
+   * Get the light state at provided index.
+   * light_exception is thrown if the index is invalid.
+   */
+  [[nodiscard]] virtual light_state_t getLight(int index) const = 0;
+
+  /**
+   * Update a light at provided index with the provided light state.
+   * light_exception is thrown if the index is invalid.
+   */
+  virtual scene& updateLight(int index, const light_state_t& lightState) = 0;
+
+  /**
+   * Remove a light at provided index.
+   * light_exception is thrown if the index is invalid.
+   */
+  virtual scene& removeLight(int index) = 0;
+
+  /**
+   * Remove all lights from the scene.
+   */
+  virtual scene& removeAllLights() = 0;
+
+  /**
    * Return true if provided file path is supported, false otherwise.
    */
   [[nodiscard]] virtual bool supports(const std::filesystem::path& filePath) = 0;
diff -Nru f3d-3.2.0+dfsg/library/public/types.h f3d-3.4.1/library/public/types.h
--- f3d-3.2.0+dfsg/library/public/types.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/types.h	2026-01-08 08:41:55.000000000 +0200
@@ -4,11 +4,14 @@
 #include "exception.h"
 #include "export.h"
 
+/// @cond
 #include <algorithm>
 #include <array>
+#include <cstdint>
 #include <iostream>
 #include <string>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -68,7 +71,7 @@
   }
 
 private:
-  double Value;
+  double Value = 0;
 };
 
 /**
@@ -135,7 +138,7 @@
 /**
  * Describe a RGB color.
  */
-class color_t : public double_array_t<3>
+class F3D_EXPORT color_t : public double_array_t<3>
 {
 public:
   inline color_t() = default;
@@ -228,10 +231,11 @@
   /**
    *  The variables of this function are based on the mathematical notation for matrices,
    *  where the coordinates correspond to the following:
-   * 
-   *        [M1_1, M1_2, M1_3]
-   *  M =   [M2_1, M2_2, M2_3]
-   *        [M3_1, M3_2, M3_3]
+   * \code
+   *       [M1_1, M1_2, M1_3]
+   * M =   [M2_1, M2_2, M2_3]
+   *       [M3_1, M3_2, M3_3]
+   * \endcode
    */
   // clang-format on
   inline transform2d_t(double M1_1, double M1_2, double M1_3, double M2_1, double M2_2, double M2_3,
@@ -247,6 +251,22 @@
     (*this)[7] = M3_2;
     (*this)[8] = M3_3;
   }
+
+  // clang-format off
+  /**
+   *  The general form of a 3x3 transformation matrix M with scale S(x,y),
+   *  translation T(x,y), and angle a (in degrees), is solved out to the following:
+   * \code
+   *     [cos(a)*S(x), -sin(a)*S(y),   T(x)]
+   * M = [sin(a)*S(x), cos(a)*S(y),    T(y)]
+   *     [0,           0,              1   ]
+   * \endcode
+   *  Using this formula, we fill each cell using the values in the constructor
+   */
+  // clang-format on
+
+  F3D_EXPORT transform2d_t(const double_array_t<2>& scale, const double_array_t<2>& translate,
+    const angle_deg_t& angleRad);
 };
 
 /**
@@ -310,6 +330,33 @@
    */
   F3D_EXPORT std::pair<bool, std::string> isValid() const;
 };
+
+enum class F3D_EXPORT light_type : std::uint8_t
+{
+  HEADLIGHT = 1,
+  CAMERA_LIGHT = 2,
+  SCENE_LIGHT = 3,
+};
+
+struct F3D_EXPORT light_state_t
+{
+  light_type type = light_type::SCENE_LIGHT;
+  point3_t position = { 0., 0., 0. };
+  color_t color = { 1., 1., 1. };
+  vector3_t direction = { 1., 0., 0. };
+  bool positionalLight = false;
+  double intensity = 1.0;
+  bool switchState = true;
+
+  [[nodiscard]] bool operator==(const light_state_t& other) const
+  {
+    return this->type == other.type && this->position == other.position &&
+      this->color == other.color && this->direction == other.direction &&
+      this->positionalLight == other.positionalLight && this->intensity == other.intensity &&
+      this->switchState == other.switchState;
+  }
+};
+
 }
 
 #endif
diff -Nru f3d-3.2.0+dfsg/library/public/utils.h f3d-3.4.1/library/public/utils.h
--- f3d-3.2.0+dfsg/library/public/utils.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/utils.h	2026-01-08 08:41:55.000000000 +0200
@@ -4,6 +4,7 @@
 #include "exception.h"
 #include "export.h"
 
+/// @cond
 #include <filesystem>
 #include <map>
 #include <optional>
@@ -11,6 +12,7 @@
 #include <sstream>
 #include <string>
 #include <vector>
+/// @endcond
 
 namespace f3d
 {
@@ -37,24 +39,26 @@
    *  - Split by quoted section and remove the quotes
    *  - Supported quotes are: '"`
    *  - Use escaped \ quotes, spaces and escape to add them verbatim
-   *  - Comments are supported with `#`, any characters after are ignored
-   *  - Use escaped \# to add it verbatim
+   *  - When keepComments argument is true, comments are supported with `#`, any characters after are ignored
+   * otherwise '#' and any characters after will be handled as standard character
+   *  - Use escaped \# to add it verbatim when using keepComments = true
    *  - Other escaped characters are also added verbatim
+   *
    * Throw a tokenize_exception if a quoted section is not closed or if finishing with an escape
    *
    * Examples:
-   * `set scene.up.direction +Z` -> `set` `scene.up.direction` `+Z`
-   * `set render.hdri.file "/path/to/file with spaces.png"` -> `set`, `render.hdri.file`, `/path/to/file with spaces.png`
-   * `set render.hdri.file '/path/to/file with spaces.png'` -> `set`, `render.hdri.file`, `/path/to/file with spaces.png`
-   * `set render.hdri.file "/path/to/file'with'quotes.png"` -> `set`, `render.hdri.file`, `/path/to/file'with'quotes.png`
-   * `set render.hdri.file /path/to/file\ spaces\ \'quotes\".png` -> `set`, `render.hdri.file`, `/path/to/file spaces 'quotes".png`
-   * `set render.hdri.file C:\\path\\to\\windows\\file.png` -> `set`, `render.hdri.file`, `C:\path\to\windows\file.png`
-   * `set scene.up.direction +Z # A comment` -> `set`, `scene.up.direction`, `+Z`
-   * `set scene.up.direction +\Z` -> `set`, `scene.up.direction`, `+Z`
-   * `set scene.up.direction "+Z` -> tokenize_exception
-   * `set scene.up.direction +Z\` -> tokenize_exception
+   *  - `set scene.up.direction +Z` -> `set` `scene.up.direction` `+Z`
+   *  - `set render.hdri.file "/path/to/file with spaces.png"` -> `set`, `render.hdri.file`, `/path/to/file with spaces.png`
+   *  - `set render.hdri.file '/path/to/file with spaces.png'` -> `set`, `render.hdri.file`, `/path/to/file with spaces.png`
+   *  - `set render.hdri.file "/path/to/file'with'quotes.png"` -> `set`, `render.hdri.file`, `/path/to/file'with'quotes.png`
+   *  - `set render.hdri.file /path/to/file\ spaces\ \'quotes\".png` -> `set`, `render.hdri.file`, `/path/to/file spaces 'quotes".png`
+   *  - `set render.hdri.file C:\\path\\to\\windows\\file.png` -> `set`, `render.hdri.file`, `C:\path\to\windows\file.png`
+   *  - `set scene.up.direction +Z # A comment` -> `set`, `scene.up.direction`, `+Z`
+   *  - `set scene.up.direction +\Z` -> `set`, `scene.up.direction`, `+Z`
+   *  - `set scene.up.direction "+Z` -> tokenize_exception
+   *  - `set scene.up.direction +Z\` -> tokenize_exception
    */
-  [[nodiscard]] static std::vector<std::string> tokenize(std::string_view str);
+  [[nodiscard]] static std::vector<std::string> tokenize(std::string_view str, bool keepComments = true);
   // clang-format on
 
   /**
@@ -63,6 +67,7 @@
    * - Transform relative path into an absolute path based on basedDirectory if provided, or the
    * current directory if not
    * - Remove any `..` if any
+   *
    * Rely on vtksys::SystemTools::CollapseFullPath but return empty string if the provided
    * string is empty.
    */
@@ -81,7 +86,7 @@
    * - `{a,b,c}`: Alternation, matches any of the given comma-separated patterns
    * - `**`: Matches any number of characters including path separators
    *
-   * Throws a `utils::glob_exception` if a character class or alternation is not closed or
+   * Throws a utils::glob_exception if a character class or alternation is not closed or
    * the expression ends with an escape.
    */
   [[nodiscard]] static std::string globToRegex(std::string_view glob, char pathSeparator = '/');
@@ -123,12 +128,13 @@
     explicit glob_exception(const std::string& what = "");
   };
 
-  /** String template allowing substitution of variables enclosed in curly braces.
-    ```
-    string_template("{greeting} {name}!")
-      .substitute({ { "greeting", "hello" }, { "name", "World" } })
-      .str() == "hello World!"
-    ```
+  /**
+   * String template allowing substitution of variables enclosed in curly braces.
+   * \code{.cpp}
+   * string_template("{greeting} {name}!")
+   *   .substitute({ { "greeting", "hello" }, { "name", "World" } })
+   *   .str() == "hello World!"
+   * \endcode
    */
   class string_template
   {
@@ -137,14 +143,18 @@
   public:
     explicit string_template(const std::string& templateString);
 
-    /** Substitute variables based on a `std::string(const std::string&)` function.
-     * Variables for which the function throws a `string_template::lookup_error` exception
+    /**
+     * Substitute variables based on a `std::string(const std::string&)` function.
+     *
+     * Variables for which the function throws a string_template::lookup_error exception
      * are left untouched.
      */
     template<typename F>
     string_template& substitute(F lookup);
 
-    /** Substitute variables based on a map.
+    /**
+     * Substitute variables based on a map.
+     *
      * Variables for which the map does not contain a key are left untouched.
      */
     string_template& substitute(const std::map<std::string, std::string>& lookup);
diff -Nru f3d-3.2.0+dfsg/library/public/window.h f3d-3.4.1/library/public/window.h
--- f3d-3.2.0+dfsg/library/public/window.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/public/window.h	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,9 @@
 #include "export.h"
 #include "image.h"
 
+/// @cond
 #include <string>
+/// @endcond
 
 namespace f3d
 {
@@ -21,17 +23,16 @@
 public:
   /**
    * Enumeration of supported window types
-   * =====================================
-   * NONE: A mock window without rendering capabilities.
-   * EXTERNAL: An external window that assume the OpenGL context would have been created by another
-   * framework.
-   * GLX: A window using GLX.
-   * WGL: A window using WGL.
-   * COCOA: A window using COCOA.
-   * EGL: An offscreen window using hardware acceleration that can run headless.
-   * OSMESA: An offscreen window using software rendering that is always run headless.
-   * WASM: A webassembly window (when libf3d is built with emscripten).
-   * UNKNOWN: An unknown window (should not happen).
+   * - NONE: A mock window without rendering capabilities.
+   * - EXTERNAL: An external window that assume the OpenGL context would have been created by
+   * another framework.
+   * - GLX: A window using GLX.
+   * - WGL: A window using WGL.
+   * - COCOA: A window using COCOA.
+   * - EGL: An offscreen window using hardware acceleration that can run headless.
+   * - OSMESA: An offscreen window using software rendering that is always run headless.
+   * - WASM: A webassembly window (when libf3d is built with emscripten).
+   * - UNKNOWN: An unknown window (should not happen).
    */
   enum class Type : unsigned char
   {
@@ -72,7 +73,7 @@
    * Perform a render of the window to the screen and save the result in a f3d::image.
    * of ChannelType BYTE and 3 or 4 components (RGB or RGBA).
    * Set noBackground to true to have a transparent background.
-   * Return the resulting f3d::image.
+   * Returns the resulting f3d::image.
    */
   [[nodiscard]] virtual image renderToImage(bool noBackground = false) = 0;
 
diff -Nru f3d-3.2.0+dfsg/library/src/animationManager.cxx f3d-3.4.1/library/src/animationManager.cxx
--- f3d-3.2.0+dfsg/library/src/animationManager.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/animationManager.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -6,10 +6,11 @@
 #include "options.h"
 #include "window_impl.h"
 
+#include "F3DStyle.h"
+#include "vtkF3DMetaImporter.h"
 #include "vtkF3DRenderer.h"
 
 #include <vtkDoubleArray.h>
-#include <vtkImporter.h>
 #include <vtkProgressBarRepresentation.h>
 #include <vtkRenderWindow.h>
 #include <vtkRendererCollection.h>
@@ -30,7 +31,7 @@
 }
 
 //----------------------------------------------------------------------------
-void animationManager::SetImporter(vtkImporter* importer)
+void animationManager::SetImporter(vtkF3DMetaImporter* importer)
 {
   this->Importer = importer;
 }
@@ -68,7 +69,17 @@
     progressRep->SetPosition(0.0, 0.0);
     progressRep->SetPosition2(1.0, 0.0);
     progressRep->SetMinimumSize(0, 5);
-    progressRep->SetProgressBarColor(1, 0, 0);
+    f3d::color_t color;
+    if (!this->Options.ui.animation_progress_color.has_value())
+    {
+      const auto [r, g, b] = F3DStyle::GetF3DBlue();
+      color = color_t(r, g, b);
+    }
+    else
+    {
+      color = this->Options.ui.animation_progress_color.value();
+    }
+    progressRep->SetProgressBarColor(color.r(), color.g(), color.b());
     progressRep->DrawBackgroundOff();
     progressRep->DragableOff();
     progressRep->SetShowBorderToOff();
@@ -101,8 +112,7 @@
     log::debug(i, ": ", this->Importer->GetAnimationName(i));
   }
 
-  bool autoplay = this->Options.scene.animation.autoplay;
-  if (autoplay)
+  if (this->Autoplay)
   {
     this->StartAnimation();
   }
@@ -158,9 +168,10 @@
 //----------------------------------------------------------------------------
 void animationManager::Tick()
 {
+  assert(this->DeltaTime > 0);
   if (this->Playing)
   {
-    this->CurrentTime += this->DeltaTime * this->Options.scene.animation.speed_factor;
+    this->CurrentTime += (this->DeltaTime * this->SpeedFactor) * this->AnimationDirection;
 
     // Modulo computation, compute CurrentTime in the time range.
     if (this->CurrentTime < this->TimeRange[0] || this->CurrentTime > this->TimeRange[1])
@@ -182,6 +193,35 @@
 }
 
 //----------------------------------------------------------------------------
+void animationManager::JumpToFrame(int frame, bool relative)
+{
+  assert(this->DeltaTime > 0);
+  const double frameDuration = (this->DeltaTime * this->SpeedFactor);
+  const double currentFrame = (this->CurrentTime - this->TimeRange[0]) / frameDuration;
+
+  double nextFrame = 0;
+  if (relative)
+  {
+    nextFrame = currentFrame + frame;
+  }
+  else if (frame >= 0)
+  {
+    nextFrame = frame;
+  }
+  else
+  {
+    nextFrame = (this->TimeRange[1] - this->TimeRange[0]) / frameDuration;
+  }
+
+  this->CurrentTime = this->TimeRange[0] + (nextFrame * this->DeltaTime * this->SpeedFactor);
+
+  if (this->LoadAtTime(this->CurrentTime))
+  {
+    this->Window.render();
+  }
+}
+
+//----------------------------------------------------------------------------
 bool animationManager::LoadAtTime(double timeValue)
 {
   assert(this->Importer);
@@ -466,11 +506,7 @@
       double timeRange[2];
       int nbTimeSteps;
       vtkNew<vtkDoubleArray> timeSteps;
-
-      // Discard timesteps, F3D only cares about elapsed time using time range and deltaTime
-      // Specifying a frame rate (60) in the next call is not needed after VTK 9.2.20230603 :
-      // VTK_VERSION_CHECK(9, 2, 20230603)
-      this->Importer->GetTemporalInformation(animIndex, 60, nbTimeSteps, timeRange, timeSteps);
+      this->Importer->GetTemporalInformation(animIndex, timeRange, nbTimeSteps, timeSteps);
 
       // Accumulate time ranges
       this->TimeRange[0] = std::min(timeRange[0], this->TimeRange[0]);
@@ -511,4 +547,45 @@
   assert(this->AvailAnimations >= 0);
   return static_cast<unsigned int>(this->AvailAnimations);
 }
+
+//----------------------------------------------------------------------------
+void animationManager::SetCheatSheetConfigured(bool configured)
+{
+  vtkF3DRenderer* ren = this->Window.GetRenderer();
+  ren->SetCheatSheetConfigured(configured);
+}
+
+//----------------------------------------------------------------------------
+void animationManager::SetAutoplay(bool enable)
+{
+  if (this->Autoplay != enable)
+  {
+    this->Autoplay = enable;
+    this->SetCheatSheetConfigured(false);
+  }
+}
+
+//----------------------------------------------------------------------------
+void animationManager::SetSpeedFactor(double speedFactor)
+{
+  if (this->SpeedFactor != speedFactor)
+  {
+    this->SpeedFactor = speedFactor;
+    this->SetCheatSheetConfigured(false);
+  }
+}
+
+//----------------------------------------------------------------------------
+void animationManager::SetAnimationDirection(int direction)
+{
+  assert(direction == 1 || direction == -1);
+  this->AnimationDirection = direction;
+}
+
+//----------------------------------------------------------------------------
+void animationManager::UpdateDynamicOptions()
+{
+  this->SetAutoplay(this->Options.scene.animation.autoplay);
+  this->SetSpeedFactor(this->Options.scene.animation.speed_factor);
+}
 }
diff -Nru f3d-3.2.0+dfsg/library/src/camera_impl.cxx f3d-3.4.1/library/src/camera_impl.cxx
--- f3d-3.2.0+dfsg/library/src/camera_impl.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/camera_impl.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -41,6 +41,7 @@
   //--------------------------------------------------------------------------
   vtkRenderer* VTKRenderer = nullptr;
   camera_state_t DefaultCamera;
+  bool SuccessfullyReset = true; // could be false only if resetToBounds was called
 };
 
 //----------------------------------------------------------------------------
@@ -63,7 +64,7 @@
 }
 
 //----------------------------------------------------------------------------
-point3_t camera_impl::getPosition()
+point3_t camera_impl::getPosition() const
 {
   point3_t pos;
   this->getPosition(pos);
@@ -71,7 +72,7 @@
 }
 
 //----------------------------------------------------------------------------
-void camera_impl::getPosition(point3_t& pos)
+void camera_impl::getPosition(point3_t& pos) const
 {
   vtkCamera* cam = this->GetVTKCamera();
   cam->GetPosition(pos.data());
@@ -88,7 +89,7 @@
 }
 
 //----------------------------------------------------------------------------
-point3_t camera_impl::getFocalPoint()
+point3_t camera_impl::getFocalPoint() const
 {
   point3_t foc;
   this->getFocalPoint(foc);
@@ -96,7 +97,7 @@
 }
 
 //----------------------------------------------------------------------------
-void camera_impl::getFocalPoint(point3_t& foc)
+void camera_impl::getFocalPoint(point3_t& foc) const
 {
   vtkCamera* cam = this->GetVTKCamera();
   cam->GetFocalPoint(foc.data());
@@ -113,7 +114,7 @@
 }
 
 //----------------------------------------------------------------------------
-vector3_t camera_impl::getViewUp()
+vector3_t camera_impl::getViewUp() const
 {
   vector3_t up;
   this->getViewUp(up);
@@ -121,7 +122,7 @@
 }
 
 //----------------------------------------------------------------------------
-void camera_impl::getViewUp(vector3_t& up)
+void camera_impl::getViewUp(vector3_t& up) const
 {
   vtkCamera* cam = this->GetVTKCamera();
   cam->GetViewUp(up.data());
@@ -138,7 +139,7 @@
 }
 
 //----------------------------------------------------------------------------
-angle_deg_t camera_impl::getViewAngle()
+angle_deg_t camera_impl::getViewAngle() const
 {
   angle_deg_t angle;
   this->getViewAngle(angle);
@@ -146,7 +147,7 @@
 }
 
 //----------------------------------------------------------------------------
-void camera_impl::getViewAngle(angle_deg_t& angle)
+void camera_impl::getViewAngle(angle_deg_t& angle) const
 {
   vtkCamera* cam = this->GetVTKCamera();
   angle = cam->GetViewAngle();
@@ -166,7 +167,7 @@
 }
 
 //----------------------------------------------------------------------------
-camera_state_t camera_impl::getState()
+camera_state_t camera_impl::getState() const
 {
   camera_state_t state;
   this->getState(state);
@@ -174,7 +175,7 @@
 }
 
 //----------------------------------------------------------------------------
-void camera_impl::getState(camera_state_t& state)
+void camera_impl::getState(camera_state_t& state) const
 {
   vtkCamera* cam = this->GetVTKCamera();
   cam->GetPosition(state.position.data());
@@ -299,14 +300,29 @@
 //----------------------------------------------------------------------------
 camera& camera_impl::resetToBounds([[maybe_unused]] double zoomFactor)
 {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251102)
+#if __ANDROID__
+  this->Internals->SuccessfullyReset = this->Internals->VTKRenderer->ResetCamera();
+#else
+  this->Internals->SuccessfullyReset =
+    this->Internals->VTKRenderer->ResetCameraScreenSpace(zoomFactor);
+#endif
+#else
 #if __ANDROID__
   this->Internals->VTKRenderer->ResetCamera();
 #else
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20230221)
   this->Internals->VTKRenderer->ResetCameraScreenSpace(zoomFactor);
-#else
-  this->Internals->VTKRenderer->ResetCameraScreenSpace();
 #endif
+  // VTK doesn't provide a way to know if the bounds were initialized, perform rough check
+  point3_t pos = this->getPosition();
+  if (vtkMath::Distance2BetweenPoints(pos, point3_t{ 0, 0, 1 }) <= 1e-6)
+  {
+    this->Internals->SuccessfullyReset = false;
+  }
+  else
+  {
+    this->Internals->SuccessfullyReset = true;
+  }
 #endif
   this->Internals->VTKRenderer->ResetCameraClippingRange();
   return *this;
@@ -319,8 +335,15 @@
 }
 
 //----------------------------------------------------------------------------
-vtkCamera* camera_impl::GetVTKCamera()
+vtkCamera* camera_impl::GetVTKCamera() const
 {
   return this->Internals->VTKRenderer->GetActiveCamera();
 }
+
+//----------------------------------------------------------------------------
+bool camera_impl::GetSuccessfullyReset() const
+{
+  return this->Internals->SuccessfullyReset;
+}
+
 };
diff -Nru f3d-3.2.0+dfsg/library/src/engine.cxx f3d-3.4.1/library/src/engine.cxx
--- f3d-3.2.0+dfsg/library/src/engine.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/engine.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -101,8 +101,8 @@
   this->Internals->Scene =
     std::make_unique<detail::scene_impl>(*this->Internals->Options, *this->Internals->Window);
 
-  // Do not create an interactor for NONE or EXTERNAL
-  if (windowType != window::Type::NONE && windowType != window::Type::EXTERNAL)
+  // Do not create an interactor for NONE
+  if (windowType != window::Type::NONE)
   {
     this->Internals->Interactor = std::make_unique<detail::interactor_impl>(
       *this->Internals->Options, *this->Internals->Window, *this->Internals->Scene);
@@ -146,9 +146,18 @@
 }
 
 //----------------------------------------------------------------------------
-engine engine::createExternal(const context::function& getProcAddress)
+engine engine::createExternal([[maybe_unused]] const context::function& getProcAddress)
 {
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 3, 20240914)
+  throw engine::no_window_exception("VTK version < 9.4 does not support external windows");
+#else
+  if (getProcAddress == nullptr)
+  {
+    throw engine::no_window_exception(
+      "Cannot create an external window without a context function");
+  }
   return { window::Type::EXTERNAL, false, getProcAddress };
+#endif
 }
 
 //----------------------------------------------------------------------------
@@ -464,6 +473,12 @@
   libInfo.Modules["OpenEXR"] = false;
 #endif
 
+#if F3D_MODULE_WEBP
+  libInfo.Modules["WebP"] = true;
+#else
+  libInfo.Modules["WebP"] = false;
+#endif
+
 #if F3D_MODULE_UI
   libInfo.Modules["ImGui"] = true;
 #else
@@ -487,6 +502,7 @@
 
   libInfo.Copyrights.emplace_back("2019-2021 Kitware SAS");
   libInfo.Copyrights.emplace_back("2021-2025 Michael Migliore, Mathieu Westphal");
+  libInfo.Copyrights.emplace_back("2025 F3D-APP Foundation");
   libInfo.License = "BSD-3-Clause";
 
   return libInfo;
@@ -497,7 +513,7 @@
 {
   std::vector<readerInformation> readersInfo;
   const auto& plugins = factory::instance()->getPlugins();
-  for (const auto& plugin : plugins)
+  for (const auto* plugin : plugins)
   {
     for (const auto& reader : plugin->getReaders())
     {
diff -Nru f3d-3.2.0+dfsg/library/src/factory.cxx.in f3d-3.4.1/library/src/factory.cxx.in
--- f3d-3.2.0+dfsg/library/src/factory.cxx.in	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/factory.cxx.in	2026-01-08 08:41:55.000000000 +0200
@@ -70,24 +70,21 @@
 //----------------------------------------------------------------------------
 bool factory::setReaderOption(const std::string& name, const std::string& value)
 {
-  for (auto& plugin : this->Plugins)
-  {
-    for (auto& reader : plugin->getReaders())
+  // Set the reader option on the first reader that accepts it
+  return std::any_of(this->Plugins.begin(), this->Plugins.end(),
+    [&](const f3d::plugin* plugin)
     {
-      if (reader->setReaderOption(name, value))
-      {
-        return true;
-      }
-    }
-  }
-  return false;
+      const auto& readers = plugin->getReaders();
+      return std::any_of(readers.begin(), readers.end(),
+        [&](const auto& reader) { return reader->setReaderOption(name, value); });
+    });
 }
 
 //----------------------------------------------------------------------------
 std::vector<std::string> factory::getAllReaderOptionNames()
 {
   std::vector<std::string> names;
-  for (const auto& plugin : this->Plugins)
+  for (const f3d::plugin* plugin : this->Plugins)
   {
     for (const auto& reader : plugin->getReaders())
     {
@@ -110,7 +107,7 @@
 //----------------------------------------------------------------------------
 void factory::autoload()
 {
-  for (auto& [str, init] : this->StaticPluginInitializers)
+  for (const auto& [str, init] : this->StaticPluginInitializers)
   {
     this->registerOnce(init());
   }
@@ -128,7 +125,7 @@
     log::debug("  Description: " + plug->getDescription());
     log::debug("  Readers:");
 
-    for (auto read : plug->getReaders())
+    for (const auto& read : plug->getReaders())
     {
       log::debug("    " + read->getLongDescription());
     }
diff -Nru f3d-3.2.0+dfsg/library/src/image.cxx f3d-3.4.1/library/src/image.cxx
--- f3d-3.2.0+dfsg/library/src/image.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/image.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -186,6 +186,7 @@
   {
     if (!fs::exists(filePath))
     {
+      delete this->Internals;
       throw read_exception("Cannot open image " + filePath.string());
     }
 
@@ -207,11 +208,13 @@
 
     if (!this->Internals->Image)
     {
+      delete this->Internals;
       throw read_exception("Cannot read image " + filePath.string());
     }
   }
   catch (const fs::filesystem_error& ex)
   {
+    delete this->Internals;
     throw read_exception(std::string("Cannot read image: ") + ex.what());
   }
 }
diff -Nru f3d-3.2.0+dfsg/library/src/init.cxx f3d-3.4.1/library/src/init.cxx
--- f3d-3.2.0+dfsg/library/src/init.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/init.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,10 @@
 #include "vtkF3DEXRReader.h"
 #endif
 
+#if F3D_MODULE_WEBP
+#include "vtkF3DWebPReader.h"
+#endif
+
 #include <vtkImageReader2Factory.h>
 #include <vtkLogger.h>
 #include <vtkNew.h>
@@ -54,8 +58,13 @@
 #endif
 
 #if F3D_MODULE_EXR
-  vtkNew<vtkF3DEXRReader> reader;
-  vtkImageReader2Factory::RegisterReader(reader);
+  vtkNew<vtkF3DEXRReader> exrReader;
+  vtkImageReader2Factory::RegisterReader(exrReader);
+#endif
+
+#if F3D_MODULE_WEBP
+  vtkNew<vtkF3DWebPReader> webpReader;
+  vtkImageReader2Factory::RegisterReader(webpReader);
 #endif
 }
 }
diff -Nru f3d-3.2.0+dfsg/library/src/interactor_impl.cxx f3d-3.4.1/library/src/interactor_impl.cxx
--- f3d-3.2.0+dfsg/library/src/interactor_impl.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/interactor_impl.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -37,6 +37,7 @@
 #include <algorithm>
 #include <chrono>
 #include <cmath>
+#include <cstdint>
 #include <map>
 #include <numeric>
 #include <regex>
@@ -50,6 +51,11 @@
 {
 using mod_t = interaction_bind_t::ModifierKeys;
 
+bool StartWith(std::string_view str, std::string_view pattern)
+{
+  return str.rfind(pattern, 0) == 0; // To avoid dependency for C++20 starts_with
+};
+
 class interactor_impl::internals
 {
 public:
@@ -57,6 +63,14 @@
   {
     std::vector<std::string> CommandVector;
     documentation_callback_t DocumentationCallback;
+    BindingType Type;
+  };
+
+  struct CommandCallbacks
+  {
+    std::function<void(const std::vector<std::string>&)> Callback;
+    std::optional<command_documentation_t> Documentation;
+    std::function<std::vector<std::string>(const std::vector<std::string>&)> CompletionCallback;
   };
 
   internals(options& options, window_impl& window, scene_impl& scene, interactor_impl& inter)
@@ -168,7 +182,7 @@
 
   //----------------------------------------------------------------------------
   // Set the view orbit position on the viewport.
-  enum class ViewType
+  enum class ViewType : std::uint8_t
   {
     VT_FRONT,
     VT_RIGHT,
@@ -412,6 +426,8 @@
         };
 
         self->AnimateCameraTransition(interpolateCameraState);
+
+        self->Style->InvokeEvent(vtkCommand::InteractionEvent, nullptr);
       }
     }
 
@@ -508,55 +524,75 @@
       }
     }
 
+    // Update the dynamic options of the animation manager so check if the cheatsheet needs an
+    // update.
+    this->AnimationManager->UpdateDynamicOptions();
     // Always render after interaction
     this->Window.render();
   }
 
   //----------------------------------------------------------------------------
-  void StartEventLoop(double deltaTime, std::function<void()> userCallBack)
+  bool StartEventLoop(double deltaTime, std::function<void()> userCallBack)
   {
+    if (this->EventLoopObserverId != -1)
+    {
+      log::info("Interaction: event loop has already been started");
+      return false;
+    }
+
     // Trigger a render to ensure Window is ready to be configured
     this->Window.render();
 
     // Copy user callback
     this->EventLoopUserCallBack = std::move(userCallBack);
 
-    // Configure UI delta time
-    vtkRenderWindow* renWin = this->Window.GetRenderWindow();
-    vtkF3DRenderer* ren = vtkF3DRenderer::SafeDownCast(renWin->GetRenderers()->GetFirstRenderer());
-    ren->SetUIDeltaTime(deltaTime);
-
-    // Configure animation delta time
-    this->AnimationManager->SetDeltaTime(deltaTime);
-
     // Create the timer
     this->EventLoopTimerId = this->VTKInteractor->CreateRepeatingTimer(deltaTime * 1000);
 
+    this->CallbackDeltaTime = deltaTime;
+
     // Create the callback and add an observer
     vtkNew<vtkCallbackCommand> timerCallBack;
     timerCallBack->SetCallback(
       [](vtkObject*, unsigned long, void* clientData, void*)
       {
         internals* that = static_cast<internals*>(clientData);
-        that->EventLoop();
+        that->EventLoop(that->CallbackDeltaTime);
       });
     this->EventLoopObserverId =
       this->VTKInteractor->AddObserver(vtkCommand::TimerEvent, timerCallBack);
     timerCallBack->SetClientData(this);
+    return true;
   }
 
   //----------------------------------------------------------------------------
-  void StopEventLoop()
+  bool StopEventLoop()
   {
+    if (this->EventLoopObserverId == -1)
+    {
+      log::info("Interaction: event loop has not been started hence cannot be stopped");
+      return false;
+    }
     this->VTKInteractor->RemoveObserver(this->EventLoopObserverId);
     this->VTKInteractor->DestroyTimer(this->EventLoopTimerId);
     this->EventLoopObserverId = -1;
     this->EventLoopTimerId = 0;
+    return true;
   }
 
   //----------------------------------------------------------------------------
-  void EventLoop()
+  void EventLoop(double deltaTime)
   {
+    if (deltaTime <= 0)
+    {
+      log::error("Interaction: delta time should be > 0");
+      return;
+    }
+    if (this->StopRequested)
+    {
+      this->Interactor.stop();
+      return;
+    }
     if (this->EventLoopUserCallBack)
     {
       this->EventLoopUserCallBack();
@@ -568,7 +604,7 @@
       {
         // XXX: Ignore the boolean return of triggerCommand,
         // error is already logged by triggerCommand
-        this->Interactor.triggerCommand(this->CommandBuffer.value());
+        this->Interactor.triggerCommand(this->CommandBuffer.value(), false);
       }
       catch (const f3d::interactor::command_runtime_exception& ex)
       {
@@ -578,9 +614,19 @@
       this->CommandBuffer.reset();
     }
 
+    this->AnimationManager->SetDeltaTime(deltaTime);
     this->AnimationManager->Tick();
 
-    if (this->RenderRequested)
+    vtkRenderWindow* renWin = this->Window.GetRenderWindow();
+    vtkF3DRenderer* ren = vtkF3DRenderer::SafeDownCast(renWin->GetRenderers()->GetFirstRenderer());
+    ren->SetUIDeltaTime(deltaTime);
+
+    // Determine if we need a full render or just a UI render
+    // At the moment, only TAA requires a full render each frame
+    bool forceRender = (this->Options.render.effect.antialiasing.enable &&
+      this->Options.render.effect.antialiasing.mode == "taa");
+
+    if (this->RenderRequested || forceRender)
     {
       this->Window.render();
       this->RenderRequested = false;
@@ -602,9 +648,8 @@
   vtkNew<vtkF3DInteractorStyle> Style;
   vtkSmartPointer<vtkF3DInteractorEventRecorder> Recorder;
   vtkNew<vtkF3DUIObserver> UIObserver;
-  std::map<unsigned long, std::pair<int, std::function<void()>>> TimerCallBacks;
 
-  std::map<std::string, std::function<void(const std::vector<std::string>&)>> Commands;
+  std::map<std::string, CommandCallbacks> Commands;
   std::optional<std::string> CommandBuffer;
 
   std::map<interaction_bind_t, BindingCommands> Bindings;
@@ -625,6 +670,9 @@
   unsigned long EventLoopTimerId = 0;
   int EventLoopObserverId = -1;
   std::atomic<bool> RenderRequested = false;
+  std::atomic<bool> StopRequested = false;
+
+  double CallbackDeltaTime = 1.0 / 30; /* Default DeltaTime (30fps) */
 };
 
 //----------------------------------------------------------------------------
@@ -641,31 +689,72 @@
 #if F3D_MODULE_UI
   vtkF3DImguiConsole* console = vtkF3DImguiConsole::SafeDownCast(vtkOutputWindow::GetInstance());
   assert(console != nullptr);
-  // Set the callback to get the list of commands
-  console->SetCommandsMatchCallback(
+  console->SetCompletionCallback(
     [this](const std::string& pattern)
     {
-      // Build a list of candidates
+      // Create a vector of candidate strings
       std::vector<std::string> candidates;
-      // Copy all commands that start with the pattern
-      auto startWith = [&pattern](const std::string& s)
+
+      // Tokenize the complete command
+      std::vector<std::string> tokens;
+      try
       {
-        return s.rfind(pattern, 0) == 0; // To avoid dependency for C++20 starts_with
-      };
-      for (auto const& [action, callback] : this->Internals->Commands)
+        tokens = utils::tokenize(pattern);
+      }
+      catch (const utils::tokenize_exception&)
       {
-        if (startWith(action))
-        {
-          candidates.push_back(action);
-        }
-        else
+        log::error("Command: unable to tokenize command:\"", pattern, "\", ignoring");
+        return candidates;
+      }
+
+      // Recover the action pattern if any
+      std::string actionPattern;
+      if (!tokens.empty())
+      {
+        actionPattern = tokens[0];
+      }
+
+      bool exact = false;
+      for (auto const& [action, callbacks] : this->Internals->Commands)
+      {
+        if (f3d::detail::StartWith(action, actionPattern))
         {
-          // List is sorted so we can break early
-          if (!candidates.empty())
+          // Copy all action that start with the pattern
+          candidates.emplace_back(action);
+          if (action == actionPattern)
           {
-            break;
+            // pattern is the actual action
+            exact = true;
           }
         }
+        // List is sorted so we can break early
+        else if (!candidates.empty())
+        {
+          break;
+        }
+      }
+
+      // If exact and the pattern is not exactly the actionPattern
+      // then it means that the command currently looks like:
+      // `action val` or `action `, so we dispatch completion to the action completer if any
+      if (exact && actionPattern != pattern)
+      {
+        candidates.clear();
+        auto complCallback = this->Internals->Commands.at(actionPattern).CompletionCallback;
+        if (complCallback)
+        {
+          // Use the completion callback of the action with its args if any
+          std::vector<std::string> argsCandidates =
+            complCallback({ tokens.begin() + 1, tokens.end() });
+          std::transform(argsCandidates.begin(), argsCandidates.end(),
+            std::back_inserter(candidates),
+            [&](const auto& argCandidate) { return actionPattern + " " + argCandidate; });
+        }
+      }
+      else if (candidates.size() == 1)
+      {
+        // Single candidate, add a space separator
+        candidates[0] += " ";
       }
       return candidates;
     });
@@ -695,29 +784,140 @@
     }
   };
 
+  // Completion method for a vector of names
+  auto complNames = [](const std::vector<std::string>& args, const std::vector<std::string>& names,
+                      size_t indexToCheck = 0)
+  {
+    std::vector<std::string> candidates;
+    if (args.size() < indexToCheck + 1)
+    {
+      // No arguments, return all option names
+      return names;
+    }
+    else if (args.size() > indexToCheck + 1)
+    {
+      // Multi arguments, do not complete
+      return candidates;
+    }
+
+    // Recover all names that starts with args[indexToCheck]
+    std::copy_if(names.begin(), names.end(), std::back_inserter(candidates),
+      [&](const std::string& name) { return f3d::detail::StartWith(name, args[indexToCheck]); });
+
+    // Create an arg pattern before the indexToCheck
+    std::string argPattern;
+    for (std::size_t i = 0; i < indexToCheck; i++)
+    {
+      argPattern += args[i] + " ";
+    }
+    // Include it in front of the candidates
+    std::transform(candidates.begin(), candidates.end(), candidates.begin(),
+      [&](const auto& argCandidate) { return argPattern + argCandidate; });
+
+    if (candidates.size() == 1)
+    {
+      // Single candidate, add a space separator
+      candidates[0] += " ";
+    }
+    return candidates;
+  };
+
+  auto complCommandActions = [&](const std::vector<std::string>& args)
+  { return complNames(args, this->getCommandActions()); };
+
+  auto complOptionNames = [&](const std::vector<std::string>& args)
+  { return complNames(args, this->Internals->Options.getAllNames()); };
+
+  static const std::map<std::string, std::vector<std::string>> COMPL_OPTIONS_SET = {
+    { "model.point_sprites.type", { "sphere", "gaussian" } },
+    { "render.effect.antialiasing.mode", { "fxaa", "ssaa", "taa" } },
+    { "render.effect.blending.mode", { "ddp", "sort", "stochastic" } },
+  };
+  auto complOptionSet = [&](const std::vector<std::string>& args)
+  {
+    std::vector<std::string> optionNames = this->Internals->Options.getAllNames();
+    std::vector<std::string> candidates;
+    if (args.size() == 0)
+    {
+      // No args, return all option names
+      return optionNames;
+    }
+    else if (args.size() == 1)
+    {
+      // One arg, check if its an option
+      if (std::find(optionNames.begin(), optionNames.end(), args[0]) != optionNames.end())
+      {
+        // Its an existing option, check if it should be completed
+        const auto it = COMPL_OPTIONS_SET.find(args[0]);
+        if (it != COMPL_OPTIONS_SET.end())
+        {
+          // Transform potential values into found option
+          std::transform(std::begin(it->second), std::end(it->second),
+            std::back_inserter(candidates),
+            [&](const auto& value) { return args[0] + " " + value; });
+        }
+        else
+        {
+          // no value completion, return option by itself
+          candidates.emplace_back(args[0]);
+        }
+      }
+      else
+      {
+        // Not an existing option, try completing with option names
+        return complNames(args, optionNames);
+      }
+    }
+    else
+    {
+      // Complete the option value if possible
+      const auto it = COMPL_OPTIONS_SET.find(args[0]);
+      if (it != COMPL_OPTIONS_SET.end())
+      {
+        return complNames(args, it->second, 1);
+      }
+    }
+
+    if (candidates.size() == 1)
+    {
+      // Single candidate, add a space separator
+      candidates[0] += " ";
+    }
+    return candidates;
+  };
+
   // Add default callbacks
-  this->addCommand("set",
+  this->addCommand(
+    "set",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 2, "set");
       this->Internals->Options.setAsString(args[0], args[1]);
-    });
+    },
+    command_documentation_t{ "set option.name values", "set a libf3d option" }, complOptionSet);
 
-  this->addCommand("toggle",
+  this->addCommand(
+    "toggle",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "toggle");
       this->Internals->Options.toggle(args[0]);
-    });
+    },
+    command_documentation_t{ "toggle option.name", "toggle a boolean libf3d option" },
+    complOptionNames);
 
-  this->addCommand("reset",
+  this->addCommand(
+    "reset",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "reset");
       this->Internals->Options.reset(args[0]);
-    });
+    },
+    command_documentation_t{ "reset option.name", "reset a libf3d option to its default values" },
+    complOptionNames);
 
-  this->addCommand("clear",
+  this->addCommand(
+    "clear",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 0, "clear");
@@ -727,26 +927,38 @@
       assert(console != nullptr);
       console->Clear();
 #endif
-    });
+    },
+    command_documentation_t{ "clear", "clear console" });
 
-  this->addCommand("print",
+  this->addCommand(
+    "print",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "print");
       log::info(this->Internals->Options.getAsString(args[0]));
-    });
+    },
+    command_documentation_t{ "print option.name", "print the value of an libf3d option" },
+    complOptionNames);
 
-  this->addCommand("set_reader_option",
+  this->addCommand(
+    "set_reader_option",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 2, "set_reader_option");
       f3d::engine::setReaderOption(args[0], args[1]);
-    });
-
-  this->addCommand("cycle_animation",
-    [&](const std::vector<std::string>&) { this->Internals->AnimationManager->CycleAnimation(); });
+    },
+    command_documentation_t{
+      "set_reader_option ReaderName.option_name value", "set a reader option" },
+    std::bind(complNames, std::placeholders::_1, f3d::engine::getAllReaderOptionNames()));
+
+  this->addCommand(
+    "cycle_animation",
+    [&](const std::vector<std::string>&) { this->Internals->AnimationManager->CycleAnimation(); },
+    command_documentation_t{
+      "cycle_animation", "cycle scene.animation.index option using model information" });
 
-  this->addCommand("cycle_anti_aliasing",
+  this->addCommand(
+    "cycle_anti_aliasing",
     [&](const std::vector<std::string>&)
     {
       bool& enabled = this->Internals->Options.render.effect.antialiasing.enable;
@@ -762,15 +974,54 @@
         {
           mode = "ssaa";
         }
+        else if (mode == "ssaa")
+        {
+          mode = "taa";
+        }
         else
         {
           enabled = false;
         }
       }
       this->Internals->Window.render();
-    });
+    },
+    command_documentation_t{
+      "cycle_anti_aliasing", "cycle between the anti-aliasing method (none,fxaa,ssaa,taa)" });
 
-  this->addCommand("cycle_coloring",
+  this->addCommand(
+    "cycle_blending",
+    [&](const std::vector<std::string>&)
+    {
+      bool& enabled = this->Internals->Options.render.effect.blending.enable;
+      std::string& mode = this->Internals->Options.render.effect.blending.mode;
+      if (!enabled)
+      {
+        enabled = true;
+        mode = "ddp";
+      }
+      else
+      {
+        if (mode == "ddp")
+        {
+          mode = "sort";
+        }
+        else if (mode == "sort")
+        {
+          mode = "stochastic";
+        }
+        else
+        {
+          enabled = false;
+        }
+      }
+      this->Internals->Window.render();
+    },
+    command_documentation_t{
+      "cycle_blending", "cycle between the blending method (none,ddp,sort,stochastic)" });
+
+  std::vector<std::string> cycleColoringValidArgs = { "field", "array", "component" };
+  this->addCommand(
+    "cycle_coloring",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "cycle_coloring");
@@ -797,57 +1048,119 @@
       }
       this->Internals->SynchronizeScivisOptions(this->Internals->Options, ren);
       this->Internals->Window.PrintColoringDescription(log::VerboseLevel::DEBUG);
-    });
+    },
+    command_documentation_t{
+      "cycle_coloring field/array/component", "cycle scivis options using model information" },
+    std::bind(complNames, std::placeholders::_1,
+      std::vector<std::string>{ "field", "array", "component" }));
 
-  this->addCommand("roll_camera",
+  this->addCommand(
+    "cycle_point_sprites",
+    [&](const std::vector<std::string>&)
+    {
+      bool& enabled = this->Internals->Options.model.point_sprites.enable;
+      std::string& type = this->Internals->Options.model.point_sprites.type;
+      if (!enabled)
+      {
+        enabled = true;
+        type = "sphere";
+      }
+      else
+      {
+        if (type == "sphere")
+        {
+          type = "gaussian";
+        }
+        else
+        {
+          enabled = false;
+        }
+      }
+      this->Internals->Window.render();
+    },
+    command_documentation_t{
+      "cycle_point_sprites", "cycle between the point sprite types (none,sphere,gaussian)" });
+
+  this->addCommand(
+    "roll_camera",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "roll_camera");
       this->Internals->Window.getCamera().roll(options::parse<int>(args[0]));
       this->Internals->Style->SetTemporaryUp(
         this->Internals->Window.getCamera().getViewUp().data());
+    },
+    command_documentation_t{ "roll_camera value", "roll the camera on its side" });
+
+  this->addCommand("jump_to_frame",
+    [&](const std::vector<std::string>& args)
+    {
+      check_args(args, 2, "jump_to_frame");
+      const int frame = options::parse<int>(args[0]);
+      const bool relative = options::parse<bool>(args[1]);
+      this->Internals->AnimationManager->SetDeltaTime(this->Internals->CallbackDeltaTime);
+      this->Internals->AnimationManager->JumpToFrame(frame, relative);
     });
 
-  this->addCommand("elevation_camera",
+  this->addCommand(
+    "elevation_camera",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "elevation_camera");
       this->Internals->Window.getCamera().elevation(options::parse<int>(args[0]));
       this->Internals->Style->SetTemporaryUp(
         this->Internals->Window.getCamera().getViewUp().data());
-    });
+    },
+    command_documentation_t{ "elevation_camera value", "tilt the camera up or down" });
 
-  this->addCommand("azimuth_camera",
+  this->addCommand(
+    "azimuth_camera",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "azimuth_camera");
       this->Internals->Window.getCamera().azimuth(options::parse<int>(args[0]));
       this->Internals->Style->SetTemporaryUp(
         this->Internals->Window.getCamera().getViewUp().data());
-    });
-
-  this->addCommand("increase_light_intensity",
-    [&](const std::vector<std::string>&) { this->Internals->IncreaseLightIntensity(false); });
-
-  this->addCommand("decrease_light_intensity",
-    [&](const std::vector<std::string>&) { this->Internals->IncreaseLightIntensity(true); });
-
-  this->addCommand("increase_opacity",
-    [&](const std::vector<std::string>&) { this->Internals->IncreaseOpacity(false); });
-
-  this->addCommand("decrease_opacity",
-    [&](const std::vector<std::string>&) { this->Internals->IncreaseOpacity(true); });
-
-  this->addCommand("print_scene_info", [&](const std::vector<std::string>&)
-    { this->Internals->Window.PrintSceneDescription(log::VerboseLevel::INFO); });
+    },
+    command_documentation_t{ "azimuth_camera value", "tilt the camera right or left" });
 
-  this->addCommand("print_coloring_info", [&](const std::vector<std::string>&)
-    { this->Internals->Window.PrintColoringDescription(log::VerboseLevel::INFO); });
+  this->addCommand(
+    "increase_light_intensity",
+    [&](const std::vector<std::string>&) { this->Internals->IncreaseLightIntensity(false); },
+    command_documentation_t{ "increase_light_intensity", "increase light intensity" });
+
+  this->addCommand(
+    "decrease_light_intensity",
+    [&](const std::vector<std::string>&) { this->Internals->IncreaseLightIntensity(true); },
+    command_documentation_t{ "decrease_light_intensity", "decrease light intensity" });
+
+  this->addCommand(
+    "increase_opacity",
+    [&](const std::vector<std::string>&) { this->Internals->IncreaseOpacity(false); },
+    command_documentation_t{ "increase_opacity", "increase opacity" });
+
+  this->addCommand(
+    "decrease_opacity",
+    [&](const std::vector<std::string>&) { this->Internals->IncreaseOpacity(true); },
+    command_documentation_t{ "decrease_opacity", "decrease opacity" });
+
+  this->addCommand(
+    "print_scene_info", [&](const std::vector<std::string>&)
+    { this->Internals->Window.PrintSceneDescription(log::VerboseLevel::INFO); },
+    command_documentation_t{ "print_scene_info", "print information about the scene" });
+
+  this->addCommand(
+    "print_coloring_info", [&](const std::vector<std::string>&)
+    { this->Internals->Window.PrintColoringDescription(log::VerboseLevel::INFO); },
+    command_documentation_t{ "print_coloring_info", "print information about coloring settings" });
+
+  this->addCommand(
+    "print_mesh_info", [&](const std::vector<std::string>&)
+    { this->Internals->Scene.PrintImporterDescription(log::VerboseLevel::INFO); },
+    command_documentation_t{ "print_mesh_info", "print information from the mesh importer" });
 
-  this->addCommand("print_mesh_info", [&](const std::vector<std::string>&)
-    { this->Internals->Scene.PrintImporterDescription(log::VerboseLevel::INFO); });
-
-  this->addCommand("print_options_info",
+  this->addCommand(
+    "print_options_info",
     [&](const std::vector<std::string>&)
     {
       for (const std::string& option : this->Internals->Options.getNames())
@@ -857,9 +1170,11 @@
         descr.append(option).append(": ").append(val);
         log::print(log::VerboseLevel::INFO, descr);
       }
-    });
+    },
+    command_documentation_t{ "print_options_info", "print libf3d options that have a value" });
 
-  this->addCommand("set_camera",
+  this->addCommand(
+    "set_camera",
     [&](const std::vector<std::string>& args)
     {
       check_args(args, 1, "set_camera");
@@ -889,36 +1204,57 @@
         throw interactor::invalid_args_exception(
           std::string("Command: set_camera arg:\"") + std::string(type) + "\" is not recognized.");
       }
-    });
+    },
+    command_documentation_t{
+      "set_camera front/top/right/isometric", "position the camera in the specified location" },
+    std::bind(complNames, std::placeholders::_1,
+      std::vector<std::string>{ "front", "top", "right", "isometric" }));
 
-  this->addCommand("toggle_volume_rendering",
+  this->addCommand(
+    "toggle_volume_rendering",
     [&](const std::vector<std::string>&)
     {
       this->Internals->Options.model.volume.enable = !this->Internals->Options.model.volume.enable;
       this->Internals->Window.render();
       this->Internals->Window.PrintColoringDescription(log::VerboseLevel::DEBUG);
-    });
-
-  this->addCommand("stop_interactor", [&](const std::vector<std::string>&) { this->stop(); });
+    },
+    command_documentation_t{
+      "toggle_volume_rendering", "toggle model.volume.enable and print coloring information" });
+
+  this->addCommand(
+    "stop_interactor", [&](const std::vector<std::string>&) { this->stop(); },
+    command_documentation_t{ "stop_interactor", "stop the interactor hence quit the application" });
 
-  this->addCommand("reset_camera",
+  this->addCommand(
+    "reset_camera",
     [&](const std::vector<std::string>&)
     {
       this->Internals->Window.getCamera().resetToDefault();
       this->Internals->Style->ResetTemporaryUp();
-    });
-
-  this->addCommand("toggle_animation",
-    [&](const std::vector<std::string>&) { this->Internals->AnimationManager->ToggleAnimation(); });
+    },
+    command_documentation_t{ "reset_camera", "reset the camera to its original location" });
 
-  this->addCommand("add_files",
+  this->addCommand(
+    "toggle_animation", [&](const std::vector<std::string>&) { this->toggleAnimation(); },
+    command_documentation_t{ "toggle_animation", "start/stop the animation" });
+
+  this->addCommand(
+    "toggle_animation_backward",
+    [&](const std::vector<std::string>&) { this->toggleAnimation(AnimationDirection::BACKWARD); },
+    command_documentation_t{ "toggle_animation_backward", "start/stop the animation backward" });
+
+  // XXX: No filesystem completion, F3DStarter add its own command anyway
+  this->addCommand(
+    "add_files",
     [&](const std::vector<std::string>& files)
     {
       this->Internals->AnimationManager->StopAnimation();
       this->Internals->Scene.add(files);
-    });
+    },
+    command_documentation_t{ "add_files", "add files to the scene" });
 
-  this->addCommand("alias",
+  this->addCommand(
+    "alias",
     [&](const std::vector<std::string>& args)
     {
       if (args.size() < 2)
@@ -936,9 +1272,11 @@
 
       log::info(
         "Alias " + aliasName + " added with command " + this->Internals->AliasMap[aliasName]);
-    });
+    },
+    command_documentation_t{ "alias", "create an alias for a command" });
 
-  this->addCommand("cycle_verbose_level",
+  this->addCommand(
+    "cycle_verbose_level",
     [&](const std::vector<std::string>&)
     {
       log::VerboseLevel currentLevel = log::getVerboseLevel();
@@ -954,16 +1292,47 @@
       ren->SetCheatSheetConfigured(false);
 
       log::info("Verbose level changed to: ", this->Internals->VerboseLevelToString(newLevel));
-    });
+    },
+    command_documentation_t{ "cycle_verbose_level", "cycle between verbose levels" });
+
+  this->addCommand(
+    "help",
+    [&](const std::vector<std::string>& args)
+    {
+      check_args(args, 1, "help");
+      const auto it = this->Internals->Commands.find(args[0]);
+      if (it != this->Internals->Commands.end())
+      {
+        if (it->second.Documentation)
+        {
+          log::print(log::VerboseLevel::INFO, it->second.Documentation->first, ": ",
+            it->second.Documentation->second);
+        }
+        else
+        {
+          log::print(log::VerboseLevel::INFO, "No documentation");
+        }
+      }
+      else
+      {
+        throw interactor::invalid_args_exception(
+          std::string("Command: help arg:\"") + args[0] + "\" is not a recognized command.");
+      }
+    },
+    command_documentation_t{ "help action", "print help about a specific command action" },
+    complCommandActions);
+
   return *this;
 }
 
 //----------------------------------------------------------------------------
-interactor& interactor_impl::addCommand(
-  std::string action, std::function<void(const std::vector<std::string>&)> callback)
+interactor& interactor_impl::addCommand(const std::string& action,
+  std::function<void(const std::vector<std::string>&)> callback,
+  std::optional<command_documentation_t> doc,
+  std::function<std::vector<std::string>(const std::vector<std::string>&)> completionCallback)
 {
-  const auto [it, success] =
-    this->Internals->Commands.insert({ std::move(action), std::move(callback) });
+  const auto [it, success] = this->Internals->Commands.insert(
+    { action, { std::move(callback), std::move(doc), std::move(completionCallback) } });
   if (!success)
   {
     throw interactor::already_exists_exception(
@@ -983,7 +1352,7 @@
 std::vector<std::string> interactor_impl::getCommandActions() const
 {
   std::vector<std::string> actions;
-  for (auto const& [action, callback] : this->Internals->Commands)
+  for (auto const& [action, callbacks] : this->Internals->Commands)
   {
     actions.emplace_back(action);
   }
@@ -991,7 +1360,14 @@
 }
 
 //----------------------------------------------------------------------------
-bool interactor_impl::triggerCommand(std::string_view command)
+interactor& interactor_impl::triggerEventLoop(double deltaTime)
+{
+  this->Internals->EventLoop(deltaTime);
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+bool interactor_impl::triggerCommand(std::string_view command, bool keepComments)
 {
   log::debug("Command: ", command);
 
@@ -1005,7 +1381,7 @@
   std::vector<std::string> tokens;
   try
   {
-    tokens = utils::tokenize(command);
+    tokens = utils::tokenize(command, keepComments);
   }
   catch (const utils::tokenize_exception&)
   {
@@ -1025,7 +1401,7 @@
     auto callbackIt = this->Internals->Commands.find(action);
     if (callbackIt != this->Internals->Commands.end())
     {
-      callbackIt->second({ tokens.begin() + 1, tokens.end() });
+      callbackIt->second.Callback({ tokens.begin() + 1, tokens.end() });
       return true;
     }
     else
@@ -1100,28 +1476,60 @@
     {
       desc = this->Internals->Options.render.effect.antialiasing.mode;
     }
-    return std::pair("Cycle anti-aliasing", std::move(desc));
+    return std::pair("Anti-aliasing", std::move(desc));
+  };
+
+  // "Cycle point sprites" , "none/sphere/gaussian"
+  auto docPS = [&]()
+  {
+    std::string desc;
+    if (!this->Internals->Options.model.point_sprites.enable)
+    {
+      desc = "none";
+    }
+    else
+    {
+      desc = this->Internals->Options.model.point_sprites.type;
+    }
+    return std::pair("Point sprites", std::move(desc));
+  };
+
+  // "Cycle blending" , "none/ddp/sort/stochastic"
+  auto docBlend = [&]()
+  {
+    std::string desc;
+    if (!this->Internals->Options.render.effect.blending.enable)
+    {
+      desc = "none";
+    }
+    else
+    {
+      desc = this->Internals->Options.render.effect.blending.mode;
+    }
+    return std::pair("Blending", std::move(desc));
   };
 
   // "Cycle animation" , "animationName"
   auto docAnim = [&]()
-  { return std::pair("Cycle animation", this->Internals->AnimationManager->GetAnimationName()); };
+  { return std::pair("Animation", this->Internals->AnimationManager->GetAnimationName()); };
 
   // "Cycle point/cell data coloring" , "POINT/CELL"
   auto docField = [&]()
-  {
-    return std::pair(
-      std::string("Cycle point/cell data coloring"), (opts.model.scivis.cells ? "CELL" : "POINT"));
-  };
+  { return std::pair(std::string("Data coloring"), (opts.model.scivis.cells ? "CELL" : "POINT")); };
 
   // "Cycle array to color with" , "arrayName"
   auto docArray = [&]()
   {
-    return std::pair("Cycle array to color with",
+    // enable + no array : ON
+    // enable + array : array
+    // no enable + array : array (forced)
+    // no enable + no array : OFF
+    return std::pair("Color array",
       (opts.model.scivis.array_name.has_value()
           ? shortName(opts.model.scivis.array_name.value(), 15) +
             (opts.model.scivis.enable ? "" : " (forced)")
-          : "OFF"));
+          : opts.model.scivis.enable ? "ON"
+                                     : "OFF"));
   };
 
   // "Cycle component to color with" , "component"
@@ -1129,8 +1537,7 @@
   {
     vtkRenderWindow* renWin = this->Internals->Window.GetRenderWindow();
     vtkF3DRenderer* ren = vtkF3DRenderer::SafeDownCast(renWin->GetRenderers()->GetFirstRenderer());
-    return std::pair(
-      "Cycle component to color with", ren->ComponentToString(opts.model.scivis.component));
+    return std::pair("Color component", ren->ComponentToString(opts.model.scivis.component));
   };
 
   // "doc", ""
@@ -1175,62 +1582,67 @@
   auto docVerbose = [&]()
   {
     return std::pair(
-      "Cycle verbose level", this->Internals->VerboseLevelToString(log::getVerboseLevel()));
+      "Verbose level", this->Internals->VerboseLevelToString(log::getVerboseLevel()));
   };
 
   // clang-format off
-  this->addBinding({mod_t::NONE, "W"}, "cycle_animation", "Scene", docAnim);
-  this->addBinding({mod_t::NONE, "C"}, "cycle_coloring field", "Scene", docField);
-  this->addBinding({mod_t::NONE, "S"}, "cycle_coloring array", "Scene", docArray);
-  this->addBinding({mod_t::NONE, "Y"}, "cycle_coloring component", "Scene", docComp);
-  this->addBinding({mod_t::NONE, "B"}, "toggle ui.scalar_bar", "Scene", std::bind(docTgl, "Toggle the scalar bar display", std::cref(opts.ui.scalar_bar)));
-  this->addBinding({mod_t::NONE, "P"}, "toggle render.effect.translucency_support", "Scene", std::bind(docTgl, "Toggle Translucency", std::cref(opts.render.effect.translucency_support)));
-  this->addBinding({mod_t::NONE, "Q"}, "toggle render.effect.ambient_occlusion","Scene", std::bind(docTgl, "Toggle ambient occlusion", std::cref(opts.render.effect.ambient_occlusion)));
-  this->addBinding({mod_t::NONE, "A"}, "cycle_anti_aliasing","Scene", docAA);
-  this->addBinding({mod_t::NONE, "T"}, "toggle render.effect.tone_mapping","Scene", std::bind(docTgl, "Toggle tone mapping", std::cref(opts.render.effect.tone_mapping)));
-  this->addBinding({mod_t::NONE, "E"}, "toggle render.show_edges","Scene", std::bind(docTglOpt, "Toggle edges display", std::cref(opts.render.show_edges)));
-  this->addBinding({mod_t::NONE, "X"}, "toggle ui.axis","Scene", std::bind(docTgl, "Toggle axes display", std::cref(opts.ui.axis)));
-  this->addBinding({mod_t::NONE, "G"}, "toggle render.grid.enable","Scene", std::bind(docTgl, "Toggle grid display", std::cref(opts.render.grid.enable)));
+  this->addBinding({mod_t::NONE, "W"}, "cycle_animation", "Scene", docAnim, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "C"}, "cycle_coloring field", "Scene", docField, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "S"}, "cycle_coloring array", "Scene", docArray, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "Y"}, "cycle_coloring component", "Scene", docComp, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "B"}, "toggle ui.scalar_bar", "Scene", std::bind(docTgl, "Scalar bar", std::cref(opts.ui.scalar_bar)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "P"}, "cycle_blending", "Scene", docBlend, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "Q"}, "toggle render.effect.ambient_occlusion","Scene", std::bind(docTgl, "Ambient occlusion", std::cref(opts.render.effect.ambient_occlusion)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "A"}, "cycle_anti_aliasing","Scene", docAA, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "T"}, "toggle render.effect.tone_mapping","Scene", std::bind(docTgl, "Toggle tone mapping", std::cref(opts.render.effect.tone_mapping)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "E"}, "toggle render.show_edges","Scene", std::bind(docTglOpt, "Toggle edges display", std::cref(opts.render.show_edges)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "X"}, "toggle ui.axis","Scene", std::bind(docTgl, "Toggle axes display", std::cref(opts.ui.axis)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "G"}, "toggle render.grid.enable","Scene", std::bind(docTgl, "Toggle grid display", std::cref(opts.render.grid.enable)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::SHIFT, "X"}, "toggle render.axes_grid.enable", "Scene", std::bind(docTgl, "Toggle axes grid display", std::cref(opts.render.axes_grid.enable)), f3d::interactor::BindingType::TOGGLE);
 #if F3D_MODULE_UI
-  this->addBinding({mod_t::NONE, "N"}, "toggle ui.filename","Scene", std::bind(docTgl, "Toggle filename display", std::cref(opts.ui.filename)));
-  this->addBinding({mod_t::NONE, "M"}, "toggle ui.metadata","Scene", std::bind(docTgl, "Toggle metadata display", std::cref(opts.ui.metadata)));
-  this->addBinding({mod_t::NONE, "Z"}, "toggle ui.fps","Scene", std::bind(docTgl, "Toggle FPS counter display", std::cref(opts.ui.fps)));
+  this->addBinding({mod_t::NONE, "N"}, "toggle ui.filename","Scene", std::bind(docTgl, "Filename", std::cref(opts.ui.filename)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "M"}, "toggle ui.metadata","Scene", std::bind(docTgl, "Metadata", std::cref(opts.ui.metadata)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::SHIFT, "N"}, "toggle ui.hdri_filename","Scene", std::bind(docTgl, "HDRI filename", std::cref(opts.ui.hdri_filename)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "Z"}, "toggle ui.fps","Scene", std::bind(docTgl, "FPS Counter", std::cref(opts.ui.fps)), f3d::interactor::BindingType::TOGGLE);
 #endif
 #if F3D_MODULE_RAYTRACING
-  this->addBinding({mod_t::NONE, "R"}, "toggle render.raytracing.enable","Scene", std::bind(docTgl, "Toggle raytracing rendering", std::cref(opts.render.raytracing.enable)));
-  this->addBinding({mod_t::NONE, "D"}, "toggle render.raytracing.denoise","Scene", std::bind(docTgl, "Toggle denoising when raytracing", std::cref(opts.render.raytracing.denoise)));
+  this->addBinding({mod_t::NONE, "R"}, "toggle render.raytracing.enable","Scene", std::bind(docTgl, "Raytracing rendering", std::cref(opts.render.raytracing.enable)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "D"}, "toggle render.raytracing.denoise","Scene", std::bind(docTgl, "Denoising when raytracing", std::cref(opts.render.raytracing.denoise)), f3d::interactor::BindingType::TOGGLE);
 #endif
-  this->addBinding({mod_t::NONE, "V"}, "toggle_volume_rendering","Scene", std::bind(docTgl, "Toggle volume rendering", std::cref(opts.model.volume.enable)));
-  this->addBinding({mod_t::NONE, "I"}, "toggle model.volume.inverse","Scene", std::bind(docTgl, "Toggle inverse volume opacity", std::cref(opts.model.volume.inverse)));
-  this->addBinding({mod_t::NONE, "O"}, "toggle model.point_sprites.enable","Scene", std::bind(docTgl, "Toggle point sprites rendering", std::cref(opts.model.point_sprites.enable)));
-  this->addBinding({mod_t::NONE, "U"}, "toggle render.background.blur.enable","Scene", std::bind(docTgl, "Toggle blur background", std::cref(opts.render.background.blur.enable)));
-  this->addBinding({mod_t::NONE, "K"}, "toggle interactor.trackball","Scene", std::bind(docTgl, "Toggle trackball interaction", std::cref(opts.interactor.trackball)));
-  this->addBinding({mod_t::NONE, "F"}, "toggle render.hdri.ambient","Scene", std::bind(docTgl, "Toggle HDRI ambient lighting", std::cref(opts.render.hdri.ambient)));
-  this->addBinding({mod_t::NONE, "J"}, "toggle render.background.skybox","Scene", std::bind(docTgl, "Toggle HDRI skybox", std::cref(opts.render.background.skybox)));
-  this->addBinding({mod_t::NONE, "L"}, "increase_light_intensity", "Scene", std::bind(docDbl, "Increase lights intensity", std::cref(opts.render.light.intensity)));
-  this->addBinding({mod_t::SHIFT, "L"}, "decrease_light_intensity", "Scene", std::bind(docDbl, "Decrease lights intensity", std::cref(opts.render.light.intensity)));
-  this->addBinding({mod_t::CTRL, "P"}, "increase_opacity", "Scene", std::bind(docDblOpt, "Increase opacity", std::cref(opts.model.color.opacity)));
-  this->addBinding({mod_t::SHIFT, "P"}, "decrease_opacity", "Scene", std::bind(docDblOpt, "Decrease opacity", std::cref(opts.model.color.opacity)));
-  this->addBinding({mod_t::SHIFT, "A"}, "toggle render.armature.enable","Scene", std::bind(docTgl, "Toggle armature", std::cref(opts.render.armature.enable)));
+  this->addBinding({mod_t::NONE, "V"}, "toggle_volume_rendering","Scene", std::bind(docTgl, "Volume rendering", std::cref(opts.model.volume.enable)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "I"}, "toggle model.volume.inverse","Scene", std::bind(docTgl, "Inverse volume opacity", std::cref(opts.model.volume.inverse)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "O"}, "cycle_point_sprites","Scene", docPS, f3d::interactor::BindingType::CYCLIC);
+  this->addBinding({mod_t::NONE, "U"}, "toggle render.background.blur.enable","Scene", std::bind(docTgl, "Blur background", std::cref(opts.render.background.blur.enable)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "K"}, "toggle interactor.trackball","Scene", std::bind(docTgl, "Trackball interaction", std::cref(opts.interactor.trackball)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "F"}, "toggle render.hdri.ambient","Scene", std::bind(docTgl, "HDRI ambient lighting", std::cref(opts.render.hdri.ambient)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "J"}, "toggle render.background.skybox","Scene", std::bind(docTgl, "HDRI skybox", std::cref(opts.render.background.skybox)), f3d::interactor::BindingType::TOGGLE);
+  this->addBinding({mod_t::NONE, "L"}, "increase_light_intensity", "Scene", std::bind(docDbl, "Increase lights intensity", std::cref(opts.render.light.intensity)), f3d::interactor::BindingType::NUMERICAL);
+  this->addBinding({mod_t::SHIFT, "L"}, "decrease_light_intensity", "Scene", std::bind(docDbl, "Decrease lights intensity", std::cref(opts.render.light.intensity)), f3d::interactor::BindingType::NUMERICAL);
+  this->addBinding({mod_t::CTRL, "P"}, "increase_opacity", "Scene", std::bind(docDblOpt, "Increase opacity", std::cref(opts.model.color.opacity)), f3d::interactor::BindingType::NUMERICAL);
+  this->addBinding({mod_t::SHIFT, "P"}, "decrease_opacity", "Scene", std::bind(docDblOpt, "Decrease opacity", std::cref(opts.model.color.opacity)), f3d::interactor::BindingType::NUMERICAL);
+  this->addBinding({mod_t::SHIFT, "A"}, "toggle render.armature.enable","Scene", std::bind(docTgl, "Armature", std::cref(opts.render.armature.enable)), f3d::interactor::BindingType::TOGGLE);
   this->addBinding({mod_t::ANY, "1"}, "set_camera front", "Camera", std::bind(docStr, "Front View camera"));
   this->addBinding({mod_t::ANY, "2"}, "elevation_camera -90", "Camera", std::bind(docStr, "Rotate camera down"));
   this->addBinding({mod_t::ANY, "3"}, "set_camera right", "Camera", std::bind(docStr, "Right View camera"));
   this->addBinding({mod_t::ANY, "4"}, "roll_camera -90", "Camera", std::bind(docStr, "Rotate camera right"));
-  this->addBinding({mod_t::ANY, "5"}, "toggle scene.camera.orthographic", "Camera", std::bind(docTglOpt, "Toggle Orthographic Projection", std::cref(opts.scene.camera.orthographic)));
+  this->addBinding({mod_t::ANY, "5"}, "toggle scene.camera.orthographic", "Camera", std::bind(docTglOpt, "Orthographic Projection", std::cref(opts.scene.camera.orthographic)), f3d::interactor::BindingType::TOGGLE);
   this->addBinding({mod_t::ANY, "6"}, "roll_camera 90", "Camera", std::bind(docStr, "Rotate camera left"));
   this->addBinding({mod_t::ANY, "7"}, "set_camera top", "Camera", std::bind(docStr, "Top View camera"));
   this->addBinding({mod_t::ANY, "8"}, "elevation_camera 90", "Camera", std::bind(docStr, "Rotate camera up"));
   this->addBinding({mod_t::ANY, "9"}, "set_camera isometric", "Camera", std::bind(docStr, "Isometric View camera"));
+  this->addBinding({mod_t::CTRL, "Y"}, "set scene.up_direction +Y", "Scene", std::bind(docStr, "Set scene up direction to +Y"));
+  this->addBinding({mod_t::CTRL, "Z"}, "set scene.up_direction +Z", "Scene", std::bind(docStr, "Set scene up direction to +Z"));
 #if F3D_MODULE_UI
-  this->addBinding({mod_t::NONE, "H"}, "toggle ui.cheatsheet", "Others", std::bind(docStr, "Toggle cheatsheet display"));
-  this->addBinding({mod_t::NONE, "Escape"}, "toggle ui.console", "Others", std::bind(docStr, "Toggle console display"));
-  this->addBinding({mod_t::ANY, "Colon"}, "toggle ui.minimal_console", "Others", std::bind(docStr, "Toggle minimal console display"));
+  this->addBinding({mod_t::NONE, "H"}, "toggle ui.cheatsheet", "Others", std::bind(docStr, "Cheatsheet"));
+  this->addBinding({mod_t::NONE, "Escape"}, "toggle ui.console", "Others", std::bind(docStr, "Console"));
+  this->addBinding({mod_t::ANY, "Colon"}, "toggle ui.minimal_console", "Others", std::bind(docStr, "Minimal console"));
 #endif
   this->addBinding({mod_t::CTRL, "Q"}, "stop_interactor", "Others", std::bind(docStr, "Stop the interactor"));
   this->addBinding({mod_t::NONE, "Return"}, "reset_camera", "Others", std::bind(docStr, "Reset camera to initial parameters"));
   this->addBinding({mod_t::NONE, "Space"}, "toggle_animation", "Others", std::bind(docStr, "Play/Pause animation if any"));
+  this->addBinding({mod_t::CTRL_SHIFT, "Space"}, "toggle_animation_backward", "Others", std::bind(docStr, "Play/Pause animation backward if any"));
   this->addBinding({mod_t::NONE, "Drop"}, "add_files", "Others", std::bind(docStr, "Add files to the scene"));
-  this->addBinding({mod_t::SHIFT, "V"}, "cycle_verbose_level", "Others", docVerbose);
+  this->addBinding({mod_t::SHIFT, "V"}, "cycle_verbose_level", "Others", docVerbose, f3d::interactor::BindingType::CYCLIC);
   // clang-format on
 
   return *this;
@@ -1239,10 +1651,10 @@
 //----------------------------------------------------------------------------
 interactor& interactor_impl::addBinding(const interaction_bind_t& bind,
   std::vector<std::string> commands, std::string group,
-  documentation_callback_t documentationCallback)
+  documentation_callback_t documentationCallback, BindingType type)
 {
   const auto [it, success] = this->Internals->Bindings.insert(
-    { bind, { std::move(commands), std::move(documentationCallback) } });
+    { bind, { std::move(commands), std::move(documentationCallback), type } });
   if (!success)
   {
     throw interactor::already_exists_exception(
@@ -1264,10 +1676,10 @@
 
 //----------------------------------------------------------------------------
 interactor& interactor_impl::addBinding(const interaction_bind_t& bind, std::string command,
-  std::string group, documentation_callback_t documentationCallback)
+  std::string group, documentation_callback_t documentationCallback, BindingType type)
 {
   return this->addBinding(bind, std::vector<std::string>{ std::move(command) }, std::move(group),
-    std::move(documentationCallback));
+    std::move(documentationCallback), type);
 }
 
 //----------------------------------------------------------------------------
@@ -1276,26 +1688,23 @@
   this->Internals->Bindings.erase(bind);
 
   // Look for the group of the removed bind
-  std::string group;
-  for (auto it = this->Internals->GroupedBinds.begin(); it != this->Internals->GroupedBinds.end();
-       it++)
-  {
-    if (it->second == bind)
-    {
-      // Binds are unique
-      // Erase the bind entry in the group
-      group = it->first;
-      this->Internals->GroupedBinds.erase(it);
-      if (this->Internals->GroupedBinds.count(group) == 0)
-      {
-        // If it was the last one, remove it from the ordered group
-        // We know the group is present and unique in the vector, so only erase once
-        auto vecIt = std::find(this->Internals->OrderedBindGroups.begin(),
-          this->Internals->OrderedBindGroups.end(), group);
-        assert(vecIt != this->Internals->OrderedBindGroups.end());
-        this->Internals->OrderedBindGroups.erase(vecIt);
-      }
-      break;
+  auto it = std::find_if(this->Internals->GroupedBinds.begin(), this->Internals->GroupedBinds.end(),
+    [&](const auto& pair) { return pair.second == bind; });
+
+  if (it != this->Internals->GroupedBinds.end())
+  {
+    // Binds are unique
+    // Erase the bind entry in the group
+    std::string group = it->first;
+    this->Internals->GroupedBinds.erase(it);
+    if (this->Internals->GroupedBinds.count(group) == 0)
+    {
+      // If it was the last one, remove it from the ordered group
+      // We know the group is present and unique in the vector, so only erase once
+      auto vecIt = std::find(this->Internals->OrderedBindGroups.begin(),
+        this->Internals->OrderedBindGroups.end(), group);
+      assert(vecIt != this->Internals->OrderedBindGroups.end());
+      this->Internals->OrderedBindGroups.erase(vecIt);
     }
   }
   return *this;
@@ -1344,22 +1753,129 @@
     throw interactor_impl::does_not_exists_exception(
       std::string("Bind: ") + bind.format() + " does not exists");
   }
+
   const auto& docFunc = it->second.DocumentationCallback;
   return docFunc ? docFunc() : std::make_pair(std::string(), std::string());
 }
 
 //----------------------------------------------------------------------------
-interactor& interactor_impl::toggleAnimation()
+f3d::interactor::BindingType interactor_impl::getBindingType(const interaction_bind_t& bind) const
+{
+  const auto& it = this->Internals->Bindings.find(bind);
+  if (it == this->Internals->Bindings.end())
+  {
+    throw interactor_impl::does_not_exists_exception(
+      std::string("Bind: ") + bind.format() + " does not exists");
+  }
+  return it->second.Type;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerModUpdate(InputModifier mod)
+{
+  this->Internals->VTKInteractor->SetControlKey(
+    mod == InputModifier::CTRL || mod == InputModifier::CTRL_SHIFT);
+  this->Internals->VTKInteractor->SetShiftKey(
+    mod == InputModifier::SHIFT || mod == InputModifier::CTRL_SHIFT);
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerMouseButton(InputAction action, MouseButton button)
+{
+  unsigned long event = vtkCommand::AnyEvent;
+
+  switch (button)
+  {
+    case MouseButton::LEFT:
+      event = action == InputAction::PRESS ? vtkCommand::LeftButtonPressEvent
+                                           : vtkCommand::LeftButtonReleaseEvent;
+      break;
+    case MouseButton::RIGHT:
+      event = action == InputAction::PRESS ? vtkCommand::RightButtonPressEvent
+                                           : vtkCommand::RightButtonReleaseEvent;
+      break;
+    case MouseButton::MIDDLE:
+      event = action == InputAction::PRESS ? vtkCommand::MiddleButtonPressEvent
+                                           : vtkCommand::MiddleButtonReleaseEvent;
+      break;
+  }
+
+  this->Internals->VTKInteractor->InvokeEvent(event, nullptr);
+
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerMousePosition(double xpos, double ypos)
+{
+  this->Internals->VTKInteractor->SetEventInformationFlipY(xpos, ypos);
+  this->Internals->VTKInteractor->InvokeEvent(vtkCommand::MouseMoveEvent, nullptr);
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerMouseWheel(WheelDirection direction)
+{
+  switch (direction)
+  {
+    case WheelDirection::LEFT:
+      this->Internals->VTKInteractor->InvokeEvent(vtkCommand::MouseWheelLeftEvent, nullptr);
+      break;
+    case WheelDirection::RIGHT:
+      this->Internals->VTKInteractor->InvokeEvent(vtkCommand::MouseWheelRightEvent, nullptr);
+      break;
+    case WheelDirection::FORWARD:
+      this->Internals->VTKInteractor->InvokeEvent(vtkCommand::MouseWheelForwardEvent, nullptr);
+      break;
+    case WheelDirection::BACKWARD:
+      this->Internals->VTKInteractor->InvokeEvent(vtkCommand::MouseWheelBackwardEvent, nullptr);
+      break;
+  }
+
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerKeyboardKey(InputAction action, std::string_view keySym)
+{
+  if (!keySym.empty())
+  {
+    this->Internals->VTKInteractor->SetKeySym(keySym.data());
+
+    this->Internals->VTKInteractor->InvokeEvent(
+      action == InputAction::PRESS ? vtkCommand::KeyPressEvent : vtkCommand::KeyReleaseEvent,
+      nullptr);
+  }
+
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::triggerTextCharacter(unsigned int codepoint)
+{
+  this->Internals->VTKInteractor->SetKeyCode(codepoint);
+  this->Internals->VTKInteractor->InvokeEvent(vtkCommand::CharEvent, nullptr);
+
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+interactor& interactor_impl::toggleAnimation(AnimationDirection direction)
 {
   assert(this->Internals->AnimationManager);
+  this->Internals->AnimationManager->SetAnimationDirection(
+    direction == AnimationDirection::FORWARD ? 1 : -1);
   this->Internals->AnimationManager->ToggleAnimation();
   return *this;
 }
 
 //----------------------------------------------------------------------------
-interactor& interactor_impl::startAnimation()
+interactor& interactor_impl::startAnimation(AnimationDirection direction)
 {
   assert(this->Internals->AnimationManager);
+  this->Internals->AnimationManager->SetAnimationDirection(
+    direction == AnimationDirection::FORWARD ? 1 : -1);
   this->Internals->AnimationManager->StartAnimation();
   return *this;
 }
@@ -1380,6 +1896,15 @@
 }
 
 //----------------------------------------------------------------------------
+interactor::AnimationDirection interactor_impl::getAnimationDirection()
+{
+  assert(this->Internals->AnimationManager);
+  return this->Internals->AnimationManager->GetAnimationDirection() == 1
+    ? AnimationDirection::FORWARD
+    : AnimationDirection::BACKWARD;
+}
+
+//----------------------------------------------------------------------------
 interactor& interactor_impl::enableCameraMovement()
 {
   this->Internals->Style->SetCameraMovementDisabled(false);
@@ -1409,11 +1934,14 @@
     this->Internals->Recorder->Off();
     this->Internals->Recorder->Clear();
 
-    this->Internals->StartEventLoop(loopTime, std::move(userCallBack));
+    bool loop = this->Internals->StartEventLoop(loopTime, std::move(userCallBack));
     this->Internals->Recorder->SetFileName(file.string().c_str());
     this->Internals->Recorder->Play();
 
-    this->Internals->StopEventLoop();
+    if (loop)
+    {
+      this->Internals->StopEventLoop();
+    }
   }
   catch (const fs::filesystem_error& ex)
   {
@@ -1469,16 +1997,20 @@
 //----------------------------------------------------------------------------
 interactor& interactor_impl::start(double loopTime, std::function<void()> userCallBack)
 {
-  this->Internals->StartEventLoop(loopTime, std::move(userCallBack));
-  this->Internals->VTKInteractor->Start();
+  if (this->Internals->StartEventLoop(loopTime, std::move(userCallBack)))
+  {
+    this->Internals->VTKInteractor->Start();
+  }
   return *this;
 }
 
 //----------------------------------------------------------------------------
 interactor& interactor_impl::stop()
 {
-  this->Internals->StopEventLoop();
-  this->Internals->VTKInteractor->ExitCallback();
+  if (this->Internals->StopEventLoop())
+  {
+    this->Internals->VTKInteractor->ExitCallback();
+  }
   return *this;
 }
 
@@ -1489,6 +2021,13 @@
   return *this;
 }
 
+//----------------------------------------------------------------------------
+interactor& interactor_impl::requestStop()
+{
+  this->Internals->StopRequested = true;
+  return *this;
+}
+
 //----------------------------------------------------------------------------
 void interactor_impl::SetAnimationManager(animationManager* manager)
 {
diff -Nru f3d-3.2.0+dfsg/library/src/log.cxx f3d-3.4.1/library/src/log.cxx
--- f3d-3.2.0+dfsg/library/src/log.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/log.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,27 @@
 
 namespace f3d
 {
+namespace detail
+{
+log::VerboseLevel convertLevel(F3DLog::Severity level)
+{
+  switch (level)
+  {
+    case F3DLog::Severity::Debug:
+      return log::VerboseLevel::DEBUG;
+    case F3DLog::Severity::Info:
+      return log::VerboseLevel::INFO;
+    case F3DLog::Severity::Warning:
+      return log::VerboseLevel::WARN;
+    case F3DLog::Severity::Error:
+      return log::VerboseLevel::ERROR;
+    case F3DLog::Severity::Quiet:
+    default:
+      return log::VerboseLevel::QUIET;
+  }
+}
+}
+
 //----------------------------------------------------------------------------
 void log::printInternal(log::VerboseLevel level, const std::string& str)
 {
@@ -113,20 +134,21 @@
 {
   detail::init::initialize();
 
-  switch (F3DLog::VerboseLevel)
+  return detail::convertLevel(F3DLog::VerboseLevel);
+}
+
+//----------------------------------------------------------------------------
+void log::forward(const forward_fn_t& callback)
+{
+  detail::init::initialize();
+
+  if (callback == nullptr)
   {
-    case F3DLog::Severity::Debug:
-      return log::VerboseLevel::DEBUG;
-    case F3DLog::Severity::Info:
-      return log::VerboseLevel::INFO;
-    case F3DLog::Severity::Warning:
-      return log::VerboseLevel::WARN;
-    case F3DLog::Severity::Error:
-      return log::VerboseLevel::ERROR;
-    case F3DLog::Severity::Quiet:
-      return log::VerboseLevel::QUIET;
-    default:
-      return log::VerboseLevel::INFO;
+    F3DLog::Forward(nullptr);
+    return;
   }
+
+  F3DLog::Forward([=](F3DLog::Severity sev, const std::string& msg)
+    { callback(detail::convertLevel(sev), msg); });
 }
 }
diff -Nru f3d-3.2.0+dfsg/library/src/scene_impl.cxx f3d-3.4.1/library/src/scene_impl.cxx
--- f3d-3.2.0+dfsg/library/src/scene_impl.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/scene_impl.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -7,13 +7,16 @@
 #include "scene.h"
 #include "window_impl.h"
 
+#include "F3DStyle.h"
 #include "factory.h"
 #include "vtkF3DGenericImporter.h"
 #include "vtkF3DMemoryMesh.h"
 #include "vtkF3DMetaImporter.h"
+#include "vtkF3DRenderer.h"
 
 #include <optional>
 #include <vtkCallbackCommand.h>
+#include <vtkLightCollection.h>
 #include <vtkProgressBarRepresentation.h>
 #include <vtkProgressBarWidget.h>
 #include <vtkTimerLog.h>
@@ -45,8 +48,8 @@
     vtkProgressBarWidget* widget;
   };
 
-  static void CreateProgressRepresentationAndCallback(
-    ProgressDataStruct* data, vtkImporter* importer, interactor_impl* interactor)
+  static void CreateProgressRepresentationAndCallback(ProgressDataStruct* data,
+    vtkImporter* importer, interactor_impl* interactor, const f3d::color_t& color)
   {
     vtkNew<vtkCallbackCommand> progressCallback;
     progressCallback->SetClientData(data);
@@ -78,8 +81,7 @@
     progressRep->SetPosition(0.0, 0.0);
     progressRep->SetPosition2(1.0, 0.0);
     progressRep->SetMinimumSize(0, 5);
-    progressRep->SetProgressBarColor(1, 1, 1);
-    progressRep->DrawBackgroundOff();
+    progressRep->SetProgressBarColor(color.r(), color.g(), color.b());
     progressRep->DragableOff();
     progressRep->SetShowBorderToOff();
     progressRep->DrawFrameOff();
@@ -94,8 +96,8 @@
       this->MetaImporter->AddImporter(importer);
     }
 
-    // Initialize the UpVector on load
-    this->Window.InitializeUpVector();
+    // Initialize the camera on load
+    this->Window.InitializeUpDirection();
 
     // Reset temporary up to apply any config values
     if (this->Interactor)
@@ -116,8 +118,18 @@
     callbackData.widget = progressWidget;
     if (this->Options.ui.loader_progress && this->Interactor)
     {
+      f3d::color_t color;
+      if (!this->Options.ui.loader_progress_color.has_value())
+      {
+        const auto [r, g, b] = F3DStyle::GetF3DYellow();
+        color = color_t(r, g, b);
+      }
+      else
+      {
+        color = this->Options.ui.loader_progress_color.value();
+      }
       scene_impl::internals::CreateProgressRepresentationAndCallback(
-        &callbackData, this->MetaImporter, this->Interactor);
+        &callbackData, this->MetaImporter, this->Interactor, color);
     }
 
     // Update the meta importer, the will only update importers that have not been updated before
@@ -140,6 +152,7 @@
     progressWidget->Off();
 
     // Initialize the animation using temporal information from the importer
+    this->AnimationManager.UpdateDynamicOptions();
     this->AnimationManager.Initialize();
 
     // Update all window options and reset camera to bounds if needed
@@ -211,12 +224,8 @@
 //----------------------------------------------------------------------------
 scene& scene_impl::add(const std::vector<std::string>& filePathStrings)
 {
-  std::vector<fs::path> paths;
-  paths.reserve(filePathStrings.size());
-  for (const std::string& str : filePathStrings)
-  {
-    paths.emplace_back(str);
-  }
+  std::vector<fs::path> paths(filePathStrings.size());
+  std::copy(filePathStrings.begin(), filePathStrings.end(), paths.begin());
   return this->add(paths);
 }
 
@@ -243,7 +252,7 @@
     }
     std::optional<std::string> forceReader = this->Internals->Options.scene.force_reader;
     // Recover the importer for the provided file path
-    f3d::reader* reader = f3d::factory::instance()->getReader(filePath.string(), forceReader);
+    const f3d::reader* reader = f3d::factory::instance()->getReader(filePath.string(), forceReader);
     if (reader)
     {
       if (forceReader)
@@ -333,6 +342,100 @@
   return *this;
 }
 
+//----------------------------------------------------------------------------
+int scene_impl::addLight(const light_state_t& lightState) const
+{
+  vtkNew<vtkLight> newLight;
+  newLight->SetLightType(static_cast<int>(lightState.type));
+  newLight->SetPosition(lightState.position.data());
+  newLight->SetColor(lightState.color.data());
+  newLight->SetPositional(lightState.positionalLight);
+  newLight->SetFocalPoint(lightState.position[0] + lightState.direction[0],
+    lightState.position[1] + lightState.direction[1],
+    lightState.position[2] + lightState.direction[2]);
+  newLight->SetIntensity(lightState.intensity);
+  newLight->SetSwitch(lightState.switchState);
+  this->Internals->Window.GetRenderer()->AddLight(newLight);
+  return this->getLightCount() - 1;
+}
+
+//----------------------------------------------------------------------------
+int scene_impl::getLightCount() const
+{
+  vtkLightCollection* lc = this->Internals->Window.GetRenderer()->GetLights();
+  return lc->GetNumberOfItems();
+}
+
+//----------------------------------------------------------------------------
+light_state_t scene_impl::getLight(int index) const
+{
+  vtkLightCollection* lc = this->Internals->Window.GetRenderer()->GetLights();
+  vtkLight* light = vtkLight::SafeDownCast(lc->GetItemAsObject(index));
+  if (!light)
+  {
+    throw scene::light_exception("No light at index " + std::to_string(index) + " to get");
+  }
+
+  const double* position = light->GetPosition();
+  const double* color = light->GetDiffuseColor();
+  const double* focalPoint = light->GetFocalPoint();
+
+  light_state_t lightState;
+  lightState.type = static_cast<light_type>(light->GetLightType());
+  lightState.position = { position[0], position[1], position[2] };
+  lightState.color = { color[0], color[1], color[2] };
+  lightState.direction = { focalPoint[0] - position[0], focalPoint[1] - position[1],
+    focalPoint[2] - position[2] };
+  lightState.positionalLight = light->GetPositional();
+  lightState.intensity = light->GetIntensity();
+  lightState.switchState = light->GetSwitch();
+  return lightState;
+}
+
+//----------------------------------------------------------------------------
+scene& scene_impl::updateLight(int index, const light_state_t& lightState)
+{
+  vtkLightCollection* lc = this->Internals->Window.GetRenderer()->GetLights();
+  vtkLight* light = vtkLight::SafeDownCast(lc->GetItemAsObject(index));
+  if (!light)
+  {
+    throw scene::light_exception("No light at index " + std::to_string(index) + " to update");
+  }
+
+  light->SetLightType(static_cast<int>(lightState.type));
+  light->SetPosition(lightState.position.data());
+  light->SetColor(lightState.color.data());
+  light->SetPositional(lightState.positionalLight);
+  light->SetFocalPoint(lightState.position[0] + lightState.direction[0],
+    lightState.position[1] + lightState.direction[1],
+    lightState.position[2] + lightState.direction[2]);
+  light->SetIntensity(lightState.intensity);
+  light->SetSwitch(lightState.switchState);
+
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+scene& scene_impl::removeLight(int index)
+{
+  vtkLightCollection* lc = this->Internals->Window.GetRenderer()->GetLights();
+  vtkLight* light = vtkLight::SafeDownCast(lc->GetItemAsObject(index));
+  if (!light)
+  {
+    throw scene::light_exception("No light at index " + std::to_string(index) + " to remove");
+  }
+
+  this->Internals->Window.GetRenderer()->RemoveLight(light);
+  return *this;
+}
+
+//----------------------------------------------------------------------------
+scene& scene_impl::removeAllLights()
+{
+  this->Internals->Window.GetRenderer()->RemoveAllLights();
+  return *this;
+}
+
 //----------------------------------------------------------------------------
 bool scene_impl::supports(const fs::path& filePath)
 {
diff -Nru f3d-3.2.0+dfsg/library/src/types.cxx f3d-3.4.1/library/src/types.cxx
--- f3d-3.2.0+dfsg/library/src/types.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/types.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,7 +1,12 @@
 #include "types.h"
 
+#include "vtkMath.h"
+
 #include <algorithm>
+#include <cmath>
+#include <iostream>
 #include <numeric>
+#include <string>
 
 namespace f3d
 {
@@ -57,4 +62,34 @@
   return { true, {} };
 }
 
+// see function explanation in types.h for explanation of logic
+//----------------------------------------------------------------------------
+F3D_EXPORT transform2d_t::transform2d_t(
+  const double_array_t<2>& scale, const double_array_t<2>& translate, const angle_deg_t& angle)
+{
+  double angleRad = std::isnan(angle) ? 0.0 : vtkMath::RadiansFromDegrees(angle);
+
+  double sinA = std::sin(angleRad);
+  double cosA = std::cos(angleRad);
+
+  (*this)[0] = cosA * (std::isnan(scale[0]) ? 1.0 : scale[0]);
+  (*this)[1] = -sinA * (std::isnan(scale[1]) ? 1.0 : scale[1]);
+  (*this)[2] = (std::isnan(translate[0]) ? 0.0 : translate[0]);
+  (*this)[3] = sinA * (std::isnan(scale[0]) ? 1.0 : scale[0]);
+  (*this)[4] = cosA * (std::isnan(scale[1]) ? 1.0 : scale[1]);
+  (*this)[5] = (std::isnan(translate[1]) ? 0.0 : translate[1]);
+  (*this)[6] = 0;
+  (*this)[7] = 0;
+  (*this)[8] = 1;
+
+  // remove negative 0.0 wherever it occurs
+  for (int i = 0; i < 9; i++)
+  {
+    if ((*this)[i] == 0.0 && std::signbit((*this)[i]))
+    {
+      (*this)[i] = 0.0;
+    }
+  }
+}
+
 }
diff -Nru f3d-3.2.0+dfsg/library/src/utils.cxx f3d-3.4.1/library/src/utils.cxx
--- f3d-3.2.0+dfsg/library/src/utils.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/utils.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -25,7 +25,7 @@
 }
 
 //----------------------------------------------------------------------------
-std::vector<std::string> utils::tokenize(std::string_view str)
+std::vector<std::string> utils::tokenize(std::string_view str, bool keepComments)
 {
   std::vector<std::string> tokens;
   std::string token;
@@ -82,7 +82,7 @@
         escaped = false;
         break;
       case '#':
-        if (!escaped && !quoted)
+        if (!escaped && !quoted && keepComments)
         {
           commented = true;
         }
@@ -193,9 +193,8 @@
             starCount++;
             i++;
           }
-          bool nextTokenSepOrEnd = i + 1 >= glob.size() ||
-            (i + 1 < glob.size() ? glob.substr(i + 1, globSeparator.size()) == globSeparator
-                                 : false);
+          bool nextTokenSepOrEnd =
+            i + 1 >= glob.size() || glob.substr(i + 1, globSeparator.size()) == globSeparator;
           if (supportGlobStars)
           {
             if (starCount > 1 && prevTokenSepOrBeg && nextTokenSepOrEnd)
diff -Nru f3d-3.2.0+dfsg/library/src/window_impl.cxx f3d-3.4.1/library/src/window_impl.cxx
--- f3d-3.2.0+dfsg/library/src/window_impl.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/src/window_impl.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,11 +2,13 @@
 
 #include "camera_impl.h"
 #include "engine.h"
+#include "interactor.h"
 #include "log.h"
 #include "macros.h"
 #include "options.h"
 #include "utils.h"
 
+#include "F3DStyle.h"
 #include "vtkF3DExternalRenderWindow.h"
 
 #include "vtkF3DGenericImporter.h"
@@ -20,6 +22,7 @@
 #include <vtkInformation.h>
 #include <vtkPNGReader.h>
 #include <vtkPointGaussianMapper.h>
+#include <vtkRenderWindowInteractor.h>
 #include <vtkRendererCollection.h>
 #include <vtkRenderingOpenGLConfigure.h>
 #include <vtkVersion.h>
@@ -37,6 +40,10 @@
 #include <vtkF3DEGLRenderWindow.h>
 #endif
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+#include <vtkMemoryResourceStream.h>
+#endif
+
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240914)
 #include <vtkOSOpenGLRenderWindow.h>
 #endif
@@ -200,9 +207,9 @@
 }
 
 //----------------------------------------------------------------------------
-void window_impl::InitializeUpVector()
+void window_impl::InitializeUpDirection()
 {
-  this->Internals->Renderer->InitializeUpVector(this->Internals->Options.scene.up_direction);
+  this->Internals->Renderer->InitializeUpDirection(this->Internals->Options.scene.up_direction);
 }
 
 //----------------------------------------------------------------------------
@@ -282,7 +289,8 @@
 //----------------------------------------------------------------------------
 window& window_impl::setSize(int width, int height)
 {
-  this->Internals->RenWin->SetSize(width, height);
+  assert(this->Internals->RenWin->GetInteractor() != nullptr);
+  this->Internals->RenWin->GetInteractor()->UpdateSize(width, height);
   return *this;
 }
 
@@ -293,8 +301,8 @@
   {
     // vtkCocoaRenderWindow has a different behavior than other render windows
     // https://gitlab.kitware.com/vtk/vtk/-/issues/18681
-    int* screenSize = this->Internals->RenWin->GetScreenSize();
-    int* winSize = this->Internals->RenWin->GetSize();
+    const int* screenSize = this->Internals->RenWin->GetScreenSize();
+    const int* winSize = this->Internals->RenWin->GetSize();
     this->Internals->RenWin->SetPosition(x, screenSize[1] - winSize[1] - y);
   }
   else
@@ -309,8 +317,14 @@
 {
   // XXX This code requires that the interactor has already been set on the render window
   vtkNew<vtkPNGReader> iconReader;
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  vtkNew<vtkMemoryResourceStream> stream;
+  stream->SetBuffer(icon, iconSize);
+  iconReader->SetStream(stream);
+#else
   iconReader->SetMemoryBuffer(icon);
   iconReader->SetMemoryBufferLength(iconSize);
+#endif
   iconReader->Update();
   this->Internals->RenWin->SetIcon(iconReader->GetOutput());
   return *this;
@@ -383,20 +397,33 @@
 
   const options& opt = this->Internals->Options;
 
-  if (this->Internals->Interactor)
-  {
-    renderer->ShowAxis(opt.ui.axis);
-    renderer->SetUseTrackball(opt.interactor.trackball);
-    renderer->SetInvertZoom(opt.interactor.invert_zoom);
-  }
+  // Update pending up direction if changed
+  renderer->SetPendingUpDirection(opt.scene.up_direction);
 
   // XXX: model.point_sprites.type only has an effect on geometry scene
   // but we set it here for practical reasons
-  const int pointSpritesSize = opt.model.point_sprites.size;
-  const vtkF3DRenderer::SplatType splatType = opt.model.point_sprites.type == "gaussian"
-    ? vtkF3DRenderer::SplatType::GAUSSIAN
-    : vtkF3DRenderer::SplatType::SPHERE;
-  renderer->SetPointSpritesProperties(splatType, pointSpritesSize);
+  renderer->SetUsePointSprites(opt.model.point_sprites.enable);
+  vtkF3DRenderer::SplatType splatType = vtkF3DRenderer::SplatType::SPHERE;
+  if (opt.model.point_sprites.enable)
+  {
+    if (opt.model.point_sprites.type == "gaussian")
+    {
+      splatType = vtkF3DRenderer::SplatType::GAUSSIAN;
+    }
+    else if (opt.model.point_sprites.type == "sphere")
+    {
+      splatType = vtkF3DRenderer::SplatType::SPHERE;
+    }
+    else
+    {
+      log::warn(opt.model.point_sprites.type,
+        R"( is an invalid point sprites type. Valid modes are: "sphere", "gaussian")");
+    }
+    renderer->SetPointSpritesType(splatType);
+    renderer->SetPointSpritesSize(
+      opt.model.point_sprites.absolute_size, opt.model.point_sprites.size);
+    renderer->SetPointSpritesUseInstancing(opt.render.effect.blending.mode != "sort");
+  }
 
   renderer->SetLineWidth(opt.render.line_width);
   renderer->SetPointSize(opt.render.point_size);
@@ -405,11 +432,70 @@
   renderer->ShowFilename(opt.ui.filename);
   renderer->SetFilenameInfo(opt.ui.filename_info);
   renderer->ShowMetaData(opt.ui.metadata);
+  renderer->ShowHDRIFilename(opt.ui.hdri_filename);
   renderer->ShowCheatSheet(opt.ui.cheatsheet);
   renderer->ShowConsole(opt.ui.console);
   renderer->ShowMinimalConsole(opt.ui.minimal_console);
-  renderer->ShowDropZone(opt.ui.dropzone);
-  renderer->SetDropZoneInfo(opt.ui.dropzone_info);
+  renderer->ShowDropZone(opt.ui.drop_zone.enable);
+  renderer->ShowDropZoneLogo(opt.ui.drop_zone.show_logo);
+  renderer->SetBackdropOpacity(opt.ui.backdrop.opacity);
+
+  if (this->Internals->Interactor)
+  {
+    renderer->SetAxesColor(opt.ui.x_color, opt.ui.y_color, opt.ui.z_color);
+    renderer->ShowAxis(opt.ui.axis);
+    renderer->SetUseTrackball(opt.interactor.trackball);
+    renderer->SetInvertZoom(opt.interactor.invert_zoom);
+
+#if F3D_MODULE_UI
+    std::string bindsStr = opt.ui.drop_zone.custom_binds;
+    std::vector<std::pair<std::string, std::string>> dropZoneBinds;
+
+    for (const std::string& token : utils::tokenize(bindsStr))
+    {
+      if (!token.empty())
+      {
+        try
+        {
+          auto bind = interaction_bind_t::parse(token);
+          auto docPair = this->Internals->Interactor->getBindingDocumentation(bind);
+          dropZoneBinds.push_back({ docPair.first, bind.format() });
+        }
+        catch (const interactor_impl::does_not_exists_exception&)
+        {
+          // skip non-existent binds
+          log::warn("Bind ", token, " does not exist and will be ignored.");
+        }
+      }
+    }
+    renderer->SetDropZoneBinds(dropZoneBinds);
+#endif
+  }
+
+  // F3D_DEPRECATED
+  // Remove this in the next major release
+  F3D_SILENT_WARNING_PUSH()
+  F3D_SILENT_WARNING_DECL(4996, "deprecated-declarations")
+
+  if (!opt.ui.dropzone_info.empty())
+  {
+    log::warn("'ui.dropzone_info' is deprecated. Please Use 'ui.drop_zone.custom_binds' instead.");
+    renderer->SetDropZoneInfo(opt.ui.dropzone_info);
+  }
+  else if (!opt.ui.drop_zone.info.empty())
+  {
+    log::warn("'ui.drop_zone.info' is deprecated. Please Use 'ui.drop_zone.custom_binds' instead.");
+    renderer->SetDropZoneInfo(opt.ui.drop_zone.info);
+  }
+
+  if (opt.ui.dropzone)
+  {
+    log::warn("'ui.dropzone' is deprecated. Please Use 'ui.drop_zone.enable' instead.");
+    renderer->ShowDropZone(opt.ui.dropzone);
+    renderer->ShowDropZoneLogo(opt.ui.dropzone);
+  }
+  F3D_SILENT_WARNING_POP()
+
   renderer->ShowArmature(opt.render.armature.enable);
 
   renderer->SetUseRaytracing(opt.render.raytracing.enable);
@@ -417,6 +503,7 @@
   renderer->SetUseRaytracingDenoiser(opt.render.raytracing.denoise);
 
   vtkF3DRenderer::AntiAliasingMode aaMode = vtkF3DRenderer::AntiAliasingMode::NONE;
+  vtkF3DRenderer::BlendingMode blendMode = vtkF3DRenderer::BlendingMode::NONE;
 
   // F3D_DEPRECATED
   // Remove this in the next major release
@@ -428,6 +515,12 @@
               "render.effect.antialiasing.enable instead");
     aaMode = vtkF3DRenderer::AntiAliasingMode::FXAA;
   }
+  if (opt.render.effect.translucency_support)
+  {
+    log::warn("render.effect.translucency_support is deprecated, please use "
+              "render.effect.blending.enable instead");
+    blendMode = vtkF3DRenderer::BlendingMode::DUAL_DEPTH_PEELING;
+  }
   F3D_SILENT_WARNING_POP()
 
   if (opt.render.effect.antialiasing.enable)
@@ -440,17 +533,42 @@
     {
       aaMode = vtkF3DRenderer::AntiAliasingMode::SSAA;
     }
+    else if (opt.render.effect.antialiasing.mode == "taa")
+    {
+      aaMode = vtkF3DRenderer::AntiAliasingMode::TAA;
+    }
     else
     {
       log::warn(opt.render.effect.antialiasing.mode,
-        R"( is an invalid antialiasing mode. Valid modes are: "fxaa", "ssaa")");
+        R"( is an invalid antialiasing mode. Valid modes are: "fxaa", "ssaa", "taa")");
+    }
+  }
+
+  if (opt.render.effect.blending.enable)
+  {
+    if (opt.render.effect.blending.mode == "ddp")
+    {
+      blendMode = vtkF3DRenderer::BlendingMode::DUAL_DEPTH_PEELING;
+    }
+    else if (opt.render.effect.blending.mode == "sort")
+    {
+      blendMode = vtkF3DRenderer::BlendingMode::SORT;
+    }
+    else if (opt.render.effect.blending.mode == "stochastic")
+    {
+      blendMode = vtkF3DRenderer::BlendingMode::STOCHASTIC;
+    }
+    else
+    {
+      log::warn(opt.render.effect.blending.mode,
+        R"( is an invalid blending mode. Valid modes are: "ddp", "sort", "stochastic")");
     }
   }
 
   renderer->SetUseSSAOPass(opt.render.effect.ambient_occlusion);
   renderer->SetAntiAliasingMode(aaMode);
   renderer->SetUseToneMappingPass(opt.render.effect.tone_mapping);
-  renderer->SetUseDepthPeelingPass(opt.render.effect.translucency_support);
+  renderer->SetBlendingMode(blendMode);
   renderer->SetBackfaceType(opt.render.backface_type);
   renderer->SetFinalShader(opt.render.effect.final_shader);
 
@@ -472,6 +590,8 @@
   renderer->ShowGrid(opt.render.grid.enable);
   renderer->SetGridColor(opt.render.grid.color);
 
+  renderer->ShowAxesGrid(opt.render.axes_grid.enable);
+
   if (!opt.scene.camera.index.has_value())
   {
     renderer->SetUseOrthographicProjection(opt.scene.camera.orthographic);
@@ -501,7 +621,6 @@
   renderer->SetColormapDiscretization(opt.model.scivis.discretization);
   renderer->ShowScalarBar(opt.ui.scalar_bar);
 
-  renderer->SetUsePointSprites(opt.model.point_sprites.enable);
   renderer->SetUseVolume(opt.model.volume.enable);
   renderer->SetUseInverseOpacityFunction(opt.model.volume.inverse);
 
@@ -517,9 +636,11 @@
       for (const interaction_bind_t& bind : this->Internals->Interactor->getBindsForGroup(group))
       {
         auto [doc, val] = this->Internals->Interactor->getBindingDocumentation(bind);
+        f3d::interactor::BindingType type = this->Internals->Interactor->getBindingType(bind);
         if (!doc.empty())
         {
-          groupList.emplace_back(std::make_tuple(bind.format(), doc, val));
+          groupList.emplace_back(
+            std::make_tuple(bind.format(), doc, val, vtkF3DUIActor::CheatSheetBindingType(type)));
         }
       }
       cheatsheet.emplace_back(std::make_pair(group, std::move(groupList)));
@@ -554,6 +675,13 @@
 bool window_impl::render()
 {
   this->UpdateDynamicOptions();
+  const options& opt = this->Internals->Options;
+  if ((!opt.scene.camera.index.has_value()) && (!this->Internals->Camera->GetSuccessfullyReset()))
+  {
+    // Camera wasn't successfully reset last time, it could be a chance that update of dynamic
+    // options will enable successful reset of camera
+    this->Internals->Camera->resetToBounds();
+  }
   this->Internals->RenWin->Render();
   return true;
 }
@@ -578,7 +706,7 @@
   exporter->SetInputConnection(rtW2if->GetOutputPort());
   exporter->ImageLowerLeftOn();
 
-  int* dims = exporter->GetDataDimensions();
+  const int* dims = exporter->GetDataDimensions();
   int cmp = exporter->GetDataNumberOfScalarComponents();
 
   image output(dims[0], dims[1], cmp);
@@ -634,4 +762,10 @@
   info->Remove(vtkF3DRenderPass::RENDER_UI_ONLY());
 #endif
 }
-};
+
+//----------------------------------------------------------------------------
+vtkF3DRenderer* window_impl::GetRenderer() const
+{
+  return this->Internals->Renderer;
+}
+}
diff -Nru f3d-3.2.0+dfsg/library/testing/CMakeLists.txt f3d-3.4.1/library/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/library/testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,24 +1,28 @@
 list(APPEND libf3dSDKTests_list
+     TestPseudoUnitTest.cxx
      TestSDKAnimation.cxx
      TestSDKCamera.cxx
      TestSDKCompareWithFile.cxx
      TestSDKDeprecatedOptions.cxx
      TestSDKDynamicLightIntensity.cxx
+     TestSDKDynamicUpDirection.cxx
      TestSDKEngine.cxx
      TestSDKEngineExceptions.cxx
+     TestSDKEngineRecreation.cxx
      TestSDKImage.cxx
-     TestSDKInteractorDropFullScene.cxx
      TestSDKInteractorCommand.cxx
-     TestSDKSceneFromMemory.cxx
-     TestSDKScene.cxx
+     TestSDKInteractorDropFullScene.cxx
      TestSDKLog.cxx
      TestSDKMultiColoring.cxx
      TestSDKOptions.cxx
      TestSDKOptionsIO.cxx
+     TestSDKRenderAndInteract.cxx
      TestSDKRenderFinalShader.cxx
+     TestSDKScene.cxx
+     TestSDKSceneFromMemory.cxx
      TestSDKUtils.cxx
+     TestSDKUI.cxx
      TestSDKWindowAuto.cxx
-     TestPseudoUnitTest.cxx
 )
 
 # Tests having UI widgets
@@ -26,37 +30,26 @@
   list(APPEND libf3dSDKTests_list
     TestSDKConsole.cxx
     TestSDKDropZone.cxx
+    TestSDKDropZoneDeprecated.cxx
+    TestSDKDropZoneNoLogo.cxx
     TestSDKDropZoneMinSize.cxx
     TestSDKDynamicBackgroundColor.cxx
     TestSDKDynamicFontFile.cxx
+    TestSDKDynamicHDRI.cxx
     TestSDKDynamicProperties.cxx
-    TestSDKInteractorCallBack.cxx
     TestSDKInteractorDocumentation.cxx
     TestSDKMultiOptions.cxx
+    TestSDKTriggerInteractions.cxx
     )
 
-  # HDRI test needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9767
-  if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221220)
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
     list(APPEND libf3dSDKTests_list
-      TestSDKDynamicHDRI.cxx
+      TestSDKInteractorCallBack.cxx
       )
   endif()
 endif()
 
-# These tests needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10155
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20230426)
-  list(APPEND libf3dSDKTests_list
-    TestSDKRenderAndInteract.cxx
-    TestSDKStartInteractor.cxx
-    )
-endif()
-
-if (VTK_VERSION VERSION_GREATER 9.2.6)
-    list(APPEND libf3dSDKTests_list
-        TestSDKEngineRecreation.cxx
-        )
-endif()
-
 # Invalid scene codepath needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11287
 # Merge with TestSDKScene.cxx when VTK 9.4.0 is required.
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240707)
@@ -72,48 +65,55 @@
 configure_file("${F3D_SOURCE_DIR}/testing/recordings/TestSDKInteractorDropFullScene.log.in"
                "${CMAKE_BINARY_DIR}/TestSDKInteractorDropFullScene.log") # world.obj; S
 
-# External window tests
-option(F3D_TESTING_ENABLE_EXTERNAL_GLFW "Test external GLFW" OFF)
-option(F3D_TESTING_ENABLE_EXTERNAL_QT "Test external QT" OFF)
-
 set(libf3dSDKTests_link_libs "")
 
-if(F3D_TESTING_ENABLE_EXTERNAL_GLFW)
-  find_package(glfw3 REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowGLFW.cxx)
-  list(APPEND libf3dSDKTests_link_libs glfw)
-endif()
-if(F3D_TESTING_ENABLE_EXTERNAL_QT)
-  find_package(Qt5 COMPONENTS OpenGL REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowQT.cxx)
-  list(APPEND libf3dSDKTests_link_libs Qt5::OpenGL)
-endif()
-if(F3D_TESTING_ENABLE_OSMESA_TESTS AND VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240914)
-  find_library(OSMesa_LIB OSMesa REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowOSMesa.cxx)
-  list(APPEND libf3dSDKTests_link_libs "${OSMesa_LIB}")
-endif()
-if(F3D_TESTING_ENABLE_GLX_TESTS AND VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240914)
-  find_package(OpenGL COMPONENTS GLX REQUIRED)
-  find_library(X11_LIB X11 REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowGLX.cxx)
-  list(APPEND libf3dSDKTests_link_libs OpenGL::GLX "${X11_LIB}")
-endif()
-if(F3D_TESTING_ENABLE_EGL_TESTS AND VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240914)
-  find_package(OpenGL COMPONENTS EGL REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowEGL.cxx)
-  list(APPEND libf3dSDKTests_link_libs OpenGL::EGL)
-endif()
-if(WIN32)
-  find_package(OpenGL COMPONENTS OpenGL REQUIRED)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowWGL.cxx)
-  list(APPEND libf3dSDKTests_link_libs OpenGL::GL)
-endif()
-if(APPLE)
-  find_library(COCOA_LIBRARY Cocoa)
-  find_library(OpenGL_LIBRARY OpenGL)
-  list(APPEND libf3dSDKTests_list TestSDKExternalWindowCOCOA.mm)
-  list(APPEND libf3dSDKTests_link_libs "${COCOA_LIBRARY}" "${OpenGL_LIBRARY}")
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240914)
+  list(APPEND libf3dSDKTests_list TestSDKStartInteractor.cxx)
+
+  # External window tests
+  option(F3D_TESTING_ENABLE_EXTERNAL_GLFW "Test external GLFW" OFF)
+  option(F3D_TESTING_ENABLE_EXTERNAL_QT "Test external QT" OFF)
+
+  if(F3D_TESTING_ENABLE_EXTERNAL_GLFW)
+    find_package(glfw3 REQUIRED)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowGLFW.cxx)
+    list(APPEND libf3dSDKTests_link_libs glfw)
+  endif()
+  if(F3D_TESTING_ENABLE_EXTERNAL_QT)
+    # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+    if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+      find_package(Qt5 COMPONENTS OpenGL REQUIRED)
+      list(APPEND libf3dSDKTests_list TestSDKExternalWindowQT.cxx)
+      list(APPEND libf3dSDKTests_link_libs Qt5::OpenGL)
+    endif()
+  endif()
+  if(F3D_TESTING_ENABLE_OSMESA_TESTS)
+    find_library(OSMesa_LIB OSMesa REQUIRED)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowOSMesa.cxx)
+    list(APPEND libf3dSDKTests_link_libs "${OSMesa_LIB}")
+  endif()
+  if(F3D_TESTING_ENABLE_GLX_TESTS)
+    find_package(OpenGL COMPONENTS GLX REQUIRED)
+    find_library(X11_LIB X11 REQUIRED)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowGLX.cxx)
+    list(APPEND libf3dSDKTests_link_libs OpenGL::GLX "${X11_LIB}")
+  endif()
+  if(F3D_TESTING_ENABLE_EGL_TESTS)
+    find_package(OpenGL COMPONENTS EGL REQUIRED)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowEGL.cxx)
+    list(APPEND libf3dSDKTests_link_libs OpenGL::EGL)
+  endif()
+  if(WIN32)
+    find_package(OpenGL COMPONENTS OpenGL REQUIRED)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowWGL.cxx)
+    list(APPEND libf3dSDKTests_link_libs OpenGL::GL)
+  endif()
+  if(APPLE)
+    find_library(COCOA_LIBRARY Cocoa)
+    find_library(OpenGL_LIBRARY OpenGL)
+    list(APPEND libf3dSDKTests_list TestSDKExternalWindowCOCOA.mm)
+    list(APPEND libf3dSDKTests_link_libs "${COCOA_LIBRARY}" "${OpenGL_LIBRARY}")
+  endif()
 endif()
 
 # create the testing file and list of tests
@@ -130,7 +130,7 @@
   CXX_VISIBILITY_PRESET hidden
   )
 
-target_compile_definitions(libf3dSDKTests PRIVATE F3D_MODULE_EXR=$<BOOL:${F3D_MODULE_EXR}>)
+target_compile_definitions(libf3dSDKTests PRIVATE GL_SILENCE_DEPRECATION)
 
 # Remove this once VTK 9.3 support is removed
 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240729)
@@ -141,6 +141,10 @@
   target_compile_definitions(libf3dSDKTests PRIVATE F3D_MODULE_EXR)
 endif ()
 
+if (F3D_MODULE_WEBP)
+  target_compile_definitions(libf3dSDKTests PRIVATE F3D_MODULE_WEBP)
+endif ()
+
 # List tests that do not require rendering
 list(APPEND libf3dSDKTestsNoRender_list
      TestSDKEngineExceptions
@@ -164,12 +168,12 @@
 set_tests_properties(libf3d::TestSDKLog PROPERTIES PASS_REGULAR_EXPRESSION
 "\
 Test Info\nTest Warning\nTest Error\n\
-Test Info\nTest Warning\nTest Error\n\
+Test Forward\nTest Info\nTest Warning\nTest Error\n\
 Test Warning\nTest Error\nTest Error\n\
 Test Print Debug\nTest Print Info\nTest Print Warning\nTest Print Error\n\
 Test Debug Coloring")
 
-if(F3D_MODULE_UI AND VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221220)
+if(F3D_MODULE_UI)
   set_tests_properties(libf3d::TestSDKDynamicHDRI PROPERTIES TIMEOUT 120)
   if(NOT F3D_TESTING_ENABLE_LONG_TIMEOUT_TESTS)
     set_tests_properties(libf3d::TestSDKDynamicHDRI PROPERTIES DISABLED ON)
@@ -177,12 +181,19 @@
 endif()
 
 if(F3D_MODULE_UI AND NOT F3D_TESTING_ENABLE_LONG_TIMEOUT_TESTS)
-  set_tests_properties(libf3d::TestSDKInteractorCallBack PROPERTIES DISABLED ON)
+  # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251001)
+    set_tests_properties(libf3d::TestSDKInteractorCallBack PROPERTIES DISABLED ON)
+  endif()
+
+  set_tests_properties(libf3d::TestSDKTriggerInteractions PROPERTIES DISABLED ON)
 endif()
 
 set_tests_properties(libf3d::TestSDKUtils PROPERTIES ENVIRONMENT "F3D_TEST_ENV_STANDARD=TestEnv;F3D_TEST_ENV_EXTENDED=TestËnv;F3D_TEST_ENV_UTF8=🤔;")
 
 target_link_libraries(libf3dSDKTests libf3d ${libf3dSDKTests_link_libs})
+target_compile_options(libf3dSDKTests PUBLIC ${f3d_compile_options_public} PRIVATE ${f3d_compile_options_private})
+target_link_options(libf3dSDKTests PUBLIC ${f3d_link_options_public})
 
 # make sure the libf3d API is compatible with the right C++ standard
 set_target_properties(libf3dSDKTests PROPERTIES CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/library/testing/PseudoUnitTest.h f3d-3.4.1/library/testing/PseudoUnitTest.h
--- f3d-3.2.0+dfsg/library/testing/PseudoUnitTest.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/PseudoUnitTest.h	2026-01-08 08:41:55.000000000 +0200
@@ -153,7 +153,7 @@
   {
     const std::string line = message.empty() ? label : (label + ": " + message);
     const std::string icon = success ? u8"\u2714" : u8"\u2718";
-    (success ? std::cout : std::cerr) << icon << " " << line << std::endl;
+    (success ? std::cout : std::cerr) << icon << " " << line << "\n";
   }
 
 private:
diff -Nru f3d-3.2.0+dfsg/library/testing/TestPseudoUnitTest.cxx f3d-3.4.1/library/testing/TestPseudoUnitTest.cxx
--- f3d-3.2.0+dfsg/library/testing/TestPseudoUnitTest.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestPseudoUnitTest.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include <iostream>
 
-int TestPseudoUnitTest(int argc, char* argv[])
+int TestPseudoUnitTest([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest metatest;
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKAnimation.cxx f3d-3.4.1/library/testing/TestSDKAnimation.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKAnimation.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKAnimation.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 #include <interactor.h>
 #include <scene.h>
 
-int TestSDKAnimation(int argc, char* argv[])
+int TestSDKAnimation([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
   f3d::engine eng = f3d::engine::create(true);
@@ -19,15 +19,42 @@
 
   inter.startAnimation();
   test("isPlaying after start", inter.isPlayingAnimation());
+  test("isPlaying forward after start",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::FORWARD);
 
   inter.toggleAnimation();
   test("isPlaying after toggle off", !inter.isPlayingAnimation());
 
   inter.toggleAnimation();
   test("isPlaying after toggle on", inter.isPlayingAnimation());
+  test("isPlaying forward toggle on",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::FORWARD);
+
+  f3d::interactor& interRef = inter.triggerEventLoop(0.1);
+  test("triggerEventLoop returns self", &interRef == &inter);
 
   inter.stopAnimation();
   test("isPlaying after stop", !inter.isPlayingAnimation());
 
+  inter.startAnimation(f3d::interactor::AnimationDirection::FORWARD);
+  test("isPlaying backward after forward start",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::FORWARD);
+  inter.stopAnimation();
+
+  inter.startAnimation(f3d::interactor::AnimationDirection::BACKWARD);
+  test("isPlaying backward after backward start",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::BACKWARD);
+  inter.stopAnimation();
+
+  inter.toggleAnimation(f3d::interactor::AnimationDirection::FORWARD);
+  test("isPlaying backward after forward toggle on",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::FORWARD);
+  inter.stopAnimation();
+
+  inter.toggleAnimation(f3d::interactor::AnimationDirection::BACKWARD);
+  test("isPlaying backward after backward toggle on",
+    inter.getAnimationDirection() == f3d::interactor::AnimationDirection::BACKWARD);
+  inter.stopAnimation();
+
   return test.result();
 }
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKCamera.cxx f3d-3.4.1/library/testing/TestSDKCamera.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKCamera.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKCamera.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -65,7 +65,7 @@
   }
 }
 
-int TestSDKCamera(int argc, char* argv[])
+int TestSDKCamera([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
   f3d::engine eng = f3d::engine::create(true);
@@ -77,7 +77,7 @@
   f3d::point3_t pointDC = win.getDisplayFromWorld(point);
   if (!comparePoint(point, win.getWorldFromDisplay(pointDC)))
   {
-    std::cerr << "coordinates conversion is not behaving as expected" << std::endl;
+    std::cerr << "coordinates conversion is not behaving as expected\n";
     return EXIT_FAILURE;
   }
 
@@ -87,7 +87,7 @@
   if (pos != testPos)
   {
     std::cerr << "set/get position is not behaving as expected: " << pos[0] << "," << pos[1] << ","
-              << pos[2] << std::endl;
+              << pos[2] << "\n";
     return EXIT_FAILURE;
   }
 
@@ -97,7 +97,7 @@
   if (foc != testFoc)
   {
     std::cerr << "set/get focal point is not behaving as expected: " << foc[0] << "," << foc[1]
-              << "," << foc[2] << std::endl;
+              << "," << foc[2] << "\n";
     return EXIT_FAILURE;
   }
 
@@ -107,7 +107,7 @@
   if (up != testUp)
   {
     std::cerr << "set/get view up is not behaving as expected: " << up[0] << "," << up[1] << ","
-              << up[2] << std::endl;
+              << up[2] << "\n";
     return EXIT_FAILURE;
   }
 
@@ -116,7 +116,7 @@
   f3d::angle_deg_t angle = cam.setViewAngle(testAngle).getViewAngle();
   if (angle != testAngle)
   {
-    std::cerr << "set/get view angle is not behaving as expected: " << angle << std::endl;
+    std::cerr << "set/get view angle is not behaving as expected: " << angle << "\n";
     return EXIT_FAILURE;
   }
 
@@ -131,13 +131,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Azimuth is not behaving as expected: " << std::endl;
+    std::cerr << "Azimuth is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -150,13 +150,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Roll is not behaving as expected: " << std::endl;
+    std::cerr << "Roll is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -169,13 +169,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Yaw is not behaving as expected: " << std::endl;
+    std::cerr << "Yaw is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -189,13 +189,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Elevation is not behaving as expected: " << std::endl;
+    std::cerr << "Elevation is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -209,13 +209,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Pitch is not behaving as expected: " << std::endl;
+    std::cerr << "Pitch is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -228,13 +228,13 @@
   if (!comparePoint(pos, expectedPos) || !comparePoint(foc, expectedFoc) ||
     !compareVec(up, expectedUp))
   {
-    std::cerr << "Dolly is not behaving as expected: " << std::endl;
+    std::cerr << "Dolly is not behaving as expected: \n";
     std::cerr << std::setprecision(12) << "position: " << pos[0] << "," << pos[1] << "," << pos[2]
-              << std::endl;
+              << "\n";
     std::cerr << std::setprecision(12) << "focal point: " << foc[0] << "," << foc[1] << ","
-              << foc[2] << std::endl;
+              << foc[2] << "\n";
     std::cerr << std::setprecision(12) << "view up: " << up[0] << "," << up[1] << "," << up[2]
-              << std::endl;
+              << "\n";
     return EXIT_FAILURE;
   }
 
@@ -308,7 +308,7 @@
   }
   catch (testFailure& e)
   {
-    std::cerr << e.what() << std::endl;
+    std::cerr << e.what() << "\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKCompareWithFile.cxx f3d-3.4.1/library/testing/TestSDKCompareWithFile.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKCompareWithFile.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKCompareWithFile.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKCompareWithFile(int argc, char* argv[])
+int TestSDKCompareWithFile([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKConsole.cxx f3d-3.4.1/library/testing/TestSDKConsole.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKConsole.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKConsole.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -3,7 +3,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKConsole(int argc, char* argv[])
+int TestSDKConsole([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::INFO);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDeprecatedOptions.cxx f3d-3.4.1/library/testing/TestSDKDeprecatedOptions.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDeprecatedOptions.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDeprecatedOptions.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -16,7 +16,7 @@
 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 #endif
 
-int TestSDKDeprecatedOptions(int argc, char* argv[])
+int TestSDKDeprecatedOptions([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
@@ -25,6 +25,7 @@
   win.setSize(300, 300);
 
   opt.render.effect.anti_aliasing = true;
+  opt.render.effect.translucency_support = true;
 
   sce.add(std::string(argv[1]) + "/data/cow.vtp");
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDropZone.cxx f3d-3.4.1/library/testing/TestSDKDropZone.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDropZone.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDropZone.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,15 +4,16 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKDropZone(int argc, char* argv[])
+int TestSDKDropZone([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::window& win = eng.getWindow();
   f3d::options& opt = eng.getOptions();
   win.setSize(300, 300);
-  opt.ui.dropzone = true;
-  opt.ui.dropzone_info = "Drop a file to open it\nPress H to show cheatsheet";
 
+  opt.ui.drop_zone.enable = true;
+  opt.ui.drop_zone.show_logo = true;
+  opt.ui.drop_zone.custom_binds = "None+Drop Ctrl+O None+H";
   win.render();
 
   return TestSDKHelpers::RenderTest(eng.getWindow(), std::string(argv[1]) + "baselines/",
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneDeprecated.cxx f3d-3.4.1/library/testing/TestSDKDropZoneDeprecated.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneDeprecated.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/library/testing/TestSDKDropZoneDeprecated.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,61 @@
+#include <engine.h>
+#include <interactor.h>
+#include <window.h>
+
+#include "TestSDKHelpers.h"
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4996)
+#elif defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+int TestSDKDropZoneDeprecated([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
+{
+  f3d::engine eng = f3d::engine::create(true);
+  f3d::window& win = eng.getWindow();
+  f3d::options& opt = eng.getOptions();
+  win.setSize(300, 300);
+
+  // --- Test using the DEPRECATED option ---
+
+  opt.ui.drop_zone.enable = true;
+  opt.ui.drop_zone.show_logo = true;
+  opt.ui.drop_zone.info = "Drop a file to open it\nPress H to show cheatsheet";
+  win.render();
+
+  bool deprecatedOptionResult1 = TestSDKHelpers::RenderTest(eng.getWindow(),
+    std::string(argv[1]) + "baselines/", std::string(argv[2]), "TestSDKDropZoneDeprecated");
+
+  if (!deprecatedOptionResult1)
+  {
+    std::cerr << "[ERROR] DEPRECATED drop_zone.info option render test failed" << '\n';
+  }
+
+  opt.ui.dropzone = true;
+  opt.ui.dropzone_info = "Drop a file to open it\nPress H to show cheatsheet";
+  win.render();
+
+  bool deprecatedOptionResult2 = TestSDKHelpers::RenderTest(eng.getWindow(),
+    std::string(argv[1]) + "baselines/", std::string(argv[2]), "TestSDKDropZoneDeprecated");
+
+  if (!deprecatedOptionResult2)
+  {
+    std::cerr << "[ERROR] DEPRECATED dropzone_info option render test failed" << '\n';
+  }
+
+  return (deprecatedOptionResult1 && deprecatedOptionResult2) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#elif defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneMinSize.cxx f3d-3.4.1/library/testing/TestSDKDropZoneMinSize.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneMinSize.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDropZoneMinSize.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -10,8 +10,9 @@
 
   win.setSize(5, 5);
 
-  opt.ui.dropzone = true;
-  opt.ui.dropzone_info = "This should not be rendered due to small viewport.";
+  opt.ui.drop_zone.enable = true;
+  opt.ui.drop_zone.show_logo = true;
+  opt.ui.drop_zone.custom_binds = "None+Drop Ctrl+O None+H";
 
   try
   {
@@ -19,7 +20,7 @@
   }
   catch (const std::exception& e)
   {
-    std::cerr << "Unexpected error during render: " << e.what() << std::endl;
+    std::cerr << "Unexpected error during render: " << e.what() << "\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneNoLogo.cxx f3d-3.4.1/library/testing/TestSDKDropZoneNoLogo.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDropZoneNoLogo.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/library/testing/TestSDKDropZoneNoLogo.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,23 @@
+#include <engine.h>
+#include <interactor.h>
+#include <window.h>
+
+#include "TestSDKHelpers.h"
+
+int TestSDKDropZoneNoLogo([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
+{
+  f3d::engine eng = f3d::engine::create(true);
+  f3d::window& win = eng.getWindow();
+  f3d::options& opt = eng.getOptions();
+  win.setSize(300, 300);
+  opt.ui.drop_zone.enable = true;
+  opt.ui.drop_zone.show_logo = false;
+  opt.ui.drop_zone.custom_binds = "None+Drop Ctrl+O None+H";
+
+  win.render();
+
+  return TestSDKHelpers::RenderTest(eng.getWindow(), std::string(argv[1]) + "baselines/",
+           std::string(argv[2]), "TestSDKDropZoneNoLogo")
+    ? EXIT_SUCCESS
+    : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicBackgroundColor.cxx f3d-3.4.1/library/testing/TestSDKDynamicBackgroundColor.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicBackgroundColor.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDynamicBackgroundColor.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKDynamicBackgroundColor(int argc, char* argv[])
+int TestSDKDynamicBackgroundColor([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicFontFile.cxx f3d-3.4.1/library/testing/TestSDKDynamicFontFile.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicFontFile.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDynamicFontFile.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKDynamicFontFile(int argc, char* argv[])
+int TestSDKDynamicFontFile([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicHDRI.cxx f3d-3.4.1/library/testing/TestSDKDynamicHDRI.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicHDRI.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDynamicHDRI.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -9,7 +9,7 @@
 
 #include <random>
 
-int TestSDKDynamicHDRI(int argc, char* argv[])
+int TestSDKDynamicHDRI([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::INFO);
 
@@ -27,7 +27,7 @@
   bool ret = win.render();
   if (!ret)
   {
-    std::cerr << "First render failed" << std::endl;
+    std::cerr << "First render failed\n";
     return EXIT_FAILURE;
   }
 
@@ -45,7 +45,7 @@
     std::string(argv[2]), "TestSDKDynamicHDRIDefault");
   if (!ret)
   {
-    std::cerr << "Render with Default HDRI failed" << std::endl;
+    std::cerr << "Render with Default HDRI failed\n";
     return EXIT_FAILURE;
   }
 
@@ -55,7 +55,7 @@
     std::string(argv[2]), "TestSDKDynamicHDRI");
   if (!ret)
   {
-    std::cerr << "Render with HDRI failed" << std::endl;
+    std::cerr << "Render with HDRI failed\n";
     return EXIT_FAILURE;
   }
 
@@ -63,7 +63,7 @@
   std::ifstream lutFile(cachePath + "/lut.vti");
   if (!lutFile.is_open())
   {
-    std::cerr << "LUT cache file not found" << std::endl;
+    std::cerr << "LUT cache file not found\n";
     return EXIT_FAILURE;
   }
 
@@ -73,7 +73,7 @@
     std::string(argv[2]), "TestSDKDynamicHDRI");
   if (!ret)
   {
-    std::cerr << "Render with HDRI with another cache path failed" << std::endl;
+    std::cerr << "Render with HDRI with another cache path failed\n";
     return EXIT_FAILURE;
   }
 
@@ -83,7 +83,7 @@
     std::string(argv[2]), "TestSDKDynamicHDRI");
   if (!ret)
   {
-    std::cerr << "Render with HDRI with existing cache path failed" << std::endl;
+    std::cerr << "Render with HDRI with existing cache path failed\n";
     return EXIT_FAILURE;
   }
 
@@ -95,7 +95,7 @@
 
   if (!ret)
   {
-    std::cerr << "Render with EXR HDRI failed" << std::endl;
+    std::cerr << "Render with EXR HDRI failed\n";
     return EXIT_FAILURE;
   }
 #endif
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicLightIntensity.cxx f3d-3.4.1/library/testing/TestSDKDynamicLightIntensity.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicLightIntensity.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDynamicLightIntensity.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKDynamicLightIntensity(int argc, char* argv[])
+int TestSDKDynamicLightIntensity([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& Scene = eng.getScene();
@@ -21,7 +21,7 @@
   if (!TestSDKHelpers::RenderTest(eng.getWindow(), std::string(argv[1]) + "baselines/",
         std::string(argv[2]), "TestSDKDynamicLightIntensity-default"))
   {
-    std::cerr << "failed for default light intensity" << std::endl;
+    std::cerr << "failed for default light intensity\n";
     return EXIT_FAILURE;
   }
 
@@ -30,7 +30,7 @@
   if (!TestSDKHelpers::RenderTest(eng.getWindow(), std::string(argv[1]) + "baselines/",
         std::string(argv[2]), "TestSDKDynamicLightIntensity-5x-brighter"))
   {
-    std::cerr << "failed for light intensity = 5" << std::endl;
+    std::cerr << "failed for light intensity = 5\n";
     return EXIT_FAILURE;
   }
 
@@ -39,7 +39,7 @@
   if (!TestSDKHelpers::RenderTest(eng.getWindow(), std::string(argv[1]) + "baselines/",
         std::string(argv[2]), "TestSDKDynamicLightIntensity-5x-darker"))
   {
-    std::cerr << "failed for light intensity = .2" << std::endl;
+    std::cerr << "failed for light intensity = .2\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicProperties.cxx f3d-3.4.1/library/testing/TestSDKDynamicProperties.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicProperties.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKDynamicProperties.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKDynamicProperties(int argc, char* argv[])
+int TestSDKDynamicProperties([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKDynamicUpDirection.cxx f3d-3.4.1/library/testing/TestSDKDynamicUpDirection.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKDynamicUpDirection.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/library/testing/TestSDKDynamicUpDirection.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,71 @@
+#include "PseudoUnitTest.h"
+
+#include <camera.h>
+#include <engine.h>
+#include <options.h>
+#include <scene.h>
+#include <window.h>
+
+#include <cmath>
+
+namespace
+{
+bool compareDouble(double a, double b, double epsilon = 1e-6)
+{
+  return std::fabs(a - b) < epsilon;
+}
+
+bool compareVec(const f3d::vector3_t& vec1, const f3d::vector3_t& vec2, double epsilon = 1e-6)
+{
+  return compareDouble(vec1[0], vec2[0], epsilon) && compareDouble(vec1[1], vec2[1], epsilon) &&
+    compareDouble(vec1[2], vec2[2], epsilon);
+}
+}
+
+int TestSDKDynamicUpDirection([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
+{
+  PseudoUnitTest test;
+
+  f3d::engine eng = f3d::engine::create(true);
+  f3d::scene& sce = eng.getScene();
+  f3d::window& win = eng.getWindow();
+  f3d::options& opt = eng.getOptions();
+  f3d::camera& cam = win.getCamera();
+
+  win.setSize(300, 300);
+
+  sce.add(std::string(argv[1]) + "/data/cow.vtp");
+
+  win.render();
+
+  f3d::vector3_t initialUp = cam.getViewUp();
+
+  test("initial up direction is +Y", compareVec(initialUp, { 0, 1, 0 }));
+
+  opt.scene.up_direction = { 0, 0, 1 };
+
+  win.render();
+
+  f3d::vector3_t newUp = cam.getViewUp();
+
+  test("camera view up rotated to +Z", compareVec(newUp, { 0, 0, 1 }));
+
+  opt.scene.up_direction = { 1, 0, 0 };
+  win.render();
+
+  newUp = cam.getViewUp();
+  test("camera view up rotated to +X", compareVec(newUp, { 1, 0, 0 }));
+
+  opt.scene.up_direction = { 0, 1, 0 };
+  win.render();
+
+  newUp = cam.getViewUp();
+  test("camera view up rotated back to +Y", compareVec(newUp, { 0, 1, 0 }));
+
+  opt.scene.up_direction = { 0, 1, 0 };
+  win.render();
+  newUp = cam.getViewUp();
+  test("setting same direction doesn't change anything", compareVec(newUp, { 0, 1, 0 }));
+
+  return test.result();
+}
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKEngine.cxx f3d-3.4.1/library/testing/TestSDKEngine.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKEngine.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKEngine.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -7,7 +7,7 @@
 
 #include <iostream>
 
-int TestSDKEngine(int argc, char* argv[])
+int TestSDKEngine([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
 
@@ -25,17 +25,17 @@
 
   // Test different flags combinations that makes sense
   f3d::engine eng0 = f3d::engine::createNone();
-  const f3d::scene& sce = eng0.getScene();
+  std::ignore = eng0.getScene();
 
   f3d::engine eng1 = f3d::engine::create();
-  const f3d::scene& sce1 = eng1.getScene();
-  const f3d::window& window1 = eng1.getWindow();
-  const f3d::interactor& inter1 = eng1.getInteractor();
+  std::ignore = eng1.getScene();
+  std::ignore = eng1.getWindow();
+  std::ignore = eng1.getInteractor();
 
   f3d::engine eng2 = f3d::engine::create(true);
-  const f3d::scene& sce2 = eng2.getScene();
-  const f3d::window& window2 = eng2.getWindow();
-  const f3d::interactor& inter2 = eng2.getInteractor();
+  std::ignore = eng2.getScene();
+  std::ignore = eng2.getWindow();
+  std::ignore = eng2.getInteractor();
 
   // Test option setters
   f3d::options opt;
@@ -44,8 +44,7 @@
   eng0.setOptions(opt);
   if (!eng0.getOptions().model.scivis.cells)
   {
-    std::cerr << "Unexpected options value using f3d::engine::setOptions(const options& opt)"
-              << std::endl;
+    std::cerr << "Unexpected options value using f3d::engine::setOptions(const options& opt)\n";
     return EXIT_FAILURE;
   }
 
@@ -53,8 +52,7 @@
   eng0.setOptions(std::move(opt));
   if (eng0.getOptions().render.line_width != 1.7)
   {
-    std::cerr << "Unexpected options value using f3d::engine::setOptions(options&& opt)"
-              << std::endl;
+    std::cerr << "Unexpected options value using f3d::engine::setOptions(options&& opt)\n";
     return EXIT_FAILURE;
   }
 
@@ -62,14 +60,14 @@
   auto libInfo = f3d::engine::getLibInfo();
   if (libInfo.License != "BSD-3-Clause")
   {
-    std::cerr << "Unexpected libInfo output" << std::endl;
+    std::cerr << "Unexpected libInfo output\n";
     return EXIT_FAILURE;
   }
 
   auto readersInfo = f3d::engine::getReadersInfo();
   if (readersInfo.size() == 0)
   {
-    std::cerr << "Unexpected readersInfo output" << std::endl;
+    std::cerr << "Unexpected readersInfo output\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKEngineExceptions.cxx f3d-3.4.1/library/testing/TestSDKEngineExceptions.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKEngineExceptions.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKEngineExceptions.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -10,10 +10,22 @@
 
 namespace fs = std::filesystem;
 
-int TestSDKEngineExceptions(int argc, char* argv[])
+int TestSDKEngineExceptions([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
+  // For coverage, check that an invalid symbol loader is not crashing
+  try
+  {
+    std::ignore = f3d::engine::createExternal([](const char*) { return nullptr; });
+  }
+  catch (const f3d::engine::no_window_exception&)
+  {
+  }
+
   PseudoUnitTest test;
 
+  test.expect<f3d::engine::no_window_exception>(
+    "create empty external context", []() { std::ignore = f3d::engine::createExternal(nullptr); });
+
   {
     f3d::engine eng = f3d::engine::createNone();
     test.expect<f3d::engine::no_window_exception>(
@@ -28,8 +40,9 @@
       [&]() { eng.setCachePath("/" + std::string(257, 'x')); });
 
     // cover operator=(engine&&)
-    eng = f3d::engine::create(false);
-    test("engine assignment operator", eng.getWindow().isOffscreen() == false);
+    // test with offscreen window because it works with all backends
+    eng = f3d::engine::create(true);
+    test("engine assignment operator", eng.getWindow().isOffscreen() == true);
   }
 
 #if defined(__linux__) || defined(__FreeBSD__)
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKEngineRecreation.cxx f3d-3.4.1/library/testing/TestSDKEngineRecreation.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKEngineRecreation.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKEngineRecreation.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,7 +1,7 @@
 #include <engine.h>
 #include <interactor.h>
 
-int TestSDKEngineRecreation(int argc, char* argv[])
+int TestSDKEngineRecreation([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   std::string filename = "TestSDKEngineRecreation";
   std::string interactionFilePath = std::string(argv[1]) + "/recordings/" + filename + ".log";
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowCOCOA.mm f3d-3.4.1/library/testing/TestSDKExternalWindowCOCOA.mm
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowCOCOA.mm	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowCOCOA.mm	2026-01-08 08:41:55.000000000 +0200
@@ -34,7 +34,7 @@
   self.openGLContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
   [self.openGLContext makeCurrentContext];
 
-  f3d::engine engine = f3d::engine::createExternal(nullptr);
+  f3d::engine engine = f3d::engine::createExternal(f3d::context::cocoa());
   engine.getWindow().setSize(300, 300);
   engine.getScene().add(_dataPath + "/data/cow.vtp");
 
@@ -48,7 +48,7 @@
 
 @end
 
-int TestSDKExternalWindowCOCOA(int argc, char* argv[])
+int TestSDKExternalWindowCOCOA([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   @autoreleasepool {
     NSApplication *app = [NSApplication sharedApplication];
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowEGL.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowEGL.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowEGL.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowEGL.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 
 #include <EGL/egl.h>
 
-int TestSDKExternalWindowEGL(int argc, char* argv[])
+int TestSDKExternalWindowEGL([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   EGLDisplay eglDpy = eglGetDisplay(EGL_DEFAULT_DISPLAY);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowGLFW.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowGLFW.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowGLFW.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowGLFW.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,17 +8,17 @@
 
 #include <iostream>
 
-int TestSDKExternalWindowGLFW(int argc, char* argv[])
+int TestSDKExternalWindowGLFW([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   // setup glfw window
   if (!glfwInit())
   {
-    std::cerr << "Can't initialize GLFW." << std::endl;
+    std::cerr << "Can't initialize GLFW.\n";
     return EXIT_FAILURE;
   }
 
   glfwSetErrorCallback([](int error, const char* desc) {
-    std::cerr << "GLFW error " << error << ": " << desc << std::endl;
+    std::cerr << "GLFW error " << error << ": " << desc << "\n";
   });
   glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
   glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
@@ -28,7 +28,7 @@
 
   if (!window)
   {
-    std::cerr << "Can't create GLFW window." << std::endl;
+    std::cerr << "Can't create GLFW window.\n";
     return EXIT_FAILURE;
   }
 
@@ -38,10 +38,10 @@
   eng.getWindow().setSize(300, 300);
 
   // key callback
-  glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int scancode, int action, int mods) {
+  glfwSetKeyCallback(window, [](GLFWwindow* win, int key, int scancode, int action, int mods) {
     if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
     {
-      glfwSetWindowShouldClose(window, 1);
+      glfwSetWindowShouldClose(win, 1);
     }
   });
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowGLX.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowGLX.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowGLX.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowGLX.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -12,79 +12,77 @@
 #include <iostream>
 #include <unistd.h>
 
-#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
-#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
 using glXCreateContextAttribsARBProc = GLXContext (*)(
   Display*, GLXFBConfig, GLXContext, Bool, const int*);
 
-int TestSDKExternalWindowGLX(int argc, char* argv[])
+int TestSDKExternalWindowGLX([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   Display* display = XOpenDisplay(nullptr);
 
   glXCreateContextAttribsARBProc glXCreateContextAttribsARB = nullptr;
 
   const char* extensions = glXQueryExtensionsString(display, DefaultScreen(display));
-  std::cout << extensions << std::endl;
+  std::cout << extensions << "\n";
 
   static int visual_attribs[] = { GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
     GLX_DOUBLEBUFFER, true, GLX_RED_SIZE, 1, GLX_GREEN_SIZE, 1, GLX_BLUE_SIZE, 1, None };
 
-  std::cout << "Getting framebuffer config" << std::endl;
+  std::cout << "Getting framebuffer config\n";
   int fbcount;
   GLXFBConfig* fbc = glXChooseFBConfig(display, DefaultScreen(display), visual_attribs, &fbcount);
   if (!fbc)
   {
-    std::cerr << "Failed to retrieve a framebuffer config" << std::endl;
+    std::cerr << "Failed to retrieve a framebuffer config\n";
     return EXIT_FAILURE;
   }
 
-  std::cout << "Getting XVisualInfo" << std::endl;
+  std::cout << "Getting XVisualInfo\n";
   XVisualInfo* vi = glXGetVisualFromFBConfig(display, fbc[0]);
 
   XSetWindowAttributes swa;
-  std::cout << "Creating colormap" << std::endl;
+  std::cout << "Creating colormap\n";
   swa.colormap = XCreateColormap(display, RootWindow(display, vi->screen), vi->visual, AllocNone);
   swa.border_pixel = 0;
   swa.event_mask = StructureNotifyMask;
 
-  std::cout << "Creating window" << std::endl;
+  std::cout << "Creating window\n";
   Window win = XCreateWindow(display, RootWindow(display, vi->screen), 0, 0, 100, 100, 0, vi->depth,
     InputOutput, vi->visual, CWBorderPixel | CWColormap | CWEventMask, &swa);
   if (!win)
   {
-    std::cerr << "Failed to create window." << std::endl;
+    std::cerr << "Failed to create window.\n";
     return EXIT_FAILURE;
   }
 
-  std::cout << "Mapping window" << std::endl;
+  std::cout << "Mapping window\n";
   XMapWindow(display, win);
 
   // Create an oldstyle context first, to get the correct function pointer for
   // glXCreateContextAttribsARB
   GLXContext ctx_old = glXCreateContext(display, vi, nullptr, GL_TRUE);
-  glXCreateContextAttribsARB =
-    (glXCreateContextAttribsARBProc)glXGetProcAddress((const GLubyte*)"glXCreateContextAttribsARB");
+  glXCreateContextAttribsARB = reinterpret_cast<glXCreateContextAttribsARBProc>(
+    glXGetProcAddress(reinterpret_cast<const GLubyte*>("glXCreateContextAttribsARB")));
   glXMakeCurrent(display, 0, nullptr);
   glXDestroyContext(display, ctx_old);
 
   if (glXCreateContextAttribsARB == nullptr)
   {
-    std::cerr << "glXCreateContextAttribsARB entry point not found. Aborting." << std::endl;
+    std::cerr << "glXCreateContextAttribsARB entry point not found. Aborting.\n";
     return EXIT_FAILURE;
   }
 
   static int context_attribs[] = { GLX_CONTEXT_MAJOR_VERSION_ARB, 3, GLX_CONTEXT_MINOR_VERSION_ARB,
     2, None };
 
-  std::cout << "Creating context" << std::endl;
+  std::cout << "Creating context\n";
   GLXContext ctx = glXCreateContextAttribsARB(display, fbc[0], nullptr, true, context_attribs);
   if (!ctx)
   {
-    std::cerr << "Failed to create GL3 context." << std::endl;
+    std::cerr << "Failed to create GL3 context.\n";
     return EXIT_FAILURE;
   }
 
-  std::cout << "Making context current" << std::endl;
+  std::cout << "Making context current\n";
   glXMakeCurrent(display, win, ctx);
 
   f3d::engine eng = f3d::engine::createExternalGLX();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowOSMesa.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowOSMesa.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowOSMesa.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowOSMesa.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 
 #include <GL/osmesa.h>
 
-int TestSDKExternalWindowOSMesa(int argc, char* argv[])
+int TestSDKExternalWindowOSMesa([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   int size[] = { 300, 300 };
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowQT.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowQT.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowQT.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowQT.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -79,7 +79,7 @@
   std::string mOutputPath;
 };
 
-int TestSDKExternalWindowQT(int argc, char* argv[])
+int TestSDKExternalWindowQT([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   QGuiApplication a(argc, argv);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowWGL.cxx f3d-3.4.1/library/testing/TestSDKExternalWindowWGL.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKExternalWindowWGL.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKExternalWindowWGL.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -23,7 +23,7 @@
   return DefWindowProc(hwnd, uMsg, wParam, lParam);
 }
 
-int TestSDKExternalWindowWGL(int argc, char* argv[])
+int TestSDKExternalWindowWGL([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   HINSTANCE hInstance = GetModuleHandle(NULL);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKHelpers.h f3d-3.4.1/library/testing/TestSDKHelpers.h
--- f3d-3.2.0+dfsg/library/testing/TestSDKHelpers.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKHelpers.h	2026-01-08 08:41:55.000000000 +0200
@@ -10,12 +10,12 @@
 namespace TestSDKHelpers
 {
 
-static bool RenderTest(const f3d::image& img, const std::string& baselinePath,
+inline bool RenderTest(const f3d::image& img, const std::string& baselinePath,
   const std::string& outputPath, const std::string& name, double threshold = 0.05)
 {
   if (baselinePath.empty() || outputPath.empty() || name.empty())
   {
-    std::cerr << "A path or name is empty, aborting" << std::endl;
+    std::cerr << "A path or name is empty, aborting\n";
     return false;
   }
 
@@ -29,7 +29,7 @@
       img.save(output);
       std::cerr << "Reference image "
                 << baseline + " does not exist, current rendering has been outputted to " << output
-                << std::endl;
+                << "\n";
       return false;
     }
   }
@@ -41,21 +41,21 @@
   if (error > threshold)
   {
     std::cerr << "Current rendering difference with reference image " << baseline << " : " << error
-              << " is higher than the threshold of " << threshold << std::endl;
-    std::cerr << "Result resolution: " << img.getWidth() << "x" << img.getHeight() << std::endl;
+              << " is higher than the threshold of " << threshold << "\n";
+    std::cerr << "Result resolution: " << img.getWidth() << "x" << img.getHeight() << "\n";
     std::cerr << "Reference resolution: " << reference.getWidth() << "x" << reference.getHeight()
-              << std::endl;
-    std::cerr << "Result image saved to " << output << std::endl;
+              << "\n";
+    std::cerr << "Result image saved to " << output << "\n";
     img.save(output);
     return false;
   }
 
   std::cout << "Successful render test against " << baseline + " with an error of " << error
-            << std::endl;
+            << "\n";
   return true;
 }
 
-static bool RenderTest(f3d::window& win, const std::string& baselinePath,
+inline bool RenderTest(f3d::window& win, const std::string& baselinePath,
   const std::string& outputPath, const std::string& name, double threshold = 0.05,
   bool noBackground = false)
 {
@@ -63,5 +63,10 @@
     win.renderToImage(noBackground), baselinePath, outputPath, name, threshold);
 }
 
+constexpr double Degrees2Radians(double degrees)
+{
+  return (3.14159265358979323846 * degrees / 180.0);
+}
+
 }
 #endif
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKImage.cxx f3d-3.4.1/library/testing/TestSDKImage.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKImage.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKImage.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -11,7 +11,7 @@
 #include <set>
 #include <sstream>
 
-int TestSDKImage(int argc, char* argv[])
+int TestSDKImage([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
@@ -24,6 +24,10 @@
 #if F3D_MODULE_EXR
   test("supported formats EXR", std::find(formats.begin(), formats.end(), ".exr") != formats.end());
 #endif
+#if F3D_MODULE_WEBP
+  test(
+    "supported formats WebP", std::find(formats.begin(), formats.end(), ".webp") != formats.end());
+#endif
 
   constexpr unsigned int width = 64;
   constexpr unsigned int height = 64;
@@ -36,13 +40,14 @@
 
   f3d::image generated(width, height, channels);
   std::vector<uint8_t> pixels(width * height * channels);
-  std::generate(std::begin(pixels), std::end(pixels), [&]() { return randGenerator() % 256; });
+  std::generate(std::begin(pixels), std::end(pixels),
+    [&]() { return static_cast<uint8_t>(randGenerator() % 256); });
   generated.setContent(pixels.data());
 
   f3d::image generated16(width, height, channels, f3d::image::ChannelType::SHORT);
   std::vector<uint16_t> pixels16(width * height * channels);
-  std::generate(
-    std::begin(pixels16), std::end(pixels16), [&]() { return randGenerator() % 65536; });
+  std::generate(std::begin(pixels16), std::end(pixels16),
+    [&]() { return static_cast<uint16_t>(randGenerator() % 65536); });
   generated16.setContent(pixels16.data());
 
   std::uniform_real_distribution<float> dist(
@@ -115,6 +120,12 @@
     exrImg.getChannelType() == f3d::image::ChannelType::FLOAT);
 #endif
 
+#if F3D_MODULE_WEBP
+  // check reading WebP
+  f3d::image webpImg(testingDir + "/data/image.webp");
+  test("check width WebP image channel type", webpImg.getWidth() == 1024);
+#endif
+
   // check reading invalid image
   test.expect<f3d::image::read_exception>(
     "read invalid image", [&]() { f3d::image invalidImg(testingDir + "/data/invalid.png"); });
@@ -140,7 +151,7 @@
   if (generated != baseline)
   {
     std::cerr << "Generated image is different from the png baseline: "
-              << generated.compare(baseline) << std::endl;
+              << generated.compare(baseline) << "\n";
     return EXIT_FAILURE;
   }
 
@@ -150,7 +161,7 @@
   if (generated != baselineTIF)
   {
     std::cerr << "Generated image is different from the tif baseline: "
-              << generated.compare(baselineTIF) << std::endl;
+              << generated.compare(baselineTIF) << "\n";
     return EXIT_FAILURE;
   }*/
 
@@ -168,7 +179,7 @@
   if (generated16 != baseline16)
   {
     std::cerr << "generated short image is different from the baseline: "
-              << generated16.compare(baseline16) << std::endl;
+              << generated16.compare(baseline16) << "\n";
     return EXIT_FAILURE;
   }
 
@@ -178,7 +189,7 @@
   if (generated16 != baseline16TIF)
   {
     std::cerr << "generated short image is different from the TIF baseline: "
-              << generated16.compare(baseline16TIF) << std::endl;
+              << generated16.compare(baseline16TIF) << "\n";
     return EXIT_FAILURE;
   }*/
 
@@ -197,7 +208,7 @@
   if (generated32 != baseline32)
   {
     std::cerr << "generated float image is different from the baseline: "
-              << generated32.compare(baseline32) << std::endl;
+              << generated32.compare(baseline32) << "\n";
     return EXIT_FAILURE;
   }
 #endif // F3D_SSIM_COMPARE
@@ -281,7 +292,7 @@
     {
       std::vector<unsigned char> buffer = img1.saveBuffer();
       std::ofstream outfile(tmpDir + "/metadata-buffer.png", std::ios::out | std::ios::binary);
-      outfile.write((const char*)&buffer[0], buffer.size());
+      outfile.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
     }
 
     f3d::image img2(tmpDir + "/metadata-buffer.png");
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKInteractorCallBack.cxx f3d-3.4.1/library/testing/TestSDKInteractorCallBack.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKInteractorCallBack.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKInteractorCallBack.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -10,7 +10,7 @@
 
 using mod_t = f3d::interaction_bind_t::ModifierKeys;
 
-int TestSDKInteractorCallBack(int argc, char* argv[])
+int TestSDKInteractorCallBack([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::scene& sce = eng.getScene();
@@ -22,12 +22,12 @@
   // Sanity checks coverage
   if (inter.playInteraction(""))
   {
-    std::cerr << "Unexcepted playInteraction output" << std::endl;
+    std::cerr << "Unexcepted playInteraction output\n";
     return EXIT_FAILURE;
   }
   if (inter.recordInteraction(""))
   {
-    std::cerr << "Unexcepted recordInteraction output" << std::endl;
+    std::cerr << "Unexcepted recordInteraction output\n";
     return EXIT_FAILURE;
   }
 
@@ -38,7 +38,7 @@
   // Dragon.vtu; SZZYB; CTRL+S; CTRL+P; SHIFT+Y; CTRL+SHIFT+B; CTRL+SHIFT+A; 7
   if (!inter.playInteraction(interactionFilePath))
   {
-    std::cerr << "Unexcepted error playing interaction" << std::endl;
+    std::cerr << "Unexcepted error playing interaction\n";
     return EXIT_FAILURE;
   }
   if (!TestSDKHelpers::RenderTest(
@@ -51,11 +51,10 @@
   try
   {
     inter.addBinding({ mod_t::ANY, "7" }, "exception");
-    std::cerr << "An exception has not been thrown when adding a existing interaction command"
-              << std::endl;
+    std::cerr << "An exception has not been thrown when adding a existing interaction command\n";
     return EXIT_FAILURE;
   }
-  catch (const f3d::interactor::already_exists_exception& ex)
+  catch (const f3d::interactor::already_exists_exception&)
   {
   }
 
@@ -106,11 +105,11 @@
   // Dragon.vtu; SZZYB; CTRL+S; CTRL+P; SHIFT+Y; CTRL+SHIFT+B; CTRL+SHIFT+A; 7
   if (!inter.playInteraction(interactionFilePath))
   {
-    std::cerr << "Unexcepted error playing interaction" << std::endl;
+    std::cerr << "Unexcepted error playing interaction\n";
     return EXIT_FAILURE;
   }
 
-  // With VTK 9.2.6 and 9.3.0, rendering is slightly different
+  // With VTK 9.3.0, rendering is slightly different
   if (!TestSDKHelpers::RenderTest(win, std::string(argv[1]) + "baselines/", std::string(argv[2]),
         filename + "Modified", 0.11))
   {
@@ -133,11 +132,11 @@
   // Dragon.vtu; SZZYB; CTRL+S; CTRL+P; SHIFT+Y; CTRL+SHIFT+B; CTRL+SHIFT+A; 7
   if (!inter.playInteraction(interactionFilePath))
   {
-    std::cerr << "Unexcepted error playing interaction" << std::endl;
+    std::cerr << "Unexcepted error playing interaction\n";
     return EXIT_FAILURE;
   }
 
-  // With VTK 9.2.6 and 9.3.0, rendering is slightly different
+  // With VTK 9.3.0, rendering is slightly different
   if (!TestSDKHelpers::RenderTest(win, std::string(argv[1]) + "baselines/", std::string(argv[2]),
         filename + "ModifiedAgain", 0.11))
   {
@@ -151,25 +150,25 @@
   // Dragon.vtu; SZZYB; CTRL+S; CTRL+P; SHIFT+Y; CTRL+SHIFT+B; CTRL+SHIFT+A; 7
   if (!inter.playInteraction(interactionFilePath))
   {
-    std::cerr << "Unexcepted error playing interaction" << std::endl;
+    std::cerr << "Unexcepted error playing interaction\n";
     return EXIT_FAILURE;
   }
   if (!TestSDKHelpers::RenderTest(
         win, std::string(argv[1]) + "baselines/", std::string(argv[2]), filename + "DefaultAgain"))
   {
-    std::cerr << "Unexcepted rendering playing interaction" << std::endl;
+    std::cerr << "Unexcepted rendering playing interaction\n";
     return EXIT_FAILURE;
   }
 
   // Check error handling
   if (inter.recordInteraction("/" + std::string(257, 'x') + "/record.ext"))
   {
-    std::cerr << "Unexcepted success recording an invalid path" << std::endl;
+    std::cerr << "Unexcepted success recording an invalid path\n";
     return EXIT_FAILURE;
   }
   if (inter.playInteraction("/" + std::string(257, 'x') + "/play.ext"))
   {
-    std::cerr << "Unexcepted success playing an invalid path" << std::endl;
+    std::cerr << "Unexcepted success playing an invalid path\n";
     return EXIT_FAILURE;
   }
 
@@ -178,8 +177,7 @@
   if (!inter.playInteraction(
         std::string(argv[1]) + "recordings/TestSDKInteractorCallBackConsoleException.log"))
   {
-    std::cerr << "Unexcepted failure playing a console command triggering an exception"
-              << std::endl;
+    std::cerr << "Unexcepted failure playing a console command triggering an exception\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKInteractorCommand.cxx f3d-3.4.1/library/testing/TestSDKInteractorCommand.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKInteractorCommand.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKInteractorCommand.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 #include <interactor.h>
 #include <options.h>
 
-int TestSDKInteractorCommand(int argc, char* argv[])
+int TestSDKInteractorCommand([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::options& options = eng.getOptions();
@@ -57,6 +57,9 @@
   inter.triggerCommand("test_toggle");
   test("addCommand", options.model.scivis.cells == false);
 
+  // Coverage help with no doc
+  test("help test_toggle", inter.triggerCommand("help test_toggle") == true);
+
   inter.removeCommand("test_toggle");
   test("removeCommand", inter.triggerCommand("test_toggle") == false);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKInteractorDocumentation.cxx f3d-3.4.1/library/testing/TestSDKInteractorDocumentation.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKInteractorDocumentation.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKInteractorDocumentation.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -7,16 +7,16 @@
 
 constexpr int nGroup = 3;
 constexpr int nBindsCamera = 9;
-constexpr std::string_view initDoc = "Toggle Orthographic Projection";
+constexpr std::string_view initDoc = "Orthographic Projection";
 constexpr std::string_view initVal = "Unset";
 
-int TestSDKInteractorDocumentation(int argc, char* argv[])
+int TestSDKInteractorDocumentation([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
   f3d::interactor& inter = eng.getInteractor();
 
   // Avoid testing something that changes often
-  int nBinds = inter.getBinds().size();
+  size_t nBinds = inter.getBinds().size();
 
   PseudoUnitTest test;
 
@@ -54,13 +54,18 @@
     test.expect<f3d::interactor::does_not_exists_exception>(
       "Empty group", [&]() { std::ignore = inter.getBindsForGroup("Camera"); });
 
+    test.expect<f3d::interactor::does_not_exists_exception>("Empty documentation",
+      [&]() { std::ignore = inter.getBindingDocumentation({ mod_t::ANY, "5" }); });
+
     test.expect<f3d::interactor::does_not_exists_exception>(
-      "Empty bind", [&]() { std::ignore = inter.getBindingDocumentation({ mod_t::ANY, "5" }); });
+      "Empty type", [&]() { std::ignore = inter.getBindingType({ mod_t::ANY, "5" }); });
   }
 
   // Add a dummy binding
-  inter.addBinding({ mod_t::ANY, "DummyBind" }, "DummyCommand", "DummyGroup",
-    []() -> std::pair<std::string, std::string> { return std::pair("DummyDoc", "DummyVal"); });
+  inter.addBinding(
+    { mod_t::ANY, "DummyBind" }, "DummyCommand", "DummyGroup",
+    []() -> std::pair<std::string, std::string> { return std::pair("DummyDoc", "DummyVal"); },
+    f3d::interactor::BindingType::CYCLIC);
 
   {
     // Test dummy binding
@@ -69,6 +74,8 @@
     test("Dummy nBinds DummyGroup", inter.getBindsForGroup("DummyGroup").size() == 1);
     const auto& [doc, val] = inter.getBindingDocumentation({ mod_t::ANY, "DummyBind" });
     test("Dummy doc and val", doc == "DummyDoc" && val == "DummyVal");
+    test("Dummy binding type",
+      inter.getBindingType({ mod_t::ANY, "DummyBind" }) == f3d::interactor::BindingType::CYCLIC);
   }
 
   // Initialize two times
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKInteractorDropFullScene.cxx f3d-3.4.1/library/testing/TestSDKInteractorDropFullScene.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKInteractorDropFullScene.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKInteractorDropFullScene.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -7,10 +7,10 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKInteractorDropFullScene(int argc, char* argv[])
+int TestSDKInteractorDropFullScene([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
-  const f3d::options& options = eng.getOptions();
+  std::ignore = eng.getOptions();
   f3d::window& win = eng.getWindow();
   f3d::interactor& inter = eng.getInteractor();
   win.setSize(300, 300);
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKInvalidForceReader.cxx f3d-3.4.1/library/testing/TestSDKInvalidForceReader.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKInvalidForceReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKInvalidForceReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -6,7 +6,7 @@
 #include <window.h>
 
 namespace fs = std::filesystem;
-int TestSDKInvalidForceReader(int argc, char* argv[])
+int TestSDKInvalidForceReader([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKLog.cxx f3d-3.4.1/library/testing/TestSDKLog.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKLog.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKLog.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,8 @@
 #include <log.h>
 
-int TestSDKLog(int argc, char* argv[])
+#include <iostream>
+
+int TestSDKLog([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setUseColoring(false);
 
@@ -10,6 +12,13 @@
   f3d::log::warn("Test Warning");
   f3d::log::error("Test Error");
 
+  // Test forwarding
+  f3d::log::setVerboseLevel(f3d::log::VerboseLevel::QUIET);
+  f3d::log::forward(
+    [&](f3d::log::VerboseLevel, const std::string&) { std::cerr << "Test Forward\n"; });
+  f3d::log::error("xxx");
+  f3d::log::forward(nullptr);
+
   f3d::log::setVerboseLevel(
     f3d::log::VerboseLevel::INFO); // Next log calls should print like default
   f3d::log::debug("Test Debug");
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKMultiColoring.cxx f3d-3.4.1/library/testing/TestSDKMultiColoring.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKMultiColoring.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKMultiColoring.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,7 +8,7 @@
 
 #include <iostream>
 
-int TestSDKMultiColoring(int argc, char* argv[])
+int TestSDKMultiColoring([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
   f3d::engine eng = f3d::engine::create(true);
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKMultiOptions.cxx f3d-3.4.1/library/testing/TestSDKMultiOptions.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKMultiOptions.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKMultiOptions.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,7 +8,7 @@
 
 #include <iostream>
 
-int TestSDKMultiOptions(int argc, char* argv[])
+int TestSDKMultiOptions([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
   f3d::engine eng = f3d::engine::create(true);
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKOptions.cxx f3d-3.4.1/library/testing/TestSDKOptions.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKOptions.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKOptions.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,13 +1,16 @@
 #include "PseudoUnitTest.h"
+#include "TestSDKHelpers.h"
 
 #include <export.h>
 #include <options.h>
 #include <types.h>
 
 #include <algorithm>
+#include <cmath>
 #include <iostream>
+#include <limits>
 
-int TestSDKOptions(int argc, char* argv[])
+int TestSDKOptions([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
@@ -143,9 +146,9 @@
   test("color operator[]", color[0] == 0.7);
   test("color operator[] const", (*colorPtr)[0] == 0.7);
   test.expect<f3d::type_access_exception>(
-    "color_t invalid access", [&]() { double val = color[3]; });
+    "color_t invalid access", [&]() { std::ignore = color[3]; });
   test.expect<f3d::type_access_exception>(
-    "color_t const invalid access", [&]() { double val = (*colorPtr)[3]; });
+    "color_t const invalid access", [&]() { std::ignore = (*colorPtr)[3]; });
   test("color red", color.r() == 0.7);
   test("color green", color.g() == 0.2);
   test("color blue", color.b() == 0.3);
@@ -192,6 +195,53 @@
     std::get<std::vector<double>>(opt.get("model.scivis.colormap")) ==
       std::vector<double>{ 0, 0, 0, 0, 1, 1, 1, 0 });
 
+  // Test transform2d_t
+  opt.setAsString("model.textures_transform", "1,0,0,0,-1,0,0,0,1");
+  test("setAsString vector transform2d", opt.getAsString("model.textures_transform"),
+    "1,0,0,0,-1,0,0,0,1");
+
+  opt.setAsString("model.textures_transform", "scale:0.1");
+  test("setAsString scale transform2d", opt.getAsString("model.textures_transform"),
+    "0.1,0,0,0,0.1,0,0,0,1");
+
+  opt.setAsString("model.textures_transform", "translation:0.51,2.1");
+  test("setAsString translation transform2d", opt.getAsString("model.textures_transform"),
+    "1,0,0.51,0,1,2.1,0,0,1");
+
+  opt.model.textures_transform = { 0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5 };
+  test("getAsString transform2d",
+    opt.getAsString("model.textures_transform") == "0.5,0,0,0,0.5,0,0,0,0.5");
+
+  opt.setAsString("model.textures_transform", "angle:90.0");
+  test("setAsString/get angle transform2d",
+    std::get<std::vector<double>>(opt.get("model.textures_transform")) ==
+      std::vector<double>{ std::cos(TestSDKHelpers::Degrees2Radians(90.0)),
+        -std::sin(TestSDKHelpers::Degrees2Radians(90.0)), 0,
+        std::sin(TestSDKHelpers::Degrees2Radians(90.0)),
+        std::cos(TestSDKHelpers::Degrees2Radians(90.0)), 0, 0, 0, 1 });
+
+  opt.setAsString("model.textures_transform", "scale:0.1;translation:0.51,2.1;angle:60.0");
+  test("setAsString/get scale/translation/angle transform2d",
+    std::get<std::vector<double>>(opt.get("model.textures_transform")) ==
+      std::vector<double>{ 0.1 * std::cos(TestSDKHelpers::Degrees2Radians(60.0)),
+        0.1 * -std::sin(TestSDKHelpers::Degrees2Radians(60.0)), 0.51,
+        0.1 * std::sin(TestSDKHelpers::Degrees2Radians(60.0)),
+        0.1 * std::cos(TestSDKHelpers::Degrees2Radians(60.0)), 2.1, 0, 0, 1 });
+
+  f3d::transform2d_t nanTransform =
+    f3d::transform2d_t(f3d::double_array_t<2>(std::vector<double>(2, 1)),
+      f3d::double_array_t<2>(std::vector<double>(2, 0)), std::numeric_limits<double>::quiet_NaN());
+  opt.model.textures_transform = nanTransform;
+  test("NaN angle transform2d", opt.getAsString("model.textures_transform") == "1,0,0,0,1,0,0,0,1");
+
+  nanTransform = f3d::transform2d_t(
+    f3d::double_array_t<2>(std::vector<double>(2, std::numeric_limits<double>::quiet_NaN())),
+    f3d::double_array_t<2>(std::vector<double>(2, std::numeric_limits<double>::quiet_NaN())),
+    std::numeric_limits<double>::quiet_NaN());
+  opt.model.textures_transform = nanTransform;
+  test(
+    "NaN values transform2d", opt.getAsString("model.textures_transform") == "1,0,0,0,1,0,0,0,1");
+
   // Test closest option
   auto closest = opt.getClosestOption("modle.sciivs.cell");
   test("closest option", closest.first == "model.scivis.cells" && closest.second == 5);
@@ -253,7 +303,7 @@
     "getAllNames find", std::find(names.begin(), names.end(), "render.point_size") != names.end());
 
   std::vector<std::string> setNames = opt.getNames();
-  test("getNames count", setNames.size() != 0 && setNames == opt2.getNames());
+  test("getNames count", setNames.size() != 0);
 
   // Test isSame/copy/hasValue
   test("isSame", opt.isSame(opt2, "render.line_width"));
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKOptionsIO.cxx f3d-3.4.1/library/testing/TestSDKOptionsIO.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKOptionsIO.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKOptionsIO.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,9 +1,11 @@
 #include "PseudoUnitTest.h"
+#include "TestSDKHelpers.h"
 
 #include <export.h>
 #include <options.h>
 #include <types.h>
 
+#include <cmath>
 #include <filesystem>
 #include <iostream>
 
@@ -44,7 +46,7 @@
   }
 };
 
-int TestSDKOptionsIO(int argc, char* argv[])
+int TestSDKOptionsIO([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   const std::string outOfRangeDoubleStr(
     "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012"
@@ -211,6 +213,50 @@
     "transform2d_t", "0,0,0,0,0,0,0,0,0", { 0, 0, 0, 0, 0, 0, 0, 0, 0 });
   test.parse<f3d::transform2d_t>(
     "transform2d_t", "0.5,0,0,0,0.5,0,0,0,0.5", { 0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5 });
+  test.parse<f3d::transform2d_t>("transform2d_t", "scale:0.1", { 0.1, 0, 0, 0, 0.1, 0, 0, 0, 1 });
+  test.parse<f3d::transform2d_t>(
+    "transform2d_t", "scale:0.1,0.2", { 0.1, 0, 0, 0, 0.2, 0, 0, 0, 1 });
+  test.parse<f3d::transform2d_t>(
+    "transform2d_t", "translation:0.51,2.1", { 1, 0, 0.51, 0, 1, 2.1, 0, 0, 1 });
+  test.parse<f3d::transform2d_t>("transform2d_t", "angle:90.0",
+    { std::cos(TestSDKHelpers::Degrees2Radians(90.0)),
+      -std::sin(TestSDKHelpers::Degrees2Radians(90.0)), 0,
+      std::sin(TestSDKHelpers::Degrees2Radians(90.0)),
+      std::cos(TestSDKHelpers::Degrees2Radians(90.0)), 0, 0, 0, 1 });
+  test.parse<f3d::transform2d_t>("transform2d_t", "scale:0.1;translation:0.51,2.1;angle:60.0",
+    { 0.1 * std::cos(TestSDKHelpers::Degrees2Radians(60.0)),
+      0.1 * -std::sin(TestSDKHelpers::Degrees2Radians(60.0)), 0.51,
+      0.1 * std::sin(TestSDKHelpers::Degrees2Radians(60.0)),
+      0.1 * std::cos(TestSDKHelpers::Degrees2Radians(60.0)), 2.1, 0, 0, 1 });
+  test.parse_expect<f3d::transform2d_t, parsing_exception>("vector too small", "1");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "vector too large", "1,2,3,4,5,6,7,8,9,0");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "text in vector notation", "1,2,three,4,5,6,7,8,9");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>("invalid argument", "rotation:45.0");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "invalid argument after scale", "scale:2;rotation:45.0");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>("no value provided for scale", "scale:");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "no value provided for scale", "scale:,angle:0.5");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>("too many scale values", "scale:1,2,3");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "no value provided for translation", "translation:");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "one value provided for translation", "translation:0.5");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "too many values provided for translation", "translation:1,2,3");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>("no value provided for angle", "angle:");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "too many angle values", "angle:30.0,45.0,60.0");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "multiple scale transforms", "scale:1,2;scale:3,4");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "multiple translation transforms", "translation:1,2;translation:3,4");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "multiple angle transforms", "angle:30.0;angle:60.0");
+  test.parse_expect<f3d::transform2d_t, parsing_exception>(
+    "comma-separated options", "scale:0.1,translation:0.51,2.1,angle:60.0");
   test.format<f3d::transform2d_t>(
     "transform2d_t", { 1, 0, 0, 0, -1, 0, 0, 0, 1 }, "1,0,0,0,-1,0,0,0,1");
   test.format<f3d::transform2d_t>(
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKRenderAndInteract.cxx f3d-3.4.1/library/testing/TestSDKRenderAndInteract.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKRenderAndInteract.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKRenderAndInteract.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -3,7 +3,7 @@
 #include <scene.h>
 #include <window.h>
 
-int TestSDKRenderAndInteract(int argc, char* argv[])
+int TestSDKRenderAndInteract([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   // Order of allocation matter for VTK
   // This tests ensure that rendering then using
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKRenderFinalShader.cxx f3d-3.4.1/library/testing/TestSDKRenderFinalShader.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKRenderFinalShader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKRenderFinalShader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKRenderFinalShader(int argc, char* argv[])
+int TestSDKRenderFinalShader([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::engine eng = f3d::engine::create(true);
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKScene.cxx f3d-3.4.1/library/testing/TestSDKScene.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKScene.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKScene.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,7 +8,7 @@
 
 namespace fs = std::filesystem;
 
-int TestSDKScene(int argc, char* argv[])
+int TestSDKScene([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
@@ -64,6 +64,52 @@
     {
       throw "rendering test failed";
     }
+  });
+
+  // light test
+  f3d::light_state_t defaultLight;
+  f3d::light_state_t redLight = defaultLight;
+  redLight.color = f3d::color_t(1.0, 0.0, 0.0);
+  test("empty light count", [&]() {
+    sce.removeAllLights();
+    return sce.getLightCount() == 0;
+  });
+  test("add default light", [&]() {
+    int index = sce.addLight(defaultLight);
+    return index == 0 && sce.getLightCount() == 1;
+  });
+  test("add red light", [&]() {
+    int index = sce.addLight(redLight);
+    return index == 1 && sce.getLightCount() == 2;
+  });
+  test("light count after add", [&]() { return sce.getLightCount() == 2; });
+  test("get light at index 0", [&]() {
+    f3d::light_state_t light = sce.getLight(0);
+    return light == defaultLight;
+  });
+  test("get light at index 1", [&]() {
+    f3d::light_state_t light = sce.getLight(1);
+    return light == redLight;
+  });
+  test.expect<f3d::scene::light_exception>(
+    "get light at invalid index", [&]() { std::ignore = sce.getLight(10); });
+  test.expect<f3d::scene::light_exception>(
+    "update light at invalid index", [&]() { sce.updateLight(10, redLight); });
+  sce.updateLight(0, redLight);
+  test("update light", sce.getLight(0) == sce.getLight(1));
+  test.expect<f3d::scene::light_exception>(
+    "remove light at invalid index", [&]() { sce.removeLight(10); });
+  test("remove light at index 0", [&]() {
+    sce.removeLight(0);
+    return sce.getLightCount() == 1;
+  });
+
+  test("render after light", [&]() {
+    if (!TestSDKHelpers::RenderTest(
+          win, std::string(argv[1]) + "baselines/", argv[2], "TestSDKSceneRedLight"))
+    {
+      throw "rendering test failed";
+    }
   });
 
   return test.result();
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKSceneFromMemory.cxx f3d-3.4.1/library/testing/TestSDKSceneFromMemory.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKSceneFromMemory.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKSceneFromMemory.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -6,7 +6,7 @@
 #include <scene.h>
 #include <window.h>
 
-int TestSDKSceneFromMemory(int argc, char* argv[])
+int TestSDKSceneFromMemory([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKSceneInvalid.cxx f3d-3.4.1/library/testing/TestSDKSceneInvalid.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKSceneInvalid.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKSceneInvalid.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,7 +4,7 @@
 #include <log.h>
 #include <scene.h>
 
-int TestSDKSceneInvalid(int argc, char* argv[])
+int TestSDKSceneInvalid([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKStartInteractor.cxx f3d-3.4.1/library/testing/TestSDKStartInteractor.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKStartInteractor.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKStartInteractor.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,13 +2,25 @@
 #include <interactor.h>
 #include <scene.h>
 
-int TestSDKStartInteractor(int argc, char* argv[])
+int TestSDKStartInteractor([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   // Using an onscreen window to mimic standard usage
   f3d::engine eng = f3d::engine::create();
   f3d::scene& sce = eng.getScene();
   sce.add(std::string(argv[1]) + "/data/cow.vtp");
   f3d::interactor& inter = eng.getInteractor();
-  inter.start(1, [&inter]() { inter.stop(); });
+  inter.start(0.1, [&inter]() { inter.stop(); });
+
+  // Call start multiple times
+  inter.start(0.1, [&inter]() {
+    inter.start(1, []() {});
+    inter.stop();
+  });
+
+  // Call stop without loop running
+  inter.stop();
+
+  // Event loop with non positive deltaTime
+  inter.triggerEventLoop(0);
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKTriggerInteractions.cxx f3d-3.4.1/library/testing/TestSDKTriggerInteractions.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKTriggerInteractions.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/library/testing/TestSDKTriggerInteractions.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,144 @@
+#include "TestSDKHelpers.h"
+
+#include <engine.h>
+#include <scene.h>
+#include <window.h>
+
+#include <iostream>
+
+struct TestTriggerHelper
+{
+  TestTriggerHelper(std::string dataPath, std::string baselinePath, std::string outputPath)
+    : DataPath(std::move(dataPath))
+    , BaselinePath(std::move(baselinePath))
+    , OutputPath(std::move(outputPath))
+  {
+  }
+
+  template<typename Func>
+  void operator()(Func&& func) const
+  {
+    f3d::engine engine = f3d::engine::create();
+    engine.getWindow().setSize(300, 300);
+    engine.getScene().add(this->DataPath);
+
+    func(engine);
+  }
+
+  // Run with baseline test
+  template<typename Func>
+  void operator()(const std::string& name, Func&& func) const
+  {
+    (*this)([&](f3d::engine& engine) {
+      func(engine);
+
+      if (!TestSDKHelpers::RenderTest(
+            engine.getWindow(), this->BaselinePath, this->OutputPath, name))
+      {
+        throw std::runtime_error("Render test failed for " + name);
+      }
+    });
+  }
+
+  std::string DataPath;
+  std::string BaselinePath;
+  std::string OutputPath;
+};
+
+int TestSDKTriggerInteractions([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
+{
+  TestTriggerHelper helper(
+    std::string(argv[1]) + "/data/cow.vtp", std::string(argv[1]) + "baselines/", argv[2]);
+
+  // Trigger mouse wheel backward and check if it zoomed out
+  helper("TestSDKTriggerInteractionsWheelBackward", [](f3d::engine& engine) {
+    engine.getInteractor().triggerMouseWheel(f3d::interactor::WheelDirection::BACKWARD);
+  });
+
+  // Trigger mouse wheel forward and check if it zoomed in
+  helper("TestSDKTriggerInteractionsWheelForward", [](f3d::engine& engine) {
+    engine.getInteractor().triggerMouseWheel(f3d::interactor::WheelDirection::FORWARD);
+  });
+
+  // Trigger edge rendering with key press "E"
+  helper("TestSDKTriggerInteractionsKeyPress", [](f3d::engine& engine) {
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::PRESS, "E");
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::RELEASE, "E");
+  });
+
+  // Trigger decreased light rendering with key press "SHIFT+L"
+  helper("TestSDKTriggerInteractionsKeyPressMod", [](f3d::engine& engine) {
+    for (int i = 0; i < 30; ++i)
+    {
+      engine.getInteractor().triggerModUpdate(f3d::interactor::InputModifier::SHIFT);
+      engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::PRESS, "L");
+    }
+  });
+
+  // Trigger character inputs
+  helper("TestSDKTriggerInteractionsTextInput", [](f3d::engine& engine) {
+    engine.getInteractor().triggerKeyboardKey(
+      f3d::interactor::InputAction::PRESS, "Escape"); // show console
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::RELEASE, "Escape");
+    engine.getInteractor().triggerTextCharacter('f');
+    engine.getInteractor().triggerTextCharacter('3');
+    engine.getInteractor().triggerTextCharacter('d');
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::PRESS, "Return");
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::RELEASE, "Return");
+  });
+
+  // Trigger rotate camera with mouse left button
+  helper("TestSDKTriggerInteractionsMouseRotate", [](f3d::engine& engine) {
+    engine.getInteractor().triggerMousePosition(150, 100);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::PRESS, f3d::interactor::MouseButton::LEFT);
+    engine.getInteractor().triggerMousePosition(150, 200);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::RELEASE, f3d::interactor::MouseButton::LEFT);
+  });
+
+  // Trigger pan camera with mouse middle button
+  helper("TestSDKTriggerInteractionsMousePan", [](f3d::engine& engine) {
+    engine.getInteractor().triggerMousePosition(150, 100);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::PRESS, f3d::interactor::MouseButton::MIDDLE);
+    engine.getInteractor().triggerMousePosition(150, 200);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::RELEASE, f3d::interactor::MouseButton::MIDDLE);
+  });
+
+  // Trigger zoom camera with mouse right button
+  helper("TestSDKTriggerInteractionsMouseZoom", [](f3d::engine& engine) {
+    engine.getInteractor().triggerMousePosition(150, 100);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::PRESS, f3d::interactor::MouseButton::RIGHT);
+    engine.getInteractor().triggerMousePosition(150, 200);
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::RELEASE, f3d::interactor::MouseButton::RIGHT);
+  });
+
+  // following calls are only there for coverage
+  // there is no interactor style action associated with these yet
+  helper([](f3d::engine& engine) {
+    engine.getInteractor().triggerMouseWheel(f3d::interactor::WheelDirection::LEFT);
+    engine.getInteractor().triggerMouseWheel(f3d::interactor::WheelDirection::RIGHT);
+  });
+
+  helper([](f3d::engine& engine) {
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::RELEASE, f3d::interactor::MouseButton::LEFT);
+  });
+
+  helper([](f3d::engine& engine) {
+    engine.getInteractor().triggerMouseButton(
+      f3d::interactor::InputAction::RELEASE, f3d::interactor::MouseButton::RIGHT);
+  });
+
+  helper([](f3d::engine& engine) {
+    engine.getInteractor().triggerKeyboardKey(f3d::interactor::InputAction::RELEASE, "Return");
+  });
+
+  helper([](f3d::engine& engine) { engine.getInteractor().triggerTextCharacter('f'); });
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKUI.cxx f3d-3.4.1/library/testing/TestSDKUI.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKUI.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/library/testing/TestSDKUI.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,39 @@
+#include <engine.h>
+#include <options.h>
+#include <scene.h>
+#include <types.h>
+#include <window.h>
+
+#include "TestSDKHelpers.h"
+
+int TestSDKUI([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
+{
+  f3d::engine eng = f3d::engine::create(true);
+
+  f3d::window& win = eng.getWindow();
+  win.setSize(300, 300);
+
+  f3d::options& options = eng.getOptions();
+
+  // Set loader_progress_color only for coverage
+  options.ui.loader_progress = true;
+  options.ui.loader_progress_color = f3d::color_t(0.f, 1.f, 1.f);
+
+  options.ui.animation_progress = true;
+  options.ui.animation_progress_color = f3d::color_t(0.f, 1.f, 0.f);
+
+  f3d::scene& sce = eng.getScene();
+  sce.add(std::string(argv[1]) + "/data/BoxAnimated.gltf");
+  sce.loadAnimationTime(0.5);
+
+  win.render();
+
+  if (!TestSDKHelpers::RenderTest(win, std::string(argv[1]) + "baselines/", std::string(argv[2]),
+        "TestSDKUIAnimationProgressColor"))
+  {
+    std::cerr << "Animation progress color failure";
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKUtils.cxx f3d-3.4.1/library/testing/TestSDKUtils.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKUtils.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKUtils.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -7,7 +7,7 @@
 
 namespace fs = std::filesystem;
 
-int TestSDKUtils(int argc, char* argv[])
+int TestSDKUtils([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   PseudoUnitTest test;
 
@@ -59,6 +59,18 @@
     f3d::utils::tokenize(R"(set render.hdri.file file # A comment)") ==
       std::vector<std::string>{ "set", "render.hdri.file", "file" });
 
+  test("tokenize comments with keep comments flag",
+    f3d::utils::tokenize(R"(set render.hdri.file file # A comment)", false) ==
+      std::vector<std::string>{ "set", "render.hdri.file", "file", "#", "A", "comment" });
+
+  test("tokenize base color",
+    f3d::utils::tokenize(R"(set render.background.color #000)") ==
+      std::vector<std::string>{ "set", "render.background.color" });
+
+  test("tokenize base color with keep comments flag",
+    f3d::utils::tokenize(R"(set render.background.color #000)", false) ==
+      std::vector<std::string>{ "set", "render.background.color", "#000" });
+
   test("tokenize escaped comments",
     f3d::utils::tokenize(R"(set render.hdri.file fi\#le)") ==
       std::vector<std::string>{ "set", "render.hdri.file", "fi#le" });
diff -Nru f3d-3.2.0+dfsg/library/testing/TestSDKWindowAuto.cxx f3d-3.4.1/library/testing/TestSDKWindowAuto.cxx
--- f3d-3.2.0+dfsg/library/testing/TestSDKWindowAuto.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/library/testing/TestSDKWindowAuto.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,7 @@
 
 #include "TestSDKHelpers.h"
 
-int TestSDKWindowAuto(int argc, char* argv[])
+int TestSDKWindowAuto([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
 {
   f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG);
   f3d::engine eng = f3d::engine::create(true);
@@ -14,20 +14,19 @@
 
   if (win.getWidth() != 300 || win.getHeight() != 300)
   {
-    std::cerr << "Unexpected window size: " << win.getWidth() << "x" << win.getHeight()
-              << std::endl;
+    std::cerr << "Unexpected window size: " << win.getWidth() << "x" << win.getHeight() << "\n";
     return EXIT_FAILURE;
   }
 
   if (win.getType() == f3d::window::Type::UNKNOWN)
   {
-    std::cerr << "Unexpected window type" << std::endl;
+    std::cerr << "Unexpected window type\n";
     return EXIT_FAILURE;
   }
 
   if (!win.isOffscreen())
   {
-    std::cerr << "Window should be offscreen" << std::endl;
+    std::cerr << "Window should be offscreen\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/LICENSE.md f3d-3.4.1/LICENSE.md
--- f3d-3.2.0+dfsg/LICENSE.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/LICENSE.md	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,7 @@
 
 - Copyright 2019-2021 Kitware SAS
 - Copyright 2021-2025 Michael Migliore, Mathieu Westphal
+- Copyright 2025 F3D-APP Foundation
 
 All rights reserved.
 
diff -Nru f3d-3.2.0+dfsg/_licenses.md f3d-3.4.1/_licenses.md
--- f3d-3.2.0+dfsg/_licenses.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/_licenses.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,7 +0,0 @@
----
-title: Licenses
-nav_order: 5
-has_children: true
-has_toc: true
-layout: default
----
diff -Nru f3d-3.2.0+dfsg/.lsan.supp f3d-3.4.1/.lsan.supp
--- f3d-3.2.0+dfsg/.lsan.supp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.lsan.supp	2026-01-08 08:41:55.000000000 +0200
@@ -18,11 +18,14 @@
 # TBB
 leak:libtbb
 
-# OSMesa
+# Mesa
 leak:libOSMesa
+leak:libGLX_mesa
+leak:libEGL_mesa
 
-# Potential mesa/VTK leak with incomplete callstack
-# forces us to hide all leaks from the tests using a render window
-# https://gitlab.kitware.com/vtk/vtk/-/issues/18504
-leak:libf3dSDKTests
-leak:vtkextPrivateTests
+# Qt
+leak:libQt5Core
+leak:libQt5Gui
+
+# png
+leak:libvtkpng
diff -Nru f3d-3.2.0+dfsg/package.json f3d-3.4.1/package.json
--- f3d-3.2.0+dfsg/package.json	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/package.json	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,33 @@
+{
+  "name": "f3d",
+  "version": "3.4.1",
+  "description": "Fast and minimalist 3D viewer framework",
+  "exports": "./dist/f3d.js",
+  "types": "./dist/f3d.d.ts",
+  "files": [
+    "./dist/f3d.*"
+  ],
+  "scripts": {
+    "_docker": "${F3D_DOCKER_CLIENT:-docker} run --rm -e CMAKE_BUILD_PARALLEL_LEVEL -v $(pwd):/src ghcr.io/f3d-app/f3d-wasm:${F3D_DOCKER_TIMESTAMP:-latest}",
+    "clean": "npm run _docker -- rm -rf /src/dist /src/_wasm_build",
+    "build": "npm run _docker -- /src/webassembly/build.sh Release",
+    "test": "npm run _docker -- ctest --test-dir /src/_wasm_build/webassembly --output-on-failure",
+    "prepare": "npm run clean && npm run build"
+  },
+  "repository": "github:f3d-app/f3d",
+  "keywords": [
+    "3d",
+    "webassembly",
+    "viewer",
+    "animation",
+    "cad",
+    "gltf",
+    "vtk",
+    "stl"
+  ],
+  "author": "F3D-APP Foundation",
+  "license": "BSD-3-Clause",
+  "bugs": "https://github.com/f3d-app/f3d/issues",
+  "funding": "https://f3d.app/thanks/",
+  "homepage": "https://f3d.app"
+}
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/configs/config.d/10_alembic.json f3d-3.4.1/plugins/alembic/configs/config.d/10_alembic.json
--- f3d-3.2.0+dfsg/plugins/alembic/configs/config.d/10_alembic.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/configs/config.d/10_alembic.json	2026-01-08 08:41:55.000000000 +0200
@@ -3,7 +3,6 @@
     "match-type": "glob",
     "match": "*.abc",
     "options": {
-      "scalar-coloring": true,
       "load-plugins": "alembic"
     }
   }
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/configs/thumbnail.d/10_alembic.json f3d-3.4.1/plugins/alembic/configs/thumbnail.d/10_alembic.json
--- f3d-3.2.0+dfsg/plugins/alembic/configs/thumbnail.d/10_alembic.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/configs/thumbnail.d/10_alembic.json	2026-01-08 08:41:55.000000000 +0200
@@ -3,7 +3,6 @@
     "match-type": "glob",
     "match": "*.abc",
     "options": {
-      "scalar-coloring": true,
       "load-plugins": "alembic"
     }
   }
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/CMakeLists.txt f3d-3.4.1/plugins/alembic/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/alembic/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,12 +2,9 @@
   vtkF3DAlembicReader
   )
 
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextAlembic
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes})
+
+vtk_module_set_properties(f3d::vtkextAlembic CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/Testing/CMakeLists.txt f3d-3.4.1/plugins/alembic/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/alembic/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,13 @@
      TestF3DAlembicReader.cxx
     )
 
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251210)
+  list(APPEND VTKExtensionsPluginAlembic_list
+       TestF3DAlembicReaderStream.cxx
+       TestF3DAlembicReaderStreamError.cxx
+      )
+endif()
+
 vtk_add_test_cxx(VTKExtensionsPluginAlembic tests
   NO_DATA NO_VALID NO_OUTPUT
   ${VTKExtensionsPluginAlembic_list}
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReader.cxx f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReader.cxx
--- f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,5 @@
   vtkNew<vtkF3DAlembicReader> reader;
   reader->SetFileName(filename);
   reader->Update();
-  reader->Print(cout);
-  return reader->GetOutput() ? EXIT_SUCCESS : EXIT_FAILURE;
+  return reader->GetOutput()->GetNumberOfPoints() > 0 ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReaderStream.cxx f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReaderStream.cxx
--- f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReaderStream.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReaderStream.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,20 @@
+#include <vtkFileResourceStream.h>
+#include <vtkNew.h>
+#include <vtkTestUtilities.h>
+
+#include "vtkF3DAlembicReader.h"
+
+#include <iostream>
+
+int TestF3DAlembicReaderStream(int vtkNotUsed(argc), char* argv[])
+{
+  std::string filename = std::string(argv[1]) + "data/suzanne.abc";
+
+  vtkNew<vtkFileResourceStream> fileStream;
+  fileStream->Open(filename.c_str());
+
+  vtkNew<vtkF3DAlembicReader> reader;
+  reader->SetStream(fileStream);
+  reader->Update();
+  return reader->GetOutput()->GetNumberOfPoints() > 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReaderStreamError.cxx f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReaderStreamError.cxx
--- f3d-3.2.0+dfsg/plugins/alembic/module/Testing/TestF3DAlembicReaderStreamError.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/alembic/module/Testing/TestF3DAlembicReaderStreamError.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,70 @@
+#include <vtkCallbackCommand.h>
+#include <vtkExecutive.h>
+#include <vtkFileResourceStream.h>
+#include <vtkNew.h>
+#include <vtkTestUtilities.h>
+
+#include "vtkF3DAlembicReader.h"
+
+#include <iostream>
+
+class ErrorEventCallback : public vtkCommand
+{
+public:
+  static ErrorEventCallback* New()
+  {
+    return new ErrorEventCallback;
+  }
+
+  void Execute(vtkObject* caller, unsigned long vtkNotUsed(evId), void* data) override
+  {
+    const vtkF3DAlembicReader* reader = reinterpret_cast<vtkF3DAlembicReader*>(caller);
+    char* message = static_cast<char*>(data);
+    if (reader && message)
+    {
+      this->Messages.emplace_back(message);
+    }
+  }
+
+  const std::vector<std::string>& GetRecordedErrorMessages() const
+  {
+    return this->Messages;
+  }
+
+private:
+  std::vector<std::string> Messages;
+};
+
+int TestF3DAlembicReaderStreamError(int vtkNotUsed(argc), char* argv[])
+{
+  vtkNew<ErrorEventCallback> errorEventCallback;
+  vtkNew<vtkCallbackCommand> nullEventCallback;
+
+  std::string filename = std::string(argv[1]) + "data/invalid.abc";
+
+  vtkNew<vtkFileResourceStream> fileStream;
+  fileStream->Open(filename.c_str());
+
+  vtkNew<vtkF3DAlembicReader> reader;
+  reader->SetStream(fileStream);
+  reader->AddObserver(vtkCommand::ErrorEvent, errorEventCallback);
+  reader->GetExecutive()->AddObserver(vtkCommand::ErrorEvent, nullEventCallback);
+
+  reader->Update();
+
+  auto errorMessages = errorEventCallback->GetRecordedErrorMessages();
+  if (errorMessages.empty())
+  {
+    std::cerr << "No error triggered.\n";
+    return EXIT_FAILURE;
+  }
+
+  auto firstMessage = errorMessages.front();
+  if (firstMessage.find("Error reading stream") == std::string::npos)
+  {
+    std::cerr << "No stream error triggered!\n";
+    return EXIT_FAILURE;
+  }
+
+  return reader->GetOutput()->GetNumberOfPoints() == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/vtkF3DAlembicReader.cxx f3d-3.4.1/plugins/alembic/module/vtkF3DAlembicReader.cxx
--- f3d-3.2.0+dfsg/plugins/alembic/module/vtkF3DAlembicReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/module/vtkF3DAlembicReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,8 @@
 #include <vtkObjectFactory.h>
 #include <vtkPointData.h>
 #include <vtkPoints.h>
+#include <vtkPolyLine.h>
+#include <vtkResourceStream.h>
 #include <vtkSmartPointer.h>
 #include <vtkStreamingDemandDrivenPipeline.h>
 
@@ -24,6 +26,9 @@
 #pragma warning(pop)
 #endif
 
+#include <stack>
+#include <tuple>
+
 using IndicesContainer = std::vector<int>;
 using V3fContainer = std::vector<Alembic::Abc::V3f>;
 using AttributesContainer = std::map<std::string, V3fContainer>;
@@ -240,7 +245,7 @@
 
 public:
   vtkSmartPointer<vtkPolyData> ProcessIPolyMesh(
-    const Alembic::AbcGeom::IPolyMesh& pmesh, double time)
+    const Alembic::AbcGeom::IPolyMesh& pmesh, double time, const Alembic::Abc::M44d& matrix)
   {
     vtkNew<vtkPolyData> polydata;
     IntermediateGeometry originalData;
@@ -258,18 +263,23 @@
 
       this->SetupIndicesStorage(faceVertexCounts, originalData.Indices);
 
+      // By default, Alembic is CW while VTK is CCW
+      // So we need to reverse the order of indices only if the mesh is not mirrored
+      const bool doReverseRotate = matrix.determinant() > 0;
+
       // Positions
       {
         V3fContainer pV3F;
         for (size_t pIndex = 0; pIndex < positions->size(); pIndex++)
         {
-          pV3F.emplace_back(
-            positions->get()[pIndex].x, positions->get()[pIndex].y, positions->get()[pIndex].z);
+          Alembic::Abc::V3f tp;
+          matrix.multVecMatrix(positions->get()[pIndex], tp);
+          pV3F.emplace_back(tp.x, tp.y, tp.z);
         }
         originalData.Attributes.insert(AttributesContainer::value_type("P", pV3F));
 
         this->UpdateIndices<Alembic::AbcGeom::Int32ArraySamplePtr>(
-          facePositionIndices, pIndicesOffset, originalData.Indices);
+          facePositionIndices, pIndicesOffset, originalData.Indices, doReverseRotate);
       }
 
       // Texture coordinate
@@ -292,12 +302,12 @@
           {
             originalData.uvFaceVarying = true;
             this->UpdateIndices<Alembic::AbcGeom::UInt32ArraySamplePtr>(
-              uvIndices, uvIndicesOffset, originalData.Indices);
+              uvIndices, uvIndicesOffset, originalData.Indices, doReverseRotate);
           }
           else
           {
             this->UpdateIndices<Alembic::AbcGeom::Int32ArraySamplePtr>(
-              facePositionIndices, uvIndicesOffset, originalData.Indices);
+              facePositionIndices, uvIndicesOffset, originalData.Indices, doReverseRotate);
           }
         }
       }
@@ -314,8 +324,9 @@
           Alembic::AbcGeom::UInt32ArraySamplePtr normalIndices = normalValue.getIndices();
           for (size_t index = 0; index < normalValue.getVals()->size(); ++index)
           {
-            Alembic::AbcGeom::V3f normal = (*(normalValue.getVals()))[index];
-            normal_v3f.emplace_back(normal[0], normal[1], normal[2]);
+            Alembic::AbcGeom::V3f normal;
+            matrix.multDirMatrix((*(normalValue.getVals()))[index], normal);
+            normal_v3f.emplace_back(normal);
           }
           originalData.Attributes.insert(AttributesContainer::value_type("N", normal_v3f));
 
@@ -324,12 +335,12 @@
             originalData.nFaceVarying = true;
 
             this->UpdateIndices<Alembic::AbcGeom::UInt32ArraySamplePtr>(
-              normalIndices, nIndicesOffset, originalData.Indices);
+              normalIndices, nIndicesOffset, originalData.Indices, doReverseRotate);
           }
           else
           {
             this->UpdateIndices<Alembic::AbcGeom::Int32ArraySamplePtr>(
-              facePositionIndices, nIndicesOffset, originalData.Indices);
+              facePositionIndices, nIndicesOffset, originalData.Indices, doReverseRotate);
           }
         }
       }
@@ -344,45 +355,98 @@
     return polydata;
   }
 
-  template<typename F>
-  void IterateIObject(
-    F func, const Alembic::Abc::IObject& parent, const Alembic::Abc::ObjectHeader& ohead)
+  vtkSmartPointer<vtkPolyData> ProcessICurves(
+    const Alembic::AbcGeom::ICurves& curve, double time, const Alembic::Abc::M44d& matrix)
   {
-    // Set this if we should continue traversing
-    Alembic::Abc::IObject nextParentObject;
+    vtkNew<vtkPolyData> polydata;
 
-    if (Alembic::AbcGeom::IXform::matches(ohead))
-    {
-      Alembic::AbcGeom::IXform xForm(parent, ohead.getName());
-      nextParentObject = xForm;
-    }
-    else if (Alembic::AbcGeom::IPolyMesh::matches(ohead))
-    {
-      Alembic::AbcGeom::IPolyMesh polymesh(parent, ohead.getName());
-      func(polymesh);
-      nextParentObject = polymesh;
-    }
+    const Alembic::AbcGeom::ICurvesSchema& schema = curve.getSchema();
+    Alembic::AbcGeom::ICurvesSchema::Sample samp;
 
-    // Recursion
-    if (nextParentObject.valid())
+    if (schema.getNumSamples() > 0)
     {
-      for (size_t i = 0; i < nextParentObject.getNumChildren(); ++i)
+      Alembic::AbcGeom::ISampleSelector selector(time);
+      schema.get(samp, selector);
+
+      Alembic::AbcGeom::P3fArraySamplePtr positions = samp.getPositions();
+      Alembic::AbcGeom::Int32ArraySamplePtr curveCounts = samp.getCurvesNumVertices();
+
+      vtkNew<vtkPoints> points;
+      vtkNew<vtkCellArray> lines;
+
+      for (size_t pIndex = 0; pIndex < positions->size(); ++pIndex)
       {
-        this->IterateIObject(func, nextParentObject, nextParentObject.getChildHeader(i));
+        const Alembic::Abc::V3f& tp = positions->get()[pIndex] * matrix;
+        points->InsertNextPoint(tp.x, tp.y, tp.z);
+      }
+
+      size_t pOffsetIndex = 0;
+      for (size_t cIndex = 0; cIndex < curveCounts->size(); ++cIndex)
+      {
+        const size_t vCount = curveCounts->get()[cIndex];
+
+        vtkNew<vtkPolyLine> polyLine;
+        polyLine->GetPointIds()->SetNumberOfIds(vCount);
+        for (size_t j = 0; j < vCount; ++j)
+        {
+          polyLine->GetPointIds()->SetId(j, pOffsetIndex + j);
+        }
+
+        lines->InsertNextCell(polyLine);
+        pOffsetIndex += vCount;
       }
+
+      polydata->SetPoints(points);
+      polydata->SetLines(lines);
     }
+    return polydata;
   }
 
   void ImportRoot(vtkAppendPolyData* append, double time)
   {
-    Alembic::Abc::IObject top = this->Archive.getTop();
-
-    auto appendMesh = [&](const Alembic::AbcGeom::IPolyMesh& polymesh)
-    { append->AddInputData(ProcessIPolyMesh(polymesh, time)); };
+    const Alembic::Abc::IObject top = this->Archive.getTop();
+    const Alembic::AbcGeom::ISampleSelector selector(time);
+    Alembic::Abc::M44d identity;
+    identity.makeIdentity();
+
+    std::stack<std::tuple<const Alembic::Abc::IObject, const Alembic::Abc::ObjectHeader,
+      const Alembic::Abc::M44d>>
+      objects;
 
     for (size_t i = 0; i < top.getNumChildren(); ++i)
     {
-      this->IterateIObject(appendMesh, top, top.getChildHeader(i));
+      objects.emplace(std::make_tuple(top, top.getChildHeader(i), identity));
+    }
+
+    while (!objects.empty())
+    {
+      const auto& [parent, ohead, matrix] = objects.top();
+      const Alembic::AbcGeom::IObject obj(parent, ohead.getName());
+      Alembic::Abc::M44d objMatrix = matrix;
+      if (Alembic::AbcGeom::IPolyMesh::matches(ohead))
+      {
+        const Alembic::AbcGeom::IPolyMesh polymesh(parent, ohead.getName());
+        append->AddInputData(this->ProcessIPolyMesh(polymesh, time, objMatrix));
+      }
+      else if (Alembic::AbcGeom::ICurves::matches(ohead))
+      {
+        const Alembic::AbcGeom::ICurves curve(parent, ohead.getName());
+        append->AddInputData(this->ProcessICurves(curve, time, objMatrix));
+      }
+      else if (Alembic::AbcGeom::IXform::matches(ohead))
+      {
+        const Alembic::AbcGeom::IXform xForm(parent, ohead.getName());
+        const Alembic::AbcGeom::IXformSchema& xFormSchema = xForm.getSchema();
+        Alembic::AbcGeom::XformSample xFormSamp;
+        xFormSchema.get(xFormSamp, selector);
+        objMatrix = xFormSamp.getMatrix() * matrix;
+      }
+
+      objects.pop();
+      for (size_t i = 0; i < obj.getNumChildren(); ++i)
+      {
+        objects.emplace(std::make_tuple(obj, obj.getChildHeader(i), objMatrix));
+      }
     }
   }
 
@@ -390,15 +454,56 @@
   {
     Alembic::Abc::IObject top = this->Archive.getTop();
 
-    auto computeRange = [&](const Alembic::AbcGeom::IPolyMesh& polymesh)
+    std::stack<std::pair<const Alembic::Abc::IObject, const Alembic::Abc::ObjectHeader>> objects;
+
+    for (size_t i = 0; i < top.getNumChildren(); ++i)
     {
-      Alembic::Abc::TimeSamplingPtr ts = polymesh.getSchema().getTimeSampling();
+      objects.emplace(std::make_pair(top, top.getChildHeader(i)));
+    }
+
+    while (!objects.empty())
+    {
+      const auto& [parent, ohead] = objects.top();
+      const Alembic::AbcGeom::IObject obj(parent, ohead.getName());
+      int numSamples = 0;
+      Alembic::Abc::TimeSamplingPtr ts;
+      if (Alembic::AbcGeom::IXform::matches(ohead))
+      {
+        const Alembic::AbcGeom::IXform xForm(parent, ohead.getName());
+        const Alembic::AbcGeom::IXformSchema& schema = xForm.getSchema();
+        ts = schema.getTimeSampling();
+        numSamples = static_cast<int>(schema.getNumSamples());
+      }
+      else if (Alembic::AbcGeom::IPolyMesh::matches(ohead))
+      {
+        const Alembic::AbcGeom::IPolyMesh polymesh(parent, ohead.getName());
+        const Alembic::AbcGeom::IPolyMeshSchema& schema = polymesh.getSchema();
+        ts = schema.getTimeSampling();
+        numSamples = static_cast<int>(schema.getNumSamples());
+      }
+      else if (Alembic::AbcGeom::ICurves::matches(ohead))
+      {
+        const Alembic::AbcGeom::ICurves curves(parent, ohead.getName());
+        const Alembic::AbcGeom::ICurvesSchema& schema = curves.getSchema();
+        ts = schema.getTimeSampling();
+        numSamples = static_cast<int>(schema.getNumSamples());
+      }
+
+      objects.pop();
+      for (size_t i = 0; i < obj.getNumChildren(); ++i)
+      {
+        objects.emplace(std::make_pair(obj, obj.getChildHeader(i)));
+      }
+
+      if (ts == nullptr)
+      {
+        continue;
+      }
 
       if (ts->getTimeSamplingType().isUniform())
       {
         double min = ts->getSampleTime(0);
-        double max = min +
-          (polymesh.getSchema().getNumSamples() - 1) * ts->getTimeSamplingType().getTimePerCycle();
+        double max = min + (numSamples - 1) * ts->getTimeSamplingType().getTimePerCycle();
         start = std::min(start, min);
         end = std::max(end, max);
       }
@@ -408,25 +513,54 @@
         start = std::min(start, times.front());
         end = std::max(end, times.back());
       }
-    };
-
-    for (size_t i = 0; i < top.getNumChildren(); ++i)
-    {
-      this->IterateIObject(computeRange, top, top.getChildHeader(i));
     }
   }
 
-  void ReadArchive(const std::string& filePath)
+  bool ReadArchive(
+    vtkResourceStream* stream, const std::string& filePath, [[maybe_unused]] vtkObject* parent)
   {
     Alembic::AbcCoreFactory::IFactory factory;
     Alembic::AbcCoreFactory::IFactory::CoreType coreType;
 
-    this->Archive = factory.getArchive(filePath, coreType);
+    if (stream)
+    {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251210)
+      // Encapsulate resource stream into an istream
+      stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+      this->Streambuf = stream->ToStreambuf();
+      this->Buffer = std::make_unique<std::istream>(this->Streambuf.get());
+      try
+      {
+        this->Archive =
+          factory.getArchive(std::vector<std::istream*>({ this->Buffer.get() }), coreType);
+      }
+      catch (Alembic::Util::v12::Exception& ex)
+      {
+        vtkErrorWithObjectMacro(parent, "Error reading stream: " << ex.what());
+        return false;
+      }
+#else
+      vtkErrorWithObjectMacro(
+        parent, "This version of VTK doesn't support reading memory stream with Alembic");
+      return false;
+#endif
+    }
+    else
+    {
+      this->Archive = factory.getArchive(filePath, coreType);
+    }
+    return this->Archive.valid();
   }
   Alembic::Abc::IArchive Archive;
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251210)
+  std::unique_ptr<std::streambuf> Streambuf;
+  std::unique_ptr<std::istream> Buffer;
+#endif
 };
 
 vtkStandardNewMacro(vtkF3DAlembicReader);
+vtkCxxSetSmartPointerMacro(vtkF3DAlembicReader, Stream, vtkResourceStream);
 
 //----------------------------------------------------------------------------
 vtkF3DAlembicReader::vtkF3DAlembicReader()
@@ -442,14 +576,17 @@
 int vtkF3DAlembicReader::RequestInformation(vtkInformation* vtkNotUsed(request),
   vtkInformationVector** vtkNotUsed(inputVector), vtkInformationVector* outputVector)
 {
-  vtkInformation* outInfo = outputVector->GetInformationObject(0);
-
-  this->Internals->ReadArchive(this->FileName);
+  if (!this->Internals->ReadArchive(this->Stream, this->FileName, this))
+  {
+    vtkErrorMacro("Unable to read this alembic file or stream");
+    return 0;
+  }
 
   double timeRange[2] = { std::numeric_limits<double>::infinity(),
     -std::numeric_limits<double>::infinity() };
   this->Internals->ExtendTimeRange(timeRange[0], timeRange[1]);
 
+  vtkInformation* outInfo = outputVector->GetInformationObject(0);
   if (timeRange[0] < timeRange[1])
   {
     outInfo->Set(vtkStreamingDemandDrivenPipeline::TIME_RANGE(), timeRange, 2);
@@ -482,8 +619,12 @@
 }
 
 //----------------------------------------------------------------------------
-void vtkF3DAlembicReader::PrintSelf(ostream& os, vtkIndent indent)
+vtkMTimeType vtkF3DAlembicReader::GetMTime()
 {
-  this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << "\n";
+  auto mtime = this->Superclass::GetMTime();
+  if (this->Stream)
+  {
+    mtime = std::max(mtime, this->Stream->GetMTime());
+  }
+  return mtime;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/alembic/module/vtkF3DAlembicReader.h f3d-3.4.1/plugins/alembic/module/vtkF3DAlembicReader.h
--- f3d-3.2.0+dfsg/plugins/alembic/module/vtkF3DAlembicReader.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/alembic/module/vtkF3DAlembicReader.h	2026-01-08 08:41:55.000000000 +0200
@@ -7,6 +7,8 @@
  * to build polygonal geometries. Vertex normals and texture
  * coordinates are not supported yet.
  *
+ * This reader supports reading streams.
+ *
  * @sa https://github.com/alembic/alembic/blob/master/README.txt
  *
  */
@@ -20,18 +22,29 @@
 
 #include <memory>
 
+class vtkResourceStream;
 class vtkF3DAlembicReader : public vtkPolyDataAlgorithm
 {
 public:
   static vtkF3DAlembicReader* New();
   vtkTypeMacro(vtkF3DAlembicReader, vtkPolyDataAlgorithm);
-  void PrintSelf(ostream& os, vtkIndent indent) override;
 
   /**
    * Set the file name.
    */
   vtkSetMacro(FileName, std::string);
 
+  /**
+   * Specify stream to read from
+   * When both `Stream` and `Filename` are set, stream is used.
+   */
+  void SetStream(vtkResourceStream* stream);
+
+  /**
+   * Overridden to take into account mtime from the internal vtkResourceStream.
+   */
+  vtkMTimeType GetMTime() override;
+
 protected:
   vtkF3DAlembicReader();
   ~vtkF3DAlembicReader() override;
@@ -39,12 +52,13 @@
   int RequestInformation(vtkInformation*, vtkInformationVector**, vtkInformationVector*) override;
   int RequestData(vtkInformation*, vtkInformationVector**, vtkInformationVector*) override;
 
-  std::string FileName;
-
 private:
   vtkF3DAlembicReader(const vtkF3DAlembicReader&) = delete;
   void operator=(const vtkF3DAlembicReader&) = delete;
 
+  vtkSmartPointer<vtkResourceStream> Stream;
+  std::string FileName;
+
   class vtkInternals;
   std::unique_ptr<vtkInternals> Internals;
 };
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/CMakeLists.txt f3d-3.4.1/plugins/assimp/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/assimp/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,12 +2,9 @@
   vtkF3DAssimpImporter
   )
 
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextAssimp
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes})
+
+vtk_module_set_properties(f3d::vtkextAssimp CXX_STANDARD 17)
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/Testing/CMakeLists.txt f3d-3.4.1/plugins/assimp/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/assimp/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -6,6 +6,13 @@
        TestF3DAssimpImporter.cxx
        TestF3DAssimpImportError.cxx
       )
+
+  if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251016)
+    list(APPEND VTKExtensionsPluginAssimp_list
+         TestF3DAssimpImporterStream.cxx
+         TestF3DAssimpImporterStreamError.cxx
+        )
+  endif()
 endif()
 
 vtk_add_test_cxx(VTKExtensionsPluginAssimp tests
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporter.cxx f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporter.cxx
--- f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,16 +1,16 @@
+#include "vtkF3DAssimpImporter.h"
+
 #include <vtkNew.h>
 #include <vtkTestUtilities.h>
 
-#include "vtkF3DAssimpImporter.h"
-
 #include <iostream>
 
 int TestF3DAssimpImporter(int vtkNotUsed(argc), char* argv[])
 {
   std::string filename = std::string(argv[1]) + "data/animatedWorld.fbx";
   vtkNew<vtkF3DAssimpImporter> importer;
-  importer->SetFileName(filename);
+  importer->SetFileName(filename.c_str());
   importer->Update();
-  importer->Print(cout);
+  importer->Print(std::cout);
   return importer->GetNumberOfAnimations() == 1 ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImportError.cxx f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImportError.cxx
--- f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImportError.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImportError.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -17,7 +17,7 @@
 
   void Execute(vtkObject* caller, unsigned long vtkNotUsed(evId), void* data) override
   {
-    auto importer = reinterpret_cast<vtkF3DAssimpImporter*>(caller);
+    const vtkF3DAssimpImporter* importer = reinterpret_cast<vtkF3DAssimpImporter*>(caller);
     char* message = static_cast<char*>(data);
     if (importer && message)
     {
@@ -47,28 +47,28 @@
   auto errorMessages = errorEventCallback->GetRecordedErrorMessages();
   if (errorMessages.empty())
   {
-    std::cerr << "No error triggered." << std::endl;
+    std::cerr << "No error triggered.\n";
     return EXIT_FAILURE;
   }
 
   auto lastMessage = errorMessages.back();
   if (lastMessage.find("Assimp error") == std::string::npos)
   {
-    std::cerr << "No Assimp error triggered!" << std::endl;
+    std::cerr << "No Assimp error triggered!\n";
     return EXIT_FAILURE;
   }
 
   if (importer->GetNumberOfAnimations() != 0)
   {
     std::cerr << "Importer has " << importer->GetNumberOfAnimations()
-              << " animations, expected 0 animation." << std::endl;
+              << " animations, expected 0 animation.\n";
     return EXIT_FAILURE;
   }
 
   if (!importer->UpdateAtTimeValue(0))
   {
     std::cerr << "Importer did not return true with no animation enabled "
-              << "when calling UpdateAtTimeValue()" << std::endl;
+              << "when calling UpdateAtTimeValue()\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporterStream.cxx f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporterStream.cxx
--- f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporterStream.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporterStream.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,34 @@
+#include "vtkF3DAssimpImporter.h"
+
+#include <vtkDataSet.h>
+#include <vtkFileResourceStream.h>
+#include <vtkMapper.h>
+#include <vtkNew.h>
+#include <vtkRenderer.h>
+#include <vtkTestUtilities.h>
+
+#include <iostream>
+
+int TestF3DAssimpImporterStream(int vtkNotUsed(argc), char* argv[])
+{
+  std::string filename = std::string(argv[1]) + "data/texturedCube.fbx";
+  vtkNew<vtkFileResourceStream> stream;
+  if (!stream->Open(filename.c_str()))
+  {
+    std::cerr << "Cannot open file\n";
+    return EXIT_FAILURE;
+  }
+
+  vtkNew<vtkF3DAssimpImporter> importer;
+  importer->SetStream(stream);
+  importer->SetMemoryHint("fbx");
+  importer->Update();
+  return importer->GetRenderer()
+           ->GetActors()
+           ->GetLastActor()
+           ->GetMapper()
+           ->GetInput()
+           ->GetNumberOfPoints() == 24
+    ? EXIT_SUCCESS
+    : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporterStreamError.cxx f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporterStreamError.cxx
--- f3d-3.2.0+dfsg/plugins/assimp/module/Testing/TestF3DAssimpImporterStreamError.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/assimp/module/Testing/TestF3DAssimpImporterStreamError.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,26 @@
+#include "vtkF3DAssimpImporter.h"
+
+#include <vtkFileResourceStream.h>
+#include <vtkNew.h>
+#include <vtkTestUtilities.h>
+
+#include <iostream>
+
+int TestF3DAssimpImporterStreamError(int vtkNotUsed(argc), char* argv[])
+{
+  std::string filename = std::string(argv[1]) + "data/animatedWorld.fbx";
+  vtkNew<vtkFileResourceStream> stream;
+  if (!stream->Open(filename.c_str()))
+  {
+    std::cerr << "Cannot open file\n";
+    return EXIT_FAILURE;
+  }
+
+  // The loaded file has external texture, which requires a filename
+  // this test the warning code path when texture cannot be loaded for this reason
+  vtkNew<vtkF3DAssimpImporter> importer;
+  importer->SetStream(stream);
+  importer->SetMemoryHint("fbx");
+  importer->Update();
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/vtkF3DAssimpImporter.cxx f3d-3.4.1/plugins/assimp/module/vtkF3DAssimpImporter.cxx
--- f3d-3.2.0+dfsg/plugins/assimp/module/vtkF3DAssimpImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/vtkF3DAssimpImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -27,9 +27,13 @@
 #include <vtkTriangleFilter.h>
 #include <vtkUniforms.h>
 #include <vtkUnsignedShortArray.h>
-
+#include <vtkVersion.h>
 #include <vtksys/SystemTools.hxx>
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+#include <vtkMemoryResourceStream.h>
+#endif
+
 #include <assimp/Exceptional.h>
 #include <assimp/Importer.hpp>
 #include <assimp/postprocess.h>
@@ -189,7 +193,15 @@
       }
       else
       {
-        std::string dir = vtksys::SystemTools::GetParentDirectory(this->Parent->GetFileName());
+        const char* filename = this->Parent->GetFileName();
+        if (!filename)
+        {
+          vtkWarningWithObjectMacro(
+            this->Parent, "Cannot read texture from file without a filename");
+          return nullptr;
+        }
+
+        std::string dir = vtksys::SystemTools::GetParentDirectory(filename);
         std::string texturePath = vtksys::SystemTools::CollapseFullPath(path, dir);
 
         // try to get the texture in the same dir as the model file
@@ -250,8 +262,15 @@
 
       if (reader)
       {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+        vtkNew<vtkMemoryResourceStream> stream;
+        stream->SetBuffer(aTexture->pcData, aTexture->mWidth);
+        reader->SetStream(stream);
+#else
         reader->SetMemoryBuffer(aTexture->pcData);
         reader->SetMemoryBufferLength(aTexture->mWidth);
+#endif
+        reader->Update();
         vTexture->SetInputConnection(reader->GetOutputPort());
       }
     }
@@ -586,14 +605,44 @@
   /**
    * Read the scene file
    */
-  bool ReadScene(const std::string& filePath)
+  bool ReadScene(vtkResourceStream* stream, const char* filePath, [[maybe_unused]] const char* hint)
   {
     try
     {
       // Work around for https://github.com/assimp/assimp/issues/4620
       this->Importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
-      this->Scene = this->Importer.ReadFile(
-        filePath, aiProcess_LimitBoneWeights | aiProcess_ValidateDataStructure);
+
+      if (stream)
+      {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+        stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+        auto memStream = vtkMemoryResourceStream::SafeDownCast(stream);
+        vtkNew<vtkMemoryResourceStream> localMemStream;
+        if (!memStream)
+        {
+          // Copy to a mem stream if needed
+          stream->Seek(0, vtkResourceStream::SeekDirection::End);
+          std::size_t size = stream->Tell();
+          stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+          std::vector<std::byte> tempBuffer;
+          tempBuffer.resize(size);
+          stream->Read(tempBuffer.data(), size);
+          localMemStream->SetBuffer(std::move(tempBuffer));
+          memStream = localMemStream;
+        }
+        this->Scene = this->Importer.ReadFileFromMemory(memStream->GetBuffer(),
+          memStream->GetSize(), aiProcess_LimitBoneWeights | aiProcess_ValidateDataStructure, hint);
+#else
+        vtkErrorWithObjectMacro(
+          this->Parent, "This version of VTK doesn't support reading memory stream with Assimp");
+        return false;
+#endif
+      }
+      else
+      {
+        this->Scene = this->Importer.ReadFile(
+          std::string(filePath), aiProcess_LimitBoneWeights | aiProcess_ValidateDataStructure);
+      }
     }
     catch (const DeadlyImportError& e)
     {
@@ -893,7 +942,6 @@
   vtkIdType ActiveCameraIndex = -1;
   std::unordered_map<std::string, vtkSmartPointer<vtkActorCollection>> NodeActors;
   std::unordered_map<std::string, vtkSmartPointer<vtkMatrix4x4>> NodeLocalMatrix;
-  std::unordered_map<std::string, vtkSmartPointer<vtkMatrix4x4>> NodeTRSMatrix;
   std::unordered_map<std::string, vtkSmartPointer<vtkMatrix4x4>> NodeGlobalMatrix;
   vtkF3DAssimpImporter* Parent;
 };
@@ -910,7 +958,8 @@
 //----------------------------------------------------------------------------
 int vtkF3DAssimpImporter::ImportBegin()
 {
-  return this->Internals->ReadScene(this->FileName);
+  return this->Internals->ReadScene(
+    this->GetStream(), this->GetFileName(), this->MemoryHint.c_str());
 }
 
 //----------------------------------------------------------------------------
@@ -1095,9 +1144,8 @@
 }
 
 //----------------------------------------------------------------------------
-bool vtkF3DAssimpImporter::GetTemporalInformation(vtkIdType animationIndex,
-  double vtkNotUsed(frameRate), int& vtkNotUsed(nbTimeSteps), double timeRange[2],
-  vtkDoubleArray* vtkNotUsed(timeSteps))
+bool vtkF3DAssimpImporter::GetTemporalInformation(vtkIdType animationIndex, double timeRange[2],
+  int& vtkNotUsed(nbTimeSteps), vtkDoubleArray* vtkNotUsed(timeSteps))
 {
   assert(animationIndex < this->GetNumberOfAnimations());
   assert(animationIndex >= 0);
@@ -1152,10 +1200,3 @@
 {
   this->Internals->ImportLights(renderer);
 }
-
-//----------------------------------------------------------------------------
-void vtkF3DAssimpImporter::PrintSelf(ostream& os, vtkIndent indent)
-{
-  this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << "\n";
-}
diff -Nru f3d-3.2.0+dfsg/plugins/assimp/module/vtkF3DAssimpImporter.h f3d-3.4.1/plugins/assimp/module/vtkF3DAssimpImporter.h
--- f3d-3.2.0+dfsg/plugins/assimp/module/vtkF3DAssimpImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/assimp/module/vtkF3DAssimpImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -7,6 +7,10 @@
  * https://github.com/assimp/assimp/blob/master/doc/Fileformats.md
  *
  * The following formats have been tested and are supported by f3d: FBX, DAE, OFF, DXF, X, 3MF
+
+ * This importer supports reading from stream but prefer memory stream over filestream.
+ * Reading from stream require to position the MemoryHint to the correct file extension.
+ * No support for reading textures from streams unless embedded.
  */
 
 #ifndef vtkF3DAssimpImporter_h
@@ -23,15 +27,6 @@
 public:
   static vtkF3DAssimpImporter* New();
   vtkTypeMacro(vtkF3DAssimpImporter, vtkF3DImporter);
-  void PrintSelf(ostream& os, vtkIndent indent) override;
-
-  ///@{
-  /**
-   * Set/Get the file name.
-   */
-  vtkSetMacro(FileName, std::string);
-  vtkGetMacro(FileName, std::string);
-  ///@}
 
   /**
    * Update actors at the given time value.
@@ -84,11 +79,17 @@
   ///@}
 
   /**
+   * Set the hint to pass to assimp when reading from memory
+   * Typically the file extension.
+   */
+  vtkSetMacro(MemoryHint, std::string);
+
+  /**
    * Get temporal information for the currently enabled animation.
    * Only defines timerange and ignore provided frameRate.
    */
-  bool GetTemporalInformation(vtkIdType animationIndex, double frameRate, int& nbTimeSteps,
-    double timeRange[2], vtkDoubleArray* timeSteps) override;
+  bool GetTemporalInformation(vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps,
+    vtkDoubleArray* timeSteps) override;
 
   /**
    * Get the number of available cameras.
@@ -115,13 +116,13 @@
   void ImportCameras(vtkRenderer*) override;
   void ImportLights(vtkRenderer*) override;
 
-  std::string FileName;
-  bool ColladaFixup = false;
-
 private:
   vtkF3DAssimpImporter(const vtkF3DAssimpImporter&) = delete;
   void operator=(const vtkF3DAssimpImporter&) = delete;
 
+  bool ColladaFixup = false;
+  std::string MemoryHint;
+
   class vtkInternals;
   std::unique_ptr<vtkInternals> Internals;
 };
diff -Nru f3d-3.2.0+dfsg/plugins/CMakeLists.txt f3d-3.4.1/plugins/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -33,10 +33,5 @@
 endif()
 
 if (F3D_PLUGIN_BUILD_VDB)
-  # VDB plugin needs vtkOpenVDBReader: https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9363
-  if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220714)
-    add_subdirectory(vdb)
-  else ()
-    message(FATAL_ERROR "VDB plugin requires VTK >= 9.2.20220714")
-  endif()
+  add_subdirectory(vdb)
 endif()
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/CMakeLists.txt f3d-3.4.1/plugins/draco/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/draco/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/draco/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -10,12 +10,7 @@
     )
 endif()
 
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextDraco
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes})
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/Testing/CMakeLists.txt f3d-3.4.1/plugins/draco/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/draco/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/draco/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,7 @@
 list(APPEND vtkextDraco_list
      TestF3DDracoReader.cxx
+     TestF3DDracoReaderError.cxx
+     TestF3DDracoReaderStream.cxx
     )
 
 vtk_add_test_cxx(vtkextDracoTests tests
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReader.cxx f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReader.cxx
--- f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,8 +1,7 @@
-#include <vtkNew.h>
-#include <vtkTestUtilities.h>
-
 #include "vtkF3DDracoReader.h"
 
+#include <vtkNew.h>
+
 #include <iostream>
 
 int TestF3DDracoReader(int vtkNotUsed(argc), char* argv[])
@@ -11,6 +10,5 @@
   vtkNew<vtkF3DDracoReader> reader;
   reader->SetFileName(filename);
   reader->Update();
-  reader->Print(cout);
-  return EXIT_SUCCESS;
+  return reader->GetOutput()->GetNumberOfPoints() == 7958 ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReaderError.cxx f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReaderError.cxx
--- f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReaderError.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReaderError.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,64 @@
+#include "vtkF3DDracoReader.h"
+
+#include <vtkCallbackCommand.h>
+#include <vtkCommand.h>
+#include <vtkExecutive.h>
+#include <vtkNew.h>
+
+#include <iostream>
+
+class ErrorEventCallback : public vtkCommand
+{
+public:
+  static ErrorEventCallback* New()
+  {
+    return new ErrorEventCallback;
+  }
+
+  void Execute(vtkObject* caller, unsigned long vtkNotUsed(evId), void* data) override
+  {
+    const vtkF3DDracoReader* reader = reinterpret_cast<vtkF3DDracoReader*>(caller);
+    char* message = static_cast<char*>(data);
+    if (reader && message)
+    {
+      this->Messages.emplace_back(message);
+    }
+  }
+
+  const std::vector<std::string>& GetRecordedErrorMessages() const
+  {
+    return this->Messages;
+  }
+
+private:
+  std::vector<std::string> Messages;
+};
+
+int TestF3DDracoReaderError(int vtkNotUsed(argc), char* argv[])
+{
+  vtkNew<ErrorEventCallback> errorEventCallback;
+  vtkNew<vtkCallbackCommand> nullEventCallback;
+
+  std::string filename = std::string(argv[1]) + "data/nonexistent.drc";
+  vtkNew<vtkF3DDracoReader> reader;
+  reader->AddObserver(vtkCommand::ErrorEvent, errorEventCallback);
+  reader->GetExecutive()->AddObserver(vtkCommand::ErrorEvent, nullEventCallback);
+  reader->SetFileName(filename);
+  reader->Update();
+
+  auto errorMessages = errorEventCallback->GetRecordedErrorMessages();
+  if (errorMessages.empty())
+  {
+    std::cerr << "No error triggered.\n";
+    return EXIT_FAILURE;
+  }
+
+  auto lastMessage = errorMessages.back();
+  if (lastMessage.find("Cannot read file") == std::string::npos)
+  {
+    std::cerr << "No draco error triggered!\n";
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReaderStream.cxx f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReaderStream.cxx
--- f3d-3.2.0+dfsg/plugins/draco/module/Testing/TestF3DDracoReaderStream.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/draco/module/Testing/TestF3DDracoReaderStream.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,21 @@
+#include "vtkF3DDracoReader.h"
+
+#include <vtkFileResourceStream.h>
+#include <vtkNew.h>
+
+#include <iostream>
+
+int TestF3DDracoReaderStream(int vtkNotUsed(argc), char* argv[])
+{
+  std::string filename = std::string(argv[1]) + "data/suzanne.drc";
+  vtkNew<vtkFileResourceStream> stream;
+  if (!stream->Open(filename.c_str()))
+  {
+    std::cerr << "Cannot open file\n";
+    return EXIT_FAILURE;
+  }
+  vtkNew<vtkF3DDracoReader> reader;
+  reader->SetStream(stream);
+  reader->Update();
+  return reader->GetOutput()->GetNumberOfPoints() == 7958 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/vtkF3DDracoReader.cxx f3d-3.4.1/plugins/draco/module/vtkF3DDracoReader.cxx
--- f3d-3.2.0+dfsg/plugins/draco/module/vtkF3DDracoReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/draco/module/vtkF3DDracoReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -12,6 +12,7 @@
 #include <vtkPointData.h>
 #include <vtkPoints.h>
 #include <vtkPolyData.h>
+#include <vtkResourceStream.h>
 
 #include "draco/compression/decode.h"
 #include "draco/draco_features.h"
@@ -20,6 +21,9 @@
 #ifndef DRACO_MESH_COMPRESSION_SUPPORTED
 #error "Please rebuild draco with DRACO_MESH_COMPRESSION cmake option enabled."
 #endif
+
+vtkCxxSetSmartPointerMacro(vtkF3DDracoReader, Stream, vtkResourceStream);
+
 class vtkF3DDracoReader::vtkInternals
 {
 public:
@@ -135,22 +139,27 @@
   {
     int nbCells = mesh->num_faces();
 
-    vtkNew<vtkIdTypeArray> triangles;
-    triangles->SetNumberOfTuples(4 * nbCells);
+    vtkNew<vtkIdTypeArray> offsets;
+    vtkNew<vtkIdTypeArray> connectivity;
+    offsets->SetNumberOfTuples(nbCells + 1);
+    connectivity->SetNumberOfTuples(3 * nbCells);
 
     for (int i = 0; i < nbCells; i++)
     {
       const draco::Mesh::Face& face = mesh->face(draco::FaceIndex(i));
 
-      triangles->SetTypedComponent(4 * i, 0, 3);
+      offsets->SetTypedComponent(i, 0, 3 * i);
+
       for (int j = 0; j < 3; j++)
       {
-        triangles->SetTypedComponent(4 * i + j + 1, 0, face[j].value());
+        connectivity->SetTypedComponent(3 * i + j, 0, face[j].value());
       }
     }
 
+    offsets->SetTypedComponent(nbCells, 0, 3 * nbCells);
+
     vtkNew<vtkCellArray> cells;
-    cells->SetCells(nbCells, triangles);
+    cells->SetData(offsets, connectivity);
 
     output->SetPolys(cells);
   }
@@ -179,11 +188,24 @@
 
   std::vector<char> data;
 
-  auto reader = draco::StdioFileReader::Open(this->FileName);
-  if (!reader->ReadFileToBuffer(&data))
+  if (this->Stream)
+  {
+    this->Stream->Seek(0, vtkResourceStream::SeekDirection::End);
+    size_t length = this->Stream->Tell();
+    this->Stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+
+    // Read stream into buffer
+    data.resize(length);
+    this->Stream->Read(data.data(), length);
+  }
+  else
   {
-    vtkErrorMacro("Cannot read file");
-    return 0;
+    auto reader = draco::StdioFileReader::Open(this->FileName);
+    if (!reader || !reader->ReadFileToBuffer(&data))
+    {
+      vtkErrorMacro("Cannot read file");
+      return 0;
+    }
   }
 
   draco::DecoderBuffer buffer;
@@ -218,10 +240,3 @@
 
   return 1;
 }
-
-//----------------------------------------------------------------------------
-void vtkF3DDracoReader::PrintSelf(ostream& os, vtkIndent indent)
-{
-  this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << "\n";
-}
diff -Nru f3d-3.2.0+dfsg/plugins/draco/module/vtkF3DDracoReader.h f3d-3.4.1/plugins/draco/module/vtkF3DDracoReader.h
--- f3d-3.2.0+dfsg/plugins/draco/module/vtkF3DDracoReader.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/draco/module/vtkF3DDracoReader.h	2026-01-08 08:41:55.000000000 +0200
@@ -7,21 +7,30 @@
  * It reads properly normals, texture coordinates, and colors.
  * All other attributes are added to the point data as generic arrays.
  *
+ * This reader supports reading streams.
+ *
  */
 
 #ifndef vtkF3DDracoReader_h
 #define vtkF3DDracoReader_h
 
 #include <vtkPolyDataAlgorithm.h>
+#include <vtkSmartPointer.h>
 
 #include <memory>
 
+class vtkResourceStream;
 class vtkF3DDracoReader : public vtkPolyDataAlgorithm
 {
 public:
   static vtkF3DDracoReader* New();
   vtkTypeMacro(vtkF3DDracoReader, vtkPolyDataAlgorithm);
-  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  /**
+   * Specify stream to read from
+   * When both `Stream` and `Filename` are set, stream is used.
+   */
+  void SetStream(vtkResourceStream* stream);
 
   ///@{
   /**
@@ -44,6 +53,7 @@
   class vtkInternals;
   std::unique_ptr<vtkInternals> Internals;
 
+  vtkSmartPointer<vtkResourceStream> Stream;
   std::string FileName;
 };
 
diff -Nru f3d-3.2.0+dfsg/plugins/native/CMakeLists.txt f3d-3.4.1/plugins/native/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/native/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -110,7 +110,7 @@
   SCORE 99
   EXTENSIONS vtk
   MIMETYPES application/vnd.vtk
-  VTK_READER vtkPDataSetReader
+  VTK_READER vtkDataSetReader
   FORMAT_DESCRIPTION "VTK Legacy"
 )
 
@@ -178,46 +178,35 @@
   CUSTOM_CODE "${CMAKE_CURRENT_SOURCE_DIR}/mdl.inl"
 )
 
-# Needs vtkResourceStream (https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9663)
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221216)
-  f3d_plugin_declare_reader(
-    NAME SPZ
-    EXTENSIONS spz
-    MIMETYPES application/vnd.spz
-    VTK_READER vtkF3DSPZReader
-    FORMAT_DESCRIPTION "Compressed 3D gaussian splats"
-  )
-  f3d_plugin_declare_reader(
-    NAME Splat
-    SCORE 90
-    EXTENSIONS splat
-    MIMETYPES application/vnd.splat
-    VTK_READER vtkF3DSplatReader
-    FORMAT_DESCRIPTION "3D Gaussian splats"
-  )
-
-  f3d_plugin_declare_reader(
-    NAME PLYReader
-    EXTENSIONS ply
-    MIMETYPES application/vnd.ply
-    VTK_READER vtkF3DPLYReader
-    FORMAT_DESCRIPTION "Polygon"
-  )
-else()
-  f3d_plugin_declare_reader(
-    NAME PLYReader
-    EXTENSIONS ply
-    MIMETYPES application/vnd.ply
-    VTK_READER vtkPLYReader
-    FORMAT_DESCRIPTION "Polygon"
-  )
-endif()
+f3d_plugin_declare_reader(
+  NAME SPZ
+  EXTENSIONS spz
+  MIMETYPES application/vnd.spz
+  VTK_READER vtkF3DSPZReader
+  FORMAT_DESCRIPTION "Compressed 3D gaussian splats"
+)
+f3d_plugin_declare_reader(
+  NAME Splat
+  SCORE 90
+  EXTENSIONS splat
+  MIMETYPES application/vnd.splat
+  VTK_READER vtkF3DSplatReader
+  FORMAT_DESCRIPTION "3D Gaussian splats"
+)
+
+f3d_plugin_declare_reader(
+  NAME PLYReader
+  EXTENSIONS ply
+  MIMETYPES application/vnd.ply
+  VTK_READER vtkF3DPLYReader
+  FORMAT_DESCRIPTION "Polygon"
+)
 
 f3d_plugin_build(
   NAME native
   VERSION 1.0
   DESCRIPTION "Native VTK I/O support"
-  VTK_MODULES IOXML IOParallel IOCityGML IOImage IOGeometry IOPLY
+  VTK_MODULES IOXML IOLegacy IOCityGML IOImage IOGeometry IOPLY
   MIMETYPE_XML_FILES
     "${CMAKE_CURRENT_SOURCE_DIR}/f3d-3d-formats.xml"
     "${CMAKE_CURRENT_SOURCE_DIR}/f3d-3d-image-formats.xml"
diff -Nru f3d-3.2.0+dfsg/plugins/native/configs/config.d/10_native.json f3d-3.4.1/plugins/native/configs/config.d/10_native.json
--- f3d-3.2.0+dfsg/plugins/native/configs/config.d/10_native.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/configs/config.d/10_native.json	2026-01-08 08:41:55.000000000 +0200
@@ -45,16 +45,16 @@
     "options": {
       "scalar-coloring": true,
       "coloring-component": "-2",
-      "translucency-support": true
+      "blending": "ddp"
     }
   },
   {
     "match-type": "glob",
     "match": "*.{splat,spz}",
     "options": {
-      "point-sprites": true,
+      "point-sprites": "gaussian",
+      "point-sprites-absolute-size": true,
       "point-sprites-size": 1,
-      "point-sprites-type": "gaussian",
       "scalar-coloring": true,
       "coloring-array": "color",
       "coloring-component": "-2",
@@ -62,8 +62,8 @@
       "interaction-trackball": true,
       "tone-mapping": false,
       "grid": false,
-      "anti-aliasing": false,
-      "translucency-support": false
+      "anti-aliasing": "none",
+      "blending": "sort"
     }
   },
   {
diff -Nru f3d-3.2.0+dfsg/plugins/native/configs/thumbnail.d/10_native.json f3d-3.4.1/plugins/native/configs/thumbnail.d/10_native.json
--- f3d-3.2.0+dfsg/plugins/native/configs/thumbnail.d/10_native.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/configs/thumbnail.d/10_native.json	2026-01-08 08:41:55.000000000 +0200
@@ -28,23 +28,23 @@
     "options": {
       "scalar-coloring": true,
       "coloring-component": "-2",
-      "translucency-support": true
+      "blending": "ddp"
     }
   },
   {
     "match-type": "glob",
     "match": "*.{splat,spz}",
     "options": {
-      "point-sprites": true,
+      "point-sprites": "gaussian",
+      "point-sprites-absolute-size": true,
       "point-sprites-size": 1,
-      "point-sprites-type": "gaussian",
       "scalar-coloring": true,
       "coloring-array": "color",
       "coloring-component": "-2",
       "tone-mapping": false,
       "grid": false,
-      "anti-aliasing": false,
-      "translucency-support": false
+      "anti-aliasing": "none",
+      "blending": "sort"
     }
   },
   {
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/CMakeLists.txt f3d-3.4.1/plugins/native/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/native/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,19 +1,12 @@
 set(classes
   vtkF3DQuakeMDLImporter
+  vtkF3DSPZReader
+  vtkF3DSplatReader
+  vtkF3DPLYReader
   )
 
-# Needs vtkResourceStream (https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9663)
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20221216)
-  set(classes "${classes}" vtkF3DSPZReader vtkF3DSplatReader vtkF3DPLYReader)
-endif()
-
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextNative
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes})
 
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/CMakeLists.txt f3d-3.4.1/plugins/native/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -6,6 +6,9 @@
     TestF3DSPZReader.cxx
     TestF3DSplatReader.cxx
     TestF3DPLYReader.cxx
+    TestF3DQuakeMDLImporterStream.cxx
+    TestF3DQuakeMDLImporterInexistent.cxx
+    TestF3DQuakeMDLParser.cxx
   )
 endif()
 
@@ -19,4 +22,7 @@
   set_tests_properties(f3d::vtkextNativeCxx-TestF3DPLYReader
     PROPERTIES
     FAIL_REGULAR_EXPRESSION "")
+  set_tests_properties(f3d::vtkextNativeCxx-TestF3DQuakeMDLImporterInexistent
+    PROPERTIES
+    FAIL_REGULAR_EXPRESSION "")
 endif()
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DPLYReader.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DPLYReader.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DPLYReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DPLYReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -19,7 +19,7 @@
 
     if (!stream->Open(pathGaussians.c_str()))
     {
-      std::cerr << "Cannot open file" << std::endl;
+      std::cerr << "Cannot open file\n";
       return EXIT_FAILURE;
     }
 
@@ -32,13 +32,13 @@
 
     if (nbGaussians != 2655)
     {
-      std::cerr << "Incorrect number of gaussians: " << nbGaussians << std::endl;
+      std::cerr << "Incorrect number of gaussians: " << nbGaussians << "\n";
       return EXIT_FAILURE;
     }
 
     if (reader->GetOutput()->GetPointData()->GetArray("sh10") == nullptr)
     {
-      std::cerr << "Cannot find spherical harmonics" << std::endl;
+      std::cerr << "Cannot find spherical harmonics\n";
       return EXIT_FAILURE;
     }
   }
@@ -49,7 +49,7 @@
     file.open(pathGaussians.c_str(), std::ios::binary);
     if (!file.is_open())
     {
-      std::cerr << "Cannot open file" << std::endl;
+      std::cerr << "Cannot open file\n";
       return EXIT_FAILURE;
     }
 
@@ -65,13 +65,13 @@
 
     if (nbGaussians != 2655)
     {
-      std::cerr << "Incorrect number of gaussians: " << nbGaussians << std::endl;
+      std::cerr << "Incorrect number of gaussians: " << nbGaussians << "\n";
       return EXIT_FAILURE;
     }
 
     if (reader->GetOutput()->GetPointData()->GetArray("sh10") == nullptr)
     {
-      std::cerr << "Cannot find spherical harmonics" << std::endl;
+      std::cerr << "Cannot find spherical harmonics\n";
       return EXIT_FAILURE;
     }
   }
@@ -86,13 +86,13 @@
 
     if (nbPoints != 5)
     {
-      std::cerr << "Incorrect number of points: " << nbPoints << std::endl;
+      std::cerr << "Incorrect number of points: " << nbPoints << "\n";
       return EXIT_FAILURE;
     }
 
     if (reader->GetOutput()->GetPointData()->GetArray("sh10") != nullptr)
     {
-      std::cerr << "Should not have spherical harmonics" << std::endl;
+      std::cerr << "Should not have spherical harmonics\n";
       return EXIT_FAILURE;
     }
   }
@@ -110,7 +110,7 @@
 
     if (nbPoints != 0)
     {
-      std::cerr << "The file should be invalid" << std::endl;
+      std::cerr << "The file should be invalid\n";
       return EXIT_FAILURE;
     }
   }
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLImporterInexistent.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLImporterInexistent.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLImporterInexistent.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLImporterInexistent.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,19 @@
+#include <vtkNew.h>
+
+#include "vtkF3DQuakeMDLImporter.h"
+
+#include <iostream>
+
+int TestF3DQuakeMDLImporterInexistent(int vtkNotUsed(argc), char* argv[])
+{
+  std::string pathInexistent = std::string(argv[1]) + "data/inexistent.mdl";
+  vtkNew<vtkF3DQuakeMDLImporter> importer;
+  importer->SetFileName(pathInexistent.c_str());
+  if (importer->Update())
+  {
+    std::cerr << "Unexpected success opening inexistent file\n";
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLImporterStream.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLImporterStream.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLImporterStream.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLImporterStream.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,33 @@
+#include <vtkDataSet.h>
+#include <vtkFileResourceStream.h>
+#include <vtkMapper.h>
+#include <vtkNew.h>
+#include <vtkRenderer.h>
+#include <vtkTestUtilities.h>
+
+#include "vtkF3DQuakeMDLImporter.h"
+
+#include <iostream>
+
+int TestF3DQuakeMDLImporterStream(int vtkNotUsed(argc), char* argv[])
+{
+  vtkNew<vtkFileResourceStream> stream;
+  std::string path = std::string(argv[1]) + "data/zombie.mdl";
+  if (!stream->Open(path.c_str()))
+  {
+    std::cerr << "Cannot open file\n";
+    return EXIT_FAILURE;
+  }
+
+  vtkNew<vtkF3DQuakeMDLImporter> importer;
+  importer->SetStream(stream);
+  importer->Update();
+  return importer->GetRenderer()
+           ->GetActors()
+           ->GetLastActor()
+           ->GetMapper()
+           ->GetInput()
+           ->GetNumberOfPoints() == 1878
+    ? EXIT_SUCCESS
+    : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLParser.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLParser.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DQuakeMDLParser.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DQuakeMDLParser.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,377 @@
+#include "vtkF3DQuakeMDLImporter.h"
+
+#include <vtkCallbackCommand.h>
+#include <vtkCommand.h>
+#include <vtkDataSet.h>
+#include <vtkMapper.h>
+#include <vtkMemoryResourceStream.h>
+#include <vtkNew.h>
+#include <vtkRenderer.h>
+#include <vtkTestUtilities.h>
+
+#include <climits>
+#include <cstring>
+#include <iostream>
+#include <regex>
+
+// Header definition : cloned from plugins/native/module/vtkF3DQuakeMDLImporter.cxx
+struct mdl_header_t
+{
+  int ident;
+  int version;
+  float scale[3];
+  float translation[3];
+  float boundingRadius;
+  float eyePosition[3];
+  int numSkins;
+  int skinWidth;
+  int skinHeight;
+  int numVertices;
+  int numTriangles;
+  int numFrames;
+  int syncType;
+  int stateFlags;
+  float size;
+};
+
+// Buffer typecasting helper
+template<typename TYPE>
+static TYPE* PeekFromVector(const std::vector<uint8_t>& buffer, const size_t& offset)
+{
+  static_assert(std::is_pod<TYPE>::value, "Vector typecast requires POD input");
+
+  if (offset + sizeof(TYPE) > buffer.size())
+  {
+    return nullptr;
+  }
+
+  return reinterpret_cast<TYPE*>(const_cast<uint8_t*>(buffer.data() + offset));
+}
+
+struct Error
+{
+  bool parsed = false;
+  std::string fileName = "";
+  std::string lineNumber = "";
+  std::string errorString = "";
+};
+
+Error ErrorCodeParser(const std::string& errorStr)
+{
+  Error err;
+  std::regex errorRegex(R"(ERROR: In (.+?), line (\d+)\n.+: (.+))");
+  std::smatch matches;
+  if (std::regex_search(errorStr, matches, errorRegex))
+  {
+    err.parsed = true;
+    err.fileName = matches[1].str();
+    err.lineNumber = matches[2].str();
+    err.errorString = matches[3].str();
+
+    std::cout << "Parsed error\n    file name : \"" << err.fileName << "\"\n    line number : \""
+              << err.lineNumber << "\"\n    error string : \"" << err.errorString << "\"\n";
+  }
+  return err;
+}
+
+void ErrorCallbackFunc(
+  vtkObject*, long unsigned int vtkNotUsed(eventId), void* clientData, void* callData)
+{
+  auto errors = static_cast<std::vector<Error>*>(clientData);
+  errors->push_back(ErrorCodeParser(static_cast<char*>(callData)));
+}
+
+int TestF3DQuakeMDLParser(int vtkNotUsed(argc), char* argv[])
+{
+  // Read in example file, can theoretically be any mdl file
+  std::string path = std::string(argv[1]) + "data/armor.mdl";
+  std::ifstream file(path, std::ios::binary);
+  std::vector<uint8_t> defaultFile(
+    (std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+
+  // setup vectors
+  std::vector<uint8_t> testNoData;
+  std::vector<uint8_t> testNonmagicHeader;
+  std::vector<uint8_t> testNormalHeader;
+  std::vector<uint8_t> testMaxedHeader;
+  std::vector<uint8_t> testMinedHeader;
+  std::vector<uint8_t> testOverflowSF;
+  std::vector<uint8_t> testOverflowVerts;
+
+  // .mdl file with no data
+  {
+    testNoData = {};
+  }
+
+  // .mdl file with only header (but incorrect magic number)
+  {
+    testNonmagicHeader = defaultFile;
+    // modify header
+    auto header = PeekFromVector<mdl_header_t>(testNonmagicHeader, 0);
+    header->ident = 0;
+  }
+
+  // .mdl file with only header (correct magic number, but no body data)
+  {
+    testNormalHeader = defaultFile;
+    // modify header
+    // no need, default file should have valid IPDO
+  }
+
+  // .mdl file with MAXED data
+  {
+    testMaxedHeader = defaultFile;
+    // modify header
+    // max numSkins, skinWidth, skinHeight, numVertices, numTriangles, numFrames, size
+    auto header = PeekFromVector<mdl_header_t>(testMaxedHeader, 0);
+    header->numSkins = INT_MAX;
+    header->skinWidth = INT_MAX;
+    header->skinHeight = INT_MAX;
+    header->numVertices = INT_MAX;
+    header->numTriangles = INT_MAX;
+    header->numFrames = INT_MAX;
+  }
+
+  // .mdl file with MINED data
+  {
+    testMinedHeader = defaultFile;
+    // modify header
+    // max numSkins, skinWidth, skinHeight, numVertices, numTriangles, numFrames, size
+    auto header = PeekFromVector<mdl_header_t>(testMinedHeader, 0);
+    header->numSkins = INT_MIN;
+    header->skinWidth = INT_MIN;
+    header->skinHeight = INT_MIN;
+    header->numVertices = INT_MIN;
+    header->numTriangles = INT_MIN;
+    header->numFrames = INT_MIN;
+  }
+
+  // .mdl file where the mdl_simpleframe_t is cut off
+  {
+    // clone the header from default
+    auto header = *PeekFromVector<mdl_header_t>(defaultFile, 0);
+    header.numSkins = 0;
+    header.skinWidth = 0;
+    header.skinHeight = 0;
+    header.numFrames = 1;
+    header.numVertices = 1;
+    header.numTriangles = 0;
+
+    int nb = 2;
+
+    // push data into vec
+    // header
+    const uint8_t* data = reinterpret_cast<const uint8_t*>(&header);
+    testOverflowSF.insert(testOverflowSF.end(), data, data + sizeof(mdl_header_t));
+    // texcoords
+    testOverflowSF.insert(testOverflowSF.end(), 12, 0);
+    // type
+    testOverflowSF.insert(testOverflowSF.end(), 4, 0xff);
+    // nb
+    const uint8_t* data_nb = reinterpret_cast<const uint8_t*>(&nb);
+    testOverflowSF.insert(testOverflowSF.end(), data_nb, data_nb + 4);
+    // void data :
+    // sizeof(mdl_vertex_t) * 2 "min/max",
+    // sizeof(float) "time",
+    // sizeof(float) nb offset,
+    // mdl_simpleframe_t_fixed_size
+    testOverflowSF.insert(testOverflowSF.end(), 2 * 4 + 4 + 4 + 24, 0);
+  }
+
+  // .mdl file where the header indicates more vertices than can physically be in the file
+  {
+    // note : most of this data was setup and can be shared with the testOverflowSF
+    testOverflowVerts = testOverflowSF;
+    // push data to vec
+    // void data :
+    // sizeof(mdl_vertex_t) "mdl_simpleframe_t::verts"
+    // (note : this was negated from the testOverflowSF test to trigger the specific error)
+    testOverflowVerts.insert(testOverflowVerts.end(), 4, 0);
+  }
+
+  // Setup
+  vtkNew<vtkCallbackCommand> errorCallback;
+  errorCallback->SetCallback(ErrorCallbackFunc);
+
+  std::string testName = "";
+
+  // Run tests
+
+  // Test "0bytes"
+  {
+    testName = "0bytes";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testNoData);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 1 || errors[0].errorString != "Invalid MDL file")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "non-magic header"
+  {
+    testName = "non-magic header";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testNonmagicHeader);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 1 || errors[0].errorString != "Incompatible MDL header")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "magic header"
+  {
+    testName = "magic header";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testNormalHeader);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 0)
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "maxed header"
+  {
+    testName = "maxed header";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testMaxedHeader);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 2 ||
+      errors[0].errorString != "CreateTexture Accessed data out of range, aborting." ||
+      errors[1].errorString != "Unable to read a texture, aborting.")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "mined header"
+  {
+    testName = "mined header";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testMinedHeader);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 2 ||
+      errors[0].errorString != "CreateMesh Accessed data out of range, aborting." ||
+      errors[1].errorString != "No frame read, there is nothing to display in this file.")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "overflowing mdl_simpleframe_t"
+  {
+    testName = "overflowing mdl_simpleframe_t";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testOverflowSF);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 2 ||
+      errors[0].errorString != "CreateMesh Accessed data out of range, aborting." ||
+      errors[1].errorString != "No frame read, there is nothing to display in this file.")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test "overflowing vertices"
+  {
+    testName = "overflowing vertices";
+    std::cout << "Running test \"" << testName << "\"\n";
+
+    std::vector<Error> errors;
+    errorCallback->SetClientData(&errors);
+
+    vtkNew<vtkMemoryResourceStream> stream;
+    stream->SetBuffer(testOverflowVerts);
+
+    vtkNew<vtkF3DQuakeMDLImporter> importer;
+    importer->SetStream(stream);
+    importer->AddObserver(vtkCommand::ErrorEvent, errorCallback);
+
+    importer->Update();
+
+    if (errors.size() != 2 ||
+      errors[0].errorString != "CreateMesh Accessed data out of range, aborting." ||
+      errors[1].errorString != "No frame read, there is nothing to display in this file.")
+    {
+      std::cerr << "Test \"" << testName << "\" failed\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DSplatReader.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DSplatReader.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DSplatReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DSplatReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -14,7 +14,7 @@
 
   if (!stream->Open(path.c_str()))
   {
-    std::cerr << "Cannot open .splat file" << std::endl;
+    std::cerr << "Cannot open .splat file\n";
     return EXIT_FAILURE;
   }
 
@@ -26,7 +26,7 @@
 
   if (nbPoints != 52293) // <-- Change this to expected number of splats
   {
-    std::cerr << "Incorrect number of splats: " << nbPoints << std::endl;
+    std::cerr << "Incorrect number of splats: " << nbPoints << "\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DSPZReader.cxx f3d-3.4.1/plugins/native/module/Testing/TestF3DSPZReader.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/Testing/TestF3DSPZReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/Testing/TestF3DSPZReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -14,7 +14,7 @@
 
   if (!stream->Open(path.c_str()))
   {
-    std::cerr << "Cannot open file" << std::endl;
+    std::cerr << "Cannot open file\n";
     return EXIT_FAILURE;
   }
 
@@ -26,7 +26,7 @@
 
   if (nbGaussians != 13296)
   {
-    std::cerr << "Incorrect number of gaussians: " << nbGaussians << std::endl;
+    std::cerr << "Incorrect number of gaussians: " << nbGaussians << "\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/vtkF3DQuakeMDLImporter.cxx f3d-3.4.1/plugins/native/module/vtkF3DQuakeMDLImporter.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/vtkF3DQuakeMDLImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/vtkF3DQuakeMDLImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,9 @@
 #include "vtkF3DQuakeMDLImporter.h"
 #include "vtkF3DQuakeMDLImporterConstants.h"
 
+#include <vtkCommand.h>
+#include <vtkDoubleArray.h>
+#include <vtkFileResourceStream.h>
 #include <vtkFloatArray.h>
 #include <vtkImageData.h>
 #include <vtkOpenGLTexture.h>
@@ -9,6 +12,10 @@
 #include <vtkPolyDataMapper.h>
 #include <vtkProperty.h>
 #include <vtkRenderer.h>
+#include <vtkResourceStream.h>
+
+#include <cstdint>
+#include <cstring>
 
 //----------------------------------------------------------------------------
 vtkStandardNewMacro(vtkF3DQuakeMDLImporter);
@@ -22,7 +29,7 @@
   // Header definition
   struct mdl_header_t
   {
-    int IDPO;
+    int ident;
     int version;
     float scale[3];
     float translation[3];
@@ -77,7 +84,7 @@
     mdl_vertex_t verts[1024]; // vertex list of the frame, maximum capacity is 1024
   };
 
-  enum FRAME_TYPE
+  enum FRAME_TYPE : std::uint8_t
   {
     SINGLE_FRAME = 0,
     GROUP_FRAME,
@@ -96,60 +103,168 @@
   };
 
   //----------------------------------------------------------------------------
+  class F3DRangeError : public std::out_of_range
+  {
+  public:
+    explicit F3DRangeError(const std::string& what = "")
+      : std::out_of_range(what)
+    {
+    }
+  };
+
+  //----------------------------------------------------------------------------
+  // Safer buffer typecasting of arbitrary buffer location
+  template<typename TYPE>
+  static const TYPE* PeekFromVector(const std::vector<uint8_t>& buffer, const size_t& offset)
+  {
+    static_assert(std::is_pod<TYPE>::value, "Vector typecast requires POD input");
+
+    if (offset + sizeof(TYPE) > buffer.size())
+    {
+      throw F3DRangeError("Requested data out of range.");
+    }
+
+    return reinterpret_cast<const TYPE*>(buffer.data() + offset);
+  }
+
+  //----------------------------------------------------------------------------
+  // Safer buffer typecasting with auto offset incrementing
+  template<typename TYPE>
+  static const TYPE* ReadFromVector(const std::vector<uint8_t>& buffer, size_t& offset)
+  {
+    const TYPE* ptr = vtkInternals::PeekFromVector<TYPE>(buffer, offset);
+    offset += sizeof(TYPE);
+    return ptr;
+  }
+
+  //----------------------------------------------------------------------------
+  // Safer buffer typecasting of arbitrary buffer location with variable length data
+  static const mdl_simpleframe_t* PeekFromVectorSimpleframe(
+    const std::vector<uint8_t>& buffer, const size_t& offset, size_t num_verts = 0)
+  {
+    static constexpr auto mdl_simpleframe_t_fixed_size =
+      sizeof(mdl_simpleframe_t) - sizeof(mdl_simpleframe_t::verts);
+    static_assert(std::is_pod<mdl_simpleframe_t>::value, "Vector typecast requires POD input");
+
+    // check that we have enough data for the given number of verts requested
+    if (offset + mdl_simpleframe_t_fixed_size + num_verts * sizeof(mdl_simpleframe_t::verts[0]) >
+      buffer.size())
+    {
+      throw F3DRangeError("Requested data out of range.");
+    }
+
+    return reinterpret_cast<const mdl_simpleframe_t*>(buffer.data() + offset);
+  }
+
+  //----------------------------------------------------------------------------
+  // Safer buffer typecasting with auto offset incrementing with variable length data
+  const mdl_simpleframe_t* ReadFromVectorSimpleframe(
+    const std::vector<uint8_t>& buffer, size_t& offset, size_t num_verts = 0)
+  {
+    static constexpr auto mdl_simpleframe_t_fixed_size =
+      sizeof(mdl_simpleframe_t) - sizeof(mdl_simpleframe_t::verts);
+    auto ptr = vtkInternals::PeekFromVectorSimpleframe(buffer, offset, num_verts);
+    offset += mdl_simpleframe_t_fixed_size + num_verts * sizeof(mdl_simpleframe_t::verts[0]);
+    return ptr;
+  }
+
+  //----------------------------------------------------------------------------
   explicit vtkInternals(vtkF3DQuakeMDLImporter* parent)
     : Parent(parent)
   {
   }
 
   //----------------------------------------------------------------------------
-  vtkSmartPointer<vtkTexture> CreateTexture(const std::vector<unsigned char>& buffer, int& offset,
+  vtkSmartPointer<vtkTexture> CreateTexture(const std::vector<uint8_t>& buffer, size_t& offset,
     int skinWidth, int skinHeight, unsigned int nbSkins, unsigned int skinIndex)
   {
-    vtkNew<vtkTexture> texture;
-    texture->SetColorModeToDirectScalars();
-    texture->UseSRGBColorSpaceOn();
-
-    // Read textures.
-    std::vector<mixed_pointer_array> skins = std::vector<mixed_pointer_array>(nbSkins);
-    for (unsigned int i = 0; i < nbSkins; i++)
+    auto make_new_skin = [&](vtkNew<vtkImageData>& skin)
     {
-      skins[i].group = *reinterpret_cast<const int*>(buffer.data() + offset);
-      if (skins[i].group == 0)
+      // check if all the data for this operation exists
+      uint64_t checkHeight = static_cast<uint64_t>(skinHeight) - 1;
+      uint64_t checkWidth = static_cast<uint64_t>(skinWidth) - 1;
+      uint64_t checkSize = static_cast<uint64_t>(offset) + (checkHeight * checkWidth);
+      if (skinHeight > 0 && skinWidth > 0 && (checkSize >= static_cast<uint64_t>(buffer.size())))
       {
-        skins[i].skin = buffer.data() + sizeof(int32_t) + offset;
-        offset += sizeof(int32_t) + skinWidth * skinHeight * sizeof(int8_t);
+        throw F3DRangeError("Skin dimensions out of bounds of file size");
       }
-      else
+
+      skin->SetDimensions(skinWidth, skinHeight, 1);
+      skin->AllocateScalars(VTK_UNSIGNED_CHAR, 3);
+      for (int x = 0; x < skinHeight; ++x)
       {
-        // XXX: groupskin not supported yet
-        vtkErrorWithObjectMacro(this->Parent, "Groupskin are not supported, aborting.");
-        return nullptr;
+        for (int y = 0; y < skinWidth; ++y)
+        {
+          auto index = *vtkInternals::PeekFromVector<uint8_t>(buffer, offset + x * skinWidth + y);
+          unsigned char* ptr = static_cast<unsigned char*>(skin->GetScalarPointer(y, x, 0));
+          std::copy(F3DMDLDefaultColorMap[index], F3DMDLDefaultColorMap[index] + 3, ptr);
+        }
       }
-    }
-
-    // Copy to imageData
-    vtkNew<vtkImageData> img;
-    img->SetDimensions(skinWidth, skinHeight, 1);
-    img->AllocateScalars(VTK_UNSIGNED_CHAR, 3);
+    };
 
-    if (skinIndex >= nbSkins)
-    {
-      skinIndex = 0;
-      vtkWarningWithObjectMacro(
-        this->Parent, "QuakeMDL.skin_index is out of bounds. Defaulting to 0.");
-    }
-    const unsigned char* selectedSkin = skins[skinIndex].skin;
-    for (int i = 0; i < skinHeight; i++)
+    // Read textures.
+    try
     {
-      for (int j = 0; j < skinWidth; j++)
+      vtkNew<vtkTexture> texture;
+      texture->SetColorModeToDirectScalars();
+      texture->UseSRGBColorSpaceOn();
+
+      if (skinIndex >= nbSkins)
       {
-        unsigned char index = *(selectedSkin + i * skinWidth + j);
-        unsigned char* ptr = static_cast<unsigned char*>(img->GetScalarPointer(j, i, 0));
-        std::copy(F3DMDLDefaultColorMap[index], F3DMDLDefaultColorMap[index] + 3, ptr);
+        skinIndex = 0;
+        vtkWarningWithObjectMacro(
+          this->Parent, "QuakeMDL.skin_index is out of bounds. Defaulting to 0.");
       }
+      int skinSize = skinWidth * skinHeight;
+      int groupSkinCount = 0;
+      for (unsigned int i = 0; i < nbSkins; i++)
+      {
+        int skinGroup = *vtkInternals::ReadFromVector<int>(buffer, offset);
+        if (skinGroup == 0)
+        {
+          // Skip the skins that are not selected
+          if (i == skinIndex)
+          {
+            vtkNew<vtkImageData> img;
+            make_new_skin(img);
+            texture->SetInputData(img);
+          }
+          offset += skinSize * sizeof(int8_t);
+        }
+        else
+        {
+          std::string skinAnimationName = "skin_" + std::to_string(groupSkinCount);
+          this->GroupSkinAnimationNames.emplace_back(skinAnimationName);
+          auto nb = *vtkInternals::ReadFromVector<int>(buffer, offset);
+          this->GroupSkins.emplace_back(nb);
+          this->GroupSkinDurations.emplace_back(nb + 1, 0.0f);
+          for (int j = 1; j <= nb; ++j)
+          {
+            auto timeValue = *vtkInternals::ReadFromVector<float>(buffer, offset);
+            this->GroupSkinDurations[groupSkinCount][j] = static_cast<double>(timeValue);
+          }
+          for (int skinIdx = 0; skinIdx < nb; ++skinIdx)
+          {
+            vtkNew<vtkImageData> skinTemp;
+            make_new_skin(skinTemp);
+            this->GroupSkins[groupSkinCount][skinIdx] = skinTemp;
+            offset += skinSize * sizeof(int8_t);
+          }
+          if (i == skinIndex)
+          {
+            texture->SetInputData(this->GroupSkins[groupSkinCount].front());
+          }
+          ++groupSkinCount;
+        }
+      }
+      return texture;
+    }
+    catch (const F3DRangeError&)
+    {
+      // Catch fatal errors thrown from overrunning the buffer
+      vtkErrorWithObjectMacro(this->Parent, "CreateTexture Accessed data out of range, aborting.");
+      return nullptr;
     }
-    texture->SetInputData(img);
-    return texture;
   }
 
   //----------------------------------------------------------------------------
@@ -196,204 +311,288 @@
   }
 
   //----------------------------------------------------------------------------
-  bool CreateMesh(const std::vector<unsigned char>& buffer, int offset, const mdl_header_t* header)
+  bool CreateMesh(const std::vector<uint8_t>& buffer, size_t offset, const mdl_header_t* header)
   {
-    constexpr int mdl_simpleframe_t_fixed_size =
-      2 * sizeof(mdl_vertex_t) + 16 * sizeof(int8_t); // Size of bboxmin, bboxmax and name.
+    try
+    {
+      const unsigned long totalProgress = header->numFrames * 2 + header->numTriangles;
+      unsigned long currentProgress = 0;
+      double progressRate;
 
-    // Read Texture Coordinates
-    const mdl_texcoord_t* texcoords =
-      reinterpret_cast<const mdl_texcoord_t*>(buffer.data() + offset);
-    offset += sizeof(mdl_texcoord_t) * header->numVertices;
-
-    // Read Triangles
-    const mdl_triangle_t* triangles =
-      reinterpret_cast<const mdl_triangle_t*>(buffer.data() + offset);
-    offset += sizeof(mdl_triangle_t) * header->numTriangles;
-
-    // Read frames
-    std::vector<plugin_frame_pointer> framePtr =
-      std::vector<plugin_frame_pointer>(header->numFrames);
-    std::vector<std::vector<int>> frameOffsets = std::vector<std::vector<int>>();
-    for (int i = 0; i < header->numFrames; i++)
-    {
-      framePtr[i].type = reinterpret_cast<const int*>(buffer.data() + offset);
-      if (*framePtr[i].type == SINGLE_FRAME)
-      {
-        framePtr[i].nb = nullptr;
-        framePtr[i].time = nullptr;
-        framePtr[i].frames =
-          reinterpret_cast<const mdl_simpleframe_t*>(buffer.data() + sizeof(int32_t) + offset);
-
-        // Size of a frame is mdl_simpleframe_t_fixed_size + mdl_vertex_t * numVertices, +
-        // sizeof(int)
-        offset += sizeof(int32_t) + mdl_simpleframe_t_fixed_size +
-          sizeof(mdl_vertex_t) * header->numVertices;
+      constexpr int mdl_simpleframe_t_fixed_size = sizeof(mdl_simpleframe_t) -
+        sizeof(mdl_simpleframe_t::verts); // Size of bboxmin, bboxmax and name.
 
-        // Always emplace in case of mixed single frame and group frame
-        frameOffsets.emplace_back(std::vector<int>());
-      }
-      else
-      {
-        framePtr[i].nb = reinterpret_cast<const int*>(buffer.data() + sizeof(int32_t) + offset);
-        // Skips parameters min and max.
-        framePtr[i].time = reinterpret_cast<const float*>(reinterpret_cast<const void*>(
-          buffer.data() + 2 * sizeof(int32_t) + 2 * sizeof(mdl_vertex_t) + offset));
-        // Points to the first frame, 4 * nbFrames for the float array
-        framePtr[i].frames =
-          reinterpret_cast<const mdl_simpleframe_t*>(buffer.data() + 2 * sizeof(int32_t) +
-            2 * sizeof(mdl_vertex_t) + (*framePtr[i].nb) * sizeof(float) + offset);
-        offset +=
-          2 * sizeof(int32_t) + 2 * sizeof(mdl_vertex_t) + (*framePtr[i].nb) * sizeof(float);
-        frameOffsets.emplace_back(std::vector<int>());
-
-        for (int j = 0; j < *framePtr[i].nb; j++)
-        {
-          // Offset for each frame
-          frameOffsets[i].emplace_back(offset);
-          offset += mdl_simpleframe_t_fixed_size + sizeof(mdl_vertex_t) * header->numVertices;
-        }
-      }
-    }
+      // Read Texture Coordinates
+      auto texcoords = vtkInternals::PeekFromVector<mdl_texcoord_t>(buffer, offset);
+      offset += sizeof(mdl_texcoord_t) * header->numVertices;
 
-    // Draw cells and scale texture coordinates
-    vtkNew<vtkCellArray> cells;
-    cells->Allocate(header->numTriangles);
-    vtkNew<vtkFloatArray> textureCoordinates;
-    textureCoordinates->SetNumberOfComponents(2);
-    textureCoordinates->SetName("TextureCoordinates");
-    textureCoordinates->Allocate(header->numTriangles * 3);
-    for (int i = 0; i < header->numTriangles; i++)
-    {
-      for (int vertex : triangles[i].vertex)
+      // Read Triangles
+      auto triangles = vtkInternals::PeekFromVector<mdl_triangle_t>(buffer, offset);
+      offset += sizeof(mdl_triangle_t) * header->numTriangles;
+
+      // Read frames
+      std::vector<plugin_frame_pointer> framePtr =
+        std::vector<plugin_frame_pointer>(header->numFrames);
+      std::vector<std::vector<size_t>> frameOffsets = std::vector<std::vector<size_t>>();
+      for (int i = 0; i < header->numFrames; i++)
       {
-        float coord_s = texcoords[vertex].coord_s;
-        float coord_t = texcoords[vertex].coord_t;
-        if (!triangles[i].facesFront && texcoords[vertex].onseam)
+        framePtr[i].type = vtkInternals::PeekFromVector<int>(buffer, offset);
+        if (*framePtr[i].type == SINGLE_FRAME)
         {
-          coord_s = coord_s + header->skinWidth * 0.5f; // Backface
-        }
+          // Alias offset
+          auto offsetAlias = offset + sizeof(int32_t);
+          framePtr[i].nb = nullptr;
+          framePtr[i].time = nullptr;
+
+          // Size of a frame is mdl_simpleframe_t_fixed_size + mdl_vertex_t * numVertices, +
+          // sizeof(int)
+
+          // Note : mdl_simpleframe_t can have *up to and including* 1024 verts. So if this data is
+          // the last in the file the peek_at_vector func will error out. As such we use a different
+          // helper.
+          framePtr[i].frames =
+            vtkInternals::ReadFromVectorSimpleframe(buffer, offsetAlias, header->numVertices);
+          // Apply alias
+          offset = offsetAlias;
 
-        // Scale s and t to range from 0.0 to 1.0
-        coord_s = (coord_s + 0.5) / header->skinWidth;
-        coord_t = (coord_t + 0.5) / header->skinHeight;
-        float coords_st[2] = { coord_s, coord_t };
-        textureCoordinates->InsertNextTuple(coords_st);
-      }
-      // triangle winding order is inverted in Quake MDL
-      vtkIdType triangle[3] = { i * 3, i * 3 + 2, i * 3 + 1 };
-      cells->InsertNextCell(3, triangle);
-    }
-
-    // Extract animation name from frame name and recover animation index accordingly
-    // Check if frame name respect standard naming scheme for single frames
-    // eg: stand1, stand2, stand3, run1, run2, run3
-    // XXX: This code assume frames are provided in order and does not check the numbering
-    // If not, just return the frame name
-    auto extract_animation_name = [&](const std::string& frameName)
-    {
-      std::string::size_type sz;
-      sz = frameName.find_first_of("0123456789");
-      if (sz == std::string::npos)
-      {
-        return frameName;
-      }
-      return frameName.substr(0, sz);
-    };
+          // Always emplace in case of mixed single frame and group frame
+          frameOffsets.emplace_back(std::vector<size_t>());
+        }
+        else
+        {
+          // Alias offset
+          auto offsetAlias = offset + sizeof(int32_t);
+          framePtr[i].nb = vtkInternals::ReadFromVector<int>(buffer, offsetAlias);
+          // Skips parameters min and max.
+          offsetAlias += (2 * sizeof(mdl_vertex_t));
+          framePtr[i].time = vtkInternals::PeekFromVector<float>(buffer, offsetAlias);
+          // Points to the first frame, 4 * nbFrames for the float array
+          // note : see above
+          framePtr[i].frames = vtkInternals::PeekFromVectorSimpleframe(
+            buffer, offsetAlias + (*framePtr[i].nb) * sizeof(float), header->numVertices);
+
+          offsetAlias += (*framePtr[i].nb) * sizeof(float);
+          // Apply alias
+          offset = offsetAlias;
+
+          frameOffsets.emplace_back(std::vector<size_t>());
+
+          // check that we won't run off the buffer during loop
+          if (offset +
+              (*framePtr[i].nb *
+                (mdl_simpleframe_t_fixed_size + sizeof(mdl_vertex_t) * header->numVertices)) >
+            buffer.size())
+          {
+            throw F3DRangeError("Requested data out of range.");
+          }
 
-    // A lambda to add an empty named animation, return the index to it
-    auto emplace_empty_animation = [&](const std::string& animName)
-    {
-      this->AnimationNames.emplace_back(animName);
-      this->AnimationTimes.emplace_back(std::vector<double>());
-      this->AnimationFrames.emplace_back(std::vector<vtkSmartPointer<vtkPolyData>>());
-      return this->AnimationNames.size() - 1;
-    };
+          for (int j = 0; j < *framePtr[i].nb; j++)
+          {
+            // Offset for each frame
+            frameOffsets[i].emplace_back(offset);
+            offset += mdl_simpleframe_t_fixed_size + sizeof(mdl_vertex_t) * header->numVertices;
+          }
+        }
 
-    // Create frames
-    size_t singleFrameAnimIdx = 0;
-    bool hasSingleFrameAnim = false;
-    for (int frameNum = 0; frameNum < header->numFrames; frameNum++)
-    {
-      plugin_frame_pointer pluginFramePtr = framePtr[frameNum];
+        currentProgress++;
+        if (i % 128 == 0)
+        {
+          progressRate = static_cast<double>(currentProgress) / totalProgress;
+          this->Parent->InvokeEvent(vtkCommand::ProgressEvent, static_cast<void*>(&progressRate));
+        }
+      }
 
-      if (*(pluginFramePtr.type) == SINGLE_FRAME)
+      // Draw cells and scale texture coordinates
+      vtkNew<vtkCellArray> cells;
+      cells->AllocateExact(header->numTriangles, 3 * header->numTriangles);
+      vtkNew<vtkFloatArray> textureCoordinates;
+      textureCoordinates->SetNumberOfComponents(2);
+      textureCoordinates->SetName("TextureCoordinates");
+      textureCoordinates->Allocate(header->numTriangles * 3);
+      for (int i = 0; i < header->numTriangles; i++)
       {
-        // Recover pointer to the single frame
-        const mdl_simpleframe_t* frame = pluginFramePtr.frames;
-
-        std::string animationName = extract_animation_name(frame->name);
-        if (!hasSingleFrameAnim || animationName != this->AnimationNames[singleFrameAnimIdx])
+        for (int vertex : triangles[i].vertex)
         {
-          // New animation, emplace it
-          singleFrameAnimIdx = emplace_empty_animation(animationName);
-          hasSingleFrameAnim = true;
+          float coord_s = texcoords[vertex].coord_s;
+          float coord_t = texcoords[vertex].coord_t;
+          if (!triangles[i].facesFront && texcoords[vertex].onseam)
+          {
+            coord_s = coord_s + header->skinWidth * 0.5f; // Backface
+          }
+
+          // Scale s and t to range from 0.0 to 1.0
+          coord_s = (coord_s + 0.5) / header->skinWidth;
+          coord_t = (coord_t + 0.5) / header->skinHeight;
+          float coords_st[2] = { coord_s, coord_t };
+          textureCoordinates->InsertNextTuple(coords_st);
         }
+        // triangle winding order is inverted in Quake MDL
+        vtkIdType triangle[3] = { i * 3, i * 3 + 2, i * 3 + 1 };
+        cells->InsertNextCell(3, triangle);
 
-        // Handle animation times
-        std::vector<double>& times = this->AnimationTimes[singleFrameAnimIdx];
-        if (times.empty())
+        currentProgress++;
+        if (i % 128 == 0)
         {
-          times.emplace_back(0.0);
+          progressRate = static_cast<double>(currentProgress) / totalProgress;
+          this->Parent->InvokeEvent(vtkCommand::ProgressEvent, static_cast<void*>(&progressRate));
         }
-        else
+      }
+
+      // Extract animation name from frame name and recover animation index accordingly
+      // Check if frame name respect standard naming scheme for single frames
+      // eg: stand1, stand2, stand3, run1, run2, run3
+      // XXX: This code assume frames are provided in order and does not check the numbering
+      // If not, just return the frame name
+      auto extract_animation_name = [&](const std::string& frameName)
+      {
+        std::string::size_type sz;
+        sz = frameName.find_first_of("0123456789");
+        if (sz == std::string::npos)
         {
-          // Single frames are 10 fps
-          times.emplace_back(times.back() + 0.1);
+          return frameName;
         }
+        return frameName.substr(0, sz);
+      };
 
-        // Create the animation frame
-        vtkSmartPointer<vtkPolyData> mesh =
-          this->CreateMeshForSimpleFrame(frame, header, triangles, cells, textureCoordinates);
-        this->AnimationFrames[singleFrameAnimIdx].emplace_back(mesh);
-      }
-      else
+      // A lambda to add an empty named animation, return the index to it
+      auto emplace_empty_animation = [&](const std::string& animName)
       {
-        // Group frame are expected to be a single animation
-        std::string animationName;
-        std::vector<double> times;
-        std::vector<vtkSmartPointer<vtkPolyData>> meshes;
-
-        // Iterate over each frame in the group
-        for (int groupFrameNum = 0; groupFrameNum < *pluginFramePtr.nb; groupFrameNum++)
-        {
-          // Recover the frame using the offsets because the struct does not store this pointer
-          const mdl_simpleframe_t* frame = reinterpret_cast<const mdl_simpleframe_t*>(
-            buffer.data() + frameOffsets[frameNum][groupFrameNum]);
+        this->AnimationNames.emplace_back(animName);
+        this->AnimationTimes.emplace_back(std::vector<double>());
+        this->AnimationFrames.emplace_back(std::vector<vtkSmartPointer<vtkPolyData>>());
+        return this->AnimationNames.size() - 1;
+      };
+
+      // Create frames
+      size_t singleFrameAnimIdx = 0;
+      bool hasSingleFrameAnim = false;
+      for (int frameNum = 0; frameNum < header->numFrames; frameNum++)
+      {
+        plugin_frame_pointer pluginFramePtr = framePtr[frameNum];
+
+        if (*(pluginFramePtr.type) == SINGLE_FRAME)
+        {
+          // Recover pointer to the single frame
+          const mdl_simpleframe_t* frame = pluginFramePtr.frames;
 
-          // Assume all frames are named identicaly in the group
-          if (animationName.empty())
+          std::string animationName = extract_animation_name(frame->name);
+          if (!hasSingleFrameAnim || animationName != this->AnimationNames[singleFrameAnimIdx])
           {
-            // Add a group_ prefix to identify group frames
-            animationName = "group_" + extract_animation_name(frame->name);
+            // New animation, emplace it
+            singleFrameAnimIdx = emplace_empty_animation(animationName);
+            hasSingleFrameAnim = true;
           }
 
-          // Recover time for this frame from the dedicated table
-          times.emplace_back(pluginFramePtr.time[groupFrameNum]);
+          // Handle animation times
+          std::vector<double>& times = this->AnimationTimes[singleFrameAnimIdx];
+          if (times.empty())
+          {
+            times.emplace_back(0.0);
+          }
+          // Single frames are 10 fps
+          times.emplace_back(times.back() + 0.1);
 
-          // Recover mesh for this frame
-          meshes.emplace_back(
-            this->CreateMeshForSimpleFrame(frame, header, triangles, cells, textureCoordinates));
-        }
-        this->AnimationNames.emplace_back(animationName);
-        this->AnimationTimes.emplace_back(times);
-        this->AnimationFrames.emplace_back(meshes);
+          // Create the animation frame
+          vtkSmartPointer<vtkPolyData> mesh =
+            this->CreateMeshForSimpleFrame(frame, header, triangles, cells, textureCoordinates);
+          this->AnimationFrames[singleFrameAnimIdx].emplace_back(mesh);
+        }
+        else
+        {
+          // Group frame are expected to be a single animation
+          std::string animationName;
+          std::vector<double> times;
+          std::vector<vtkSmartPointer<vtkPolyData>> meshes;
+
+          // groupFrames always start at 0.0
+          times.emplace_back(0.0);
+          // Iterate over each frame in the group
+          for (int groupFrameNum = 0; groupFrameNum < *pluginFramePtr.nb; groupFrameNum++)
+          {
+
+            // Recover the frame using the offsets because the struct does not store this pointer
+            auto frame = vtkInternals::PeekFromVectorSimpleframe(
+              buffer, frameOffsets[frameNum][groupFrameNum], header->numVertices);
+
+            // Assume all frames are named identicaly in the group
+            if (animationName.empty())
+            {
+              // Add a group_ prefix to identify group frames
+              animationName = "group_" + extract_animation_name(frame->name);
+            }
+
+            // Recover time for this frame from the dedicated table
+            times.emplace_back(pluginFramePtr.time[groupFrameNum]);
+
+            // Recover mesh for this frame
+            meshes.emplace_back(
+              this->CreateMeshForSimpleFrame(frame, header, triangles, cells, textureCoordinates));
+          }
+          this->AnimationNames.emplace_back(animationName);
+          this->AnimationTimes.emplace_back(times);
+          this->AnimationFrames.emplace_back(meshes);
+        }
+
+        currentProgress++;
+        if (frameNum % 128 == 0)
+        {
+          progressRate = static_cast<double>(currentProgress) / totalProgress;
+          this->Parent->InvokeEvent(vtkCommand::ProgressEvent, static_cast<void*>(&progressRate));
+        }
       }
+
+      currentProgress = totalProgress;
+      progressRate = 1.0;
+      this->Parent->InvokeEvent(vtkCommand::ProgressEvent, static_cast<void*>(&progressRate));
+      return true;
+    }
+    catch (const F3DRangeError&)
+    {
+      // Catch fatal errors thrown from overrunning the buffer
+      vtkErrorWithObjectMacro(this->Parent, "CreateMesh Accessed data out of range, aborting.");
+      return false;
     }
-    return true;
   }
 
   //----------------------------------------------------------------------------
-  bool ReadScene(const std::string& filePath)
+  bool ReadScene(vtkResourceStream* stream)
   {
-    std::ifstream inputStream(filePath, std::ios::binary);
-    std::vector<unsigned char> buffer(std::istreambuf_iterator<char>(inputStream), {});
+    // Recover length of stream
+    stream->Seek(0, vtkResourceStream::SeekDirection::End);
+    size_t length = stream->Tell();
+    stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+
+    // Read stream into buffer
+    std::vector<uint8_t> buffer(length);
+    stream->Read(buffer.data(), length);
 
     // Read header
     // XXX: This is completely unsafe, should be rewritten using modern API
-    int offset = 0;
-    const mdl_header_t* header = reinterpret_cast<const mdl_header_t*>(buffer.data());
-    offset += sizeof(mdl_header_t);
+    size_t offset = 0;
+    const mdl_header_t* header;
+    try
+    {
+      header = vtkInternals::ReadFromVector<mdl_header_t>(buffer, offset);
+    }
+    catch (const F3DRangeError&)
+    {
+      vtkErrorWithObjectMacro(this->Parent, "Invalid MDL file");
+      return false;
+    }
+
+    // Check magic number for "IPDO" or "IDST"
+    if (!(header->ident == 0x4F504449 || header->ident == 0x54534449))
+    {
+      vtkErrorWithObjectMacro(this->Parent, "Incompatible MDL header");
+      return false;
+    }
+
+    // Check version for v6 exactly
+    if (header->version != 6)
+    {
+      vtkErrorWithObjectMacro(this->Parent, "Unsupported MDL version. Only version 6 is supported");
+      return false;
+    }
 
     // Create textures
     if (header->numSkins > 0 && header->skinWidth > 0 && header->skinHeight > 0)
@@ -420,7 +619,6 @@
 
   //----------------------------------------------------------------------------
   vtkF3DQuakeMDLImporter* Parent;
-  std::string Description;
   vtkSmartPointer<vtkPolyDataMapper> Mapper;
   vtkSmartPointer<vtkTexture> Texture;
 
@@ -428,6 +626,10 @@
   std::vector<std::vector<double>> AnimationTimes;
   std::vector<std::vector<vtkSmartPointer<vtkPolyData>>> AnimationFrames;
 
+  std::vector<std::string> GroupSkinAnimationNames;
+  std::vector<std::vector<vtkSmartPointer<vtkImageData>>> GroupSkins;
+  std::vector<std::vector<double>> GroupSkinDurations;
+
   vtkIdType ActiveAnimation = -1;
 };
 
@@ -440,7 +642,21 @@
 //----------------------------------------------------------------------------
 int vtkF3DQuakeMDLImporter::ImportBegin()
 {
-  return this->Internals->ReadScene(this->FileName);
+  // Stream is higher priority than filename.
+  vtkResourceStream* stream = this->GetStream();
+  vtkNew<vtkFileResourceStream> fileStream;
+  if (!stream)
+  {
+    if (!fileStream->Open(this->GetFileName()))
+    {
+      vtkErrorMacro("Unable to open " << this->GetFileName() << " , aborting.");
+      return 0;
+    }
+
+    stream = fileStream;
+  }
+
+  return this->Internals->ReadScene(stream);
 }
 
 //----------------------------------------------------------------------------
@@ -466,20 +682,38 @@
 {
   if (this->Internals->ActiveAnimation != -1)
   {
-    const std::vector<double>& times =
-      this->Internals->AnimationTimes[this->Internals->ActiveAnimation];
-
     // Find frameIndex for the provided timeValue so that t0 <= timeValue < t1
+    // Animation range is from [Start time of first frame, Finish time of last frame]
 
-    // First time >= value
-    const auto found = std::lower_bound(times.begin(), times.end(), timeValue);
-    // If none, select last, if found, select distance
-    const size_t i = found == times.end() ? times.size() - 1 : std::distance(times.begin(), found);
-    // If found time > timeValue, the the previous one
-    const size_t frameIndex = *found > timeValue && i > 0 ? i - 1 : i;
+    // First time >= value, excluding the last element as it only represents finish time of the last
+    // frame
 
-    this->Internals->Mapper->SetInputData(
-      this->Internals->AnimationFrames[this->Internals->ActiveAnimation][frameIndex]);
+    // Animation in AnimationFrames are mesh animations and at greater indices texture animations
+    // are rendered
+    bool isMeshAnimation = this->Internals->ActiveAnimation <
+      static_cast<vtkIdType>(this->Internals->AnimationNames.size());
+    size_t animIndex = isMeshAnimation
+      ? this->Internals->ActiveAnimation
+      : this->Internals->ActiveAnimation - this->Internals->AnimationNames.size();
+    const std::vector<double>& times = isMeshAnimation
+      ? this->Internals->AnimationTimes[animIndex]
+      : this->Internals->GroupSkinDurations[animIndex];
+    const auto found = std::lower_bound(times.begin(), times.end() - 1, timeValue);
+    // If found at finish time of last frame, select last frame's start time (second last value),
+    // else select distance
+    const size_t i =
+      (found == times.end() - 1) ? times.size() - 2 : std::distance(times.begin(), found);
+    // If time at index i > timeValue, then choose the previous frame
+    const size_t frameIndex = times[i] > timeValue && i > 0 ? i - 1 : i;
+    if (isMeshAnimation)
+    {
+      this->Internals->Mapper->SetInputData(
+        this->Internals->AnimationFrames[animIndex][frameIndex]);
+    }
+    else
+    {
+      this->Internals->Texture->SetInputData(this->Internals->GroupSkins[animIndex][frameIndex]);
+    }
   }
   return true;
 }
@@ -487,21 +721,31 @@
 //----------------------------------------------------------------------------
 vtkIdType vtkF3DQuakeMDLImporter::GetNumberOfAnimations()
 {
-  return this->Internals->AnimationNames.size();
+  return static_cast<vtkIdType>(
+    this->Internals->AnimationNames.size() + this->Internals->GroupSkinAnimationNames.size());
 }
 
 //----------------------------------------------------------------------------
 std::string vtkF3DQuakeMDLImporter::GetAnimationName(vtkIdType animationIndex)
 {
-  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size()));
+  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size() +
+                            this->Internals->GroupSkinAnimationNames.size()));
   assert(animationIndex >= 0);
-  return this->Internals->AnimationNames[animationIndex];
+
+  if (animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size()))
+  {
+    return this->Internals->AnimationNames[animationIndex];
+  }
+  // Skin group animations are rendered for indices larger than frame animations
+  return this->Internals
+    ->GroupSkinAnimationNames[animationIndex - this->Internals->AnimationNames.size()];
 }
 
 //----------------------------------------------------------------------------
 void vtkF3DQuakeMDLImporter::EnableAnimation(vtkIdType animationIndex)
 {
-  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size()));
+  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size() +
+                            this->Internals->GroupSkinAnimationNames.size()));
   assert(animationIndex >= 0);
   this->Internals->ActiveAnimation = animationIndex;
 }
@@ -515,22 +759,35 @@
 //----------------------------------------------------------------------------
 bool vtkF3DQuakeMDLImporter::IsAnimationEnabled(vtkIdType animationIndex)
 {
-  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size()));
+  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size() +
+                            this->Internals->GroupSkinAnimationNames.size()));
   assert(animationIndex >= 0);
   return this->Internals->ActiveAnimation == animationIndex;
 }
 
 //----------------------------------------------------------------------------
-bool vtkF3DQuakeMDLImporter::GetTemporalInformation(vtkIdType animationIndex,
-  double vtkNotUsed(frameRate), int& vtkNotUsed(nbTimeSteps), double timeRange[2],
-  vtkDoubleArray* vtkNotUsed(timeSteps))
+bool vtkF3DQuakeMDLImporter::GetTemporalInformation(
+  vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps, vtkDoubleArray* timeSteps)
 {
-  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size()));
+  assert(animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size() +
+                            this->Internals->GroupSkinAnimationNames.size()));
   assert(animationIndex >= 0);
 
-  const std::vector<double>& times = this->Internals->AnimationTimes[animationIndex];
-  // F3D does not care about timesteps, only set time range
+  const std::vector<double>& times =
+    animationIndex < static_cast<vtkIdType>(this->Internals->AnimationNames.size())
+    ? this->Internals->AnimationTimes[animationIndex]
+    : this->Internals->GroupSkinDurations[animationIndex - this->Internals->AnimationNames.size()];
+
   timeRange[0] = times.front();
-  timeRange[1] = times.back();
+  // If single frame, keep animation duration = 0
+  timeRange[1] = times.size() == 2 ? times.front() : times.back();
+
+  nbTimeSteps = static_cast<int>(times.size());
+  timeSteps->SetNumberOfTuples(times.size());
+
+  for (unsigned int i = 0; i < times.size(); ++i)
+  {
+    timeSteps->SetValue(i, times[i]);
+  }
   return true;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/vtkF3DQuakeMDLImporter.h f3d-3.4.1/plugins/native/module/vtkF3DQuakeMDLImporter.h
--- f3d-3.2.0+dfsg/plugins/native/module/vtkF3DQuakeMDLImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/vtkF3DQuakeMDLImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -5,6 +5,9 @@
  * This reader is based on implementations of Quake 1's MDL, defined in
  * https://book.leveldesignbook.com/appendix/resources/formats/mdl It reads vertices, normals and
  * texture coordinate data from .mdl files. Supports animations.
+ *
+ * This reader expects a header "ident" value of either 0x4F504449 ("IDPO") or 0x54534449 ("IDST")
+ * This reader expects a header "version" value of exactly 6
  */
 
 #ifndef vtkF3DQuakeMDLImporter_h
@@ -21,11 +24,6 @@
   vtkTypeMacro(vtkF3DQuakeMDLImporter, vtkF3DImporter);
 
   /**
-   * Set the file name.
-   */
-  vtkSetMacro(FileName, std::string);
-
-  /**
    * Update actors at the given time value.
    */
   bool UpdateAtTimeValue(double timeValue) override;
@@ -61,8 +59,8 @@
   /**
    * Get temporal information for the currently enabled animations.
    */
-  bool GetTemporalInformation(vtkIdType animationIndex, double frameRate, int& nbTimeSteps,
-    double timeRange[2], vtkDoubleArray* timeSteps) override;
+  bool GetTemporalInformation(vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps,
+    vtkDoubleArray* timeSteps) override;
 
   ///@{
   /**
@@ -84,7 +82,6 @@
   void operator=(const vtkF3DQuakeMDLImporter&) = delete;
 
   struct vtkInternals;
-  std::string FileName;
   unsigned int SkinIndex = 0;
 
   std::unique_ptr<vtkInternals> Internals;
diff -Nru f3d-3.2.0+dfsg/plugins/native/module/vtkF3DSPZReader.cxx f3d-3.4.1/plugins/native/module/vtkF3DSPZReader.cxx
--- f3d-3.2.0+dfsg/plugins/native/module/vtkF3DSPZReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/native/module/vtkF3DSPZReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -16,7 +16,7 @@
 namespace
 {
 //----------------------------------------------------------------------------
-void UncompressGzip(
+bool UncompressGzip(
   const std::vector<unsigned char>& compressed, std::vector<unsigned char>& uncompressed)
 {
   std::vector<uint8_t> buffer(8192);
@@ -35,12 +35,16 @@
     stream.avail_out = static_cast<unsigned int>(buffer.size());
 
     res = inflate(&stream, Z_NO_FLUSH);
-    assert(res == Z_OK || res == Z_STREAM_END);
+    if (!(res == Z_OK || res == Z_STREAM_END))
+    {
+      return false;
+    }
 
     uncompressed.insert(
       uncompressed.end(), buffer.data(), buffer.data() + buffer.size() - stream.avail_out);
   }
   inflateEnd(&stream);
+  return true;
 }
 
 //----------------------------------------------------------------------------
@@ -99,7 +103,7 @@
 };
 
 //----------------------------------------------------------------------------
-struct PackedRotation
+struct PackedRotationV2
 {
   uint8_t packed[3]; // 24-bits
 
@@ -115,6 +119,42 @@
 };
 
 //----------------------------------------------------------------------------
+struct PackedRotationV3
+{
+  uint8_t packed[4]; // 32-bits
+
+  std::array<float, 4> decode() const
+  {
+    float rotation[4];
+    constexpr float sqrt12 = 0.707106781186547524401f; // 1/sqrt(2)
+    constexpr uint32_t compMask = (1u << 9u) - 1u;
+
+    uint32_t comp = packed[0] + (packed[1] << 8) + (packed[2] << 16) + (packed[3] << 24);
+
+    const int largestCompIndex = comp >> 30;
+    float sumSquares = 0;
+    for (int i = 3; i >= 0; --i)
+    {
+      if (i != largestCompIndex)
+      {
+        float mag = comp & compMask;
+        uint32_t isNegative = (comp >> 9u) & 0x1u;
+        comp = comp >> 10u;
+        rotation[i] = sqrt12 * (mag / float(compMask));
+        if (isNegative == 1)
+        {
+          rotation[i] = -rotation[i];
+        }
+        sumSquares += rotation[i] * rotation[i];
+      }
+    }
+    rotation[largestCompIndex] = std::sqrt(1.0f - sumSquares);
+
+    return { rotation[0], rotation[1], rotation[2], rotation[3] };
+  }
+};
+
+//----------------------------------------------------------------------------
 template<int Degree>
 struct SphericalHarmonics
 {
@@ -275,12 +315,25 @@
   std::vector<unsigned char> uncompressed;
   uncompressed.reserve(uncompressedLength);
 
-  ::UncompressGzip(compressed, uncompressed);
+  if (!::UncompressGzip(compressed, uncompressed))
+  {
+    vtkErrorMacro("Invalid GZIP file");
+    return 0;
+  }
 
-  Header* header = reinterpret_cast<Header*>(uncompressed.data());
+  const Header* header = reinterpret_cast<Header*>(uncompressed.data());
 
-  assert(header->magic == 0x5053474e);
-  assert(header->version == 2);
+  if (header->magic != 0x5053474e)
+  {
+    vtkErrorMacro("Incompatible SPZ header");
+    return 0;
+  }
+
+  if (header->version < 2 || header->version > 3)
+  {
+    vtkErrorMacro("Incompatible SPZ version. Only 2 and 3 are supported");
+    return 0;
+  }
 
   uint32_t nbSplats = header->numPoints;
 
@@ -346,18 +399,31 @@
 
   output->GetPointData()->AddArray(scaleArray);
 
-  // rotation is stored just after the 16-bytes header, positions, colors, alphas and scales
-  PackedRotation* rotation =
-    reinterpret_cast<PackedRotation*>(uncompressed.data() + 16 + (9 + 4 + 3) * nbSplats);
-
   vtkNew<vtkFloatArray> rotationArray;
   rotationArray->SetNumberOfComponents(4);
   rotationArray->SetNumberOfTuples(nbSplats);
   rotationArray->SetName("rotation");
 
-  for (vtkIdType splatIndex = 0; splatIndex < static_cast<vtkIdType>(nbSplats); splatIndex++)
+  // rotation is stored just after the 16-bytes header, positions, colors, alphas and scales
+  int rotationShift = 16 + (9 + 4 + 3) * nbSplats;
+
+  if (header->version == 2)
   {
-    rotationArray->SetTypedTuple(splatIndex, (rotation++)->decode().data());
+    PackedRotationV2* rotation =
+      reinterpret_cast<PackedRotationV2*>(uncompressed.data() + rotationShift);
+    for (vtkIdType splatIndex = 0; splatIndex < static_cast<vtkIdType>(nbSplats); splatIndex++)
+    {
+      rotationArray->SetTypedTuple(splatIndex, (rotation++)->decode().data());
+    }
+  }
+  else if (header->version == 3)
+  {
+    PackedRotationV3* rotation =
+      reinterpret_cast<PackedRotationV3*>(uncompressed.data() + rotationShift);
+    for (vtkIdType splatIndex = 0; splatIndex < static_cast<vtkIdType>(nbSplats); splatIndex++)
+    {
+      rotationArray->SetTypedTuple(splatIndex, (rotation++)->decode().data());
+    }
   }
 
   output->GetPointData()->AddArray(rotationArray);
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/CMakeLists.txt f3d-3.4.1/plugins/occt/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/occt/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,13 +2,8 @@
   vtkF3DOCCTReader
   )
 
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextOCCT
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes})
 
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/Testing/CMakeLists.txt f3d-3.4.1/plugins/occt/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/plugins/occt/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,8 +2,20 @@
      TestF3DOCCTReader.cxx
     )
 
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.5.20251223)
+  list(APPEND vtkextOCCT_list
+     TestF3DOCCTReaderStream.cxx
+     TestF3DOCCTReaderStreamError.cxx
+      )
+endif()
+
 vtk_add_test_cxx(vtkextOCCTTests tests
   NO_DATA NO_VALID NO_OUTPUT
   ${vtkextOCCT_list}
   ${F3D_SOURCE_DIR}/testing/ ${CMAKE_BINARY_DIR}/Testing/Temporary/)
 vtk_test_cxx_executable(vtkextOCCTTests tests)
+
+if(F3D_PLUGIN_OCCT_COLORING_SUPPORT)
+  target_compile_definitions(vtkextOCCTTests
+    PRIVATE F3D_PLUGIN_OCCT_XCAF)
+endif()
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReader.cxx f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReader.cxx
--- f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -16,17 +16,20 @@
   reader->SetFileName(filename);
   reader->SetFileFormat(format);
   reader->Update();
-  reader->Print(cout);
-  return reader->GetOutput()->GetNumberOfBlocks() > 0;
+  reader->Print(std::cout);
+  return reader->GetOutput()->GetNumberOfPoints() > 0;
 }
 
 int TestF3DOCCTReader(int vtkNotUsed(argc), char* argv[])
 {
   const std::string data = std::string(argv[1]) + "data";
-  return (testReader(data + "/f3d.stp", vtkF3DOCCTReader::FILE_FORMAT::STEP) &&
-           testReader(data + "/f3d.igs", vtkF3DOCCTReader::FILE_FORMAT::IGES) &&
-           testReader(data + "/f3d.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP) &&
-           testReader(data + "/f3d.xbf", vtkF3DOCCTReader::FILE_FORMAT::XBF))
-    ? EXIT_SUCCESS
-    : EXIT_FAILURE;
+  bool ret = true;
+  ret &= testReader(data + "/f3d.stp", vtkF3DOCCTReader::FILE_FORMAT::STEP);
+  ret &= testReader(data + "/f3d.igs", vtkF3DOCCTReader::FILE_FORMAT::IGES);
+  ret &= testReader(data + "/f3d.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP);
+  ret &= testReader(data + "/f3d.bin.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP);
+#if F3D_PLUGIN_OCCT_XCAF
+  ret &= testReader(data + "/f3d.xbf", vtkF3DOCCTReader::FILE_FORMAT::XBF);
+#endif
+  return ret ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReaderStream.cxx f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReaderStream.cxx
--- f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReaderStream.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReaderStream.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,40 @@
+#include <vtkFileResourceStream.h>
+#include <vtkMultiBlockDataSet.h>
+#include <vtkNew.h>
+#include <vtkTestUtilities.h>
+
+#include "vtkF3DOCCTReader.h"
+
+#include <iostream>
+
+bool testReaderStream(const std::string& filename, const vtkF3DOCCTReader::FILE_FORMAT& format)
+{
+  vtkNew<vtkFileResourceStream> fileStream;
+  fileStream->Open(filename.c_str());
+
+  vtkNew<vtkF3DOCCTReader> reader;
+  reader->RelativeDeflectionOn();
+  reader->SetLinearDeflection(0.1);
+  reader->SetAngularDeflection(0.5);
+  reader->ReadWireOn();
+  reader->SetStream(fileStream);
+  reader->SetFileFormat(format);
+  reader->Update();
+  return reader->GetOutput()->GetNumberOfPoints() > 0;
+}
+
+int TestF3DOCCTReaderStream(int vtkNotUsed(argc), char* argv[])
+{
+  const std::string data = std::string(argv[1]) + "data";
+  bool ret = true;
+  ret &= testReaderStream(data + "/f3d.stp", vtkF3DOCCTReader::FILE_FORMAT::STEP);
+  // OCCT doesn't support reading IGES stream yet
+  // https://dev.opencascade.org/content/reading-iges-stream-seems-broken-770
+  //  ret &= testReaderStream(data + "/f3d.igs", vtkF3DOCCTReader::FILE_FORMAT::IGES);
+  ret &= testReaderStream(data + "/f3d.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP);
+  ret &= testReaderStream(data + "/f3d.bin.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP);
+#if F3D_PLUGIN_OCCT_XCAF
+  ret &= testReaderStream(data + "/f3d.xbf", vtkF3DOCCTReader::FILE_FORMAT::XBF);
+#endif
+  return ret ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReaderStreamError.cxx f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReaderStreamError.cxx
--- f3d-3.2.0+dfsg/plugins/occt/module/Testing/TestF3DOCCTReaderStreamError.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/plugins/occt/module/Testing/TestF3DOCCTReaderStreamError.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,89 @@
+#include <vtkCallbackCommand.h>
+#include <vtkExecutive.h>
+#include <vtkFileResourceStream.h>
+#include <vtkMultiBlockDataSet.h>
+#include <vtkNew.h>
+#include <vtkTestUtilities.h>
+
+#include "vtkF3DOCCTReader.h"
+
+#include <iostream>
+
+class ErrorEventCallback : public vtkCommand
+{
+public:
+  static ErrorEventCallback* New()
+  {
+    return new ErrorEventCallback;
+  }
+
+  void Execute(vtkObject* caller, unsigned long vtkNotUsed(evId), void* data) override
+  {
+    const vtkF3DOCCTReader* reader = reinterpret_cast<vtkF3DOCCTReader*>(caller);
+    char* message = static_cast<char*>(data);
+    if (reader && message)
+    {
+      this->Messages.emplace_back(message);
+    }
+  }
+
+  const std::vector<std::string>& GetRecordedErrorMessages() const
+  {
+    return this->Messages;
+  }
+
+private:
+  std::vector<std::string> Messages;
+};
+
+bool testReaderStreamError(const std::string& filename, const vtkF3DOCCTReader::FILE_FORMAT& format)
+{
+  vtkNew<ErrorEventCallback> errorEventCallback;
+  vtkNew<vtkCallbackCommand> nullEventCallback;
+
+  vtkNew<vtkFileResourceStream> fileStream;
+  fileStream->Open(filename.c_str());
+
+  vtkNew<vtkF3DOCCTReader> reader;
+  reader->AddObserver(vtkCommand::ErrorEvent, errorEventCallback);
+  reader->GetExecutive()->AddObserver(vtkCommand::ErrorEvent, nullEventCallback);
+
+  reader->RelativeDeflectionOn();
+  reader->SetLinearDeflection(0.1);
+  reader->SetAngularDeflection(0.5);
+  reader->ReadWireOn();
+  reader->SetStream(fileStream);
+  reader->SetFileFormat(format);
+  reader->Update();
+
+  auto errorMessages = errorEventCallback->GetRecordedErrorMessages();
+  if (errorMessages.empty())
+  {
+    std::cerr << "No error triggered.\n";
+    return false;
+  }
+
+  auto firstMessage = errorMessages.front();
+  if (firstMessage.find("Failed to read ") == std::string::npos)
+  {
+    std::cerr << "No stream error reported ";
+    return false;
+  }
+
+  return reader->GetOutput()->GetNumberOfPoints() == 0 ? true : false;
+}
+
+int TestF3DOCCTReaderStreamError(int vtkNotUsed(argc), char* argv[])
+{
+  const std::string data = std::string(argv[1]) + "data";
+  bool ret = true;
+  ret &= testReaderStreamError(data + "/invalid.stp", vtkF3DOCCTReader::FILE_FORMAT::STEP);
+  // OCCT doesn't support reading IGES stream yet, when it does, this test output will change
+  // https://dev.opencascade.org/content/reading-iges-stream-seems-broken-770
+  ret &= testReaderStreamError(data + "/f3d.igs", vtkF3DOCCTReader::FILE_FORMAT::IGES);
+  ret &= testReaderStreamError(data + "/invalid.brep", vtkF3DOCCTReader::FILE_FORMAT::BREP);
+#if F3D_PLUGIN_OCCT_XCAF
+  ret &= testReaderStreamError(data + "/invalid.xbf", vtkF3DOCCTReader::FILE_FORMAT::XBF);
+#endif
+  return ret ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/vtkF3DOCCTReader.cxx f3d-3.4.1/plugins/occt/module/vtkF3DOCCTReader.cxx
--- f3d-3.2.0+dfsg/plugins/occt/module/vtkF3DOCCTReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/vtkF3DOCCTReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -63,6 +63,7 @@
 #include <vtkPointData.h>
 #include <vtkPoints.h>
 #include <vtkPolyData.h>
+#include <vtkResourceStream.h>
 #include <vtkTransform.h>
 #include <vtkTransformFilter.h>
 #include <vtkUnsignedCharArray.h>
@@ -74,6 +75,8 @@
 #include <unordered_map>
 #include <vector>
 
+vtkCxxSetSmartPointerMacro(vtkF3DOCCTReader, Stream, vtkResourceStream);
+
 class vtkF3DOCCTReader::vtkInternals
 {
 #if F3D_PLUGIN_OCCT_XCAF
@@ -551,6 +554,12 @@
 vtkF3DOCCTReader::~vtkF3DOCCTReader() = default;
 
 //----------------------------------------------------------------------------
+vtkResourceStream* vtkF3DOCCTReader::GetStream()
+{
+  return this->Stream;
+}
+
+//----------------------------------------------------------------------------
 class ProgressIndicator : public Message_ProgressIndicator
 {
 public:
@@ -585,14 +594,36 @@
   reader.SetNameMode(true);
   reader.SetLayerMode(true);
 
-  if (reader.ReadFile(that->GetFileName().c_str()) == IFSelect_RetDone)
+  IFSelect_ReturnStatus ret;
+  vtkResourceStream* stream = that->GetStream();
+  if (stream)
+  {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251223)
+    // Encapsulate resource stream into an istream
+    // Reading happen on ReadStream call, no need to keep buffers around
+    stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+    auto strbuf = stream->ToStreambuf();
+    std::istream buffer(strbuf.get());
+    ret = reader.ReadStream("", buffer);
+#else
+    vtkErrorWithObjectMacro(
+      that, "This version of VTK doesn't support reading memory stream with OCCT");
+    return false;
+#endif
+  }
+  else
+  {
+    ret = reader.ReadFile(that->GetFileName().c_str());
+  }
+
+  if (ret == IFSelect_RetDone)
   {
     ProgressIndicator pi(that);
     return reader.Transfer(doc, pi.Start());
   }
   else
   {
-    vtkErrorWithObjectMacro(that, "Failed opening file " << that->GetFileName());
+    vtkErrorWithObjectMacro(that, "Failed to read file or stream");
     return false;
   }
 }
@@ -612,15 +643,45 @@
     ProgressIndicator pIndicator(this);
     const Message_ProgressRange pRange = pIndicator.Start();
 
-    bool success = false;
-    try
-    {
-      success = BinTools::Read(shape, this->GetFileName().c_str(), pRange);
+    bool success = true;
+    vtkResourceStream* stream = this->GetStream();
+    if (stream)
+    {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251223)
+      // Encapsulate resource stream into an istream
+      stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+      this->Streambuf = stream->ToStreambuf();
+      this->Buffer = std::make_unique<std::istream>(this->Streambuf.get());
+
+      try
+      {
+        BinTools::Read(shape, *this->Buffer, pRange);
+      }
+      catch (Storage_StreamTypeMismatchError&)
+      {
+        stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+        this->Streambuf = stream->ToStreambuf();
+        this->Buffer = std::make_unique<std::istream>(this->Streambuf.get());
+        const BRep_Builder builder;
+        BRepTools::Read(shape, *this->Buffer, builder, pRange);
+      }
+      success = !shape.IsNull();
+#else
+      vtkErrorMacro("This version of VTK doesn't support reading memory stream with OCCT");
+      return 0;
+#endif
     }
-    catch (Storage_StreamTypeMismatchError&)
+    else
     {
-      const BRep_Builder builder;
-      success = BRepTools::Read(shape, this->GetFileName().c_str(), builder, pRange);
+      try
+      {
+        success = BinTools::Read(shape, this->GetFileName().c_str(), pRange);
+      }
+      catch (Storage_StreamTypeMismatchError&)
+      {
+        const BRep_Builder builder;
+        success = BRepTools::Read(shape, this->GetFileName().c_str(), builder, pRange);
+      }
     }
 
     if (success)
@@ -640,7 +701,7 @@
     }
     else
     {
-      vtkErrorWithObjectMacro(this, "Failed opening file " << this->GetFileName());
+      vtkErrorMacro("Failed to read BREP file or stream");
       return 0;
     }
   }
@@ -648,27 +709,55 @@
 #if F3D_PLUGIN_OCCT_XCAF
   Handle(TDocStd_Document) doc;
   XCAFApp_Application::GetApplication()->NewDocument("MDTV-XCAF", doc);
+  bool success = true;
   if (this->FileFormat == FILE_FORMAT::STEP)
   {
     STEPCAFControl_Reader reader;
-    TransferToDocument(this, reader, doc);
+    success = TransferToDocument(this, reader, doc);
   }
   else if (this->FileFormat == FILE_FORMAT::IGES)
   {
     IGESCAFControl_Reader reader;
-    TransferToDocument(this, reader, doc);
+    success = TransferToDocument(this, reader, doc);
   }
   else if (this->FileFormat == FILE_FORMAT::XBF)
   {
     Handle(TDocStd_Application) app = new TDocStd_Application();
     BinXCAFDrivers::DefineFormat(app);
-    if (app->Open(this->GetFileName().c_str(), doc) != PCDM_RS_OK)
+
+    vtkResourceStream* stream = this->GetStream();
+    if (stream)
     {
-      vtkErrorWithObjectMacro(this, "Failed to read XBF file");
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251223)
+      // Encapsulate resource stream into an istream
+      stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+      this->Streambuf = stream->ToStreambuf();
+      this->Buffer = std::make_unique<std::istream>(this->Streambuf.get());
+      if (app->Open(*this->Buffer, doc) != PCDM_RS_OK)
+      {
+        vtkErrorMacro("Failed to read XBF stream");
+        success = false;
+      }
+#else
+      vtkErrorMacro("This version of VTK doesn't support reading memory stream with OCCT");
       return 0;
+#endif
+    }
+    else
+    {
+      if (app->Open(this->GetFileName().c_str(), doc) != PCDM_RS_OK)
+      {
+        vtkErrorWithObjectMacro(this, "Failed to read XBF file");
+        success = false;
+      }
     }
   }
 
+  if (!success)
+  {
+    return 0;
+  }
+
   this->Internals->ShapeTool = XCAFDoc_DocumentTool::ShapeTool(doc->Main());
 
   TDF_LabelSequence topLevelShapes;
@@ -709,8 +798,29 @@
   {
     reader = new IGESControl_Reader();
   }
+  assert(reader);
+
+  vtkResourceStream* stream = this->GetStream();
+  IFSelect_ReturnStatus ret;
+  if (stream)
+  {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251223)
+    // Encapsulate resource stream into an istream
+    stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+    this->Streambuf = stream->ToStreambuf();
+    this->Buffer = std::make_unique<std::istream>(this->Streambuf.get());
+    ret = reader->ReadStream("", *this->Buffer);
+#else
+    vtkErrorMacro("This version of VTK doesn't support reading memory stream with OCCT");
+    return false;
+#endif
+  }
+  else
+  {
+    ret = reader->ReadFile(this->GetFileName().c_str());
+  }
 
-  if (reader && reader->ReadFile(this->GetFileName().c_str()) == IFSelect_RetDone)
+  if (ret == IFSelect_RetDone)
   {
     ProgressIndicator pi(this);
     reader->TransferRoots(pi.Start());
@@ -730,6 +840,11 @@
       }
     }
   }
+  else
+  {
+    vtkErrorMacro("Failed to read STEP or IGES file");
+    return 0;
+  }
 
   delete reader;
 #endif
@@ -755,3 +870,14 @@
   }
   // clang-format
 }
+
+//----------------------------------------------------------------------------
+vtkMTimeType vtkF3DOCCTReader::GetMTime()
+{
+  auto mtime = this->Superclass::GetMTime();
+  if (this->Stream)
+  {
+    mtime = std::max(mtime, this->Stream->GetMTime());
+  }
+  return mtime;
+}
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/vtkF3DOCCTReader.h f3d-3.4.1/plugins/occt/module/vtkF3DOCCTReader.h
--- f3d-3.2.0+dfsg/plugins/occt/module/vtkF3DOCCTReader.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/vtkF3DOCCTReader.h	2026-01-08 08:41:55.000000000 +0200
@@ -9,18 +9,22 @@
  * and LinearDeflection.
  * Reading 1D cells (wires) is optional.
  *
+ * This reader support reading streams for all supported formats but IGES.
+ * https://dev.opencascade.org/content/reading-iges-stream-seems-broken-770
+ *
  */
 
 #ifndef vtkF3DOCCTReader_h
 #define vtkF3DOCCTReader_h
 
 #include <vtkMultiBlockDataSetAlgorithm.h>
+#include <vtkSmartPointer.h>
 #include <vtkVersion.h>
 
 #include <memory>
 
 class vtkInformationDoubleVectorKey;
-
+class vtkResourceStream;
 class vtkF3DOCCTReader : public vtkMultiBlockDataSetAlgorithm
 {
 public:
@@ -88,12 +92,26 @@
 
   ///@{
   /**
+   * Specify stream to read from
+   * When both `Stream` and `Filename` are set, stream is used.
+   */
+  void SetStream(vtkResourceStream* stream);
+  vtkResourceStream* GetStream();
+  ///@}
+
+  ///@{
+  /**
    * Get/Set the file name.
    */
   vtkSetMacro(FileName, std::string);
   vtkGetMacro(FileName, std::string);
   ///@}
 
+  /**
+   * Overridden to take into account mtime from the internal vtkResourceStream.
+   */
+  vtkMTimeType GetMTime() override;
+
 protected:
   vtkF3DOCCTReader();
   ~vtkF3DOCCTReader() override;
@@ -108,12 +126,16 @@
   std::unique_ptr<vtkInternals> Internals;
 
   std::string FileName;
+  vtkSmartPointer<vtkResourceStream> Stream;
 
   double LinearDeflection = 0.1;
   double AngularDeflection = 0.5;
   bool RelativeDeflection = false;
   bool ReadWire = false;
   FILE_FORMAT FileFormat = FILE_FORMAT::STEP;
+
+  std::unique_ptr<std::streambuf> Streambuf;
+  std::unique_ptr<std::istream> Buffer;
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/plugins/occt/module/vtk.module f3d-3.4.1/plugins/occt/module/vtk.module
--- f3d-3.2.0+dfsg/plugins/occt/module/vtk.module	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/occt/module/vtk.module	2026-01-08 08:41:55.000000000 +0200
@@ -3,6 +3,7 @@
 DESCRIPTION
   A VTK module for the OCCT plugin
 DEPENDS
+  VTK::IOCore
   VTK::CommonCore
   VTK::CommonExecutionModel
   VTK::FiltersGeneral
diff -Nru f3d-3.2.0+dfsg/plugins/usd/module/Testing/TestF3DUSDImporter.cxx f3d-3.4.1/plugins/usd/module/Testing/TestF3DUSDImporter.cxx
--- f3d-3.2.0+dfsg/plugins/usd/module/Testing/TestF3DUSDImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/usd/module/Testing/TestF3DUSDImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -11,7 +11,7 @@
 {
   std::string filename = std::string(argv[1]) + "data/suzanne.usd";
   vtkNew<vtkF3DUSDImporter> importer;
-  importer->SetFileName(filename);
+  importer->SetFileName(filename.c_str());
   importer->DisableAnimation(0);
 
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250507)
@@ -22,6 +22,6 @@
 #endif
 
   importer->Update();
-  importer->Print(cout);
+  importer->Print(std::cout);
   return importer->GetRenderer() ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff -Nru f3d-3.2.0+dfsg/plugins/usd/module/vtkF3DUSDImporter.cxx f3d-3.4.1/plugins/usd/module/vtkF3DUSDImporter.cxx
--- f3d-3.2.0+dfsg/plugins/usd/module/vtkF3DUSDImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/usd/module/vtkF3DUSDImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -34,6 +34,10 @@
 #include <vtkTriangleFilter.h>
 #include <vtkVersion.h>
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+#include <vtkMemoryResourceStream.h>
+#endif
+
 #if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 3, 0)
 #include <vtkCapsuleSource.h>
 #endif
@@ -757,7 +761,7 @@
 
     if (occlusionImage)
     {
-      int* size = occlusionImage->GetDimensions();
+      const int* size = occlusionImage->GetDimensions();
 
       maxWidth = std::max(maxWidth, size[0]);
       maxHeight = std::max(maxHeight, size[1]);
@@ -765,7 +769,7 @@
 
     if (roughnessImage)
     {
-      int* size = roughnessImage->GetDimensions();
+      const int* size = roughnessImage->GetDimensions();
 
       maxWidth = std::max(maxWidth, size[0]);
       maxHeight = std::max(maxHeight, size[1]);
@@ -773,7 +777,7 @@
 
     if (metallicImage)
     {
-      int* size = metallicImage->GetDimensions();
+      const int* size = metallicImage->GetDimensions();
 
       maxWidth = std::max(maxWidth, size[0]);
       maxHeight = std::max(maxHeight, size[1]);
@@ -886,6 +890,7 @@
         if (!reader)
         {
           // cannot read the image file
+          vtkErrorWithObjectMacro(nullptr, "Cannot create reader for image: " << assetPath);
           return nullptr;
         }
 
@@ -895,6 +900,7 @@
         if (!asset)
         {
           // cannot get USD asset
+          vtkErrorWithObjectMacro(nullptr, "Cannot recover USD asset");
           return nullptr;
         }
 
@@ -903,11 +909,18 @@
         if (!buffer)
         {
           // buffer invalid
+          vtkErrorWithObjectMacro(nullptr, "Cannot recover buffer");
           return nullptr;
         }
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+        vtkNew<vtkMemoryResourceStream> stream;
+        stream->SetBuffer(buffer.get(), asset->GetSize());
+        reader->SetStream(stream);
+#else
         reader->SetMemoryBuffer(buffer.get());
         reader->SetMemoryBufferLength(asset->GetSize());
+#endif
         reader->Update();
 
         tex = reader->GetOutput();
@@ -1237,7 +1250,7 @@
 //----------------------------------------------------------------------------
 int vtkF3DUSDImporter::ImportBegin()
 {
-  this->Internals->ReadScene(this->FileName);
+  this->Internals->ReadScene(this->GetFileName());
 
   return 1;
 }
@@ -1259,18 +1272,9 @@
 
 //----------------------------------------------------------------------------
 bool vtkF3DUSDImporter::GetTemporalInformation(vtkIdType vtkNotUsed(animationIndex),
-  double frameRate, int& nbTimeSteps, double timeRange[2], vtkDoubleArray* timeSteps)
+  double timeRange[2], int& vtkNotUsed(nbTimeSteps), vtkDoubleArray* vtkNotUsed(timeSteps))
 {
   this->Internals->GetTimeRange(timeRange);
-
-  nbTimeSteps = static_cast<int>((timeRange[1] - timeRange[0]) * frameRate);
-
-  for (int i = 0; i < nbTimeSteps; i++)
-  {
-    double timestep = timeRange[0] + static_cast<double>(i) / frameRate;
-    timeSteps->InsertNextTypedTuple(&timestep);
-  }
-
   return true;
 }
 
@@ -1286,6 +1290,5 @@
 void vtkF3DUSDImporter::PrintSelf(ostream& os, vtkIndent indent)
 {
   this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << "\n";
   os << indent << "AnimationEnabled: " << std::boolalpha << this->AnimationEnabled << "\n";
 }
diff -Nru f3d-3.2.0+dfsg/plugins/usd/module/vtkF3DUSDImporter.h f3d-3.4.1/plugins/usd/module/vtkF3DUSDImporter.h
--- f3d-3.2.0+dfsg/plugins/usd/module/vtkF3DUSDImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/plugins/usd/module/vtkF3DUSDImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -36,11 +36,6 @@
   void PrintSelf(ostream& os, vtkIndent indent) override;
 
   /**
-   * Set the file name.
-   */
-  vtkSetMacro(FileName, std::string);
-
-  /**
    * Get the level of animation support in this importer, which is always
    * AnimationSupportLevel::UNIQUE
    */
@@ -66,36 +61,47 @@
   }
 
   /**
-   * Information key used to propagate the array name used as texture coordinates
+   * Return if the animation is enabled
    */
-  static vtkInformationStringKey* TCOORDS_NAME();
-
-protected:
-  vtkF3DUSDImporter();
-  ~vtkF3DUSDImporter() override;
-
-  int ImportBegin() override;
-  void ImportActors(vtkRenderer*) override;
-
   bool IsAnimationEnabled(vtkIdType vtkNotUsed(animationIndex)) override
   {
     return this->AnimationEnabled;
   }
 
+  /**
+   * Return 1 if an animation is available, 0 otherwise
+   */
   vtkIdType GetNumberOfAnimations() override;
 
-  bool GetTemporalInformation(vtkIdType animationIndex, double frameRate, int& nbTimeSteps,
-    double timeRange[2], vtkDoubleArray* timeSteps) override;
+  /**
+   * Information key used to propagate the array name used as texture coordinates
+   */
+  static vtkInformationStringKey* TCOORDS_NAME();
+
+  /**
+   * Recover animation timeRange, all other args are ignored
+   */
+  bool GetTemporalInformation(vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps,
+    vtkDoubleArray* timeSteps) override;
 
+  /**
+   * Update importer at provided timeValue
+   */
   bool UpdateAtTimeValue(double timeValue) override;
 
-  std::string FileName;
-  bool AnimationEnabled = false;
+protected:
+  vtkF3DUSDImporter();
+  ~vtkF3DUSDImporter() override;
+
+  int ImportBegin() override;
+  void ImportActors(vtkRenderer*) override;
 
 private:
   vtkF3DUSDImporter(const vtkF3DUSDImporter&) = delete;
   void operator=(const vtkF3DUSDImporter&) = delete;
 
+  bool AnimationEnabled = false;
+
   class vtkInternals;
   std::unique_ptr<vtkInternals> Internals;
 };
diff -Nru f3d-3.2.0+dfsg/pyproject.toml f3d-3.4.1/pyproject.toml
--- f3d-3.2.0+dfsg/pyproject.toml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/pyproject.toml	2026-01-08 08:41:55.000000000 +0200
@@ -4,15 +4,12 @@
 
 [project]
 name = "f3d"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 dynamic = ["version"]
 description = "F3D, a fast and minimalist 3D viewer"
 readme = "README.md"
 license = {text = "BSD 3-Clause License"}
-authors = [
-  {name = "Michael Migliore"},
-  {name = "Mathieu Westphal"},
-]
+authors = [{name = "F3D-APP Foundation"}]
 maintainers = [
   {name = "Michael Migliore", email = "mcmigliore+pip@gmail.com"}
 ]
@@ -45,14 +42,14 @@
 ninja.make-fallback = false
 ninja.minimum-version = "1.10"
 wheel.install-dir = "f3d"
-wheel.license-files = ["LICENSE.md", "doc/THIRD_PARTY_LICENSES.md"]
+wheel.license-files = ["LICENSE.md", "THIRD_PARTY_LICENSES.md"]
 wheel.packages = []
 
 [tool.setuptools_scm]
 version_file = "_version.py"
 version_scheme = "no-guess-dev"
 local_scheme = "no-local-version"
-fallback_version = "3.2.0"
+fallback_version = "3.4.1"
 
 [tool.scikit-build.cmake.define]
 CMAKE_OSX_DEPLOYMENT_TARGET = "10.15"
diff -Nru f3d-3.2.0+dfsg/python/CMakeLists.txt f3d-3.4.1/python/CMakeLists.txt
--- f3d-3.2.0+dfsg/python/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -13,7 +13,7 @@
 include(GNUInstallDirs)
 include(f3dPython)
 
-find_package(Python 3.9 COMPONENTS Interpreter Development)
+find_package(Python 3.10 COMPONENTS Interpreter Development)
 find_package(pybind11 2.9.2 REQUIRED)
 
 pybind11_add_module(pyf3d MODULE F3DPythonBindings.cxx)
@@ -115,6 +115,6 @@
 install(FILES "${CMAKE_CURRENT_BINARY_DIR}/__init__.py-install" RENAME "__init__.py"
   DESTINATION ${f3d_python_install_path} COMPONENT python)
 if (F3D_BINDINGS_PYTHON_GENERATE_STUBS)
-  install(FILES "${f3d_module_dir}/__init__.pyi" "${f3d_module_dir}/pyf3d.pyi" "${CMAKE_CURRENT_SOURCE_DIR}/py.typed"
+  install(FILES "${f3d_module_dir}/pyf3d.pyi" "${CMAKE_CURRENT_SOURCE_DIR}/py.typed"
     DESTINATION ${f3d_python_install_path} COMPONENT python)
 endif()
diff -Nru f3d-3.2.0+dfsg/python/F3DPythonBindings.cxx f3d-3.4.1/python/F3DPythonBindings.cxx
--- f3d-3.2.0+dfsg/python/F3DPythonBindings.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/F3DPythonBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -171,7 +171,20 @@
         }
         catch (const f3d::options::incompatible_exception&)
         {
-          if (std::holds_alternative<std::string>(value))
+          // failed to set an `int`, try again as `double`
+          if (std::holds_alternative<int>(value))
+          {
+            opts.set(key, static_cast<double>(std::get<int>(value)));
+          }
+          // failed to set a `vector<int>`, try again as `vector<double>`
+          else if (std::holds_alternative<std::vector<int>>(value))
+          {
+            const std::vector<int>& ints = std::get<std::vector<int>>(value);
+            const std::vector<double> doubles(ints.begin(), ints.end());
+            opts.set(key, doubles);
+          }
+          // failed to set a `string`, parse and try again
+          else if (std::holds_alternative<std::string>(value))
           {
             try
             {
@@ -184,7 +197,7 @@
           }
           else
           {
-            throw py::attribute_error(key);
+            throw py::type_error(key);
           }
         }
       })
@@ -227,6 +240,7 @@
   utils //
     .def_static("text_distance", &f3d::utils::textDistance)
     .def_static("collapse_path", &f3d::utils::collapsePath)
+    .def_static("tokenize", &f3d::utils::tokenize, py::arg("str"), py::arg("keep_comments") = true)
     .def_static(
       "glob_to_regex", &f3d::utils::globToRegex, py::arg("glob"), py::arg("path_separator") = '/')
     .def_static("get_env", &f3d::utils::getEnv)
@@ -242,7 +256,6 @@
     .value("SHIFT", f3d::interaction_bind_t::ModifierKeys::SHIFT)
     .value("CTRL_SHIFT", f3d::interaction_bind_t::ModifierKeys::CTRL_SHIFT)
     .export_values();
-
   interaction_bind.def(py::init<const f3d::interaction_bind_t::ModifierKeys&, const std::string&>())
     .def_readwrite("mod", &f3d::interaction_bind_t::mod)
     .def_readwrite("inter", &f3d::interaction_bind_t::inter)
@@ -250,16 +263,67 @@
 
   py::class_<f3d::interactor, std::unique_ptr<f3d::interactor, py::nodelete>> interactor(
     module, "Interactor");
+
+  py::enum_<f3d::interactor::BindingType>(interactor, "BindingType")
+    .value("CYCLIC", f3d::interactor::BindingType::CYCLIC)
+    .value("NUMERICAL", f3d::interactor::BindingType::NUMERICAL)
+    .value("TOGGLE", f3d::interactor::BindingType::TOGGLE)
+    .value("OTHER", f3d::interactor::BindingType::OTHER)
+    .export_values();
+  py::enum_<f3d::interactor::MouseButton>(interactor, "MouseButton")
+    .value("LEFT", f3d::interactor::MouseButton::LEFT)
+    .value("MIDDLE", f3d::interactor::MouseButton::MIDDLE)
+    .value("RIGHT", f3d::interactor::MouseButton::RIGHT)
+    .export_values();
+
+  py::enum_<f3d::interactor::WheelDirection>(interactor, "WheelDirection")
+    .value("FORWARD", f3d::interactor::WheelDirection::FORWARD)
+    .value("BACKWARD", f3d::interactor::WheelDirection::BACKWARD)
+    .value("LEFT", f3d::interactor::WheelDirection::LEFT)
+    .value("RIGHT", f3d::interactor::WheelDirection::RIGHT)
+    .export_values();
+
+  py::enum_<f3d::interactor::InputAction>(interactor, "InputAction")
+    .value("PRESS", f3d::interactor::InputAction::PRESS)
+    .value("RELEASE", f3d::interactor::InputAction::RELEASE)
+    .export_values();
+
+  py::enum_<f3d::interactor::InputModifier>(interactor, "InputModifier")
+    .value("NONE", f3d::interactor::InputModifier::NONE)
+    .value("CTRL", f3d::interactor::InputModifier::CTRL)
+    .value("SHIFT", f3d::interactor::InputModifier::SHIFT)
+    .value("CTRL_SHIFT", f3d::interactor::InputModifier::CTRL_SHIFT)
+    .export_values();
+
+  py::enum_<f3d::interactor::AnimationDirection>(interactor, "AnimationDirection")
+    .value("FORWARD", f3d::interactor::AnimationDirection::FORWARD)
+    .value("BACKWARD", f3d::interactor::AnimationDirection::BACKWARD)
+    .export_values();
+
   interactor //
-    .def("toggle_animation", &f3d::interactor::toggleAnimation, "Toggle the animation")
-    .def("start_animation", &f3d::interactor::startAnimation, "Start the animation")
+    .def("toggle_animation", &f3d::interactor::toggleAnimation, "Toggle the animation",
+      py::arg("direction") = f3d::interactor::AnimationDirection::FORWARD)
+    .def("start_animation", &f3d::interactor::startAnimation, "Start the animation",
+      py::arg("direction") = f3d::interactor::AnimationDirection::FORWARD)
     .def("stop_animation", &f3d::interactor::stopAnimation, "Stop the animation")
     .def("is_playing_animation", &f3d::interactor::isPlayingAnimation,
       "Returns True if the animation is currently started")
+    .def("get_animation_direction", &f3d::interactor::getAnimationDirection,
+      "Returns the current animation direction")
     .def("enable_camera_movement", &f3d::interactor::enableCameraMovement,
       "Enable the camera interaction")
     .def("disable_camera_movement", &f3d::interactor::disableCameraMovement,
       "Disable the camera interaction")
+    .def("trigger_mod_update", &f3d::interactor::triggerModUpdate, "Trigger a key modifier update")
+    .def("trigger_mouse_button", &f3d::interactor::triggerMouseButton, "Trigger a mouse button")
+    .def(
+      "trigger_mouse_position", &f3d::interactor::triggerMousePosition, "Trigger a mouse position")
+    .def("trigger_mouse_wheel", &f3d::interactor::triggerMouseWheel, "Trigger a mouse wheel")
+    .def("trigger_keyboard_key", &f3d::interactor::triggerKeyboardKey, "Trigger a keyboard input")
+    .def("trigger_text_character", &f3d::interactor::triggerTextCharacter,
+      "Trigger a text character input")
+    .def(
+      "trigger_event_loop", &f3d::interactor::triggerEventLoop, "Manually trigger the event loop.")
     .def("play_interaction", &f3d::interactor::playInteraction, "Play an interaction file")
     .def("record_interaction", &f3d::interactor::recordInteraction, "Record an interaction file")
     .def("start", &f3d::interactor::start, "Start the interactor and the event loop",
@@ -267,27 +331,39 @@
     .def("stop", &f3d::interactor::stop, "Stop the interactor and the event loop")
     .def(
       "request_render", &f3d::interactor::requestRender, "Request a render on the next event loop")
+    .def("request_stop", &f3d::interactor::requestStop, "Stop on the next event loop")
     .def("init_commands", &f3d::interactor::initCommands,
       "Remove all commands and add all default command callbacks")
-    .def("add_command", &f3d::interactor::addCommand, "Add a command")
+    .def("add_command", &f3d::interactor::addCommand, "Add a command", py::arg("action"),
+      py::arg("callback"), py::arg("doc") = std::nullopt, py::arg("completionCallback") = nullptr)
     .def("remove_command", &f3d::interactor::removeCommand, "Remove a command")
     .def("get_command_actions", &f3d::interactor::getCommandActions, "Get all command actions")
-    .def("trigger_command", &f3d::interactor::triggerCommand, "Trigger a command")
+    .def("trigger_command", &f3d::interactor::triggerCommand, "Trigger a command",
+      py::arg("command"), py::arg("keep_comments") = true)
     .def("init_bindings", &f3d::interactor::initBindings,
       "Remove all bindings and add default bindings")
-    .def("add_binding",
-      py::overload_cast<const f3d::interaction_bind_t&, std::string, std::string,
-        std::function<std::pair<std::string, std::string>()>>(&f3d::interactor::addBinding),
-      "Add a binding command")
-    .def("add_binding",
-      py::overload_cast<const f3d::interaction_bind_t&, std::vector<std::string>, std::string,
-        std::function<std::pair<std::string, std::string>()>>(&f3d::interactor::addBinding),
-      "Add binding commands")
     .def("remove_binding", &f3d::interactor::removeBinding, "Remove interaction commands")
     .def("get_bind_groups", &f3d::interactor::getBindGroups)
     .def("get_binds_for_group", &f3d::interactor::getBindsForGroup)
     .def("get_binds", &f3d::interactor::getBinds)
-    .def("get_binding_documentation", &f3d::interactor::getBindingDocumentation);
+    .def("get_binding_documentation", &f3d::interactor::getBindingDocumentation)
+    .def("get_binding_type", &f3d::interactor::getBindingType);
+
+  interactor
+    .def("add_binding",
+      py::overload_cast<const f3d::interaction_bind_t&, std::string, std::string,
+        std::function<std::pair<std::string, std::string>()>, f3d::interactor::BindingType>(
+        &f3d::interactor::addBinding),
+      "Add a binding command", py::arg("bind"), py::arg("command"), py::arg("group"),
+      py::arg("documentationCallback") = nullptr,
+      py::arg("type") = f3d::interactor::BindingType::OTHER)
+    .def("add_binding",
+      py::overload_cast<const f3d::interaction_bind_t&, std::vector<std::string>, std::string,
+        std::function<std::pair<std::string, std::string>()>, f3d::interactor::BindingType>(
+        &f3d::interactor::addBinding),
+      "Add binding commands", py::arg("bind"), py::arg("command"), py::arg("group"),
+      py::arg("documentationCallback") = nullptr,
+      py::arg("type") = f3d::interactor::BindingType::OTHER);
 
   // f3d::mesh_t
   py::class_<f3d::mesh_t>(module, "Mesh")
@@ -304,6 +380,57 @@
     .def_readwrite("face_sides", &f3d::mesh_t::face_sides)
     .def_readwrite("face_indices", &f3d::mesh_t::face_indices);
 
+  // f3d::color_t
+  py::class_<f3d::color_t>(module, "Color")
+    .def(py::init<>())
+    .def(py::init<double, double, double>(), py::arg("r"), py::arg("g"), py::arg("b"))
+    .def_property(
+      "r", [](const f3d::color_t& color) { return color.r(); },
+      [](f3d::color_t& color, double value) { color[0] = value; })
+    .def_property(
+      "g", [](const f3d::color_t& color) { return color.g(); },
+      [](f3d::color_t& color, double value) { color[1] = value; })
+    .def_property(
+      "b", [](const f3d::color_t& color) { return color.b(); },
+      [](f3d::color_t& color, double value) { color[2] = value; })
+    .def(
+      "to_tuple",
+      [](const f3d::color_t& color) { return std::make_tuple(color.r(), color.g(), color.b()); },
+      "Convert color to a tuple of (r, g, b)")
+    .def(
+      "from_tuple",
+      [](f3d::color_t& color, const std::tuple<double, double, double>& tuple)
+      {
+        color[0] = std::get<0>(tuple);
+        color[1] = std::get<1>(tuple);
+        color[2] = std::get<2>(tuple);
+      },
+      "Set color from a tuple of (r, g, b)");
+
+  py::enum_<f3d::light_type>(module, "LightType")
+    .value("HEADLIGHT", f3d::light_type::HEADLIGHT)
+    .value("CAMERA_LIGHT", f3d::light_type::CAMERA_LIGHT)
+    .value("SCENE_LIGHT", f3d::light_type::SCENE_LIGHT)
+    .export_values();
+
+  // f3d::light_state_t
+  py::class_<f3d::light_state_t>(module, "LightState")
+    .def(py::init<>())
+    .def(py::init<const f3d::light_type&, const f3d::point3_t&, const f3d::color_t&,
+           const f3d::vector3_t&, const bool&, const double&, const bool&>(),
+      py::arg("type") = f3d::light_type::SCENE_LIGHT,
+      py::arg("position") = f3d::point3_t({ 0.0, 0.0, 0.0 }),
+      py::arg_v("color", f3d::color_t({ 1.0, 1.0, 1.0 }), "Color(1.0, 1.0, 1.0)"),
+      py::arg("direction") = f3d::vector3_t({ 1.0, 0.0, 0.0 }), py::arg("positional_light") = false,
+      py::arg("intensity") = 1.0, py::arg("switch_state") = true)
+    .def_readwrite("type", &f3d::light_state_t::type)
+    .def_readwrite("color", &f3d::light_state_t::color)
+    .def_readwrite("position", &f3d::light_state_t::position)
+    .def_readwrite("direction", &f3d::light_state_t::direction)
+    .def_readwrite("positional_light", &f3d::light_state_t::positionalLight)
+    .def_readwrite("intensity", &f3d::light_state_t::intensity)
+    .def_readwrite("switch_state", &f3d::light_state_t::switchState);
+
   // f3d::scene
   py::class_<f3d::scene, std::unique_ptr<f3d::scene, py::nodelete>> scene(module, "Scene");
   scene //
@@ -319,7 +446,15 @@
       "Add a surfacic mesh from memory into the scene", py::arg("mesh"))
     .def("load_animation_time", &f3d::scene::loadAnimationTime)
     .def("animation_time_range", &f3d::scene::animationTimeRange)
-    .def("available_animations", &f3d::scene::availableAnimations);
+    .def("available_animations", &f3d::scene::availableAnimations)
+    .def("add_light", &f3d::scene::addLight, "Add a light to the scene", py::arg("light_state"))
+    .def(
+      "remove_light", &f3d::scene::removeLight, "Remove a light from the scene", py::arg("index"))
+    .def("update_light", &f3d::scene::updateLight, "Update a light in the scene", py::arg("index"),
+      py::arg("light_state"))
+    .def("get_light", &f3d::scene::getLight, "Get a light from the scene", py::arg("index"))
+    .def("get_light_count", &f3d::scene::getLightCount, "Get the number of lights in the scene")
+    .def("remove_all_lights", &f3d::scene::removeAllLights, "Remove all lights from the scene");
 
   // f3d::camera_state_t
   py::class_<f3d::camera_state_t>(module, "CameraState")
@@ -435,6 +570,19 @@
       "Create an engine with an EGL window (Windows/Linux only)")
     .def_static("create_osmesa", &f3d::engine::createOSMesa,
       "Create an engine with an OSMesa window (Windows/Linux only)")
+    .def_static(
+      "create_external",
+      [](py::object py_get_proc)
+      {
+        f3d::context::function func = [py_get_proc](const char* name) -> f3d::context::fptr
+        {
+          uintptr_t addr = py::int_(py_get_proc(py::bytes(name)));
+          return reinterpret_cast<f3d::context::fptr>(addr);
+        };
+        return f3d::engine::createExternal(func);
+      },
+      py::arg("get_proc_address"),
+      "Create an engine with an existing context via a get_proc_address callback")
     .def_static("create_external_glx", &f3d::engine::createExternalGLX,
       "Create an engine with an existing GLX context (Linux only)")
     .def_static("create_external_wgl", &f3d::engine::createExternalWGL,
@@ -485,11 +633,17 @@
     .value("QUIET", f3d::log::VerboseLevel::QUIET)
     .export_values();
 
+  auto forwardWrapper = [](f3d::log::forward_fn_t callback) { f3d::log::forward(callback); };
+
+  module.add_object("forwardcleanup",
+    py::capsule(&forwardWrapper, nullptr, [](PyObject*) { f3d::log::forward(nullptr); }));
+
   log //
     .def_static("set_verbose_level", &f3d::log::setVerboseLevel, py::arg("level"),
       py::arg("force_std_err") = false)
     .def_static("get_verbose_level", &f3d::log::getVerboseLevel)
     .def_static("set_use_coloring", &f3d::log::setUseColoring)
     .def_static("print", [](f3d::log::VerboseLevel& level, const std::string& message)
-      { f3d::log::print(level, message); });
+      { f3d::log::print(level, message); })
+    .def_static("forward", forwardWrapper, py::arg("callback"));
 }
diff -Nru f3d-3.2.0+dfsg/python/generate_stubs.py f3d-3.4.1/python/generate_stubs.py
--- f3d-3.2.0+dfsg/python/generate_stubs.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/generate_stubs.py	2026-01-08 08:41:55.000000000 +0200
@@ -1,15 +1,17 @@
+import ast
 import re
 import subprocess
 import sys
 from argparse import ArgumentParser
 from contextlib import contextmanager
+from dataclasses import dataclass
 from difflib import unified_diff
 from pathlib import Path
 from tempfile import gettempdir
-from typing import Callable, Iterable
+from typing import Callable, Iterable, Literal, Sequence
 
 
-def main():
+def main(argv: Sequence[str] | None = None):
     argparser = ArgumentParser()
     argparser.add_argument(
         "-o",
@@ -17,65 +19,91 @@
         help="output directory for the post-processed stubs (default: %(default)s)",
         default=f"{gettempdir()}/stubs",
     )
-    args = argparser.parse_args()
+    args = argparser.parse_args(argv)
 
     stubs = run_pybind11_stubgen(Path(args.into))
     if diff := postprocess_generated_stubs(stubs):
         print("\n".join(diff))
 
 
-def run_pybind11_stubgen(out_dir: Path, module: str = "f3d"):
+def run_pybind11_stubgen(
+    out_dir: Path, module: str = "f3d", submodule: str = "f3d.pyf3d"
+):
     stubgen_cmd = (
         # use current python interpreter to run stubs generation for the `f3d` module
-        *(sys.executable, "-m", "pybind11_stubgen", module),
+        *(sys.executable, "-m", "pybind11_stubgen", submodule),
         # fix enum for default values in `Image.save()` and `Image.save_buffer()`
         *("--enum-class-locations", "SaveFormat:Image"),
+        # more enums
+        *("--enum-class-locations", "AnimationDirection:Interactor"),
+        *("--enum-class-locations", "BindingType:Interactor"),
+        *("--enum-class-locations", "LightType:f3d"),
         # ignore `f3d.vector3_t` and `f3d.point3_t` as we dont actually map them
         # but let them auto-convert from and to `tuple[float, float, float]`
         # (all occurrences will be postprocessed later)
         *("--ignore-unresolved-names", r"f3d\.(vector3_t|point3_t)"),
         # output directory so we can retrieve and post process
         *("--output-dir", out_dir),
+        "--exit-code",
     )
     with retrieve_changed_files(out_dir, f"{module}/**/*.pyi") as changed_files:
         subprocess.check_call(stubgen_cmd)
     return changed_files
 
 
-def postprocess_generated_stubs(filenames: Iterable[Path]):
-    replacements: list[tuple[str, str | Callable[[re.Match[str]], str]]] = [
-        (
-            # change `point3_t` and `vector3_t` parameter annotations and return types
-            # to `tuple[float, float, float]`
-            r"((:|->)\s*)f3d\.(vector3_t|point3_t)",
-            r"\1tuple[float, float, float]",
+def postprocess_generated_stub(source: str):
+    type_fixes = (
+        TypeFix(
+            # change `point3_t` and `vector3_t` to `tuple[float, float, float]`
+            r"f3d\.(vector3_t|point3_t)",
+            r"tuple[float, float, float]",
         ),
-        (
+        TypeFix(
             # add missing template parameter to raw `os.PathLike` (`os.PathLike[str]`)
-            r"(PathLike)(?!\[)",
+            r"(os\.PathLike)(?!\[)",
             r"\1[str]",
         ),
-        (
-            # remove `_pybind11_conduit_v1_` static methods
-            r"^\s+@staticmethod\s+def _pybind11_conduit_v1_\(\*args, *\*\*kwargs\):\s*\.\.\.[\n\r]",
-            "",
+        TypeFix(
+            # replace `Sequence[...]` by `list[...]` in function returns
+            # because pybind outputs `std::vector` as `list`
+            r"collections\.abc\.Sequence\[",
+            r"list[",
+            only_for=("return", "cb_arg"),
+        ),
+        TypeFix(
+            # replace `Sequence[...]` by `list[...]` in function arguments
+            # because pybind accepts `std::vector` as any `Sequence`
+            r"list\[",
+            r"collections.abc.Sequence[",
+            only_for=("arg", "cb_return"),
         ),
-        (
-            # replace `list[...]` by `Sequence[...]` in function parameters (but not in returns)
-            # because pybind's `std::vector` typecaster actually accepts sequences not just lists
-            r"(def .*list\[.+)(\s*->.*)",
-            lambda m: re.sub(r" list\[", r" typing.Sequence[", m.group(1)) + m.group(2),
+        TypeFix(
+            # replace `None` with `Any` in callback returns
+            # so that functions expecting a `void` callback can accept anything
+            # (and discard the return)
+            r"None",
+            r"typing.Any",
+            only_for=("cb_return",),
         ),
-    ]
+    )
+    extra_imports = "pathlib", "os"
+
+    transformer = StubTransformer(type_fixes, extra_imports)
+    tree = ast.parse(source)
+    tree = transformer.visit(tree)
+    return ast.unparse(tree)
+
 
+def postprocess_generated_stubs(filenames: Iterable[Path]):
     diff: list[str] = []
 
     for filename in filenames:
-        processed = original = filename.read_text()
-        for pattern, repl in replacements:
-            processed = re.sub(pattern, repl, processed, flags=re.MULTILINE)
+        source = filename.read_text()
+        processed = postprocess_generated_stub(source)
         filename.write_text(processed)
 
+        # parse and uparse source to normalize it and get a proper diff
+        original = ast.unparse(ast.parse(source))
         diff += unified_diff(
             original.splitlines(),
             processed.splitlines(),
@@ -88,6 +116,72 @@
     return diff
 
 
+TypeFixTarget = Literal["arg", "return", "attr", "cb_arg", "cb_return"]
+
+
+@dataclass
+class TypeFix:
+    pattern: str | re.Pattern[str]
+    substitution: str | Callable[[re.Match[str]], str]
+    only_for: tuple[TypeFixTarget, ...] | None = None
+
+
+class StubTransformer(ast.NodeTransformer):
+    def __init__(
+        self, type_fixes: Iterable[TypeFix] = (), extra_imports: Iterable[str] = ()
+    ) -> None:
+        super().__init__()
+        self.type_fixes = list(type_fixes)
+        self.extra_imports = list(extra_imports)
+
+    def visit_Import(self, node: ast.Import):
+        """add extra imports before the first original import"""
+        if self.extra_imports:
+            new_imports = [ast.Import([ast.alias(name)]) for name in self.extra_imports]
+            self.extra_imports.clear()
+            return new_imports + [node]
+        else:
+            return node
+
+    def _fix_annotation(self, annotation: ast.expr, target: TypeFixTarget):
+        src = ast.unparse(annotation)
+        for fix in self.type_fixes:
+            if fix.only_for and target not in fix.only_for:
+                continue
+            if re.search(fix.pattern, src):
+                annotation = ast.parse(
+                    re.sub(fix.pattern, fix.substitution, src), "<string>", "eval"
+                ).body
+                src = ast.unparse(annotation)
+        return annotation
+
+    def visit_FunctionDef(self, node: ast.FunctionDef):
+        if node.returns:
+            node.returns = self._fix_annotation(node.returns, "return")
+        return self.generic_visit(node)
+
+    def visit_AnnAssign(self, node: ast.AnnAssign):
+        if node.annotation:
+            node.annotation = self._fix_annotation(node.annotation, "attr")
+        return node
+
+    def visit_arg(self, node: ast.arg):
+        if isinstance(node.annotation, ast.Subscript) and re.match(
+            r"(typing\.|collections\.abc\.)?Callable",
+            ast.unparse(node.annotation.value),
+        ):
+            assert isinstance(node.annotation.slice, ast.Tuple)
+            args, ret = node.annotation.slice.elts
+            node.annotation.slice.elts = [
+                self._fix_annotation(args, "cb_arg"),
+                self._fix_annotation(ret, "cb_return"),
+            ]
+            return node
+        elif node.annotation:
+            node.annotation = self._fix_annotation(node.annotation, "arg")
+        return node
+
+
 @contextmanager
 def retrieve_changed_files(directory: Path, files_glob: str):
     mtimes = {f: f.stat().st_mtime for f in directory.glob(files_glob)}
@@ -98,5 +192,5 @@
     )
 
 
-if __name__ == "__main__":
+if __name__ == "__main__":  # pragma: nocover
     main()
diff -Nru f3d-3.2.0+dfsg/python/__init__.py.in f3d-3.4.1/python/__init__.py.in
--- f3d-3.2.0+dfsg/python/__init__.py.in	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/__init__.py.in	2026-01-08 08:41:55.000000000 +0200
@@ -12,18 +12,24 @@
 from pathlib import Path
 from typing import Any, Iterable, Mapping, Union
 
-F3D_ABSOLUTE_DLLS = [
+F3D_ABSOLUTE_DLLS: list[str] = [
     # @F3D_ABSOLUTE_DLLS_FIXUP@
 ]
-F3D_RELATIVE_DLLS = [
+F3D_RELATIVE_DLLS: list[str] = [
     # @F3D_RELATIVE_DLLS_FIXUP@
 ]
-if sys.version_info >= (3, 8) and sys.platform == "win32":
-    for abs_path in F3D_ABSOLUTE_DLLS:
-        os.add_dll_directory(abs_path)
-    root = Path(__file__).parent
-    for rel_path in F3D_RELATIVE_DLLS:
-        os.add_dll_directory((root / rel_path).resolve())
+
+
+def _add_dll_directories():
+    if sys.version_info >= (3, 8) and sys.platform == "win32":
+        for abs_path in F3D_ABSOLUTE_DLLS:
+            os.add_dll_directory(abs_path)
+        root = Path(__file__).parent
+        for rel_path in F3D_RELATIVE_DLLS:
+            os.add_dll_directory((root / rel_path).resolve())
+
+
+_add_dll_directories()
 
 from .pyf3d import *
 
diff -Nru f3d-3.2.0+dfsg/python/test_generate_stubs.py f3d-3.4.1/python/test_generate_stubs.py
--- f3d-3.2.0+dfsg/python/test_generate_stubs.py	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/python/test_generate_stubs.py	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,137 @@
+import ast
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from textwrap import dedent
+
+from generate_stubs import StubTransformer, TypeFix, main, postprocess_generated_stub
+from pytest import importorskip
+
+
+def test_main():
+    importorskip("f3d")
+    importorskip("pybind11_stubgen")
+    with TemporaryDirectory() as tmp:
+        main(["--into", tmp])
+        assert (Path(tmp) / "f3d/pyf3d.pyi").is_file()
+
+
+def test_transformer_f3d_point_vector():
+    src = """
+    class A:
+        p: f3d.point3_t
+        v: f3d.vector3_t
+        def f(p: f3d.point3_t) -> f3d.point3_t: ...
+        def g(v: f3d.vector3_t) -> f3d.vector3_t: ...
+    """
+    expected = """
+    class A:
+        p: tuple[float,float,float]
+        v: tuple[float,float,float]
+        def f(p: tuple[float,float,float]) -> tuple[float,float,float]: ...
+        def g(v: tuple[float,float,float]) -> tuple[float,float,float]: ...
+    """
+    assert postprocess_generated_stub(dedent(src)) == parse_unparse(expected)
+
+
+def test_transformer_f3d_lists():
+    src = """
+    class A:
+        xs: list[T]  # should stay
+        def f(xs: collections.abc.Sequence[T]) -> collections.abc.Sequence[T]: ...  # return should be `list`
+        def g(cb: Callable[[collections.abc.Sequence[T]], collections.abc.Sequence[T]]): ...  # cb arg shloud become `list`
+        def h(cb: collections.abc.Callable[[collections.abc.Sequence[str]], list[str]]=None): ...
+    """
+    expected = """
+    class A:
+        xs: list[T]
+        def f(xs: collections.abc.Sequence[T]) -> list[T]: ...
+        def g(cb: Callable[[list[T]], collections.abc.Sequence[T]]): ...
+        def h(cb: collections.abc.Callable[[list[str]], collections.abc.Sequence[str]]=None): ...
+    """
+    assert postprocess_generated_stub(dedent(src)) == parse_unparse(expected)
+
+
+def test_transformer_f3d_void_callbacks():
+    src = """
+    class A:
+        def g(cb: Callable[[A,B], C]): ...
+        def g(cb: Callable[[A,B], None]): ... # cb return can be `Any`
+    """
+    expected = """
+    class A:
+        def g(cb: Callable[[A,B], C]): ...
+        def g(cb: Callable[[A,B], typing.Any]): ...
+    """
+    assert postprocess_generated_stub(dedent(src)) == parse_unparse(expected)
+
+
+def test_transformer_f3d_path():
+    src = """
+    class A:
+        xs: pathlib.Path  # should be `pathlib.Path`
+        def f(p: os.PathLike | str | bytes) -> pathlib.Path: ...   # arg should be `os.PathLike[str]|str|bytes`
+    """
+    expected = """
+    class A:
+        xs: pathlib.Path
+        def f(p: os.PathLike[str] | str | bytes) -> pathlib.Path: ...
+    """
+    assert postprocess_generated_stub(dedent(src)) == parse_unparse(expected)
+
+
+def test_transformer_typefix_matching():
+    transformer = StubTransformer(
+        type_fixes=[
+            TypeFix(r"T", t.upper(), only_for=(t,))
+            for t in ("arg", "return", "attr", "cb_arg", "cb_return")
+        ],
+    )
+    src = """
+    __all__: list[T] = ...
+
+    def f1(a: T, b: T | None) -> list[T]:...
+
+    class A:
+        a: T
+        b: T
+        def f(a: T, b: T) -> T | None: ...
+        def f(xs: list[T], cb: Callable[[T, T], T]) -> T: ...
+    """
+    expected = """
+    __all__: list[ATTR] = ...
+
+    def f1(a: ARG, b: ARG | None) -> list[RETURN]: ...
+
+    class A:
+        a: ATTR
+        b: ATTR
+        def f(a: ARG, b: ARG) -> RETURN | None: ...
+        def f(xs: list[ARG], cb: Callable[[CB_ARG, CB_ARG], CB_RETURN]) -> RETURN: ...
+    """
+    tree = transformer.visit(ast.parse(dedent(src)))
+    assert ast.unparse(tree) == parse_unparse(expected)
+
+
+def test_transformer_extra_imports():
+    transformer = StubTransformer(
+        extra_imports=("hello", "world"),
+    )
+    src = """
+    from __future__ import annotations
+    # extra imports should go here
+    import foo
+    import bar
+    """
+    expected = """
+    from __future__ import annotations
+    import hello
+    import world
+    import foo
+    import bar
+    """
+    tree = transformer.visit(ast.parse(dedent(src)))
+    assert ast.unparse(tree) == parse_unparse(expected)
+
+
+def parse_unparse(src: str):
+    return ast.unparse(ast.parse(dedent(src)))
diff -Nru f3d-3.2.0+dfsg/python/testing/CMakeLists.txt f3d-3.4.1/python/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/python/testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,7 @@
      test_camera.py
      test_interactor.py
      test_image.py
+     test_log.py
      test_options.py
      test_utils.py
     )
@@ -15,6 +16,7 @@
 list(APPEND pyf3dTests_list
      test_image_compare.py
      test_scene.py
+     test_interactor_start.py
     )
 
 list(APPEND pyf3dTestsNoRender_list
@@ -23,12 +25,6 @@
      test_utils.py
     )
 
-# These tests needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10155
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20230426)
-list(APPEND pyf3dTests_list
-     test_interactor_start.py)
-endif ()
-
 # Add all the ADD_TEST for each test
 foreach(test ${pyf3dTests_list})
   get_filename_component (TName TestPython_${test} NAME_WE)
diff -Nru f3d-3.2.0+dfsg/python/testing/test_camera.py f3d-3.4.1/python/testing/test_camera.py
--- f3d-3.2.0+dfsg/python/testing/test_camera.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_camera.py	2026-01-08 08:41:55.000000000 +0200
@@ -92,11 +92,15 @@
 
 
 @pytest.mark.xfail(reason="CameraState equality not implemented")
-def test_state_compare():
+def test_state_compare_eq():
     state1 = f3d.CameraState((1, 2, 3), (1, 22, 3), (0, 0, 1), 32)
     state2 = f3d.CameraState((1, 2, 3), (1, 22, 3), (0, 0, 1), 32)
     assert state1 == state2
 
+
+@pytest.mark.xfail(reason="CameraState equality not implemented")
+def test_state_compare_neq():
+    state1 = f3d.CameraState((1, 2, 3), (1, 22, 3), (0, 0, 1), 32)
     state3 = f3d.CameraState((1, 2, 3), (1, 22, 3), (0, 0, 1), 25)
     assert state1 != state3
 
diff -Nru f3d-3.2.0+dfsg/python/testing/test_engine.py f3d-3.4.1/python/testing/test_engine.py
--- f3d-3.2.0+dfsg/python/testing/test_engine.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_engine.py	2026-01-08 08:41:55.000000000 +0200
@@ -5,7 +5,6 @@
 import f3d
 
 
-@pytest.fixture
 def test_plugins_list():
     base_dir = Path(f3d.__file__).parent
     plugins = f3d.Engine.get_plugins_list(base_dir / "share/f3d/plugins")
diff -Nru f3d-3.2.0+dfsg/python/testing/test_interactor.py f3d-3.4.1/python/testing/test_interactor.py
--- f3d-3.2.0+dfsg/python/testing/test_interactor.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_interactor.py	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,10 @@
     return "dummyDoc", "dummyValue"
 
 
+def compl_fn(args: list[str]):
+    return ["compl"]
+
+
 def test_command(capfd: pytest.CaptureFixture[str]):
     engine = f3d.Engine.create(True)
     inter = engine.interactor
@@ -28,8 +32,40 @@
     out, _err = capfd.readouterr()
     assert out == "['arg1', 'arg2']\n"
 
+    # Check a command can be triggered with #
+    inter.add_command("my_cmd", print_fn)
+    inter.trigger_command("my_cmd arg1 arg2 #000", False)
+    inter.remove_command("my_cmd")
+    out, _err = capfd.readouterr()
+    assert out == "['arg1', 'arg2', '#000']\n"
+
     # Smoke test
     inter.init_commands()
+    inter.add_command("my_cmd2", print_fn, ("my_cmd2", "doc"), compl_fn)
+
+
+def test_command_completion(capfd: pytest.CaptureFixture[str]):
+    engine = f3d.Engine.create(True)
+    inter = engine.interactor
+
+    # Check default commands can be removed
+    actions = inter.get_command_actions()
+    for action in actions:
+        inter.remove_command(action)
+    assert len(inter.get_command_actions()) == 0
+
+    # Smoke test
+    inter.init_commands()
+    inter.add_command("cmd", print_fn, ("cmd", "doc"), compl_fn)
+
+    engine.window.render()
+
+    inter.trigger_keyboard_key(inter.InputAction.PRESS, "Escape")
+    inter.trigger_keyboard_key(inter.InputAction.RELEASE, "Escape")
+    for c in "cmd ":
+        inter.trigger_text_character(ord(c))
+    inter.trigger_keyboard_key(inter.InputAction.PRESS, "Tab")
+    inter.trigger_keyboard_key(inter.InputAction.RELEASE, "Tab")
 
 
 def test_binding():
@@ -57,26 +93,127 @@
         "dummy command",
         "DummyGroup",
         doc_fn,
+        f3d.Interactor.BindingType.CYCLIC,
     )
     inter.add_binding(
         f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.CTRL, "P"),
         "dummy command",
         "DummyGroup",
         doc_fn,
+        f3d.Interactor.BindingType.NUMERICAL,
     )
     inter.add_binding(
         f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.SHIFT, "P"),
         "dummy command",
         "DummyGroup",
         doc_fn,
+        f3d.Interactor.BindingType.TOGGLE,
     )
     inter.add_binding(
         f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.CTRL_SHIFT, "P"),
         ["dummy command", "dummy command"],
         "DummyGroup",
         doc_fn,
+        f3d.Interactor.BindingType.OTHER,
     )
     assert len(inter.get_bind_groups()) == 1
     assert len(inter.get_binds()) == 5
+    assert (
+        inter.get_binding_documentation(
+            f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.CTRL, "P")
+        )
+        == doc_fn()
+    )
+    assert (
+        inter.get_binding_type(
+            f3d.InteractionBind(f3d.InteractionBind.ModifierKeys.CTRL, "P")
+        )
+        == f3d.Interactor.BindingType.NUMERICAL
+    )
 
     inter.init_bindings()
+
+
+def test_trigger_key(capfd: pytest.CaptureFixture[str]):
+    engine = f3d.Engine.create(True)
+
+    engine.window.render()
+
+    engine.interactor.trigger_keyboard_key(f3d.Interactor.InputAction.PRESS, "E")
+    engine.interactor.trigger_keyboard_key(f3d.Interactor.InputAction.RELEASE, "E")
+    assert engine.options["render.show_edges"] is True
+
+    engine.interactor.trigger_mod_update(f3d.Interactor.InputModifier.SHIFT)
+    engine.interactor.trigger_keyboard_key(f3d.Interactor.InputAction.PRESS, "L")
+    engine.interactor.trigger_keyboard_key(f3d.Interactor.InputAction.RELEASE, "L")
+    assert engine.options["render.light.intensity"] == 0.98
+
+    engine.interactor.trigger_mouse_button(
+        f3d.Interactor.InputAction.PRESS, f3d.Interactor.MouseButton.LEFT
+    )
+    engine.interactor.trigger_mouse_position(100, 100)
+    engine.interactor.trigger_mouse_wheel(f3d.Interactor.WheelDirection.FORWARD)
+    engine.interactor.trigger_text_character(0)
+
+
+def test_interactor_animation(capfd: pytest.CaptureFixture[str]):
+    engine = f3d.Engine.create(True)
+    engine.window.render()
+
+    engine.interactor.start_animation()  # Play Forward
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.FORWARD
+    )
+    engine.interactor.toggle_animation()  # Pause
+    assert not engine.interactor.is_playing_animation()
+
+    engine.interactor.start_animation(
+        f3d.Interactor.AnimationDirection.FORWARD
+    )  # Play Forward
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.FORWARD
+    )
+    engine.interactor.stop_animation()  # Pause
+    assert not engine.interactor.is_playing_animation()
+
+    engine.interactor.start_animation(
+        f3d.Interactor.AnimationDirection.BACKWARD
+    )  # Play Backward
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.BACKWARD
+    )
+    engine.interactor.stop_animation()  # Pause
+
+    engine.interactor.toggle_animation()
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.FORWARD
+    )
+    engine.interactor.stop_animation()  # Pause
+
+    engine.interactor.toggle_animation(
+        f3d.Interactor.AnimationDirection.FORWARD
+    )  # Play Forward
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.FORWARD
+    )
+    engine.interactor.stop_animation()
+
+    engine.interactor.toggle_animation(
+        f3d.Interactor.AnimationDirection.BACKWARD
+    )  # Play Backward
+    assert (
+        engine.interactor.is_playing_animation()
+        and engine.interactor.get_animation_direction()
+        == f3d.Interactor.AnimationDirection.BACKWARD
+    )
+    engine.interactor.stop_animation()
diff -Nru f3d-3.2.0+dfsg/python/testing/test_interactor_start.py f3d-3.4.1/python/testing/test_interactor_start.py
--- f3d-3.2.0+dfsg/python/testing/test_interactor_start.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_interactor_start.py	2026-01-08 08:41:55.000000000 +0200
@@ -18,3 +18,9 @@
     inter = engine.interactor
     inter.request_render()
     inter.start(1 / 30, partial(stop_fn, inter))
+
+
+def test_request_stop():
+    engine = f3d.Engine.create(True)
+
+    engine.interactor.start(1 / 30, engine.interactor.request_stop)
diff -Nru f3d-3.2.0+dfsg/python/testing/test_log.py f3d-3.4.1/python/testing/test_log.py
--- f3d-3.2.0+dfsg/python/testing/test_log.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_log.py	2026-01-08 08:41:55.000000000 +0200
@@ -1,57 +1,76 @@
-import subprocess
-import sys
+from f3d import Log
+from pytest import CaptureFixture, mark
 
 
-def test_default_level():
-    assert (
-        run_python(
-            "from f3d import Log",
-            "Log.set_use_coloring(False)",
-            "Log.print(Log.DEBUG, 'debug')",
-            "Log.print(Log.INFO, 'info')",
-        )
-        == "info\n"
-    )
-
-
-def test_debug():
-    assert (
-        run_python(
-            "from f3d import Log",
-            "Log.set_use_coloring(False)",
-            "Log.set_verbose_level(Log.DEBUG)",
-            "Log.print(Log.DEBUG, 'debug')",
-        )
-        == "debug\n"
-    )
-
-
-def test_coloring():
-    assert (
-        run_python(
-            "from f3d import Log",
-            "Log.set_use_coloring(True)",
-            "Log.print(Log.INFO, 'info')",
-        )
-        == "info\x1b[0m\n"
-    )
+@mark.parametrize(
+    "level, expected_out, expected_err",
+    [
+        (None, "info\n", "warn\nerror\n"),
+        (Log.DEBUG, "debug\ninfo\n", "warn\nerror\n"),
+        (Log.INFO, "info\n", "warn\nerror\n"),
+        (Log.WARN, "", "warn\nerror\n"),
+        (Log.ERROR, "", "error\n"),
+        (Log.QUIET, "", ""),
+    ],
+)
+def test_levels(
+    level: Log.VerboseLevel | None,
+    expected_out: str,
+    expected_err: str,
+    capfd: CaptureFixture[str],
+):
+    if level is not None:
+        Log.set_verbose_level(level)
+    Log.set_use_coloring(False)
+
+    Log.print(Log.DEBUG, "debug")
+    Log.print(Log.INFO, "info")
+    Log.print(Log.WARN, "warn")
+    Log.print(Log.ERROR, "error")
+
+    out, err = capfd.readouterr()
+    assert out == expected_out
+    assert err == expected_err
+
+
+def test_coloring(capfd: CaptureFixture[str]):
+    Log.set_verbose_level(Log.DEBUG)
+    Log.set_use_coloring(True)
+
+    Log.print(Log.DEBUG, "debug")
+    Log.print(Log.INFO, "info")
+    Log.print(Log.WARN, "warn")
+    Log.print(Log.ERROR, "error")
+
+    out, err = capfd.readouterr()
+    assert out == "debug\ninfo\n"
+    assert err == "\x1b[33mwarn\x1b[0m\n\x1b[31;1merror\x1b[0m\n"
 
 
 def test_get_verbose_level():
-    assert (
-        run_python(
-            "from f3d import Log",
-            "Log.set_verbose_level(Log.DEBUG)",
-            "print(Log.get_verbose_level() == Log.DEBUG)",
-            "Log.set_verbose_level(Log.WARN)",
-            "print(Log.get_verbose_level() == Log.WARN)",
-        )
-        == "True\nTrue\n"
-    )
-
-
-def run_python(*statements: str):
-    return subprocess.check_output(
-        [sys.executable, "-c", "; ".join(statements)],
-        text=True,
-    )
+    Log.set_verbose_level(Log.DEBUG)
+    assert Log.get_verbose_level() == Log.DEBUG
+    Log.set_verbose_level(Log.WARN)
+    assert Log.get_verbose_level() == Log.WARN
+
+
+def test_forward():
+    forwarded: dict[Log.VerboseLevel, list[str]] = {}
+
+    def forward(level: Log.VerboseLevel, message: str):
+        forwarded.setdefault(level, []).append(message)
+
+    Log.forward(forward)
+    Log.set_verbose_level(Log.VerboseLevel.DEBUG)
+
+    Log.print(Log.VerboseLevel.DEBUG, "._.")
+    Log.print(Log.VerboseLevel.INFO, "^_^")
+    Log.print(Log.VerboseLevel.WARN, "O_o")
+    Log.print(Log.VerboseLevel.ERROR, "x_x")
+
+    assert forwarded == {
+        Log.VerboseLevel.DEBUG: ["._."],
+        Log.VerboseLevel.INFO: ["^_^"],
+        Log.VerboseLevel.WARN: ["O_o"],
+        Log.VerboseLevel.ERROR: ["x_x"],
+    }
diff -Nru f3d-3.2.0+dfsg/python/testing/test_options.py f3d-3.4.1/python/testing/test_options.py
--- f3d-3.2.0+dfsg/python/testing/test_options.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_options.py	2026-01-08 08:41:55.000000000 +0200
@@ -20,6 +20,16 @@
     options["model.point_sprites.type"] = "sphere"
 
 
+def test_set_double_from_int():
+    options = f3d.Options()
+    options["scene.animation.speed_factor"] = 3
+
+
+def test_set_double_vector_from_ints():
+    options = f3d.Options()
+    options["scene.up_direction"] = 1, 2, 3
+
+
 def test_getitem():
     engine = f3d.Engine.create_none()
     options = engine.options
@@ -52,7 +62,7 @@
 
 def test_set_incompatible_value_type():
     options = f3d.Options()
-    with pytest.raises(AttributeError):
+    with pytest.raises(TypeError):
         options["ui.axis"] = 1.12
 
 
@@ -148,13 +158,13 @@
 def test_update_from_invalid_kv_pairs():
     options = f3d.Options()
 
-    items = (
+    items: Any = (
         ("ui.axis", True),
         ("model.material.roughness", 0.8),
         ("a", "b", 3),
     )
     with pytest.raises(ValueError):
-        options.update(items)  # type: ignore
+        options.update(items)
 
 
 def test_is_same():
diff -Nru f3d-3.2.0+dfsg/python/testing/test_scene.py f3d-3.4.1/python/testing/test_scene.py
--- f3d-3.2.0+dfsg/python/testing/test_scene.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_scene.py	2026-01-08 08:41:55.000000000 +0200
@@ -34,6 +34,7 @@
     sphere2 = testing_dir / "data/mb/recursive/mb_2_0.vtp"
     cube = testing_dir / "data/mb/recursive/mb_0_0.vtu"
     reference = testing_dir / "baselines/TestPythonScene.png"
+    reference_red_light = testing_dir / "baselines/TestPythonSceneRedLight.png"
     output = Path(tempfile.gettempdir()) / "TestPythonScene.png"
 
     engine = f3d.Engine.create(True)
@@ -52,3 +53,39 @@
     img.save(output)
 
     assert img.compare(f3d.Image(reference)) < 0.05
+
+    engine.scene.remove_all_lights()
+    red_light = f3d.LightState(color=f3d.Color(1.0, 0.0, 0.0))
+    green_light = f3d.LightState(
+        color=f3d.Color(0.0, 1.0, 0.0),
+        position=(0.0, 1.0, 0.0),
+        direction=(0.0, -1.0, 0.0),
+        intensity=0.5,
+        positional_light=True,
+        switch_state=True,
+        type=f3d.LightType.HEADLIGHT,
+    )
+    engine.scene.add_light(red_light)
+    engine.scene.add_light(green_light)
+
+    assert engine.scene.get_light_count() == 2
+    assert engine.scene.get_light(0).color.to_tuple() == (1.0, 0.0, 0.0)
+    assert engine.scene.get_light(1).color.to_tuple() == (0.0, 1.0, 0.0)
+
+    blue_light = green_light
+    blue_light.color = f3d.Color(0.0, 0.0, 1.0)
+    blue_light.type = f3d.LightType.CAMERA_LIGHT
+    engine.scene.update_light(1, blue_light)
+    assert engine.scene.get_light(1).color.to_tuple() == blue_light.color.to_tuple()
+    assert engine.scene.get_light(1).position == blue_light.position
+    assert engine.scene.get_light(1).direction == blue_light.direction
+    assert engine.scene.get_light(1).intensity == blue_light.intensity
+    assert engine.scene.get_light(1).type == f3d.LightType.CAMERA_LIGHT
+
+    engine.scene.remove_light(1)
+    assert engine.scene.get_light_count() == 1
+
+    img = engine.window.render_to_image()
+    img.save(output)
+
+    assert img.compare(f3d.Image(reference_red_light)) < 0.05
diff -Nru f3d-3.2.0+dfsg/python/testing/test_utils.py f3d-3.4.1/python/testing/test_utils.py
--- f3d-3.2.0+dfsg/python/testing/test_utils.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/python/testing/test_utils.py	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,19 @@
 def test_collapse_path():
     assert f3d.Utils.collapse_path("/folder/../file.ext", ".") == Path("/file.ext")
 
+    # ensure we're getting a `Path` back
+    assert isinstance(f3d.Utils.collapse_path("/folder/../file.ext", Path(".")), Path)
+    # ensure the typing says we're getting `Path` back (pytest won't know but type checkers would)
+    assert f3d.Utils.collapse_path(Path("/folder/../file.ext"), ".").parent
+
+
+def test_tokenize():
+    out = f3d.Utils.tokenize("my_cmd arg1 arg2")
+    assert out == ["my_cmd", "arg1", "arg2"]
+
+    out = f3d.Utils.tokenize("my_cmd arg1 arg2 #000", False)
+    assert out == ["my_cmd", "arg1", "arg2", "#000"]
+
 
 def test_glob_to_regex():
     assert f3d.Utils.glob_to_regex("*vt?") == ".*vt."
@@ -22,11 +35,11 @@
 
 
 def test_get_env():
-    assert f3d.Utils.get_env("F3D_TEST_ENV_EMPTY") == None
+    assert f3d.Utils.get_env("F3D_TEST_ENV_EMPTY") is None
 
 
 def test_get_know_folder():
     if os.name == "nt":
-        assert f3d.Utils.get_known_folder(f3d.Utils.KnownFolder.PICTURES) != None
+        assert f3d.Utils.get_known_folder(f3d.Utils.KnownFolder.PICTURES) is not None
     else:
-        assert f3d.Utils.get_known_folder(f3d.Utils.KnownFolder.PICTURES) == None
+        assert f3d.Utils.get_known_folder(f3d.Utils.KnownFolder.PICTURES) is None
diff -Nru f3d-3.2.0+dfsg/README.md f3d-3.4.1/README.md
--- f3d-3.2.0+dfsg/README.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/README.md	2026-01-08 08:41:55.000000000 +0200
@@ -1,8 +1,8 @@
-[![CI](https://img.shields.io/github/actions/workflow/status/f3d-app/f3d/ci.yml?label=CI&logo=github)](https://github.com/f3d-app/f3d/actions/workflows/ci.yml) [![Packaging](https://img.shields.io/github/actions/workflow/status/f3d-app/f3d-superbuild/nightly.yml?label=Packaging&logo=github)](https://github.com/f3d-app/f3d-superbuild) [![codecov](https://codecov.io/gh/f3d-app/f3d/branch/master/graph/badge.svg?token=siwG82IXK7)](https://codecov.io/gh/f3d-app/f3d) [![Downloads](https://img.shields.io/github/downloads/f3d-app/f3d/total.svg)](https://github.com/f3d-app/f3d/releases) [![Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/f3d-app) [![Discord](https://discordapp.com/api/guilds/1046005690809978911/widget.png?style=shield)](https://discord.f3d.app) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
+[![CI](https://img.shields.io/github/actions/workflow/status/f3d-app/f3d/ci.yml?label=CI&logo=github)](https://github.com/f3d-app/f3d/actions/workflows/ci.yml) [![Packaging](https://img.shields.io/github/actions/workflow/status/f3d-app/f3d-superbuild/nightly.yml?label=Packaging&logo=github)](https://github.com/f3d-app/f3d-superbuild) [![codecov](https://codecov.io/gh/f3d-app/f3d/branch/master/graph/badge.svg?token=siwG82IXK7)](https://codecov.io/gh/f3d-app/f3d) [![Downloads](https://img.shields.io/github/downloads/f3d-app/f3d/total.svg)](https://github.com/f3d-app/f3d/releases) [![Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://f3d.app/thanks) [![Discord](https://discordapp.com/api/guilds/1046005690809978911/widget.png?style=shield)](https://discord.f3d.app) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
 
 # F3D - Fast and minimalist 3D viewer
 
-By Michael Migliore and Mathieu Westphal.
+By the F3D-APP Foundation.
 
 <img src="https://raw.githubusercontent.com/f3d-app/f3d/master/resources/logo.svg" align="left" width="20px"/>
 F3D (pronounced `/fɛd/`) is a fast and minimalist 3D viewer desktop application. It supports many file formats, from digital content to scientific datasets (including glTF, USD, STL, STEP, PLY, OBJ, FBX, Alembic), can show animations and support thumbnails and many rendering and texturing options including real time physically based rendering and raytracing.
@@ -10,23 +10,23 @@
 
 It is fully controllable from the command line and support configuration files. It can provide thumbnails, support interactive hotkeys, drag&drop and integration into file managers.
 
-F3D also contains the libf3d, a simple library to render meshes, with a C++17 API, Python Bindings, and experimental Java and Javascript bindings.
+F3D also contains the libf3d, a simple library to render meshes, with a C++17 API, C, Python and Javascript Bindings, and experimental Java bindings.
 
-<img src="https://media.githubusercontent.com/media/f3d-app/f3d-media/95b76c22d927bb24759bbe0246b6260121f2933b/media/typical.png" width="640">
+<img src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/typical.png" width="640" />
 
 _A typical render by F3D_
 
-<img src="https://user-images.githubusercontent.com/3129530/194735261-dd6f1c1c-fa57-47b0-9d27-f735d18ccd5e.gif" width="640">
+<img src="https://user-images.githubusercontent.com/3129530/194735261-dd6f1c1c-fa57-47b0-9d27-f735d18ccd5e.gif" width="640" />
 
 _Animation of a glTF file within F3D_
 
-<img src="https://user-images.githubusercontent.com/3129530/194735272-5bcd3e7c-a333-41f5-8066-9b0bec9885e8.png" width="640">
+<img src="https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/directScalars.png" width="640" />
 
 _A direct scalars render by F3D_
 
-See the [gallery](doc/GALLERY.md) for more images, take a look at the [changelog](doc/CHANGELOG.md) or go to the [install guide](doc/user/INSTALLATION.md) to download and install F3D!
+See the [gallery](https://f3d.app/gallery) for more images, take a look at the [changelog](doc/CHANGELOG.md) or go to the [download page](https://f3d.app/download) to download and install F3D!
 
-You can even use F3D directly in your [browser](https://f3d.app/web)!
+You can even use F3D directly in your [browser](https://f3d.app/viewer)!
 
 If you need any help or want to discuss with other F3D users and developers, head over to our [discord](https://discord.f3d.app).
 
@@ -40,26 +40,34 @@
 
 Optionally, append `--output=/path/to/img.png` to save the rendering into an image file.
 
-See the [Quickstart Guide](doc/user/QUICKSTART.md) for more information about getting started with F3D.
+See the [Quickstart Guide](doc/user/01-QUICKSTART.md) for more information about getting started with F3D.
 
 # Documentation
 
-- To get started, please take a look at the [user documentation](doc/user/README_USER.md).
+- To get started, please take a look at the [user documentation](doc/user/01-QUICKSTART.md).
 - If you need any help, are looking for a feature or found a bug, please open an [issue](https://github.com/f3d-app/f3d/issues).
-- If you want to use the libf3d, please take a look at its [documentation](doc/libf3d/README_LIBF3D.md).
-- If you want to build F3D, please take a look at the [developer documentation](doc/dev/README_DEV.md).
+- If you want to use the libf3d, please take a look at its [documentation](doc/libf3d/01-OVERVIEW.md).
+- If you want to build F3D, please take a look at the [contribution guide](CONTRIBUTING.md).
 
 # Support
 
 F3D needs your help!
 
 If you can, please consider sponsoring F3D. Even a small donation would help us offset the recurring maintenance costs.
-With enough sponsors we would be able to make F3D grow faster and stronger! Read more about it [here](doc/user/SPONSORING.md).
+With enough sponsors we would be able to make F3D grow faster and stronger! Read more about it [here](https://f3d.app/thanks).
 
-If you are an industry user of F3D and want to make sure it can keep growing and being maintained, [please reach out](doc/user/SPONSORING.md#industry-sponsors)!
+If you are an industry user of F3D and want to make sure it can keep growing and being maintained, [please reach out](https://f3d.app/thanks)!
 
 In any case, please star it on github and share the word about it!
 
+## Sponsors
+
+Many thanks to our sponsors for supporting F3D
+
+<a href="https://nlnet.nl/project/F3D/" target="_blank"><img src="https://nlnet.nl/image/logos/NGI0Core_tag.svg" height="45"/></a>
+<a href="https://www.lambdatest.com/?utm_source=f3d&utm_medium=sponsor" target="_blank"><img src="https://www.lambdatest.com/blue-logo.png" height="45" /></a>
+<a href="https://www.opendronemap.org/" target="_blank"><img src="https://f3d.app/assets/images/opendronemap-95d4ad6e24c091a06ec00e1828e1eb38.png" height="45" /></a>
+
 # Vision
 
 As a minimalist 3D viewer F3D aims to:
@@ -81,7 +89,7 @@
 
 # Contributing
 
-F3D as a community-driven, inclusive and beginner-friendly project. We love to see how the project is growing thanks to the contributions from the community. We would love to see your face in the list below! If you want to contribute to F3D, you are very welcome to! Take a look at our [contribution documentation](CONTRIBUTING.md), [governance documentation](doc/dev/GOVERNANCE.md) and [code of conduct](CODE_OF_CONDUCT.md).
+F3D as a community-driven, inclusive and beginner-friendly project. We love to see how the project is growing thanks to the contributions from the community. We would love to see your face in the list below! If you want to contribute to F3D, you are very welcome to! Take a look at our [contribution documentation](CONTRIBUTING.md), [governance documentation](doc/dev/10-GOVERNANCE.md) and [code of conduct](CODE_OF_CONDUCT.md).
 
 <a href="https://github.com/f3d-app/f3d/graphs/contributors">
   <img src="https://contrib.rocks/image?repo=f3d-app/f3d" />
@@ -89,10 +97,10 @@
 
 # Acknowledgments
 
-F3D was initially created by [Kitware SAS](https://www.kitware.eu/) and is relying on many awesome open source projects, including [VTK](https://vtk.org/), [OCCT](https://dev.opencascade.org/), [Assimp](https://www.assimp.org/), [Alembic](http://www.alembic.io/), [Draco](https://google.github.io/draco/), [OpenUSD](https://openusd.org/release/index.html), [OpenVDB](https://www.openvdb.org/), [OSPRay](https://www.ospray.org/) and [ImGui](https://github.com/ocornut/imgui/).
+F3D was initially created by [Kitware SAS](https://www.kitware.eu/), by Joachim Pouderoux, Michael Migliore and Mathieu Westphal, and is relying on many awesome open source projects, including [VTK](https://vtk.org/), [OCCT](https://dev.opencascade.org/), [Assimp](https://www.assimp.org/), [Alembic](http://www.alembic.io/), [Draco](https://google.github.io/draco/), [OpenUSD](https://openusd.org/release/index.html), [OpenVDB](https://www.openvdb.org/), [OSPRay](https://www.ospray.org/) and [ImGui](https://github.com/ocornut/imgui/).
 
 # License
 
 F3D can be used and distributed under the 3-Clause BSD License, see the [license](LICENSE.md).
-F3D integrate the sources of other libraries and tools, all under permissive licenses, see the [third party licenses](doc/THIRD_PARTY_LICENSES.md).
+F3D integrate the sources of other libraries and tools, all under permissive licenses, see the [third party licenses](THIRD_PARTY_LICENSES.md).
 F3D packages relies on other libraries and tools, all under permissive licenses, all listed in the respective packages.
diff -Nru f3d-3.2.0+dfsg/resources/app.f3d.F3D.metainfo.xml f3d-3.4.1/resources/app.f3d.F3D.metainfo.xml
--- f3d-3.2.0+dfsg/resources/app.f3d.F3D.metainfo.xml	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/resources/app.f3d.F3D.metainfo.xml	2026-01-08 08:41:55.000000000 +0200
@@ -14,33 +14,33 @@
         </p> 
     </description>
     <url type="homepage">https://f3d.app/</url>
-    <url type="help">https://f3d.app/doc/user/README_USER.html</url>
+    <url type="help">https://f3d.app/docs/next/user/QUICKSTART</url>
     <url type="bugtracker">https://github.com/f3d-app/f3d/issues</url>
-    <developer_name>Michael Migliore and Mathieu Westphal</developer_name>
+    <developer_name>F3D-APP Foundation</developer_name>
     <screenshots>
         <screenshot type="default">
-            <image>https://media.githubusercontent.com/media/f3d-app/f3d-media/95b76c22d927bb24759bbe0246b6260121f2933b/media/typical.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/typical.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735363-d2be23bb-90b6-4e90-941d-14b6dc24b476.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/realisticMask.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735370-4d1f067c-704c-4118-9d94-c1adf4039c39.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/step.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735376-3a476643-00d6-4cfc-9a88-e0dd33658564.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/realisticHelmet.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735272-5bcd3e7c-a333-41f5-8066-9b0bec9885e8.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/directScalars.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735378-b8be4df2-e030-4d13-b12f-24806cbb385b.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/cad.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735377-127fa5d8-ece5-40c7-8176-672279ebdacc.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/volume.png</image>
         </screenshot>
         <screenshot>
-            <image>https://user-images.githubusercontent.com/3129530/194735354-486999dd-3c57-4ab1-a9ad-b8e97bac0da0.png</image>
+            <image>https://media.githubusercontent.com/media/f3d-app/f3d-website/refs/heads/main/static/images/volumeMedical.png</image>
         </screenshot>
     </screenshots>
     <content_rating type="oars-1.1"/>
diff -Nru f3d-3.2.0+dfsg/resources/configs/config.d/05_all.json f3d-3.4.1/resources/configs/config.d/05_all.json
--- f3d-3.2.0+dfsg/resources/configs/config.d/05_all.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/resources/configs/config.d/05_all.json	2026-01-08 08:41:55.000000000 +0200
@@ -5,12 +5,14 @@
       "tone-mapping": true,
       "grid": true,
       "loading-progress": true,
-      "anti-aliasing": true,
+      "anti-aliasing": "fxaa",
       "filename": true,
+      "hdri-filename": true,
       "camera-direction": "-1,-0.5,-1",
       "hdri-ambient": true,
-      "translucency-support": true,
-      "animation-progress": true
+      "blending": "ddp",
+      "animation-progress": true,
+      "remove-empty-file-groups": true
     }
   }
 ]
diff -Nru f3d-3.2.0+dfsg/resources/configs/thumbnail.d/05_all.json f3d-3.4.1/resources/configs/thumbnail.d/05_all.json
--- f3d-3.2.0+dfsg/resources/configs/thumbnail.d/05_all.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/resources/configs/thumbnail.d/05_all.json	2026-01-08 08:41:55.000000000 +0200
@@ -1,15 +1,14 @@
 [
   {
     "options": {
-      "anti-aliasing": true,
-      "anti-aliasing-mode": "ssaa",
+      "anti-aliasing": "ssaa",
       "camera-direction": "-1,-0.5,-1",
       "hdri-ambient": true,
       "max-size": 100,
       "no-background": true,
       "verbose": "quiet",
       "tone-mapping": true,
-      "translucency-support": true
+      "blending": "ddp"
     }
   }
 ]
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/resources/logo-black.png and /tmp/gJkK3JzSDT/f3d-3.4.1/resources/logo-black.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/resources/logo-white.png and /tmp/gJkK3JzSDT/f3d-3.4.1/resources/logo-white.png differ
diff -Nru f3d-3.2.0+dfsg/_sass/custom/custom.scss f3d-3.4.1/_sass/custom/custom.scss
--- f3d-3.2.0+dfsg/_sass/custom/custom.scss	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/_sass/custom/custom.scss	1970-01-01 02:00:00.000000000 +0200
@@ -1,69 +0,0 @@
-kbd {
-    // TODO: use just the docs variables?
-    --kbd-color-background: #e6e1e8;
-    --kbd-color-border: #959396;
-    --kbd-color-text: #27262b;
-
-    // Background/text colors
-    background-color: var(--kbd-color-background);
-    color: var(--kbd-color-text);
-
-    // Border/shadow
-    border-radius: 0.25rem;
-    background-clip: padding-box;
-    border: 1px solid var(--kbd-color-border);
-    box-shadow: 0 2px 0 1px var(--kbd-color-border);
-
-    // Remove the text cursor
-    cursor: default;
-
-    // text style
-    font-size: 0.75em;
-    line-height: 1;
-    min-width: 0.75rem;
-    text-align: center;
-    padding: 2px 5px;
-    margin: 0 3px;
-    position: relative;
-    top: -1px;
-
-    // Push effect on hover
-    &:hover, li:hover & {
-        box-shadow: 0 1px 0 0.5px var(--kbd-color-border);
-        top: 1px;
-    }
-
-    // Transition is fun
-    transition: all 0.1s;
-}
-
-div.install-buttons {
-    text-align: center;
-    display: block;
-    line-height: 1;
-
-    .btn {
-        margin: .5em;
-    }
-}
-
-img.cm {
-    min-height: 10px;
-}
-
-div.popup-container {
-    position: fixed;
-    top: 25%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    width: 500px;
-    max-width: 75vw;
-    padding: 1em 1.5em;
-    box-shadow: 3px 2px 4px 2px rgba(0, 0, 0, 0.4);
-    border-radius: .5em;
-    
-    .btn {
-        display: block;
-        margin-left: auto;
-    }
-}
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/Test3DGaussiansSplatting.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/Test3DGaussiansSplatting.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/Test3DGaussiansSplattingStochastic.png f3d-3.4.1/testing/baselines/Test3DGaussiansSplattingStochastic.png
--- f3d-3.2.0+dfsg/testing/baselines/Test3DGaussiansSplattingStochastic.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/Test3DGaussiansSplattingStochastic.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:563ea9015c36a11fa434a210cdb53455d813d5810301b2a23777782b34363701
+size 156630
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/Test3DGSPLYHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/Test3DGSPLYHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/Test3DGSPLY.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/Test3DGSPLY.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/Test3DSImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/Test3DSImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/Test3MF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/Test3MF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestABCAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestABCAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestABCNonFaceVarying.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestABCNonFaceVarying.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestABC.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestABC.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimation.png f3d-3.4.1/testing/baselines/TestAlembicAnimation.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimation.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembicAnimation.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5976c3470a4af02022f1009bf4dfb49dfc0103c2686fc1de43fd05807d1291d2
+size 18687
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimationXForm.png f3d-3.4.1/testing/baselines/TestAlembicAnimationXForm.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimationXForm.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembicAnimationXForm.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f448a2d1812bec6187899c17fe1472078237354ee804bf2b9a3456460123e2a
+size 6484
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimationXFormRotation.png f3d-3.4.1/testing/baselines/TestAlembicAnimationXFormRotation.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembicAnimationXFormRotation.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembicAnimationXFormRotation.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef866f5a2f4c63102356be51a1d2963280f9fcb517ad33897dbdb6bd36d91372
+size 6540
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembicCurves.png f3d-3.4.1/testing/baselines/TestAlembicCurves.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembicCurves.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembicCurves.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db96b243b15bf35e0c48ac798520832ba900a5fda9716dd14ecf940cce2dee11
+size 19887
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembicNonFaceVarying.png f3d-3.4.1/testing/baselines/TestAlembicNonFaceVarying.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembicNonFaceVarying.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembicNonFaceVarying.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5e16d00ab66470e6ce87b164b8eb23c3a1a99cc82a36260835e59727b352e7e0
+size 20535
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAlembic.png f3d-3.4.1/testing/baselines/TestAlembic.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAlembic.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAlembic.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0cbbc3db73a79f1a33521443f05654ec0170171c8339f0430d4b7856adc35f86
+size 12548
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAMRDataSet.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAMRDataSet.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationAllAnimations.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationAllAnimations.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationAutoplay.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationAutoplay.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationGenericImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationGenericImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationGenericImporterPointSprites.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationGenericImporterPointSprites.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationGenericImporterScalarRange.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationGenericImporterScalarRange.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationIndexDeprecated.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationIndexDeprecated.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationIndicesMulti.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationIndicesMulti.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationIndicesSingle.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationIndicesSingle.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationInputChangeColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationInputChangeColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationInputChangePointSprites.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationInputChangePointSprites.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationMorphColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationMorphColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationSkinColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationSkinColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationTimeLimitsHigh.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationTimeLimitsHigh.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationTimeLimitsLow.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationTimeLimitsLow.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationUserMatrixColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationUserMatrixColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAnimationUserMatrixPointSprites.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAnimationUserMatrixPointSprites.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAssimpAnimationNegativeIndex.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAssimpAnimationNegativeIndex.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAssimpMetaDataImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAssimpMetaDataImporter.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAxesCustomColor.png f3d-3.4.1/testing/baselines/TestAxesCustomColor.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAxesCustomColor.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAxesCustomColor.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5d4295bd1346c54008ce4e85d2ae64277b97790070c1c5f4ac22a268e2cca16
+size 31131
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestAxesGridEnable.png f3d-3.4.1/testing/baselines/TestAxesGridEnable.png
--- f3d-3.2.0+dfsg/testing/baselines/TestAxesGridEnable.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestAxesGridEnable.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8b287e77525f75008c4c9464aada60e9101e08635aea6fd9ade7f466a3a2f56
+size 30268
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestAxis.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestAxis.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityMedium.png f3d-3.4.1/testing/baselines/TestBackdropOpacityMedium.png
--- f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityMedium.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestBackdropOpacityMedium.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f298e533066c3031232cde76dd6bab149c363d9816cd5f6b586e1451b2e8f7e
+size 27906
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityOpaque.png f3d-3.4.1/testing/baselines/TestBackdropOpacityOpaque.png
--- f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityOpaque.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestBackdropOpacityOpaque.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:88e9833fce85d8d977166f951581a6779c8816698cbe1b05ea238d1f06b2398f
+size 27913
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityTransparent.png f3d-3.4.1/testing/baselines/TestBackdropOpacityTransparent.png
--- f3d-3.2.0+dfsg/testing/baselines/TestBackdropOpacityTransparent.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestBackdropOpacityTransparent.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c72dc07dd4dd977990800a6c7e59cc82238012a7676d12f12518b99b26cebdf3
+size 27677
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBackfaceHidden.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBackfaceHidden.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBackfaceVisible.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBackfaceVisible.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBadRef.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBadRef.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBinaryBREP.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBinaryBREP.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBREPDefines.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBREPDefines.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestBREP.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestBREP.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraClipping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraClipping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraConfiguration.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraConfiguration.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraDirection.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraDirection.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraIndexConfiguration.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraIndexConfiguration.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraOrthographic.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraOrthographic.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraOrtho.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraOrtho.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraPersp.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraPersp.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCameraZoomFactor.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCameraZoomFactor.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCityGML.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCityGML.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCollinearVectors.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCollinearVectors.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorDiscretization.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorDiscretization.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorMap16bits.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorMap16bits.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorMap32bits.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorMap32bits.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorMapFileFullPath.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorMapFileFullPath.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorMapFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorMapFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColormap.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColormap.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestColorMapStem.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestColorMapStem.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptAlias.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCommandScriptAlias.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptAzimuth.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCommandScriptAzimuth.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptBasic.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCommandScriptBasic.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptElevation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCommandScriptElevation.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToFirstFrame.png f3d-3.4.1/testing/baselines/TestCommandScriptJumpToFirstFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToFirstFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptJumpToFirstFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12961dfdbbbfc97c83611ade91b57960c5ba2f5f5da2b223d1dadd4346273d61
+size 23574
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToLastFrame.png f3d-3.4.1/testing/baselines/TestCommandScriptJumpToLastFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToLastFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptJumpToLastFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:35ba2ad708a7bf2930f681330a3a1459bac2262db9f62e04f366447f3b0d8cc3
+size 32505
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToMiddleFrame.png f3d-3.4.1/testing/baselines/TestCommandScriptJumpToMiddleFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToMiddleFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptJumpToMiddleFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5c8558925b2d1af7f1e5d0b89917fb5dabf8da3b006ec5667bfec88bdb50fa07
+size 29825
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToNextFrame.png f3d-3.4.1/testing/baselines/TestCommandScriptJumpToNextFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToNextFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptJumpToNextFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e5f26bd871959d17cf987650659a3e77b0fb6ab041096666c5c8651b0b215d04
+size 29276
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToPreviousFrame.png f3d-3.4.1/testing/baselines/TestCommandScriptJumpToPreviousFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptJumpToPreviousFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptJumpToPreviousFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02ca2de3dd22e5b7dd233112264d04888b74d3b5af873ecf150e4c2a65e554c9
+size 20945
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptRemoveCurrentFileGroup.png f3d-3.4.1/testing/baselines/TestCommandScriptRemoveCurrentFileGroup.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptRemoveCurrentFileGroup.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptRemoveCurrentFileGroup.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:291b05f7bc74ec965988f498e09b54ef8b9771937e10102f93ecc3e38de3c243
+size 23061
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptRemoveFileGroups.png f3d-3.4.1/testing/baselines/TestCommandScriptRemoveFileGroups.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptRemoveFileGroups.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCommandScriptRemoveFileGroups.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:052c0c327a6db7c0eef4b4409db2d9596268556833e95d34c6e4841e73ab8bb9
+size 7698
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestCommandScriptReset.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestCommandScriptReset.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestComponentName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestComponentName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigFileBuild.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigFileBuild.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigFileHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigFileHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigFileMultiFileSTL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigFileMultiFileSTL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigFileMultiFileVTP.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigFileMultiFileVTP.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigFileUpperCase.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigFileUpperCase.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigImperative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigImperative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigOrder.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigOrder.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigReset.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigReset.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigResolution.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigResolution.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConfigStemBuild.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConfigStemBuild.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConsoleBadgeError.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConsoleBadgeError.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConsoleBadgeQuiet.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConsoleBadgeQuiet.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestConsoleBadgeWarning.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestConsoleBadgeWarning.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestCustomOptionsNone.png f3d-3.4.1/testing/baselines/TestCustomOptionsNone.png
--- f3d-3.2.0+dfsg/testing/baselines/TestCustomOptionsNone.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestCustomOptionsNone.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bafa0183864345a260c056f406f8f13ab78179b0adcda37e75473d581a4fcc4
+size 6972
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDAEAnimationLights.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDAEAnimationLights.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDAE.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDAE.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAlembic.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAlembic.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAndCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAndCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAssimpDAE.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAssimpDAE.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAssimpDXF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAssimpDXF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAssimpFBX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAssimpFBX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileAssimpOFF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileAssimpOFF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileDraco.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileDraco.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileExodus.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileExodus.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileHDRIFilename.png f3d-3.4.1/testing/baselines/TestDefaultConfigFileHDRIFilename.png
--- f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileHDRIFilename.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestDefaultConfigFileHDRIFilename.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a337e90780f1b8b9d19174fa0c8cb35e35f2486e32cb6a1909251004308c60ae
+size 128662
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileNetCDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileNetCDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileOCCT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileOCCT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFilePLY.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFilePLY.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileQuakeMDL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileQuakeMDL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileSPLAT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileSPLAT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileSPZ.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileSPZ.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileSTL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileSTL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileTIFF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileTIFF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileUSD.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileUSD.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileVDB.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileVDB.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileVTI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileVTI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileVTKHDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileVTKHDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigFileVTU.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigFileVTU.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigRemoveEmptyFileGroups.png f3d-3.4.1/testing/baselines/TestDefaultConfigRemoveEmptyFileGroups.png
--- f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigRemoveEmptyFileGroups.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestDefaultConfigRemoveEmptyFileGroups.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c3376ff725a62e6a50acd52ccfee8af07930b709b5b9d4c45428de4ea1102580
+size 45002
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefaultConfigTranslucent.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefaultConfigTranslucent.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDefines.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDefines.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDepthPeeling.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDepthPeeling.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDepthPeelingToneMapping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDepthPeelingToneMapping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDicom.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDicom.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDRACOColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDRACOColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDRACO.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDRACO.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestDXF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestDXF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestEdges.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestEdges.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestEmbeddedTextureFBX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestEmbeddedTextureFBX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestExactMatch.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestExactMatch.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestExodusConfig.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestExodusConfig.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestExodus.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestExodus.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestF3DRenderer.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestF3DRenderer.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBX16bits.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBX16bits.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXAnimationCamera.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXAnimationCamera.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXAnimationLights.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXAnimationLights.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXBoneAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXBoneAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXBone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXBone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXNormalMapping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXNormalMapping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFBXSkinningAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFBXSkinningAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFilenameCommasSpaces.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFilenameCommasSpaces.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestFilenameHDRIFilename.png f3d-3.4.1/testing/baselines/TestFilenameHDRIFilename.png
--- f3d-3.2.0+dfsg/testing/baselines/TestFilenameHDRIFilename.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestFilenameHDRIFilename.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64354a705d45a8a098d5e4209f8f56c88b21864aa36da2ec8a4b4e60a2fd1eb5
+size 141442
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFilename.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFilename.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFilenameWhiteBg.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFilenameWhiteBg.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFinalShaderNegativeFileName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFinalShaderNegativeFileName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFinalShaderNegative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFinalShaderNegative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFont.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFont.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFontScale2.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFontScale2.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestFontScale3.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestFontScale3.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestForceReaderGLTFDraco.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestForceReaderGLTFDraco.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestForceReaderPass.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestForceReaderPass.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGenRef.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGenRef.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFDracoImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFDracoImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFDracoImporterWithoutCompression.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFDracoImporterWithoutCompression.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFImporterUnlit.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFImporterUnlit.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFImporterWithAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFImporterWithAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFMorph.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFMorph.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFRigArmatureNoArmature.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFRigArmatureNoArmature.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFRigArmature.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFRigArmature.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFRigArmatureSphereTube.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFRigArmatureSphereTube.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFRigArmatureWithOpacity.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFRigArmatureWithOpacity.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFSkin.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFSkin.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGLTFURI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGLTFURI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridAbsolute.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridAbsolute.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridClipping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridClipping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridColor.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridColor.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridOptions.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridUp000.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridUp000.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridUp100.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridUp100.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridUp123.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridUp123.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridWithDepthPeeling.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridWithDepthPeeling.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridY.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridY.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestGridZ.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestGridZ.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRI8Bit.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRI8Bit.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIAmbientOnlyNoBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIAmbientOnlyNoBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIAmbientOnly.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIAmbientOnly.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurCoCLarge.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurCoCLarge.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurCoCMedium.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurCoCMedium.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurCoCNegative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurCoCNegative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurCoCSmall.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurCoCSmall.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurCoCZero.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurCoCZero.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlur.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlur.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIBlurRatio.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIBlurRatio.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRICache.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRICache.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIDefault.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIDefault.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIEdges.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIEdges.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIEXR.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIEXR.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestHDRIFilenameDefault.png f3d-3.4.1/testing/baselines/TestHDRIFilenameDefault.png
--- f3d-3.2.0+dfsg/testing/baselines/TestHDRIFilenameDefault.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestHDRIFilenameDefault.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cdcc87251868fc2094161426e6e340af9009636b237295b6b9765c90f0934f18
+size 24330
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestHDRIFilename.png f3d-3.4.1/testing/baselines/TestHDRIFilename.png
--- f3d-3.2.0+dfsg/testing/baselines/TestHDRIFilename.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestHDRIFilename.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:048518d23b213114626f6a65f371ffdae9dc50e9ca810982ac13b4d37afdc9a5
+size 90757
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRINone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRINone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIOrient.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIOrient.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIRaytracingAmbientOnlyNoBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIRaytracingAmbientOnlyNoBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIRaytracingAmbientOnly.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIRaytracingAmbientOnly.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIRaytracingNone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIRaytracingNone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIRaytracing.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIRaytracing.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIRaytracingSkyboxOnly.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIRaytracingSkyboxOnly.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRISkyboxOnly.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRISkyboxOnly.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIToneMapping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIToneMapping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHDRIToneMappingSSAA.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHDRIToneMappingSSAA.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEColorMapFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEColorMapFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEConfig.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEConfig.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEFontFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEFontFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEInput.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEInput.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMEInteractionDropHDRICollapse.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMEInteractionDropHDRICollapse.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestHOMETexture.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestHOMETexture.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestIGESDefines.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestIGESDefines.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestIGES.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestIGES.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionActors.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionActors.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionAndCLIBlending.png f3d-3.4.1/testing/baselines/TestInteractionAndCLIBlending.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionAndCLIBlending.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionAndCLIBlending.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:43f54a98e3bfb9a79ef63db148b68776195c71546ed664cf5c973aa5ec0028a1
+size 7202
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationBackward.png f3d-3.4.1/testing/baselines/TestInteractionAnimationBackward.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationBackward.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionAnimationBackward.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27cc6bc8c480c8d1550d83899f6e071d86fdf9f76db1b6f97598faf2ebaaf99c
+size 2295
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationCameraMovement.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationCameraMovement.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationCycleAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationCycleAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationCycleAnimationSingle.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationCycleAnimationSingle.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationDropHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationDropHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationFast.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationFast.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationFBXBone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationFBXBone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationFrameRate.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationFrameRate.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationIndexDeprecatedCycleAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationIndexDeprecatedCycleAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationInvert.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationInvert.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionAnimationSlow.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionAnimationSlow.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionAxesGridToggle.png f3d-3.4.1/testing/baselines/TestInteractionAxesGridToggle.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionAxesGridToggle.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionAxesGridToggle.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:681f63e2f2a3dec912950ed08d6201deaf1dfe6a53fdaa0fabadf396ae650da6
+size 30182
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCameraHotkeys.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCameraHotkeys.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCameraUpdate.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCameraUpdate.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetAnimationName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetAnimationName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetBlackBG.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetBlackBG.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetCentered.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetCentered.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetConfigFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetConfigFile.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetCycle.png f3d-3.4.1/testing/baselines/TestInteractionCheatsheetCycle.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetCycle.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionCheatsheetCycle.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f0a5c3d622790457c510d3e4dbf6b476c88e95a01ccc7fb824c8efda8fcd477b
+size 107595
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetMultiModifierBinding.png f3d-3.4.1/testing/baselines/TestInteractionCheatsheetMultiModifierBinding.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetMultiModifierBinding.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionCheatsheetMultiModifierBinding.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d7271f9076dc1d160abd5f8348c34ed1ed6cc14347ec4753dcf4331dd11c1d1
+size 78771
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetNoFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetNoFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetOpacity.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetOpacity.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheet.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheet.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetScalarsNoArray.png f3d-3.4.1/testing/baselines/TestInteractionCheatsheetScalarsNoArray.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetScalarsNoArray.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionCheatsheetScalarsNoArray.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b383763d010cbeeb3414afc432d33e2e76e58da382a8edadf987c598c40b76a
+size 73357
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetScalars.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetScalars.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetSSAA.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetSSAA.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCheatsheetWhiteBG.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCheatsheetWhiteBG.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileAndCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileAndCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileBindings.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileBindings.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileImperativeNoData.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileImperativeNoData.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileImperative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileImperative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileMulti.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileMulti.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileNoColorBar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileNoColorBar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConfigFileOptional.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConfigFileOptional.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteCamera.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteCamera.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteCamera.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteCamera.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:269835844616655ef05a637222e4f73a5dfdd32440fa56dc2f37b1eb059fe780
+size 8730
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteCandidates.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteCandidates.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteColoring.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteColoring.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteColoring.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteColoring.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ce18b7cbe4f4875dd2eab782bb5a3920a42a57e9e509f6838a19a1f506aa3595
+size 17140
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:153814d04cee4e49f042e6a84c5222a405968ed3953b4c888d4236d2bc35a0e3
+size 13646
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:561e70d9af06e1bda31323659c08b7e625d1a7977d12c4ce8f5acb014a31c042
+size 13583
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseSensitive.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseSensitive.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseSensitive.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteDirCaseSensitive.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cbd0701f4470a565842202a367c99e99191b0bf4b89d527236159107b5ee083a
+size 8982
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteFilesystem.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteFilesystem.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteFilesystem.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteFilesystem.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfb4dce4426507d32245c5058f4471e498ac578aaf2fffb6aa7fa78e189b9617
+size 18695
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteHelp.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteHelp.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteHelp.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteHelp.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78b8df51989956082b2799549063e83fdb42eea960cd5c06ab6354ea0317b2da
+size 8730
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteHide.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteHide.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteNoMatch.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteNoMatch.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoComplete.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoComplete.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteReaderOptions.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteReaderOptions.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteReaderOptions.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteReaderOptions.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:56cf9c87dd900e83f2a7c831ebf62bccd39d15b4e167056ddd3d1541058aed49
+size 11017
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteSetDisplayCompl.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteSetDisplayCompl.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteSetDisplayCompl.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteSetDisplayCompl.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a44e9b3692a11e7320e673b41810df10010b2cb6af40279e4c599530ec308f59
+size 30904
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteSetOptions.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteSetOptions.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteSetOptions.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteSetOptions.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:19796d887390f74c6383aa95d7bb7c2f8033f060d0a433c8b112011c6cdfae2f
+size 62717
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteToggleOptions.png f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteToggleOptions.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleAutoCompleteToggleOptions.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleAutoCompleteToggleOptions.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a170ed5617f96c56f04bada070a261979de47e504778039c7bfb12839b89ed53
+size 43922
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleCamera.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleCamera.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleClear.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleClear.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleEmptyCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleEmptyCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleHistoryEmpty.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleHistoryEmpty.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleHistoryHide.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleHistoryHide.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleHistory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleHistory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleInvalidCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleInvalidCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleInvalidOption.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleInvalidOption.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleLoadNext.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleLoadNext.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleOpen.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleOpen.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleOpenWarningClick.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleOpenWarningClick.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleOpenWarningKeyboard.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleOpenWarningKeyboard.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleOverCheatSheet.png f3d-3.4.1/testing/baselines/TestInteractionConsoleOverCheatSheet.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleOverCheatSheet.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionConsoleOverCheatSheet.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6202b5b590b3315b6f33d98fead7e6607aefc50aba76df6d6554577659ac6036
+size 5081
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleReload.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleReload.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleRender.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleRender.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleScrollbar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleScrollbar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleToggleGrid.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleToggleGrid.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionConsoleTypingSceneInfo.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionConsoleTypingSceneInfo.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionCorrectCameraForVolumeSwitch.png f3d-3.4.1/testing/baselines/TestInteractionCorrectCameraForVolumeSwitch.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionCorrectCameraForVolumeSwitch.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionCorrectCameraForVolumeSwitch.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a9421ed9dc6922703dda1d5c545f20c4a89200040768471ee9e85b22c6fcf48
+size 23390
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleBlending.png f3d-3.4.1/testing/baselines/TestInteractionCycleBlending.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleBlending.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionCycleBlending.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4e12b09b65c0567c2bad02fec701b513f909b4ff44595de30aee158d4d1590f
+size 26002
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleCellInvalidIndex.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCycleCellInvalidIndex.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleCell.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCycleCell.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleComp.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCycleComp.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleScalarsCompCheck.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCycleScalarsCompCheck.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionCycleScalars.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionCycleScalars.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDirectoryEmpty.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDirectoryEmpty.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDirectoryLoop.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDirectoryLoop.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDirectory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDirectory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDragRotateVertical.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDragRotateVertical.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropFileBackslashes.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropFileBackslashes.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropFiles.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropFiles.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropHDRIExr.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropHDRIExr.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropHDRIInvert.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropHDRIInvert.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropHDRIModifiers.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropHDRIModifiers.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropHDRIMulti.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropHDRIMulti.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionDropSameFiles.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionDropSameFiles.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionDynamicUpDirection.png f3d-3.4.1/testing/baselines/TestInteractionDynamicUpDirection.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionDynamicUpDirection.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionDynamicUpDirection.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:730038141ae7b856e656347ae26a70ed43d09363f23dfe32b8875c5ec1e1df4f
+size 22643
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionElevationCameraRotation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionElevationCameraRotation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionFocalPointPickingDefault.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionFocalPointPickingDefault.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionFocalPointPickingPoints.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionFocalPointPickingPoints.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionFocalPointPickingShift.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionFocalPointPickingShift.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIBlur.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIBlur.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRICache.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRICache.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIChange.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIChange.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIFullFromNone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIFullFromNone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRILoop.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRILoop.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIMove.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIMove.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRaytracingFullFromNone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRaytracingFullFromNone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRaytracingLoop.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRaytracingLoop.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRaytracingRemoveAmbient.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRaytracingRemoveAmbient.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRaytracingRemoveBoth.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRaytracingRemoveBoth.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRaytracingRemoveSkybox.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRaytracingRemoveSkybox.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIReload.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIReload.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRemoveAmbient.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRemoveAmbient.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRemoveBoth.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRemoveBoth.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionHDRIRemoveSkybox.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionHDRIRemoveSkybox.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionInitUpVectorOption.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionInitUpVectorOption.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionInvalidFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionInvalidFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionInvertZoom.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionInvertZoom.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionLightIntensity.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionLightIntensity.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionLoadParentDirectory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionLoadParentDirectory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleBadgeRemains.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleBadgeRemains.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleEscape.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleEscape.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleInvalidCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleInvalidCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleOpen.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleOpen.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleOverCheatSheetAndFilename.png f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleOverCheatSheetAndFilename.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleOverCheatSheetAndFilename.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleOverCheatSheetAndFilename.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1033a1b0bcf06399789ba225fb12585fbc6ffdca523e8572eb6fdd33c9fc4ca
+size 5097
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleOverCheatSheet.png f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleOverCheatSheet.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleOverCheatSheet.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleOverCheatSheet.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1033a1b0bcf06399789ba225fb12585fbc6ffdca523e8572eb6fdd33c9fc4ca
+size 5097
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleResize.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleResize.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleReturn.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleReturn.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMinimalConsoleValidCommand.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMinimalConsoleValidCommand.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMultiFileColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMultiFileColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMultiFileDrop.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMultiFileDrop.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMultiFileLoadParentDirectory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMultiFileLoadParentDirectory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionMultiFileVolume.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionMultiFileVolume.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionNoModelScrollBar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionNoModelScrollBar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionNoModelScrollWheel.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionNoModelScrollWheel.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionOpacity.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionOpacity.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionOrthographicProjection.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionOrthographicProjection.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionPanWithShift.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionPanWithShift.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionPointCloud.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionPointCloud.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionPointCloudTAA.png f3d-3.4.1/testing/baselines/TestInteractionPointCloudTAA.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionPointCloudTAA.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionPointCloudTAA.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9cc29a194c927c2b6ca4f81418878f7876bc4b5d72f297eb0682bb6e13db257
+size 34975
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionPointSpritesCycle.png f3d-3.4.1/testing/baselines/TestInteractionPointSpritesCycle.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionPointSpritesCycle.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionPointSpritesCycle.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:467f04da82eb3f7af5cda71acd6627fde9cac8f3c4ca209d067e48fd0b355196
+size 21429
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionPostFX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionPostFX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionRaytracingDenoise.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionRaytracingDenoise.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionReload.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionReload.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionResetCamera.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionResetCamera.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionResetCameraWithCameraIndex.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionResetCameraWithCameraIndex.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionRollCameraRotation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionRollCameraRotation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionRotateCamera90.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionRotateCamera90.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionRotateCameraMinus90.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionRotateCameraMinus90.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionStochasticTAA.png f3d-3.4.1/testing/baselines/TestInteractionStochasticTAA.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionStochasticTAA.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionStochasticTAA.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:55a0020a1a60fb0805e2477439594a06121ba1cbfe6f9204a35cf65c8d4ae3eb
+size 30595
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionSwitchFileNextCameraKeeping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionSwitchFileNextCameraKeeping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionSwitchFileNoCameraKeeping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionSwitchFileNoCameraKeeping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionSwitchFilePrevCameraKeeping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionSwitchFilePrevCameraKeeping.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAADolly.png f3d-3.4.1/testing/baselines/TestInteractionTAADolly.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAADolly.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionTAADolly.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b4d596e4a6c851aa1979444f65305c706ab19765ba043387a14138fc01a626b
+size 19443
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAAMiddleClick.png f3d-3.4.1/testing/baselines/TestInteractionTAAMiddleClick.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAAMiddleClick.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionTAAMiddleClick.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3cc0217bbbed7609639abac9ad3efa292dab8de0656a66eec99e5e528398b68b
+size 24927
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAA.png f3d-3.4.1/testing/baselines/TestInteractionTAA.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInteractionTAA.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInteractionTAA.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb64c21c32d4d84e5fae50f4c0e896c07212652f491bb04a1852cd5788d113a1
+size 25587
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionTensorsCycleComp.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionTensorsCycleComp.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionTrackballBind.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionTrackballBind.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionTrackball.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionTrackball.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionVerticalDragRotate.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionVerticalDragRotate.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionVolumeAfterColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionVolumeAfterColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionVolumeCycle.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionVolumeCycle.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionVolumeInverse.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionVolumeInverse.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionZoomToggleOrthographicProjection.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionZoomToggleOrthographicProjection.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInteractionZoomToMouse.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInteractionZoomToMouse.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestInvalidFileFileNameEmpty.png f3d-3.4.1/testing/baselines/TestInvalidFileFileNameEmpty.png
--- f3d-3.2.0+dfsg/testing/baselines/TestInvalidFileFileNameEmpty.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestInvalidFileFileNameEmpty.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2ad971223b9f76caf77f02eff93fd23c47823c5096e39ff75c2af2ac3f61621b
+size 10022
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestInvalidFileFileName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestInvalidFileFileName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLightIntensityBrighterFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLightIntensityBrighterFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLightIntensityBrighter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLightIntensityBrighter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLightIntensityDarkerFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLightIntensityDarkerFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLightIntensityDarker.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLightIntensityDarker.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLineWidthFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLineWidthFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestLineWidth.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestLineWidth.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMatchFirstConfigGlobOptionBlock.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMatchFirstConfigGlobOptionBlock.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMatchSecondConfigGlobOptionBlock.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMatchSecondConfigGlobOptionBlock.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMaterialFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMaterialFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMaterial.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMaterial.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMaxBaseIOR.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMaxBaseIOR.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMaxSizeAboveMultiFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMaxSizeAboveMultiFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMaxSizeBelow.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMaxSizeBelow.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMetaDataImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMetaDataImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMetaData.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMetaData.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMHD.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMHD.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMinBaseIOR.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMinBaseIOR.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMorphColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMorphColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiblockMetaData.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiblockMetaData.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationAnySingle.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationAnySingle.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationIndices.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationIndices.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationMultiUnique.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationMultiUnique.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationNoAnimationSupport.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationNoAnimationSupport.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationNoneMulti.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationNoneMulti.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileAnimationUniqueUnique.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileAnimationUniqueUnique.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileCameraIndex.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileCameraIndex.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileColoringTexture.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileColoringTexture.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileInvalidFilesFileName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileInvalidFilesFileName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileInvalid.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileInvalid.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileMetaData.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileMetaData.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileModeDir.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileModeDir.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileNonCoherentComponentNames.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileNonCoherentComponentNames.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFilePositionals.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFilePositionals.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRecursive.png f3d-3.4.1/testing/baselines/TestMultiFileRecursive.png
--- f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRecursive.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestMultiFileRecursive.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e2b095f14f9b270f1e1c2129f8122dfbc070d7723c28083872601365c2c93124
+size 3752
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRegexA.png f3d-3.4.1/testing/baselines/TestMultiFileRegexA.png
--- f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRegexA.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestMultiFileRegexA.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb04868416dc0c92a72569f0a3bcd7be52ca20777e50146a8c52cbfce04be461
+size 7596
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRegexB.png f3d-3.4.1/testing/baselines/TestMultiFileRegexB.png
--- f3d-3.2.0+dfsg/testing/baselines/TestMultiFileRegexB.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestMultiFileRegexB.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a86a986cfa28e7a3604ea79f51000dc409cd2406dbeb4ec81607c3940a0a929d
+size 8529
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestMultiFileUnsupportedFilesFileName.png f3d-3.4.1/testing/baselines/TestMultiFileUnsupportedFilesFileName.png
--- f3d-3.2.0+dfsg/testing/baselines/TestMultiFileUnsupportedFilesFileName.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestMultiFileUnsupportedFilesFileName.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:173b74f25c20a8f1a3ff714a8abe5138f47726bb9f1063fa96d262ab4502edfc
+size 23397
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiFileVolume.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiFileVolume.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiInputArg.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiInputArg.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestMultiInputMultiArgs.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestMultiInputMultiArgs.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNetCDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNetCDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNoBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNoBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNoConfigWithConfig.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNoConfigWithConfig.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNoFileEmptyFileName.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNoFileEmptyFileName.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNoFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNoFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNonExistentFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNonExistentFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestNRRD.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestNRRD.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestOBJ.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestOBJ.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestOBJWebP.png f3d-3.4.1/testing/baselines/TestOBJWebP.png
--- f3d-3.2.0+dfsg/testing/baselines/TestOBJWebP.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestOBJWebP.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:427fd3b1067fea277c7a85a080805c9a25ccdd7d0dc1871ef6db866ba49b9f17
+size 39686
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestOBJWithTGATexture.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestOBJWithTGATexture.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestOFF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestOFF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPLY.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPLY.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloudBar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloudBar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloudDefaultScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloudDefaultScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloudFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloudFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloud.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloud.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloudUG.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloudUG.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPointCloudVolume.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPointCloudVolume.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPTS.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPTS.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPythonCompareWithFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPythonCompareWithFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPythonSceneMemory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPythonSceneMemory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestPythonScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestPythonScene.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestPythonSceneRedLight.png f3d-3.4.1/testing/baselines/TestPythonSceneRedLight.png
--- f3d-3.2.0+dfsg/testing/baselines/TestPythonSceneRedLight.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestPythonSceneRedLight.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:65a6c6b10811d0ab4b592f5bab06be4d875dd520fde971a9ce1bd6ae7e9e17fe
+size 3012
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLActorCollection.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLActorCollection.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationBetween.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationBetween.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationGroupFrame.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationGroupFrame.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationLastFrame.png f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationLastFrame.png
--- f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationLastFrame.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationLastFrame.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c717ada4a66cb43061493e8c5b19976cff3b9117f030f6aee2ed24d329f81eff
+size 13173
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationMulti.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationMulti.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLAnimationSimpleFrame.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLAnimationSimpleFrame.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLDisableAnimation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLDisableAnimation.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLGroupSkin.png f3d-3.4.1/testing/baselines/TestQuakeMDLGroupSkin.png
--- f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLGroupSkin.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestQuakeMDLGroupSkin.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3db647329f6aa6b860e3d4895efd7c2da2019a7d6624f96dd40370d650910963
+size 10959
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLSkinIndexNegative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLSkinIndexNegative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLSkinIndexNonInteger.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLSkinIndexNonInteger.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLSkinIndexOutOfBounds.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLSkinIndexOutOfBounds.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLSkinIndexOverflow.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLSkinIndexOverflow.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestQuakeMDLSkinIndex.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestQuakeMDLSkinIndex.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingDefaultConfigFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingDefaultConfigFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingDenoise.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingDenoise.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingGLTF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingGLTF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingNoBackground.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingNoBackground.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingNoDenoise.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingNoDenoise.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingPointCloud.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingPointCloud.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingScalarBar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingScalarBar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRaytracingThumbnailConfigFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRaytracingThumbnailConfigFile.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestRemoveEmptyFileGroups.png f3d-3.4.1/testing/baselines/TestRemoveEmptyFileGroups.png
--- f3d-3.2.0+dfsg/testing/baselines/TestRemoveEmptyFileGroups.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestRemoveEmptyFileGroups.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c9b640f999174147c28d834bcb6045992932634bebfabd77cadbd8d156221c21
+size 17210
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRemoveFileGroups.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRemoveFileGroups.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRenderingBackendAuto.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRenderingBackendAuto.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRenderingBackendEGL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRenderingBackendEGL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRenderingBackendGLX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRenderingBackendGLX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRenderingBackendOSMesa.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRenderingBackendOSMesa.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestRenderingBackendWGL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestRenderingBackendWGL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestScalarsCell.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestScalarsCell.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestScalars.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestScalars.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestScalarsRange.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestScalarsRange.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestScalarsWithBarContinuity.png f3d-3.4.1/testing/baselines/TestScalarsWithBarContinuity.png
--- f3d-3.2.0+dfsg/testing/baselines/TestScalarsWithBarContinuity.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestScalarsWithBarContinuity.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62d8b326d394b159c1c0e6feac681ce0e23563650e432e1315cb3e973b0da6e7
+size 32760
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestScalarsWithBar.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestScalarsWithBar.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKCompareWithFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKCompareWithFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKConsole.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKConsole.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDeprecatedOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDeprecatedOptions.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKDropZoneDeprecated.png f3d-3.4.1/testing/baselines/TestSDKDropZoneDeprecated.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKDropZoneDeprecated.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKDropZoneDeprecated.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bc755be2bc07a5d8219e88a02553a592773a2ff59846d0b06e11c1fa5e4aa81
+size 6344
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKDropZoneNoLogo.png f3d-3.4.1/testing/baselines/TestSDKDropZoneNoLogo.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKDropZoneNoLogo.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKDropZoneNoLogo.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e9c52d006df78999a295ee8f340641d5085b4aeef54dbfbc49672279c7753ff0
+size 6283
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDropZone.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDropZone.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicBackgrounColor.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicBackgrounColor.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicFontFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicFontFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicHDRIDefault.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicHDRIDefault.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicHDRIExr.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicHDRIExr.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicHDRI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicHDRI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicLightIntensity-5x-brighter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicLightIntensity-5x-brighter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicLightIntensity-5x-darker.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicLightIntensity-5x-darker.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicLightIntensity-default.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicLightIntensity-default.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKDynamicProperties.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKDynamicProperties.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowCOCOA.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowCOCOA.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowEGL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowEGL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowGLFW.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowGLFW.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowGLX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowGLX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowOSMesa.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowOSMesa.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowQT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowQT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKExternalWindowWGL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKExternalWindowWGL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKImage16.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKImage16.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKImage.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKImage.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractionDirectory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractionDirectory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractorCallBackDefaultAgain.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractorCallBackDefaultAgain.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractorCallBackDefault.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractorCallBackDefault.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractorCallBackModifiedAgain.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractorCallBackModifiedAgain.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractorCallBackModified.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractorCallBackModified.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKInteractorDropFullScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKInteractorDropFullScene.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKMultiColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKMultiColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKMultiOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKMultiOptions.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKRenderFinalShaderNegative.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKRenderFinalShaderNegative.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKRenderFinalShaderVignette.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKRenderFinalShaderVignette.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKSceneFromMemory.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKSceneFromMemory.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKScene.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKScene.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKSceneRedLight.png f3d-3.4.1/testing/baselines/TestSDKSceneRedLight.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKSceneRedLight.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKSceneRedLight.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:913b4bf81ee025b506bb72725362c717921117f100a2b7b22c57cfee89d1eab7
+size 2881
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsKeyPressMod.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsKeyPressMod.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsKeyPressMod.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsKeyPressMod.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3cb09e6c8b76a0cec625f39c858a8203b0a6c1ab9a06cbd1a19032106a7ce00f
+size 16866
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsKeyPress.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsKeyPress.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsKeyPress.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsKeyPress.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6cd67be44fd72b3b55afdde9510062b9951f96e61ebd681f04ac7673f76bacc2
+size 47054
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMousePan.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMousePan.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMousePan.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMousePan.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:635642efe13140fb0d6a0245a856df345b45f30c4e8a0df785a1aa828ef7527e
+size 17043
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMouseRotate.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMouseRotate.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMouseRotate.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMouseRotate.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f6f7604482429497e881908259d00a0e2f49bbcd4da9e0f76bc3af7c613cce2
+size 14464
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMouseZoom.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMouseZoom.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsMouseZoom.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsMouseZoom.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a1c1633258e81684e3acad0b444b0c6e69d1af0cd14a3e4caa45692da14f0fd5
+size 9202
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsTextInput.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsTextInput.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsTextInput.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsTextInput.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54c3d9336fb3cf46eb737fcf2cdeaac1659868b0f57d44edc659d51091eb2112
+size 17779
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsWheelBackward.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsWheelBackward.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsWheelBackward.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsWheelBackward.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a07f3783d09a1425684775ca7f5fb6fca388c9ddeff5137400987a7d6a2d2b45
+size 14920
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsWheelForward.png f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsWheelForward.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKTriggerInteractionsWheelForward.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKTriggerInteractionsWheelForward.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0cb0eb352a814dea40e66b02e3ac325f7ef79731cc4c6c21fc82221b5fd89a21
+size 22272
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSDKUIAnimationProgressColor.png f3d-3.4.1/testing/baselines/TestSDKUIAnimationProgressColor.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSDKUIAnimationProgressColor.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSDKUIAnimationProgressColor.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:178fd2204059648f0f3b462ed7a85daafd5718a65a9ef8a35da8f8174edfb40d
+size 1035
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSDKWindowStandard.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSDKWindowStandard.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSkinColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSkinColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSkinningManyBones.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSkinningManyBones.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSPLAT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSPLAT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree0.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSPZDegree0.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree1.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSPZDegree1.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree1Stochastic.png f3d-3.4.1/testing/baselines/TestSPZDegree1Stochastic.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree1Stochastic.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSPZDegree1Stochastic.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f5b747552c4f9158252f96610139617b31a763477932ea736babfe5627245c3
+size 62567
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree2.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSPZDegree2.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSPZDegree3.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSPZDegree3.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestSPZVersion3.png f3d-3.4.1/testing/baselines/TestSPZVersion3.png
--- f3d-3.2.0+dfsg/testing/baselines/TestSPZVersion3.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestSPZVersion3.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bf7282926ecfb4d5aa10f4a9fcb0c7cb7d7585cce3501b2d89f64088789e612
+size 21660
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSSAO.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSSAO.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSTEPDefines.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSTEPDefines.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSTEP.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSTEP.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestSTL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestSTL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureColor.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureColor.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureColorWithOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureColorWithOptions.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureEmissive.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureEmissive.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureMatCap.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureMatCap.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureMatCapWithEdges.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureMatCapWithEdges.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureMatCapWithTCoords.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureMatCapWithTCoords.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureMaterial.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureMaterial.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureMaterialWithOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureMaterialWithOptions.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextureNormal.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextureNormal.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTextures.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTextures.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTexturesTransformGL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTexturesTransformGL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTexturesTransform.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTexturesTransform.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTGATextureFBX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTGATextureFBX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileAlembic.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileAlembic.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileAssimpDAE.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileAssimpDAE.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileAssimpDXF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileAssimpDXF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileAssimpFBX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileAssimpFBX.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileAssimpOFF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileAssimpOFF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileDraco.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileDraco.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileExodus.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileExodus.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileNetCDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileNetCDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileOCCT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileOCCT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFilePLY.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFilePLY.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileQuakeMDL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileQuakeMDL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileSPLAT.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileSPLAT.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileSPZ.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileSPZ.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileSTL.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileSTL.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileUSD.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileUSD.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileVDB.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileVDB.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileVTI.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileVTI.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileVTKHDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileVTKHDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestThumbnailConfigFileVTU.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestThumbnailConfigFileVTU.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTIFF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTIFF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTimeRangeLessThanZeroNegativeAnimationTime.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTimeRangeLessThanZeroNegativeAnimationTime.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTimeRangeLessThanZeroNoAnimationTime.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTimeRangeLessThanZeroNoAnimationTime.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestTimeRangeLessThanZeroWithAnimationTime.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestTimeRangeLessThanZeroWithAnimationTime.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestToneMapping.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestToneMapping.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUnsupportedFile.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUnsupportedFile.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUpDirectionNoSign.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUpDirectionNoSign.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDAGlyphs.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDAGlyphs.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDAInstancing.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDAInstancing.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDAPrimitives.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDAPrimitives.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDAPrimitivesZAxis.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDAPrimitivesZAxis.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDInterpolation.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDInterpolation.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSD.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSD.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDPurpose.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDPurpose.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDTeapot.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDTeapot.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDZAnimated.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDZAnimated.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDZMaterials.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDZMaterials.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDZMemEXR.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDZMemEXR.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUSDZRigged.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUSDZRigged.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUserMatrixColoring.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUserMatrixColoring.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestUTF8.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestUTF8.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVDBCommandScriptReaderOptions.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVDBCommandScriptReaderOptions.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVDBDefinesDownsamplingFactor.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVDBDefinesDownsamplingFactor.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVDBPoints.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVDBPoints.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVDBVolume.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVDBVolume.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVerboseMultiFileCompare.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVerboseMultiFileCompare.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVerboseWarning.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVerboseWarning.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeCells.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeCells.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeColoringArray.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeColoringArray.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeComp.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeComp.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeDirect.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeDirect.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeInverse.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeInverse.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolumeMag.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolumeMag.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVolume.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVolume.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVRMLImporter.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVRMLImporter.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTICell.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTICell.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestVTKHDFPartitionedDataSetCollection.png f3d-3.4.1/testing/baselines/TestVTKHDFPartitionedDataSetCollection.png
--- f3d-3.2.0+dfsg/testing/baselines/TestVTKHDFPartitionedDataSetCollection.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestVTKHDFPartitionedDataSetCollection.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:34c023255995d46ac79558a3f86b20d8b875a32a11379b3ba4ccfcd9b382a00c
+size 25991
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTKHDF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTKHDF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTK.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTK.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTM.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTM.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTP.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTP.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTR.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTR.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTS.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTS.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestVTU.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestVTU.png differ
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestWasmAnimation.png f3d-3.4.1/testing/baselines/TestWasmAnimation.png
--- f3d-3.2.0+dfsg/testing/baselines/TestWasmAnimation.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestWasmAnimation.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a30ceaa8886625797a79d1a0d6e0552adbb6728d1b491b7aa16b52077d29abd2
+size 11979
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestWasmCamera.png f3d-3.4.1/testing/baselines/TestWasmCamera.png
--- f3d-3.2.0+dfsg/testing/baselines/TestWasmCamera.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestWasmCamera.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:309b673a58499d3ee65ca81ac27b1d2ec7a2b4fbb44d20d1951ce92df60eea14
+size 42431
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestWasmInteraction.png f3d-3.4.1/testing/baselines/TestWasmInteraction.png
--- f3d-3.2.0+dfsg/testing/baselines/TestWasmInteraction.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestWasmInteraction.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54646d78614adc1a14a12d053748da225eda888d49693243a8f440f99eff3061
+size 11866
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestWasmOptions.png f3d-3.4.1/testing/baselines/TestWasmOptions.png
--- f3d-3.2.0+dfsg/testing/baselines/TestWasmOptions.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestWasmOptions.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93fec13ecad9312c379fc4ed45d8dddd9668d8867cdf031c6bf481bc4fc72f49
+size 29397
diff -Nru f3d-3.2.0+dfsg/testing/baselines/TestWasmSplats.png f3d-3.4.1/testing/baselines/TestWasmSplats.png
--- f3d-3.2.0+dfsg/testing/baselines/TestWasmSplats.png	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/baselines/TestWasmSplats.png	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4e1241e53f3369f898f3a0fba0a93a49744aa58c4e1a7d1ada01e91b8c5a0328
+size 156489
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestWinUserProfileNonStandard.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestWinUserProfileNonStandard.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestXBFDefines.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestXBFDefines.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestXBF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestXBF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestXCAFColors.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestXCAFColors.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestXCAFColorsXBF.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestXCAFColorsXBF.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestXDG_CONFIG_HOME.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestXDG_CONFIG_HOME.png differ
Binary files /tmp/F9lVeNRpm1/f3d-3.2.0+dfsg/testing/baselines/TestX.png and /tmp/gJkK3JzSDT/f3d-3.4.1/testing/baselines/TestX.png differ
diff -Nru f3d-3.2.0+dfsg/testing/configs/match_type_no_match.json f3d-3.4.1/testing/configs/match_type_no_match.json
--- f3d-3.2.0+dfsg/testing/configs/match_type_no_match.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/configs/match_type_no_match.json	2026-01-08 08:41:55.000000000 +0200
@@ -2,7 +2,7 @@
   {
     "match-type": "glob",
     "options": {
-      "anti-aliasing": true
+      "anti-aliasing": "fxaa"
     }
   }
 ]
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionAndCLIBlending.log f3d-3.4.1/testing/recordings/TestInteractionAndCLIBlending.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionAndCLIBlending.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionAndCLIBlending.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,19 @@
+# StreamVersion 1.2
+ConfigureEvent 1267 1374 0 0 0 0 0
+ExposeEvent 0 1406 0 0 0 0 0
+RenderEvent 0 1406 0 0 0 0 0
+
+# P
+KeyPressEvent -465 475 0 112 1 p 0
+CharEvent -465 475 0 112 1 p 0
+KeyReleaseEvent -465 475 0 112 1 p 0
+
+# P
+KeyPressEvent -465 475 0 112 1 p 0
+CharEvent -465 475 0 112 1 p 0
+KeyReleaseEvent -465 475 0 112 1 p 0
+
+# Right
+KeyPressEvent -465 475 0 0 1 Right 0
+CharEvent -465 475 0 0 1 Right 0
+KeyReleaseEvent -465 475 0 0 1 Right 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionAnimationBackward.log f3d-3.4.1/testing/recordings/TestInteractionAnimationBackward.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionAnimationBackward.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionAnimationBackward.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,20 @@
+# StreamVersion 1.2
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+
+# Ctrl + Shift + Space
+KeyPressEvent 1006 -1296 0 0 1 Control_L 0
+KeyPressEvent 1006 -1296 2 0 1 Shift_L 0
+KeyPressEvent 1006 -1296 3 0 1 space 0
+TimerEvent 1006 -1296 3 0 1 space 0
+TimerEvent 1006 -1296 3 0 1 space 0
+TimerEvent 1006 -1296 3 0 1 space 0
+TimerEvent 1006 -1296 3 0 1 space 0
+TimerEvent 1006 -1296 3 0 1 Shift_L 0
+TimerEvent 1006 -1296 2 0 1 Control_L 0
+TimerEvent 1006 -1296 2 0 1 Control_L 0
+TimerEvent 1006 -1296 2 0 1 Control_L 0
+TimerEvent 1006 -1296 2 0 1 Control_L 0
+KeyReleaseEvent 1006 -1296 3 0 1 space 0
+KeyReleaseEvent 1006 -1296 3 0 1 Shift_L 0
+KeyReleaseEvent 1006 -1296 2 0 1 Control_L 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionAxesGridToggle.log f3d-3.4.1/testing/recordings/TestInteractionAxesGridToggle.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionAxesGridToggle.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionAxesGridToggle.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,9 @@
+# StreamVersion 1.2
+ExposeEvent 0 599 0 0 0 0 0
+RenderEvent 0 599 0 0 0 0 0
+KeyPressEvent 923 -249 0 0 1 Shift_L 0
+CharEvent 923 -249 0 0 1 Shift_L 0
+KeyPressEvent 923 -249 1 88 1 X 0
+CharEvent 923 -249 1 88 1 X 0
+KeyReleaseEvent 923 -249 1 88 1 X 0
+KeyReleaseEvent 923 -249 1 0 1 Shift_L 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetConfigFile.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetConfigFile.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetConfigFile.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetConfigFile.log	2026-01-08 08:41:55.000000000 +0200
@@ -21,3 +21,19 @@
 MouseWheelBackwardEvent 91 150 0 0 0 h 0
 MouseWheelBackwardEvent 91 150 0 0 0 h 0
 MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetCycle.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetCycle.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetCycle.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetCycle.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,28 @@
+# StreamVersion 1.1
+ExposeEvent 0 599 0 0 0 0
+RenderEvent 0 599 0 0 0 0
+
+# H
+KeyPressEvent 738 539 0 104 1 h
+CharEvent 738 539 0 104 1 h
+KeyReleaseEvent 738 539 0 104 1 h
+
+# A
+KeyPressEvent 738 539 0 104 1 a
+CharEvent 738 539 0 104 1 a
+KeyReleaseEvent 738 539 0 104 1 a
+
+# A
+KeyPressEvent 738 539 0 104 1 a
+CharEvent 738 539 0 104 1 a
+KeyReleaseEvent 738 539 0 104 1 a
+
+# P
+KeyPressEvent 738 539 0 104 1 p
+CharEvent 738 539 0 104 1 p
+KeyReleaseEvent 738 539 0 104 1 p
+
+# O
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheet.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheet.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheet.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheet.log	1970-01-01 02:00:00.000000000 +0200
@@ -1,6 +0,0 @@
-# StreamVersion 1.1
-ExposeEvent 0 599 0 0 0 0
-RenderEvent 0 599 0 0 0 0
-KeyPressEvent 738 539 0 104 1 h
-CharEvent 738 539 0 104 1 h
-KeyReleaseEvent 738 539 0 104 1 h
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetMultiModifierBinding.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetMultiModifierBinding.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetMultiModifierBinding.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetMultiModifierBinding.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,34 @@
+# StreamVersion 1.1
+ExposeEvent 0 150 0 0 0 0
+RenderEvent 0 150 0 0 0 0
+
+# H
+KeyPressEvent 0 150 0 104 1 h
+CharEvent 0 150 0 104 1 h
+KeyReleaseEvent 0 150 0 104 1 h
+
+# Move hover cheatsheet
+MouseMoveEvent 91 150 0 0 0 h 0
+
+# Scroll down to max
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
+MouseWheelBackwardEvent 91 150 0 0 1 h 0
+MouseWheelBackwardEvent 91 150 0 0 0 h 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetOpacity.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetOpacity.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetOpacity.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetOpacity.log	2026-01-08 08:41:55.000000000 +0200
@@ -16,3 +16,14 @@
 MouseWheelBackwardEvent 91 350 0 0 0 h 0
 MouseWheelBackwardEvent 91 350 0 0 1 h 0
 MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 1 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 1 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 1 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 1 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
+MouseWheelBackwardEvent 91 350 0 0 1 h 0
+MouseWheelBackwardEvent 91 350 0 0 0 h 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetScalars.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetScalars.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetScalars.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetScalars.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,15 +1,23 @@
 # StreamVersion 1.1
 ExposeEvent 0 599 0 0 0 0
 RenderEvent 0 599 0 0 0 0
+
+# H
 KeyPressEvent 738 539 0 104 1 h
 CharEvent 738 539 0 104 1 h
 KeyReleaseEvent 738 539 0 104 1 h
+
+# S
 KeyPressEvent -772 549 0 115 1 s
 CharEvent -772 549 0 115 1 s
 KeyReleaseEvent -772 549 0 115 1 s
+
+# S
 KeyPressEvent -772 549 0 115 1 s
 CharEvent -772 549 0 115 1 s
 KeyReleaseEvent -772 549 0 115 1 s
+
+# S
 KeyPressEvent -772 549 0 115 1 s
 CharEvent -772 549 0 115 1 s
 KeyReleaseEvent -772 549 0 115 1 s
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetScalarsNoArray.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetScalarsNoArray.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetScalarsNoArray.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetScalarsNoArray.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,8 @@
+# StreamVersion 1.1
+ExposeEvent 0 599 0 0 0 0
+RenderEvent 0 599 0 0 0 0
+
+# H
+KeyPressEvent 738 539 0 104 1 h
+CharEvent 738 539 0 104 1 h
+KeyReleaseEvent 738 539 0 104 1 h
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetSSAA.log f3d-3.4.1/testing/recordings/TestInteractionCheatsheetSSAA.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCheatsheetSSAA.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionCheatsheetSSAA.log	1970-01-01 02:00:00.000000000 +0200
@@ -1,13 +0,0 @@
-# StreamVersion 1.1
-ExposeEvent 0 599 0 0 0 0
-RenderEvent 0 599 0 0 0 0
-KeyPressEvent 738 539 0 104 1 h
-CharEvent 738 539 0 104 1 h
-KeyReleaseEvent 738 539 0 104 1 h
-MouseMoveEvent 91 350 0 0 0 h 0
-KeyPressEvent 738 539 0 104 1 a
-CharEvent 738 539 0 104 1 a
-KeyReleaseEvent 738 539 0 104 1 a
-KeyPressEvent 738 539 0 104 1 a
-CharEvent 738 539 0 104 1 a
-KeyReleaseEvent 738 539 0 104 1 a
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConfigFileAndCommand.log f3d-3.4.1/testing/recordings/TestInteractionConfigFileAndCommand.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConfigFileAndCommand.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionConfigFileAndCommand.log	2026-01-08 08:41:55.000000000 +0200
@@ -3,35 +3,56 @@
 ExposeEvent 0 1406 0 0 0 0 0
 RenderEvent 0 1406 0 0 0 0 0
 TimerEvent 0 1406 0 0 0 0 0
+
+# O
 KeyPressEvent 345 106 0 111 1 o 0
 CharEvent 345 106 0 111 1 o 0
 TimerEvent 345 106 0 111 1 o 0
 KeyReleaseEvent 345 106 0 111 1 o 0
 TimerEvent 345 106 0 111 1 o 0
+
+# O
+KeyPressEvent 345 106 0 111 1 o 0
+CharEvent 345 106 0 111 1 o 0
+TimerEvent 345 106 0 111 1 o 0
+KeyReleaseEvent 345 106 0 111 1 o 0
+TimerEvent 345 106 0 111 1 o 0
+
+# X
 KeyPressEvent 345 106 0 120 1 x 0
 CharEvent 345 106 0 120 1 x 0
 TimerEvent 345 106 0 120 1 x 0
 KeyReleaseEvent 345 106 0 120 1 x 0
 TimerEvent 345 106 0 120 1 x 0
+
+# Right
 KeyPressEvent 345 106 0 0 1 Right 0
 CharEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
 KeyReleaseEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
+
+# N
 KeyPressEvent 345 106 0 110 1 n 0
 CharEvent 345 106 0 110 1 n 0
 TimerEvent 345 106 0 110 1 n 0
 KeyReleaseEvent 345 106 0 110 1 n 0
 TimerEvent 345 106 0 110 1 n 0
+
+# Right
 KeyPressEvent 345 106 0 0 1 Right 0
 CharEvent 345 106 0 0 1 Right 0
 KeyReleaseEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
+
+# Right
 KeyPressEvent 345 106 0 0 1 Right 0
 CharEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
 KeyReleaseEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
+
+# Right
 KeyPressEvent 345 106 0 0 1 Right 0
 CharEvent 345 106 0 0 1 Right 0
 TimerEvent 345 106 0 0 1 Right 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteCamera.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteCamera.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteCamera.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteCamera.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,45 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 115 1 s 0
+CharEvent 199 142 0 115 1 s 0
+KeyReleaseEvent 199 142 0 115 1 s 0
+
+KeyPressEvent 199 142 0 101 1 e 0
+CharEvent 199 142 0 101 1 e 0
+KeyReleaseEvent 199 142 0 101 1 e 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 95 1 underscore 0
+CharEvent 199 142 0 95 1 underscore 0
+KeyReleaseEvent 199 142 0 95 1 underscore 0
+
+KeyPressEvent 199 142 0 99 1 c 0
+CharEvent 199 142 0 99 1 c 0
+KeyReleaseEvent 199 142 0 99 1 c 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 105 1 i 0
+CharEvent 199 142 0 105 1 i 0
+KeyReleaseEvent 199 142 0 105 1 i 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteColoring.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteColoring.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteColoring.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteColoring.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,41 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 99 1 c 0
+CharEvent 199 142 0 99 1 c 0
+KeyReleaseEvent 199 142 0 99 1 c 0
+
+KeyPressEvent 199 142 0 121 1 y 0
+CharEvent 199 142 0 121 1 y 0
+KeyReleaseEvent 199 142 0 121 1 y 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 99 1 c 0
+CharEvent 199 142 0 99 1 c 0
+KeyReleaseEvent 199 142 0 99 1 c 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveLowercaseF.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,83 @@
+# StreamVersion 1.2
+TimerEvent 0 0 0 0 0 0 0
+# Esc
+KeyPressEvent 2260 481 0 27 1 Escape 0
+CharEvent 2260 481 0 27 1 Escape 0
+TimerEvent 2260 481 0 27 1 Escape 0
+KeyReleaseEvent 2260 481 0 27 1 Escape 0
+TimerEvent 2260 481 0 27 1 Escape 0
+# a
+KeyPressEvent 2260 481 0 97 1 a 0
+CharEvent 2260 481 0 97 1 a 0
+TimerEvent 2260 481 0 97 1 a 0
+KeyReleaseEvent 2260 481 0 97 1 a 0
+TimerEvent 2260 481 0 97 1 a 0
+# d
+KeyPressEvent 2260 481 0 100 1 d 0
+CharEvent 2260 481 0 100 1 d 0
+TimerEvent 2260 481 0 100 1 d 0
+KeyReleaseEvent 2260 481 0 100 1 d 0
+TimerEvent 2260 481 0 100 1 d 0
+# d
+KeyPressEvent 2260 481 0 100 1 d 0
+CharEvent 2260 481 0 100 1 d 0
+TimerEvent 2260 481 0 100 1 d 0
+KeyReleaseEvent 2260 481 0 100 1 d 0
+TimerEvent 2260 481 0 100 1 d 0
+# Shift+_
+KeyPressEvent 2260 481 1 0 1 Shift_L 0
+TimerEvent 2260 481 1 0 1 Shift_L 0
+KeyPressEvent 2260 481 1 95 1 underscore 0
+CharEvent 2260 481 1 95 1 underscore 0
+TimerEvent 2260 481 1 95 1 underscore 0
+KeyReleaseEvent 2260 481 1 95 1 underscore 0
+TimerEvent 2260 481 1 95 1 underscore 0
+KeyReleaseEvent 2260 481 0 0 1 Shift_L 0
+TimerEvent 2260 481 0 0 1 Shift_L 0
+# f
+KeyPressEvent 2260 481 0 102 1 f 0
+CharEvent 2260 481 0 102 1 f 0
+TimerEvent 2260 481 0 102 1 f 0
+KeyReleaseEvent 2260 481 0 102 1 f 0
+TimerEvent 2260 481 0 102 1 f 0
+# i
+KeyPressEvent 2260 481 0 105 1 i 0
+CharEvent 2260 481 0 105 1 i 0
+TimerEvent 2260 481 0 105 1 i 0
+KeyReleaseEvent 2260 481 0 105 1 i 0
+TimerEvent 2260 481 0 105 1 i 0
+# l
+KeyPressEvent 2260 481 0 108 1 l 0
+CharEvent 2260 481 0 108 1 l 0
+TimerEvent 2260 481 0 108 1 l 0
+# e
+KeyPressEvent 2260 481 0 101 1 e 0
+CharEvent 2260 481 0 101 1 e 0
+KeyReleaseEvent 2260 481 0 108 1 l 0
+TimerEvent 2260 481 0 108 1 l 0
+KeyReleaseEvent 2260 481 0 101 1 e 0
+TimerEvent 2260 481 0 101 1 e 0
+# s
+KeyPressEvent 2260 481 0 115 1 s 0
+CharEvent 2260 481 0 115 1 s 0
+TimerEvent 2260 481 0 115 1 s 0
+KeyReleaseEvent 2260 481 0 115 1 s 0
+TimerEvent 2260 481 0 115 1 s 0
+# Space
+KeyPressEvent 2260 481 0 32 1 space 0
+CharEvent 2260 481 0 32 1 space 0
+TimerEvent 2260 481 0 32 1 space 0
+KeyReleaseEvent 2260 481 0 32 1 space 0
+TimerEvent 2260 481 0 32 1 space 0
+# f
+KeyPressEvent 2260 481 0 102 1 f 0
+CharEvent 2260 481 0 102 1 f 0
+TimerEvent 2260 481 0 102 1 f 0
+KeyReleaseEvent 2260 481 0 102 1 f 0
+TimerEvent 2260 481 0 102 1 f 0
+# Tab
+KeyPressEvent 2260 481 0 9 1 Tab 0
+CharEvent 2260 481 0 9 1 Tab 0
+TimerEvent 2260 481 0 9 1 Tab 0
+KeyReleaseEvent 2260 481 0 9 1 Tab 0
+TimerEvent 2260 481 0 9 1 Tab 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseInsensitiveUppercaseF.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,87 @@
+# StreamVersion 1.2
+TimerEvent 0 0 0 0 0 0 0
+# Esc
+KeyPressEvent 2238 568 0 27 1 Escape 0
+CharEvent 2238 568 0 27 1 Escape 0
+TimerEvent 2238 568 0 27 1 Escape 0
+KeyReleaseEvent 2238 568 0 27 1 Escape 0
+TimerEvent 2238 568 0 27 1 Escape 0
+# a
+KeyPressEvent 2238 568 0 97 1 a 0
+CharEvent 2238 568 0 97 1 a 0
+TimerEvent 2238 568 0 97 1 a 0
+KeyReleaseEvent 2238 568 0 97 1 a 0
+TimerEvent 2238 568 0 97 1 a 0
+# d
+KeyPressEvent 2238 568 0 100 1 d 0
+CharEvent 2238 568 0 100 1 d 0
+TimerEvent 2238 568 0 100 1 d 0
+KeyReleaseEvent 2238 568 0 100 1 d 0
+TimerEvent 2238 568 0 100 1 d 0
+# d
+KeyPressEvent 2238 568 0 100 1 d 0
+CharEvent 2238 568 0 100 1 d 0
+TimerEvent 2238 568 0 100 1 d 0
+KeyReleaseEvent 2238 568 0 100 1 d 0
+TimerEvent 2238 568 0 100 1 d 0
+# Shift+_
+KeyPressEvent 2238 568 1 0 1 Shift_L 0
+TimerEvent 2238 568 1 0 1 Shift_L 0
+KeyPressEvent 2238 568 1 95 1 underscore 0
+CharEvent 2238 568 1 95 1 underscore 0
+TimerEvent 2238 568 1 95 1 underscore 0
+KeyReleaseEvent 2238 568 1 95 1 underscore 0
+TimerEvent 2238 568 1 95 1 underscore 0
+KeyReleaseEvent 2238 568 0 0 1 Shift_L 0
+TimerEvent 2238 568 0 0 1 Shift_L 0
+# f
+KeyPressEvent 2238 568 0 102 1 f 0
+CharEvent 2238 568 0 102 1 f 0
+TimerEvent 2238 568 0 102 1 f 0
+KeyReleaseEvent 2238 568 0 102 1 f 0
+# i
+KeyPressEvent 2238 568 0 105 1 i 0
+CharEvent 2238 568 0 105 1 i 0
+TimerEvent 2238 568 0 105 1 i 0
+KeyReleaseEvent 2238 568 0 105 1 i 0
+TimerEvent 2238 568 0 105 1 i 0
+# l
+KeyPressEvent 2238 568 0 108 1 l 0
+CharEvent 2238 568 0 108 1 l 0
+TimerEvent 2238 568 0 108 1 l 0
+# e
+KeyPressEvent 2238 568 0 101 1 e 0
+CharEvent 2238 568 0 101 1 e 0
+KeyReleaseEvent 2238 568 0 108 1 l 0
+TimerEvent 2238 568 0 108 1 l 0
+KeyReleaseEvent 2238 568 0 101 1 e 0
+TimerEvent 2238 568 0 101 1 e 0
+# s
+KeyPressEvent 2238 568 0 115 1 s 0
+CharEvent 2238 568 0 115 1 s 0
+TimerEvent 2238 568 0 115 1 s 0
+KeyReleaseEvent 2238 568 0 115 1 s 0
+TimerEvent 2238 568 0 115 1 s 0
+# Space
+KeyPressEvent 2238 568 0 32 1 space 0
+CharEvent 2238 568 0 32 1 space 0
+TimerEvent 2238 568 0 32 1 space 0
+KeyReleaseEvent 2238 568 0 32 1 space 0
+TimerEvent 2238 568 0 32 1 space 0
+# Shift+F
+KeyPressEvent 2238 568 1 0 1 Shift_L 0
+TimerEvent 2238 568 1 0 1 Shift_L 0
+TimerEvent 2238 568 1 0 1 Shift_L 0
+KeyPressEvent 2238 568 1 70 1 F 0
+CharEvent 2238 568 1 70 1 F 0
+TimerEvent 2238 568 1 70 1 F 0
+KeyReleaseEvent 2238 568 0 0 1 Shift_L 0
+TimerEvent 2238 568 0 0 1 Shift_L 0
+KeyReleaseEvent 2238 568 0 102 1 f 0
+TimerEvent 2238 568 0 102 1 f 0
+# Tab
+KeyPressEvent 2238 568 0 9 1 Tab 0
+CharEvent 2238 568 0 9 1 Tab 0
+TimerEvent 2238 568 0 9 1 Tab 0
+KeyReleaseEvent 2238 568 0 9 1 Tab 0
+TimerEvent 2238 568 0 9 1 Tab 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseSensitive.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseSensitive.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseSensitive.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteDirCaseSensitive.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,87 @@
+# StreamVersion 1.2
+TimerEvent 0 0 0 0 0 0 0
+# Esc
+KeyPressEvent 1429 -80 0 27 1 Escape 0
+CharEvent 1429 -80 0 27 1 Escape 0
+TimerEvent 1429 -80 0 27 1 Escape 0
+KeyReleaseEvent 1429 -80 0 27 1 Escape 0
+TimerEvent 1429 -80 0 27 1 Escape 0
+# a
+KeyPressEvent 1429 -80 0 97 1 a 0
+CharEvent 1429 -80 0 97 1 a 0
+TimerEvent 1429 -80 0 97 1 a 0
+KeyReleaseEvent 1429 -80 0 97 1 a 0
+TimerEvent 1429 -80 0 97 1 a 0
+# d
+KeyPressEvent 1429 -80 0 100 1 d 0
+CharEvent 1429 -80 0 100 1 d 0
+TimerEvent 1429 -80 0 100 1 d 0
+KeyReleaseEvent 1429 -80 0 100 1 d 0
+TimerEvent 1429 -80 0 100 1 d 0
+# d
+KeyPressEvent 1429 -80 0 100 1 d 0
+CharEvent 1429 -80 0 100 1 d 0
+TimerEvent 1429 -80 0 100 1 d 0
+KeyReleaseEvent 1429 -80 0 100 1 d 0
+TimerEvent 1429 -80 0 100 1 d 0
+# Shift+_
+KeyPressEvent 1429 -80 1 0 1 Shift_L 0
+TimerEvent 1429 -80 1 0 1 Shift_L 0
+KeyPressEvent 1429 -80 1 95 1 underscore 0
+CharEvent 1429 -80 1 95 1 underscore 0
+TimerEvent 1429 -80 1 95 1 underscore 0
+KeyReleaseEvent 1429 -80 0 0 1 Shift_L 0
+TimerEvent 1429 -80 0 0 1 Shift_L 0
+KeyReleaseEvent 1429 -80 0 45 1 minus 0
+TimerEvent 1429 -80 0 45 1 minus 0
+# f
+KeyPressEvent 1429 -80 0 102 1 f 0
+CharEvent 1429 -80 0 102 1 f 0
+TimerEvent 1429 -80 0 102 1 f 0
+KeyReleaseEvent 1429 -80 0 102 1 f 0
+TimerEvent 1429 -80 0 102 1 f 0
+# i
+KeyPressEvent 1429 -80 0 105 1 i 0
+CharEvent 1429 -80 0 105 1 i 0
+TimerEvent 1429 -80 0 105 1 i 0
+KeyReleaseEvent 1429 -80 0 105 1 i 0
+TimerEvent 1429 -80 0 105 1 i 0
+# l
+KeyPressEvent 1429 -80 0 108 1 l 0
+CharEvent 1429 -80 0 108 1 l 0
+TimerEvent 1429 -80 0 108 1 l 0
+# e
+KeyPressEvent 1429 -80 0 101 1 e 0
+CharEvent 1429 -80 0 101 1 e 0
+TimerEvent 1429 -80 0 101 1 e 0
+KeyReleaseEvent 1429 -80 0 108 1 l 0
+TimerEvent 1429 -80 0 108 1 l 0
+# s
+KeyPressEvent 1429 -80 0 115 1 s 0
+CharEvent 1429 -80 0 115 1 s 0
+KeyReleaseEvent 1429 -80 0 101 1 e 0
+TimerEvent 1429 -80 0 101 1 e 0
+KeyReleaseEvent 1429 -80 0 115 1 s 0
+TimerEvent 1429 -80 0 115 1 s 0
+# Space
+KeyPressEvent 1429 -80 0 32 1 space 0
+CharEvent 1429 -80 0 32 1 space 0
+TimerEvent 1429 -80 0 32 1 space 0
+KeyReleaseEvent 1429 -80 0 32 1 space 0
+TimerEvent 1429 -80 0 32 1 space 0
+# Shift+F
+KeyPressEvent 1429 -80 1 0 1 Shift_L 0
+TimerEvent 1429 -80 1 0 1 Shift_L 0
+KeyPressEvent 1429 -80 1 70 1 F 0
+CharEvent 1429 -80 1 70 1 F 0
+TimerEvent 1429 -80 1 70 1 F 0
+KeyReleaseEvent 1429 -80 0 0 1 Shift_L 0
+TimerEvent 1429 -80 0 0 1 Shift_L 0
+KeyReleaseEvent 1429 -80 0 102 1 f 0
+TimerEvent 1429 -80 0 102 1 f 0
+# Tab
+KeyPressEvent 1429 -80 0 9 1 Tab 0
+CharEvent 1429 -80 0 9 1 Tab 0
+TimerEvent 1429 -80 0 9 1 Tab 0
+KeyReleaseEvent 1429 -80 0 9 1 Tab 0
+TimerEvent 1429 -80 0 9 1 Tab 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteFilesystemError.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteFilesystemError.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteFilesystemError.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteFilesystemError.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,1489 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 100 1 d 0
+CharEvent 199 142 0 100 1 d 0
+KeyReleaseEvent 199 142 0 100 1 d 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 102 1 f 0
+CharEvent 199 142 0 102 1 f 0
+KeyReleaseEvent 199 142 0 102 1 f 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 47 1 slash 0
+CharEvent 199 142 0 47 1 slash 0
+KeyReleaseEvent 199 142 0 47 1 slash 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteFilesystem.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteFilesystem.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteFilesystem.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteFilesystem.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,101 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 97 1 a 0
+CharEvent 199 142 0 97 1 a 0
+KeyReleaseEvent 199 142 0 97 1 a 0
+
+KeyPressEvent 199 142 0 100 1 d 0
+CharEvent 199 142 0 100 1 d 0
+KeyReleaseEvent 199 142 0 100 1 d 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 102 1 f 0
+CharEvent 199 142 0 102 1 f 0
+KeyReleaseEvent 199 142 0 102 1 f 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+KeyPressEvent 199 142 0 46 1 period 0
+CharEvent 199 142 0 46 1 period 0
+KeyReleaseEvent 199 142 0 46 1 period 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 100 1 d 0
+CharEvent 199 142 0 100 1 d 0
+KeyReleaseEvent 199 142 0 100 1 d 0
+
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+KeyPressEvent 199 142 0 46 1 period 0
+CharEvent 199 142 0 46 1 period 0
+KeyReleaseEvent 199 142 0 46 1 period 0
+
+KeyPressEvent 199 142 0 46 1 period 0
+CharEvent 199 142 0 46 1 period 0
+KeyReleaseEvent 199 142 0 46 1 period 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 46 1 period 0
+CharEvent 199 142 0 46 1 period 0
+KeyReleaseEvent 199 142 0 46 1 period 0
+
+KeyPressEvent 199 142 0 46 1 period 0
+CharEvent 199 142 0 46 1 period 0
+KeyReleaseEvent 199 142 0 46 1 period 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 84 1 T 0
+CharEvent 199 142 0 84 1 T 0
+KeyReleaseEvent 199 142 0 84 1 T 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 100 1 d 0
+CharEvent 199 142 0 100 1 d 0
+KeyReleaseEvent 199 142 0 100 1 d 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteHelp.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteHelp.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteHelp.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteHelp.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,29 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 104 1 h 0
+CharEvent 199 142 0 104 1 h 0
+KeyReleaseEvent 199 142 0 104 1 h 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 104 1 h 0
+CharEvent 199 142 0 104 1 h 0
+KeyReleaseEvent 199 142 0 104 1 h 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteReaderOptions.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteReaderOptions.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteReaderOptions.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteReaderOptions.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,45 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 115 1 s 0
+CharEvent 199 142 0 115 1 s 0
+KeyReleaseEvent 199 142 0 115 1 s 0
+
+KeyPressEvent 199 142 0 101 1 e 0
+CharEvent 199 142 0 101 1 e 0
+KeyReleaseEvent 199 142 0 101 1 e 0
+
+KeyPressEvent 199 142 0 116 1 t 0
+CharEvent 199 142 0 116 1 t 0
+KeyReleaseEvent 199 142 0 116 1 t 0
+
+KeyPressEvent 199 142 0 95 1 underscore 0
+CharEvent 199 142 0 95 1 underscore 0
+KeyReleaseEvent 199 142 0 95 1 underscore 0
+
+KeyPressEvent 199 142 0 114 1 r 0
+CharEvent 199 142 0 114 1 r 0
+KeyReleaseEvent 199 142 0 114 1 r 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 81 1 Q 0
+CharEvent 199 142 0 81 1 Q 0
+KeyReleaseEvent 199 142 0 81 1 Q 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteSetDisplayCompl.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteSetDisplayCompl.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteSetDisplayCompl.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteSetDisplayCompl.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,75 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+# Esc
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+# S
+KeyPressEvent 199 142 0 115 1 s 0
+CharEvent 199 142 0 115 1 s 0
+KeyReleaseEvent 199 142 0 115 1 s 0
+
+# E
+KeyPressEvent 199 142 0 101 1 e 0
+CharEvent 199 142 0 101 1 e 0
+KeyReleaseEvent 199 142 0 101 1 e 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Space
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# M
+KeyPressEvent 199 142 0 109 1 m 0
+CharEvent 199 142 0 109 1 m 0
+KeyReleaseEvent 199 142 0 109 1 m 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# P
+KeyPressEvent 199 142 0 112 1 p 0
+CharEvent 199 142 0 112 1 p 0
+KeyReleaseEvent 199 142 0 112 1 p 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# T
+KeyPressEvent 199 142 0 116 1 t 0
+CharEvent 199 142 0 116 1 t 0
+KeyReleaseEvent 199 142 0 116 1 t 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteSetOptions.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteSetOptions.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteSetOptions.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteSetOptions.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,85 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+# Esc
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+# S
+KeyPressEvent 199 142 0 115 1 s 0
+CharEvent 199 142 0 115 1 s 0
+KeyReleaseEvent 199 142 0 115 1 s 0
+
+# E
+KeyPressEvent 199 142 0 101 1 e 0
+CharEvent 199 142 0 101 1 e 0
+KeyReleaseEvent 199 142 0 101 1 e 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Space
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# M
+KeyPressEvent 199 142 0 109 1 m 0
+CharEvent 199 142 0 109 1 m 0
+KeyReleaseEvent 199 142 0 109 1 m 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# P
+KeyPressEvent 199 142 0 112 1 p 0
+CharEvent 199 142 0 112 1 p 0
+KeyReleaseEvent 199 142 0 112 1 p 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# T
+KeyPressEvent 199 142 0 116 1 t 0
+CharEvent 199 142 0 116 1 t 0
+KeyReleaseEvent 199 142 0 116 1 t 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# S
+KeyPressEvent 199 142 0 115 1 s 0
+CharEvent 199 142 0 115 1 s 0
+KeyReleaseEvent 199 142 0 115 1 s 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Enter
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteToggleOptions.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteToggleOptions.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteToggleOptions.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteToggleOptions.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,70 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+# Esc
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+# T
+KeyPressEvent 199 142 0 116 1 t 0
+CharEvent 199 142 0 116 1 t 0
+KeyReleaseEvent 199 142 0 116 1 t 0
+
+# O
+KeyPressEvent 199 142 0 111 1 o 0
+CharEvent 199 142 0 111 1 o 0
+KeyReleaseEvent 199 142 0 111 1 o 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Tab
+KeyPressEvent 199 142 0 32 1 space 0
+CharEvent 199 142 0 32 1 space 0
+KeyReleaseEvent 199 142 0 32 1 space 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# M
+KeyPressEvent 199 142 0 109 1 m 0
+CharEvent 199 142 0 109 1 m 0
+KeyReleaseEvent 199 142 0 109 1 m 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# P
+KeyPressEvent 199 142 0 112 1 p 0
+CharEvent 199 142 0 112 1 p 0
+KeyReleaseEvent 199 142 0 112 1 p 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# E
+KeyPressEvent 199 142 0 101 1 e 0
+CharEvent 199 142 0 101 1 e 0
+KeyReleaseEvent 199 142 0 101 1 e 0
+
+# Tab
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+# Enter
+KeyPressEvent 199 142 0 13 1 Return 0
+CharEvent 199 142 0 13 1 Return 0
+KeyReleaseEvent 199 142 0 13 1 Return 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteTokenizeError.log f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteTokenizeError.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleAutoCompleteTokenizeError.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleAutoCompleteTokenizeError.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,17 @@
+# StreamVersion 1.2
+EnterEvent 293 98 0 0 0 0 0
+RenderEvent 199 142 0 0 0 0 0
+
+KeyPressEvent 199 142 0 27 1 Escape 0
+CharEvent 199 142 0 27 1 Escape 0
+KeyReleaseEvent 199 142 0 27 1 Escape 0
+
+KeyPressEvent 199 142 0 34 1 quotedbl 0
+CharEvent 199 142 0 34 1 quotedbl 0
+KeyReleaseEvent 199 142 0 34 1 quotedbl 0
+
+KeyPressEvent 199 142 0 9 1 Tab 0
+CharEvent 199 142 0 9 1 Tab 0
+KeyReleaseEvent 199 142 0 9 1 Tab 0
+
+TimerEvent 351 169 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleOverCheatSheet.log f3d-3.4.1/testing/recordings/TestInteractionConsoleOverCheatSheet.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionConsoleOverCheatSheet.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionConsoleOverCheatSheet.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,24 @@
+# StreamVersion 1.2
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+KeyPressEvent 640 -4 0 104 1 h 0
+CharEvent 640 -4 0 104 1 h 0
+TimerEvent 640 -4 0 104 1 h 0
+KeyReleaseEvent 640 -4 0 104 1 h 0
+TimerEvent 640 -4 0 104 1 h 0
+TimerEvent 640 -4 0 104 1 h 0
+TimerEvent 640 -4 0 104 1 h 0
+TimerEvent 640 -4 0 104 1 h 0
+KeyPressEvent 640 -4 0 27 1 Escape 0
+CharEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
+KeyReleaseEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
+TimerEvent 640 -4 0 27 1 Escape 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCorrectCameraForVolumeSwitch.log f3d-3.4.1/testing/recordings/TestInteractionCorrectCameraForVolumeSwitch.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCorrectCameraForVolumeSwitch.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionCorrectCameraForVolumeSwitch.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,8 @@
+# StreamVersion 1.2
+ExposeEvent 0 599 0 0 0 0 0
+RenderEvent 0 599 0 0 0 0 0
+TimerEvent 0 599 0 0 0 0 0
+TimerEvent 0 599 0 0 0 0 0
+KeyPressEvent 1481 -57 0 118 1 v 0
+CharEvent 1481 -57 0 118 1 v 0
+KeyReleaseEvent 1481 -57 0 118 1 v 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionCycleBlending.log f3d-3.4.1/testing/recordings/TestInteractionCycleBlending.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionCycleBlending.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionCycleBlending.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,18 @@
+# StreamVersion 1.1
+ExposeEvent 0 599 0 0 0 0
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 589 299 0 118 1 p
+CharEvent 589 299 0 118 1 p
+KeyReleaseEvent 589 299 0 118 1 p
+KeyPressEvent 589 299 0 118 1 p
+CharEvent 589 299 0 118 1 p
+KeyReleaseEvent 589 299 0 118 1 p
+KeyPressEvent 589 299 0 118 1 p
+CharEvent 589 299 0 118 1 p
+KeyReleaseEvent 589 299 0 118 1 p
+KeyPressEvent 589 299 0 118 1 p
+CharEvent 589 299 0 118 1 p
+KeyReleaseEvent 589 299 0 118 1 p
+KeyPressEvent 589 299 0 118 1 p
+CharEvent 589 299 0 118 1 p
+KeyReleaseEvent 589 299 0 118 1 p
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionDynamicUpDirection.log f3d-3.4.1/testing/recordings/TestInteractionDynamicUpDirection.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionDynamicUpDirection.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionDynamicUpDirection.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,9 @@
+# StreamVersion 1.2
+ExposeEvent 0 299 0 0 0 0 0
+RenderEvent 0 299 0 0 0 0 0
+
+# Ctrl + Z
+KeyPressEvent 150 150 0 0 1 Control_L 0
+KeyPressEvent 150 150 2 122 1 z 0
+KeyReleaseEvent 150 150 2 122 1 z 0
+KeyReleaseEvent 150 150 2 0 1 Control_L 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionInitUpVectorOption.log f3d-3.4.1/testing/recordings/TestInteractionInitUpVectorOption.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionInitUpVectorOption.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionInitUpVectorOption.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,171 +1,58 @@
 # StreamVersion 1.2
 EnterEvent 727 307 0 0 0 Return 0
-RenderEvent 672 314 0 0 0 Return 0
-
 LeftButtonPressEvent 737 282 0 0 0 Return 0
 StartInteractionEvent 737 282 0 0 0 Return 0
 MouseMoveEvent 736 282 0 0 0 Return 0
-RenderEvent 736 282 0 0 0 Return 0
-InteractionEvent 736 282 0 0 0 Return 0
 MouseMoveEvent 735 282 0 0 0 Return 0
-RenderEvent 735 282 0 0 0 Return 0
-InteractionEvent 735 282 0 0 0 Return 0
-TimerEvent 735 282 0 0 0 Return 0
 MouseMoveEvent 734 282 0 0 0 Return 0
-RenderEvent 734 282 0 0 0 Return 0
-InteractionEvent 734 282 0 0 0 Return 0
 MouseMoveEvent 733 282 0 0 0 Return 0
-RenderEvent 733 282 0 0 0 Return 0
-InteractionEvent 733 282 0 0 0 Return 0
 MouseMoveEvent 722 284 0 0 0 Return 0
-RenderEvent 722 284 0 0 0 Return 0
-InteractionEvent 722 284 0 0 0 Return 0
 MouseMoveEvent 720 284 0 0 0 Return 0
-RenderEvent 720 284 0 0 0 Return 0
-InteractionEvent 720 284 0 0 0 Return 0
 MouseMoveEvent 714 284 0 0 0 Return 0
-RenderEvent 714 284 0 0 0 Return 0
-InteractionEvent 714 284 0 0 0 Return 0
 MouseMoveEvent 712 285 0 0 0 Return 0
-RenderEvent 712 285 0 0 0 Return 0
-InteractionEvent 712 285 0 0 0 Return 0
 MouseMoveEvent 710 285 0 0 0 Return 0
-RenderEvent 710 285 0 0 0 Return 0
-InteractionEvent 710 285 0 0 0 Return 0
 MouseMoveEvent 707 285 0 0 0 Return 0
-RenderEvent 707 285 0 0 0 Return 0
-InteractionEvent 707 285 0 0 0 Return 0
 MouseMoveEvent 706 285 0 0 0 Return 0
-RenderEvent 706 285 0 0 0 Return 0
-InteractionEvent 706 285 0 0 0 Return 0
 MouseMoveEvent 703 285 0 0 0 Return 0
-RenderEvent 703 285 0 0 0 Return 0
-InteractionEvent 703 285 0 0 0 Return 0
 MouseMoveEvent 700 285 0 0 0 Return 0
-RenderEvent 700 285 0 0 0 Return 0
-InteractionEvent 700 285 0 0 0 Return 0
 MouseMoveEvent 696 285 0 0 0 Return 0
-RenderEvent 696 285 0 0 0 Return 0
-InteractionEvent 696 285 0 0 0 Return 0
 MouseMoveEvent 694 285 0 0 0 Return 0
-RenderEvent 694 285 0 0 0 Return 0
-InteractionEvent 694 285 0 0 0 Return 0
 MouseMoveEvent 692 285 0 0 0 Return 0
-RenderEvent 692 285 0 0 0 Return 0
-InteractionEvent 692 285 0 0 0 Return 0
 MouseMoveEvent 689 284 0 0 0 Return 0
-RenderEvent 689 284 0 0 0 Return 0
-InteractionEvent 689 284 0 0 0 Return 0
 MouseMoveEvent 686 283 0 0 0 Return 0
-RenderEvent 686 283 0 0 0 Return 0
-InteractionEvent 686 283 0 0 0 Return 0
 MouseMoveEvent 684 283 0 0 0 Return 0
-RenderEvent 684 283 0 0 0 Return 0
-InteractionEvent 684 283 0 0 0 Return 0
 MouseMoveEvent 682 283 0 0 0 Return 0
-RenderEvent 682 283 0 0 0 Return 0
-InteractionEvent 682 283 0 0 0 Return 0
 MouseMoveEvent 680 283 0 0 0 Return 0
-RenderEvent 680 283 0 0 0 Return 0
-InteractionEvent 680 283 0 0 0 Return 0
 MouseMoveEvent 679 283 0 0 0 Return 0
-RenderEvent 679 283 0 0 0 Return 0
-InteractionEvent 679 283 0 0 0 Return 0
 MouseMoveEvent 678 282 0 0 0 Return 0
-RenderEvent 678 282 0 0 0 Return 0
-InteractionEvent 678 282 0 0 0 Return 0
 MouseMoveEvent 677 282 0 0 0 Return 0
-RenderEvent 677 282 0 0 0 Return 0
-InteractionEvent 677 282 0 0 0 Return 0
 MouseMoveEvent 676 282 0 0 0 Return 0
-RenderEvent 676 282 0 0 0 Return 0
-InteractionEvent 676 282 0 0 0 Return 0
 MouseMoveEvent 675 282 0 0 0 Return 0
-RenderEvent 675 282 0 0 0 Return 0
-InteractionEvent 675 282 0 0 0 Return 0
-TimerEvent 675 282 0 0 0 Return 0
 MouseMoveEvent 674 282 0 0 0 Return 0
-RenderEvent 674 282 0 0 0 Return 0
-InteractionEvent 674 282 0 0 0 Return 0
 MouseMoveEvent 674 281 0 0 0 Return 0
-RenderEvent 674 281 0 0 0 Return 0
-InteractionEvent 674 281 0 0 0 Return 0
 MouseMoveEvent 673 281 0 0 0 Return 0
-RenderEvent 673 281 0 0 0 Return 0
-InteractionEvent 673 281 0 0 0 Return 0
 MouseMoveEvent 672 281 0 0 0 Return 0
-RenderEvent 672 281 0 0 0 Return 0
-InteractionEvent 672 281 0 0 0 Return 0
 MouseMoveEvent 671 281 0 0 0 Return 0
-RenderEvent 671 281 0 0 0 Return 0
-InteractionEvent 671 281 0 0 0 Return 0
 MouseMoveEvent 670 281 0 0 0 Return 0
-RenderEvent 670 281 0 0 0 Return 0
-InteractionEvent 670 281 0 0 0 Return 0
 MouseMoveEvent 670 280 0 0 0 Return 0
-RenderEvent 670 280 0 0 0 Return 0
-InteractionEvent 670 280 0 0 0 Return 0
 MouseMoveEvent 669 280 0 0 0 Return 0
-RenderEvent 669 280 0 0 0 Return 0
-InteractionEvent 669 280 0 0 0 Return 0
 MouseMoveEvent 668 280 0 0 0 Return 0
-RenderEvent 668 280 0 0 0 Return 0
-InteractionEvent 668 280 0 0 0 Return 0
 MouseMoveEvent 667 280 0 0 0 Return 0
-RenderEvent 667 280 0 0 0 Return 0
-InteractionEvent 667 280 0 0 0 Return 0
 MouseMoveEvent 665 280 0 0 0 Return 0
-RenderEvent 665 280 0 0 0 Return 0
-InteractionEvent 665 280 0 0 0 Return 0
 MouseMoveEvent 662 279 0 0 0 Return 0
-RenderEvent 662 279 0 0 0 Return 0
-InteractionEvent 662 279 0 0 0 Return 0
 MouseMoveEvent 660 279 0 0 0 Return 0
-RenderEvent 660 279 0 0 0 Return 0
-InteractionEvent 660 279 0 0 0 Return 0
 MouseMoveEvent 657 278 0 0 0 Return 0
-RenderEvent 657 278 0 0 0 Return 0
-InteractionEvent 657 278 0 0 0 Return 0
 MouseMoveEvent 655 278 0 0 0 Return 0
-RenderEvent 655 278 0 0 0 Return 0
-InteractionEvent 655 278 0 0 0 Return 0
 MouseMoveEvent 653 278 0 0 0 Return 0
-RenderEvent 653 278 0 0 0 Return 0
-InteractionEvent 653 278 0 0 0 Return 0
 MouseMoveEvent 651 278 0 0 0 Return 0
-RenderEvent 651 278 0 0 0 Return 0
-InteractionEvent 651 278 0 0 0 Return 0
 MouseMoveEvent 650 277 0 0 0 Return 0
-RenderEvent 650 277 0 0 0 Return 0
-InteractionEvent 650 277 0 0 0 Return 0
 MouseMoveEvent 648 277 0 0 0 Return 0
-RenderEvent 648 277 0 0 0 Return 0
-InteractionEvent 648 277 0 0 0 Return 0
 MouseMoveEvent 647 277 0 0 0 Return 0
-RenderEvent 647 277 0 0 0 Return 0
-InteractionEvent 647 277 0 0 0 Return 0
 MouseMoveEvent 646 277 0 0 0 Return 0
-RenderEvent 646 277 0 0 0 Return 0
-InteractionEvent 646 277 0 0 0 Return 0
 MouseMoveEvent 645 277 0 0 0 Return 0
-RenderEvent 645 277 0 0 0 Return 0
-InteractionEvent 645 277 0 0 0 Return 0
-TimerEvent 645 277 0 0 0 Return 0
-TimerEvent 645 277 0 0 0 Return 0
-TimerEvent 645 277 0 0 0 Return 0
 MouseMoveEvent 644 277 0 0 0 Return 0
-RenderEvent 644 277 0 0 0 Return 0
-InteractionEvent 644 277 0 0 0 Return 0
 MouseMoveEvent 643 277 0 0 0 Return 0
-RenderEvent 643 277 0 0 0 Return 0
-InteractionEvent 643 277 0 0 0 Return 0
 MouseMoveEvent 643 276 0 0 0 Return 0
-RenderEvent 643 276 0 0 0 Return 0
-InteractionEvent 643 276 0 0 0 Return 0
 MouseMoveEvent 642 276 0 0 0 Return 0
-RenderEvent 642 276 0 0 0 Return 0
-InteractionEvent 642 276 0 0 0 Return 0
-TimerEvent 642 276 0 0 0 Return 0
-TimerEvent 642 276 0 0 0 Return 0
 LeftButtonReleaseEvent 642 276 0 0 0 Return 0
 EndInteractionEvent 642 276 0 0 0 Return 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionMinimalConsoleOverCheatSheetAndFilename.log f3d-3.4.1/testing/recordings/TestInteractionMinimalConsoleOverCheatSheetAndFilename.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionMinimalConsoleOverCheatSheetAndFilename.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionMinimalConsoleOverCheatSheetAndFilename.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,34 @@
+# StreamVersion 1.2
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+KeyPressEvent 678 -273 0 104 1 h 0
+CharEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+KeyReleaseEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+TimerEvent 678 -273 0 104 1 h 0
+KeyPressEvent 678 -273 0 110 1 n 0
+CharEvent 678 -273 0 110 1 n 0
+TimerEvent 678 -273 0 110 1 n 0
+KeyReleaseEvent 678 -273 0 110 1 n 0
+TimerEvent 678 -273 0 110 1 n 0
+TimerEvent 678 -273 0 110 1 n 0
+TimerEvent 678 -273 0 110 1 n 0
+TimerEvent 678 -273 0 110 1 n 0
+KeyPressEvent 678 -273 0 58 1 colon 0
+CharEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+KeyReleaseEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
+TimerEvent 678 -273 0 58 1 colon 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionMinimalConsoleOverCheatSheet.log f3d-3.4.1/testing/recordings/TestInteractionMinimalConsoleOverCheatSheet.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionMinimalConsoleOverCheatSheet.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionMinimalConsoleOverCheatSheet.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,25 @@
+# StreamVersion 1.2
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+TimerEvent 0 289 0 0 0 0 0
+KeyPressEvent 1022 196 0 104 1 h 0
+CharEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+KeyReleaseEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+TimerEvent 1022 196 0 104 1 h 0
+KeyPressEvent 1022 196 0 58 1 colon 0
+CharEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
+KeyReleaseEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
+TimerEvent 1022 196 0 58 1 colon 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionNoModelScrollBar.log f3d-3.4.1/testing/recordings/TestInteractionNoModelScrollBar.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionNoModelScrollBar.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionNoModelScrollBar.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,765 +1,17 @@
 # StreamVersion 1.2
-ExposeEvent 0 599 0 0 0 0 0
-RenderEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-EnterEvent 613 2 0 0 0 0 0
-MouseMoveEvent 613 2 0 0 0 0 0
-MouseMoveEvent 607 14 0 0 0 0 0
-MouseMoveEvent 603 22 0 0 0 0 0
-MouseMoveEvent 601 34 0 0 0 0 0
-MouseMoveEvent 595 44 0 0 0 0 0
-MouseMoveEvent 591 54 0 0 0 0 0
-MouseMoveEvent 587 62 0 0 0 0 0
-MouseMoveEvent 581 74 0 0 0 0 0
-MouseMoveEvent 577 82 0 0 0 0 0
-MouseMoveEvent 571 94 0 0 0 0 0
-MouseMoveEvent 569 104 0 0 0 0 0
-MouseMoveEvent 565 112 0 0 0 0 0
-MouseMoveEvent 561 122 0 0 0 0 0
-MouseMoveEvent 559 130 0 0 0 0 0
-MouseMoveEvent 555 138 0 0 0 0 0
-TimerEvent 555 138 0 0 0 0 0
-MouseMoveEvent 553 146 0 0 0 0 0
-MouseMoveEvent 549 152 0 0 0 0 0
-MouseMoveEvent 547 158 0 0 0 0 0
-MouseMoveEvent 545 164 0 0 0 0 0
-MouseMoveEvent 543 172 0 0 0 0 0
-MouseMoveEvent 541 176 0 0 0 0 0
-MouseMoveEvent 539 179 0 0 0 0 0
-MouseMoveEvent 537 185 0 0 0 0 0
-MouseMoveEvent 536 187 0 0 0 0 0
-MouseMoveEvent 536 192 0 0 0 0 0
-MouseMoveEvent 532 196 0 0 0 0 0
-MouseMoveEvent 532 200 0 0 0 0 0
-MouseMoveEvent 532 201 0 0 0 0 0
-MouseMoveEvent 530 204 0 0 0 0 0
-MouseMoveEvent 530 206 0 0 0 0 0
-MouseMoveEvent 530 207 0 0 0 0 0
-MouseMoveEvent 529 208 0 0 0 0 0
-TimerEvent 529 208 0 0 0 0 0
-MouseMoveEvent 529 209 0 0 0 0 0
-MouseMoveEvent 528 210 0 0 0 0 0
-MouseMoveEvent 528 213 0 0 0 0 0
-MouseMoveEvent 528 214 0 0 0 0 0
-MouseMoveEvent 527 215 0 0 0 0 0
-MouseMoveEvent 527 217 0 0 0 0 0
-MouseMoveEvent 525 218 0 0 0 0 0
-MouseMoveEvent 525 219 0 0 0 0 0
-MouseMoveEvent 525 220 0 0 0 0 0
-MouseMoveEvent 524 223 0 0 0 0 0
-MouseMoveEvent 522 227 0 0 0 0 0
-MouseMoveEvent 521 228 0 0 0 0 0
-MouseMoveEvent 519 233 0 0 0 0 0
-MouseMoveEvent 519 235 0 0 0 0 0
-MouseMoveEvent 518 236 0 0 0 0 0
-TimerEvent 518 236 0 0 0 0 0
-MouseMoveEvent 516 239 0 0 0 0 0
-MouseMoveEvent 514 243 0 0 0 0 0
-MouseMoveEvent 512 249 0 0 0 0 0
-MouseMoveEvent 512 253 0 0 0 0 0
-MouseMoveEvent 510 258 0 0 0 0 0
-MouseMoveEvent 508 262 0 0 0 0 0
-MouseMoveEvent 507 266 0 0 0 0 0
-MouseMoveEvent 505 270 0 0 0 0 0
-MouseMoveEvent 503 273 0 0 0 0 0
-MouseMoveEvent 501 279 0 0 0 0 0
-MouseMoveEvent 499 283 0 0 0 0 0
-MouseMoveEvent 495 287 0 0 0 0 0
-MouseMoveEvent 493 291 0 0 0 0 0
-MouseMoveEvent 492 294 0 0 0 0 0
-MouseMoveEvent 490 298 0 0 0 0 0
-MouseMoveEvent 488 301 0 0 0 0 0
-MouseMoveEvent 486 305 0 0 0 0 0
-TimerEvent 486 305 0 0 0 0 0
-MouseMoveEvent 486 308 0 0 0 0 0
-MouseMoveEvent 483 310 0 0 0 0 0
-MouseMoveEvent 483 314 0 0 0 0 0
-MouseMoveEvent 481 317 0 0 0 0 0
-MouseMoveEvent 479 319 0 0 0 0 0
-MouseMoveEvent 479 320 0 0 0 0 0
-MouseMoveEvent 478 321 0 0 0 0 0
-MouseMoveEvent 477 324 0 0 0 0 0
-MouseMoveEvent 477 326 0 0 0 0 0
-MouseMoveEvent 476 327 0 0 0 0 0
-MouseMoveEvent 474 327 0 0 0 0 0
-MouseMoveEvent 474 328 0 0 0 0 0
-MouseMoveEvent 475 328 0 0 0 0 0
-MouseMoveEvent 475 329 0 0 0 0 0
-TimerEvent 475 329 0 0 0 0 0
-MouseMoveEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-TimerEvent 474 329 0 0 0 0 0
-KeyPressEvent 474 329 0 104 1 h 0
-CharEvent 474 329 0 104 1 h 0
-TimerEvent 474 329 0 104 1 h 0
-TimerEvent 474 329 0 104 1 h 0
-KeyReleaseEvent 474 329 0 104 1 h 0
-TimerEvent 474 329 0 104 1 h 0
-TimerEvent 474 329 0 104 1 h 0
-TimerEvent 474 329 0 104 1 h 0
-MouseMoveEvent 474 330 0 0 0 h 0
-MouseMoveEvent 474 330 0 0 0 h 0
-MouseMoveEvent 474 331 0 0 0 h 0
-TimerEvent 474 331 0 0 0 h 0
-MouseMoveEvent 474 332 0 0 0 h 0
-MouseMoveEvent 474 333 0 0 0 h 0
-MouseMoveEvent 472 334 0 0 0 h 0
-MouseMoveEvent 472 337 0 0 0 h 0
-MouseMoveEvent 471 339 0 0 0 h 0
-MouseMoveEvent 471 340 0 0 0 h 0
-MouseMoveEvent 470 341 0 0 0 h 0
-MouseMoveEvent 470 342 0 0 0 h 0
-MouseMoveEvent 470 343 0 0 0 h 0
-MouseMoveEvent 467 345 0 0 0 h 0
-MouseMoveEvent 467 346 0 0 0 h 0
-MouseMoveEvent 465 349 0 0 0 h 0
-MouseMoveEvent 465 351 0 0 0 h 0
-MouseMoveEvent 464 352 0 0 0 h 0
-MouseMoveEvent 463 353 0 0 0 h 0
-MouseMoveEvent 463 356 0 0 0 h 0
-TimerEvent 463 356 0 0 0 h 0
-MouseMoveEvent 463 357 0 0 0 h 0
-MouseMoveEvent 462 359 0 0 0 h 0
-MouseMoveEvent 462 360 0 0 0 h 0
-MouseMoveEvent 460 361 0 0 0 h 0
-MouseMoveEvent 460 362 0 0 0 h 0
-MouseMoveEvent 460 363 0 0 0 h 0
-MouseMoveEvent 459 365 0 0 0 h 0
-MouseMoveEvent 459 368 0 0 0 h 0
-MouseMoveEvent 458 369 0 0 0 h 0
-MouseMoveEvent 458 370 0 0 0 h 0
-MouseMoveEvent 456 372 0 0 0 h 0
-MouseMoveEvent 458 372 0 0 0 h 0
-MouseMoveEvent 458 373 0 0 0 h 0
-MouseMoveEvent 458 374 0 0 0 h 0
-MouseMoveEvent 458 375 0 0 0 h 0
-TimerEvent 458 375 0 0 0 h 0
-MouseMoveEvent 456 376 0 0 0 h 0
-MouseMoveEvent 456 377 0 0 0 h 0
-MouseMoveEvent 456 378 0 0 0 h 0
-MouseMoveEvent 456 379 0 0 0 h 0
-MouseMoveEvent 456 380 0 0 0 h 0
-MouseMoveEvent 457 380 0 0 0 h 0
-MouseMoveEvent 457 381 0 0 0 h 0
-MouseMoveEvent 458 381 0 0 0 h 0
-MouseMoveEvent 458 382 0 0 0 h 0
-TimerEvent 458 382 0 0 0 h 0
-MouseMoveEvent 460 383 0 0 0 h 0
-MouseMoveEvent 461 385 0 0 0 h 0
-MouseMoveEvent 461 386 0 0 0 h 0
-MouseMoveEvent 462 386 0 0 0 h 0
-MouseMoveEvent 459 384 0 0 0 h 0
-MouseMoveEvent 459 386 0 0 0 h 0
-MouseMoveEvent 460 386 0 0 0 h 0
-MouseMoveEvent 461 387 0 0 0 h 0
-MouseMoveEvent 462 387 0 0 0 h 0
-MouseMoveEvent 465 388 0 0 0 h 0
-MouseMoveEvent 465 389 0 0 0 h 0
-MouseMoveEvent 466 389 0 0 0 h 0
-MouseMoveEvent 467 389 0 0 0 h 0
-MouseMoveEvent 468 391 0 0 0 h 0
-MouseMoveEvent 470 391 0 0 0 h 0
-TimerEvent 470 391 0 0 0 h 0
-MouseMoveEvent 471 392 0 0 0 h 0
-MouseMoveEvent 471 393 0 0 0 h 0
-MouseMoveEvent 472 393 0 0 0 h 0
-MouseMoveEvent 473 394 0 0 0 h 0
-MouseMoveEvent 476 396 0 0 0 h 0
-MouseMoveEvent 478 397 0 0 0 h 0
-MouseMoveEvent 479 397 0 0 0 h 0
-MouseMoveEvent 479 398 0 0 0 h 0
-MouseMoveEvent 480 398 0 0 0 h 0
-MouseMoveEvent 480 400 0 0 0 h 0
-MouseMoveEvent 481 400 0 0 0 h 0
-MouseMoveEvent 482 401 0 0 0 h 0
-MouseMoveEvent 482 402 0 0 0 h 0
-MouseMoveEvent 483 402 0 0 0 h 0
-MouseMoveEvent 483 404 0 0 0 h 0
-TimerEvent 483 404 0 0 0 h 0
-MouseMoveEvent 485 404 0 0 0 h 0
-MouseMoveEvent 485 405 0 0 0 h 0
-MouseMoveEvent 486 406 0 0 0 h 0
-MouseMoveEvent 486 408 0 0 0 h 0
-MouseMoveEvent 486 406 0 0 0 h 0
-MouseMoveEvent 488 406 0 0 0 h 0
-MouseMoveEvent 490 406 0 0 0 h 0
-MouseMoveEvent 491 408 0 0 0 h 0
-MouseMoveEvent 492 408 0 0 0 h 0
-MouseMoveEvent 492 410 0 0 0 h 0
-MouseMoveEvent 495 410 0 0 0 h 0
-MouseMoveEvent 496 410 0 0 0 h 0
-TimerEvent 496 410 0 0 0 h 0
-MouseMoveEvent 497 410 0 0 0 h 0
-MouseMoveEvent 497 411 0 0 0 h 0
-MouseMoveEvent 500 411 0 0 0 h 0
-MouseMoveEvent 501 412 0 0 0 h 0
-MouseMoveEvent 502 412 0 0 0 h 0
-MouseMoveEvent 503 412 0 0 0 h 0
-MouseMoveEvent 503 413 0 0 0 h 0
-MouseMoveEvent 504 413 0 0 0 h 0
-MouseMoveEvent 505 413 0 0 0 h 0
-TimerEvent 505 413 0 0 0 h 0
-MouseMoveEvent 505 414 0 0 0 h 0
-MouseMoveEvent 506 414 0 0 0 h 0
-MouseMoveEvent 507 414 0 0 0 h 0
-MouseMoveEvent 508 414 0 0 0 h 0
-MouseMoveEvent 506 415 0 0 0 h 0
-TimerEvent 506 415 0 0 0 h 0
-MouseMoveEvent 507 415 0 0 0 h 0
-MouseMoveEvent 508 415 0 0 0 h 0
-MouseMoveEvent 508 416 0 0 0 h 0
-TimerEvent 508 416 0 0 0 h 0
-MouseMoveEvent 509 416 0 0 0 h 0
-MouseMoveEvent 510 416 0 0 0 h 0
-TimerEvent 510 416 0 0 0 h 0
-MouseMoveEvent 511 416 0 0 0 h 0
-MouseMoveEvent 511 417 0 0 0 h 0
-MouseMoveEvent 512 417 0 0 0 h 0
-MouseMoveEvent 513 417 0 0 0 h 0
-MouseMoveEvent 513 418 0 0 0 h 0
-MouseMoveEvent 514 418 0 0 0 h 0
-MouseMoveEvent 515 418 0 0 0 h 0
-MouseMoveEvent 514 418 0 0 0 h 0
-TimerEvent 514 418 0 0 0 h 0
-MouseMoveEvent 515 418 0 0 0 h 0
-MouseMoveEvent 516 418 0 0 0 h 0
-TimerEvent 516 418 0 0 0 h 0
-TimerEvent 516 418 0 0 0 h 0
-MouseMoveEvent 517 418 0 0 0 h 0
-TimerEvent 517 418 0 0 0 h 0
-MouseMoveEvent 517 418 0 0 0 h 0
-MouseMoveEvent 518 418 0 0 0 h 0
-MouseMoveEvent 519 418 0 0 0 h 0
-MouseMoveEvent 519 419 0 0 0 h 0
-MouseMoveEvent 521 419 0 0 0 h 0
-MouseMoveEvent 522 419 0 0 0 h 0
-MouseMoveEvent 523 419 0 0 0 h 0
-MouseMoveEvent 524 419 0 0 0 h 0
-TimerEvent 524 419 0 0 0 h 0
-MouseMoveEvent 525 419 0 0 0 h 0
-MouseMoveEvent 526 419 0 0 0 h 0
-MouseMoveEvent 527 419 0 0 0 h 0
-MouseMoveEvent 527 420 0 0 0 h 0
-TimerEvent 527 420 0 0 0 h 0
-MouseMoveEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-LeftButtonPressEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-MouseMoveEvent 528 420 0 0 0 h 0
-TimerEvent 528 420 0 0 0 h 0
-MouseMoveEvent 528 419 0 0 0 h 0
-TimerEvent 528 419 0 0 0 h 0
-MouseMoveEvent 528 418 0 0 0 h 0
-MouseMoveEvent 528 418 0 0 0 h 0
-TimerEvent 528 418 0 0 0 h 0
-MouseMoveEvent 528 417 0 0 0 h 0
-MouseMoveEvent 528 416 0 0 0 h 0
-MouseMoveEvent 528 415 0 0 0 h 0
-MouseMoveEvent 528 414 0 0 0 h 0
-MouseMoveEvent 528 413 0 0 0 h 0
-MouseMoveEvent 528 412 0 0 0 h 0
-MouseMoveEvent 528 411 0 0 0 h 0
-TimerEvent 528 411 0 0 0 h 0
-MouseMoveEvent 528 409 0 0 0 h 0
-MouseMoveEvent 528 408 0 0 0 h 0
-MouseMoveEvent 528 407 0 0 0 h 0
-MouseMoveEvent 528 406 0 0 0 h 0
-MouseMoveEvent 528 405 0 0 0 h 0
-MouseMoveEvent 528 404 0 0 0 h 0
-MouseMoveEvent 528 403 0 0 0 h 0
-MouseMoveEvent 528 402 0 0 0 h 0
-MouseMoveEvent 528 401 0 0 0 h 0
-MouseMoveEvent 528 400 0 0 0 h 0
-MouseMoveEvent 526 399 0 0 0 h 0
-MouseMoveEvent 526 398 0 0 0 h 0
-TimerEvent 526 398 0 0 0 h 0
-MouseMoveEvent 526 397 0 0 0 h 0
-MouseMoveEvent 526 396 0 0 0 h 0
-MouseMoveEvent 526 395 0 0 0 h 0
-MouseMoveEvent 526 394 0 0 0 h 0
-MouseMoveEvent 526 393 0 0 0 h 0
-MouseMoveEvent 526 392 0 0 0 h 0
-MouseMoveEvent 526 391 0 0 0 h 0
-MouseMoveEvent 526 390 0 0 0 h 0
-MouseMoveEvent 526 389 0 0 0 h 0
-MouseMoveEvent 526 388 0 0 0 h 0
-MouseMoveEvent 526 387 0 0 0 h 0
-TimerEvent 526 387 0 0 0 h 0
-MouseMoveEvent 526 386 0 0 0 h 0
-MouseMoveEvent 527 386 0 0 0 h 0
-MouseMoveEvent 527 385 0 0 0 h 0
-MouseMoveEvent 527 384 0 0 0 h 0
-MouseMoveEvent 527 383 0 0 0 h 0
-MouseMoveEvent 527 382 0 0 0 h 0
-MouseMoveEvent 527 381 0 0 0 h 0
-MouseMoveEvent 527 379 0 0 0 h 0
-MouseMoveEvent 527 378 0 0 0 h 0
-MouseMoveEvent 527 377 0 0 0 h 0
-MouseMoveEvent 527 376 0 0 0 h 0
-TimerEvent 527 376 0 0 0 h 0
-MouseMoveEvent 527 375 0 0 0 h 0
-MouseMoveEvent 528 375 0 0 0 h 0
-MouseMoveEvent 528 373 0 0 0 h 0
-MouseMoveEvent 528 371 0 0 0 h 0
-MouseMoveEvent 528 370 0 0 0 h 0
-MouseMoveEvent 528 369 0 0 0 h 0
-MouseMoveEvent 528 368 0 0 0 h 0
-MouseMoveEvent 528 367 0 0 0 h 0
-MouseMoveEvent 530 367 0 0 0 h 0
-MouseMoveEvent 530 364 0 0 0 h 0
-MouseMoveEvent 530 363 0 0 0 h 0
-MouseMoveEvent 530 362 0 0 0 h 0
-MouseMoveEvent 530 361 0 0 0 h 0
-MouseMoveEvent 530 360 0 0 0 h 0
-MouseMoveEvent 530 358 0 0 0 h 0
-TimerEvent 530 358 0 0 0 h 0
-MouseMoveEvent 528 357 0 0 0 h 0
-MouseMoveEvent 528 356 0 0 0 h 0
-MouseMoveEvent 530 355 0 0 0 h 0
-MouseMoveEvent 530 354 0 0 0 h 0
-MouseMoveEvent 530 353 0 0 0 h 0
-MouseMoveEvent 530 352 0 0 0 h 0
-MouseMoveEvent 530 351 0 0 0 h 0
-MouseMoveEvent 530 350 0 0 0 h 0
-MouseMoveEvent 530 348 0 0 0 h 0
-MouseMoveEvent 530 347 0 0 0 h 0
-MouseMoveEvent 530 346 0 0 0 h 0
-MouseMoveEvent 530 345 0 0 0 h 0
-MouseMoveEvent 530 344 0 0 0 h 0
-MouseMoveEvent 530 343 0 0 0 h 0
-TimerEvent 530 343 0 0 0 h 0
-MouseMoveEvent 531 343 0 0 0 h 0
-MouseMoveEvent 531 342 0 0 0 h 0
-MouseMoveEvent 531 341 0 0 0 h 0
-MouseMoveEvent 531 340 0 0 0 h 0
-MouseMoveEvent 531 339 0 0 0 h 0
-MouseMoveEvent 531 338 0 0 0 h 0
-MouseMoveEvent 531 337 0 0 0 h 0
-MouseMoveEvent 531 336 0 0 0 h 0
-MouseMoveEvent 531 335 0 0 0 h 0
-MouseMoveEvent 531 334 0 0 0 h 0
-MouseMoveEvent 531 333 0 0 0 h 0
-MouseMoveEvent 531 332 0 0 0 h 0
-MouseMoveEvent 531 330 0 0 0 h 0
-TimerEvent 531 330 0 0 0 h 0
-MouseMoveEvent 531 329 0 0 0 h 0
-MouseMoveEvent 531 328 0 0 0 h 0
-MouseMoveEvent 531 327 0 0 0 h 0
-MouseMoveEvent 532 327 0 0 0 h 0
-MouseMoveEvent 532 326 0 0 0 h 0
-MouseMoveEvent 532 325 0 0 0 h 0
-MouseMoveEvent 532 324 0 0 0 h 0
-MouseMoveEvent 533 323 0 0 0 h 0
-MouseMoveEvent 533 322 0 0 0 h 0
-MouseMoveEvent 533 321 0 0 0 h 0
-MouseMoveEvent 533 320 0 0 0 h 0
-MouseMoveEvent 534 318 0 0 0 h 0
-MouseMoveEvent 534 317 0 0 0 h 0
-MouseMoveEvent 534 316 0 0 0 h 0
-TimerEvent 534 316 0 0 0 h 0
-MouseMoveEvent 534 315 0 0 0 h 0
-MouseMoveEvent 534 314 0 0 0 h 0
-MouseMoveEvent 534 313 0 0 0 h 0
-MouseMoveEvent 535 312 0 0 0 h 0
-MouseMoveEvent 535 311 0 0 0 h 0
-MouseMoveEvent 535 310 0 0 0 h 0
-MouseMoveEvent 535 309 0 0 0 h 0
-MouseMoveEvent 535 308 0 0 0 h 0
-MouseMoveEvent 535 307 0 0 0 h 0
-MouseMoveEvent 537 304 0 0 0 h 0
-MouseMoveEvent 537 302 0 0 0 h 0
-MouseMoveEvent 537 301 0 0 0 h 0
-MouseMoveEvent 537 300 0 0 0 h 0
-TimerEvent 537 300 0 0 0 h 0
-MouseMoveEvent 537 299 0 0 0 h 0
-MouseMoveEvent 538 299 0 0 0 h 0
-MouseMoveEvent 538 298 0 0 0 h 0
-MouseMoveEvent 538 297 0 0 0 h 0
-MouseMoveEvent 538 296 0 0 0 h 0
-MouseMoveEvent 538 295 0 0 0 h 0
-MouseMoveEvent 538 294 0 0 0 h 0
-MouseMoveEvent 538 293 0 0 0 h 0
-MouseMoveEvent 538 292 0 0 0 h 0
-MouseMoveEvent 538 291 0 0 0 h 0
-MouseMoveEvent 538 290 0 0 0 h 0
-TimerEvent 538 290 0 0 0 h 0
-MouseMoveEvent 538 289 0 0 0 h 0
-MouseMoveEvent 538 288 0 0 0 h 0
-MouseMoveEvent 538 287 0 0 0 h 0
-MouseMoveEvent 538 286 0 0 0 h 0
-MouseMoveEvent 538 285 0 0 0 h 0
-MouseMoveEvent 538 284 0 0 0 h 0
-MouseMoveEvent 538 283 0 0 0 h 0
-MouseMoveEvent 538 282 0 0 0 h 0
-MouseMoveEvent 539 282 0 0 0 h 0
-TimerEvent 539 282 0 0 0 h 0
-MouseMoveEvent 539 281 0 0 0 h 0
-MouseMoveEvent 539 280 0 0 0 h 0
-MouseMoveEvent 539 279 0 0 0 h 0
-MouseMoveEvent 539 278 0 0 0 h 0
-MouseMoveEvent 539 277 0 0 0 h 0
-MouseMoveEvent 540 276 0 0 0 h 0
-MouseMoveEvent 540 275 0 0 0 h 0
-MouseMoveEvent 540 274 0 0 0 h 0
-TimerEvent 540 274 0 0 0 h 0
-MouseMoveEvent 540 273 0 0 0 h 0
-MouseMoveEvent 540 272 0 0 0 h 0
-MouseMoveEvent 539 271 0 0 0 h 0
-MouseMoveEvent 539 270 0 0 0 h 0
-MouseMoveEvent 539 269 0 0 0 h 0
-MouseMoveEvent 539 268 0 0 0 h 0
-MouseMoveEvent 539 267 0 0 0 h 0
-MouseMoveEvent 539 266 0 0 0 h 0
-TimerEvent 539 266 0 0 0 h 0
-MouseMoveEvent 539 265 0 0 0 h 0
-MouseMoveEvent 539 264 0 0 0 h 0
-MouseMoveEvent 539 263 0 0 0 h 0
-MouseMoveEvent 539 262 0 0 0 h 0
-MouseMoveEvent 539 261 0 0 0 h 0
-MouseMoveEvent 539 260 0 0 0 h 0
-MouseMoveEvent 539 259 0 0 0 h 0
-MouseMoveEvent 539 258 0 0 0 h 0
-TimerEvent 539 258 0 0 0 h 0
-MouseMoveEvent 539 257 0 0 0 h 0
-MouseMoveEvent 539 256 0 0 0 h 0
-MouseMoveEvent 539 255 0 0 0 h 0
-MouseMoveEvent 539 254 0 0 0 h 0
-MouseMoveEvent 539 253 0 0 0 h 0
-MouseMoveEvent 540 253 0 0 0 h 0
-MouseMoveEvent 540 252 0 0 0 h 0
-MouseMoveEvent 540 251 0 0 0 h 0
-TimerEvent 540 251 0 0 0 h 0
-MouseMoveEvent 540 250 0 0 0 h 0
-MouseMoveEvent 540 249 0 0 0 h 0
-MouseMoveEvent 540 248 0 0 0 h 0
-MouseMoveEvent 540 247 0 0 0 h 0
-MouseMoveEvent 540 246 0 0 0 h 0
-MouseMoveEvent 540 245 0 0 0 h 0
-MouseMoveEvent 540 244 0 0 0 h 0
-MouseMoveEvent 540 243 0 0 0 h 0
-MouseMoveEvent 540 242 0 0 0 h 0
-MouseMoveEvent 540 241 0 0 0 h 0
-MouseMoveEvent 540 239 0 0 0 h 0
-TimerEvent 540 239 0 0 0 h 0
-MouseMoveEvent 540 238 0 0 0 h 0
-MouseMoveEvent 539 238 0 0 0 h 0
-MouseMoveEvent 539 237 0 0 0 h 0
-MouseMoveEvent 539 236 0 0 0 h 0
-MouseMoveEvent 539 235 0 0 0 h 0
-MouseMoveEvent 539 234 0 0 0 h 0
-MouseMoveEvent 539 233 0 0 0 h 0
-MouseMoveEvent 539 232 0 0 0 h 0
-MouseMoveEvent 539 231 0 0 0 h 0
-MouseMoveEvent 539 230 0 0 0 h 0
-MouseMoveEvent 539 229 0 0 0 h 0
-MouseMoveEvent 539 228 0 0 0 h 0
-TimerEvent 539 228 0 0 0 h 0
-MouseMoveEvent 539 227 0 0 0 h 0
-MouseMoveEvent 539 226 0 0 0 h 0
-MouseMoveEvent 539 225 0 0 0 h 0
-MouseMoveEvent 539 224 0 0 0 h 0
-MouseMoveEvent 539 223 0 0 0 h 0
-MouseMoveEvent 539 222 0 0 0 h 0
-MouseMoveEvent 539 221 0 0 0 h 0
-MouseMoveEvent 539 220 0 0 0 h 0
-MouseMoveEvent 539 219 0 0 0 h 0
-MouseMoveEvent 537 218 0 0 0 h 0
-MouseMoveEvent 537 216 0 0 0 h 0
-MouseMoveEvent 537 215 0 0 0 h 0
-MouseMoveEvent 537 214 0 0 0 h 0
-TimerEvent 537 214 0 0 0 h 0
-MouseMoveEvent 538 213 0 0 0 h 0
-MouseMoveEvent 538 212 0 0 0 h 0
-MouseMoveEvent 538 211 0 0 0 h 0
-MouseMoveEvent 538 210 0 0 0 h 0
-MouseMoveEvent 538 209 0 0 0 h 0
-MouseMoveEvent 538 208 0 0 0 h 0
-MouseMoveEvent 538 207 0 0 0 h 0
-MouseMoveEvent 538 206 0 0 0 h 0
-MouseMoveEvent 538 205 0 0 0 h 0
-MouseMoveEvent 538 204 0 0 0 h 0
-TimerEvent 538 204 0 0 0 h 0
-MouseMoveEvent 538 203 0 0 0 h 0
-MouseMoveEvent 538 202 0 0 0 h 0
-MouseMoveEvent 538 201 0 0 0 h 0
-MouseMoveEvent 537 201 0 0 0 h 0
-MouseMoveEvent 537 200 0 0 0 h 0
-MouseMoveEvent 537 199 0 0 0 h 0
-MouseMoveEvent 537 198 0 0 0 h 0
-MouseMoveEvent 537 197 0 0 0 h 0
-MouseMoveEvent 537 196 0 0 0 h 0
-MouseMoveEvent 537 195 0 0 0 h 0
-TimerEvent 537 195 0 0 0 h 0
-MouseMoveEvent 537 194 0 0 0 h 0
-MouseMoveEvent 537 193 0 0 0 h 0
-MouseMoveEvent 537 192 0 0 0 h 0
-MouseMoveEvent 537 191 0 0 0 h 0
-MouseMoveEvent 537 190 0 0 0 h 0
-MouseMoveEvent 537 189 0 0 0 h 0
-MouseMoveEvent 537 188 0 0 0 h 0
-TimerEvent 537 188 0 0 0 h 0
-MouseMoveEvent 537 187 0 0 0 h 0
-MouseMoveEvent 537 186 0 0 0 h 0
-MouseMoveEvent 537 185 0 0 0 h 0
-MouseMoveEvent 537 184 0 0 0 h 0
-MouseMoveEvent 537 183 0 0 0 h 0
-MouseMoveEvent 537 182 0 0 0 h 0
-MouseMoveEvent 537 181 0 0 0 h 0
-TimerEvent 537 181 0 0 0 h 0
-MouseMoveEvent 537 180 0 0 0 h 0
-MouseMoveEvent 537 179 0 0 0 h 0
-MouseMoveEvent 537 178 0 0 0 h 0
-MouseMoveEvent 537 177 0 0 0 h 0
-MouseMoveEvent 537 176 0 0 0 h 0
-MouseMoveEvent 537 175 0 0 0 h 0
-MouseMoveEvent 538 175 0 0 0 h 0
-MouseMoveEvent 538 174 0 0 0 h 0
-TimerEvent 538 174 0 0 0 h 0
-MouseMoveEvent 538 173 0 0 0 h 0
-MouseMoveEvent 538 172 0 0 0 h 0
-MouseMoveEvent 538 171 0 0 0 h 0
-MouseMoveEvent 538 170 0 0 0 h 0
-MouseMoveEvent 538 169 0 0 0 h 0
-MouseMoveEvent 538 168 0 0 0 h 0
-MouseMoveEvent 538 167 0 0 0 h 0
-TimerEvent 538 167 0 0 0 h 0
-MouseMoveEvent 538 166 0 0 0 h 0
-MouseMoveEvent 538 165 0 0 0 h 0
-MouseMoveEvent 538 164 0 0 0 h 0
-MouseMoveEvent 538 163 0 0 0 h 0
-MouseMoveEvent 538 162 0 0 0 h 0
-MouseMoveEvent 538 161 0 0 0 h 0
-MouseMoveEvent 538 160 0 0 0 h 0
-MouseMoveEvent 538 159 0 0 0 h 0
-MouseMoveEvent 538 158 0 0 0 h 0
-MouseMoveEvent 538 157 0 0 0 h 0
-TimerEvent 538 157 0 0 0 h 0
-MouseMoveEvent 538 156 0 0 0 h 0
-MouseMoveEvent 538 155 0 0 0 h 0
-MouseMoveEvent 538 154 0 0 0 h 0
-MouseMoveEvent 538 153 0 0 0 h 0
-MouseMoveEvent 538 152 0 0 0 h 0
-MouseMoveEvent 538 151 0 0 0 h 0
-MouseMoveEvent 538 150 0 0 0 h 0
-MouseMoveEvent 538 149 0 0 0 h 0
-MouseMoveEvent 538 148 0 0 0 h 0
-MouseMoveEvent 538 147 0 0 0 h 0
-TimerEvent 538 147 0 0 0 h 0
-MouseMoveEvent 538 146 0 0 0 h 0
-MouseMoveEvent 538 145 0 0 0 h 0
-MouseMoveEvent 538 144 0 0 0 h 0
-MouseMoveEvent 538 143 0 0 0 h 0
-MouseMoveEvent 538 142 0 0 0 h 0
-MouseMoveEvent 538 141 0 0 0 h 0
-MouseMoveEvent 538 140 0 0 0 h 0
-MouseMoveEvent 538 139 0 0 0 h 0
-MouseMoveEvent 538 138 0 0 0 h 0
-MouseMoveEvent 538 135 0 0 0 h 0
-MouseMoveEvent 538 134 0 0 0 h 0
-TimerEvent 538 134 0 0 0 h 0
-MouseMoveEvent 538 133 0 0 0 h 0
-MouseMoveEvent 538 132 0 0 0 h 0
-MouseMoveEvent 538 131 0 0 0 h 0
-MouseMoveEvent 538 130 0 0 0 h 0
-MouseMoveEvent 538 129 0 0 0 h 0
-MouseMoveEvent 538 128 0 0 0 h 0
-MouseMoveEvent 538 127 0 0 0 h 0
-MouseMoveEvent 538 126 0 0 0 h 0
-MouseMoveEvent 538 125 0 0 0 h 0
-MouseMoveEvent 538 123 0 0 0 h 0
-TimerEvent 538 123 0 0 0 h 0
-MouseMoveEvent 538 122 0 0 0 h 0
-MouseMoveEvent 538 121 0 0 0 h 0
-MouseMoveEvent 538 120 0 0 0 h 0
-MouseMoveEvent 538 119 0 0 0 h 0
-MouseMoveEvent 538 118 0 0 0 h 0
-MouseMoveEvent 538 117 0 0 0 h 0
-MouseMoveEvent 538 116 0 0 0 h 0
-MouseMoveEvent 538 115 0 0 0 h 0
-TimerEvent 538 115 0 0 0 h 0
-MouseMoveEvent 538 114 0 0 0 h 0
-MouseMoveEvent 538 113 0 0 0 h 0
-MouseMoveEvent 539 113 0 0 0 h 0
-MouseMoveEvent 539 112 0 0 0 h 0
-MouseMoveEvent 539 111 0 0 0 h 0
-MouseMoveEvent 538 111 0 0 0 h 0
-MouseMoveEvent 538 110 0 0 0 h 0
-MouseMoveEvent 538 109 0 0 0 h 0
-MouseMoveEvent 538 108 0 0 0 h 0
-TimerEvent 538 108 0 0 0 h 0
-MouseMoveEvent 538 107 0 0 0 h 0
-MouseMoveEvent 538 106 0 0 0 h 0
-MouseMoveEvent 538 105 0 0 0 h 0
-MouseMoveEvent 538 104 0 0 0 h 0
-MouseMoveEvent 538 103 0 0 0 h 0
-MouseMoveEvent 538 102 0 0 0 h 0
-TimerEvent 538 102 0 0 0 h 0
-MouseMoveEvent 538 101 0 0 0 h 0
-MouseMoveEvent 538 100 0 0 0 h 0
-MouseMoveEvent 538 101 0 0 0 h 0
-MouseMoveEvent 538 100 0 0 0 h 0
-MouseMoveEvent 538 99 0 0 0 h 0
-MouseMoveEvent 538 98 0 0 0 h 0
-MouseMoveEvent 538 97 0 0 0 h 0
-TimerEvent 538 97 0 0 0 h 0
-MouseMoveEvent 538 96 0 0 0 h 0
-MouseMoveEvent 538 95 0 0 0 h 0
-MouseMoveEvent 538 94 0 0 0 h 0
-MouseMoveEvent 538 93 0 0 0 h 0
-TimerEvent 538 93 0 0 0 h 0
-MouseMoveEvent 538 92 0 0 0 h 0
-MouseMoveEvent 538 91 0 0 0 h 0
-MouseMoveEvent 538 90 0 0 0 h 0
-MouseMoveEvent 538 89 0 0 0 h 0
-MouseMoveEvent 538 88 0 0 0 h 0
-MouseMoveEvent 537 87 0 0 0 h 0
-TimerEvent 537 87 0 0 0 h 0
-MouseMoveEvent 537 86 0 0 0 h 0
-MouseMoveEvent 537 85 0 0 0 h 0
-MouseMoveEvent 537 84 0 0 0 h 0
-MouseMoveEvent 537 83 0 0 0 h 0
-MouseMoveEvent 537 82 0 0 0 h 0
-TimerEvent 537 82 0 0 0 h 0
-MouseMoveEvent 537 81 0 0 0 h 0
-MouseMoveEvent 536 81 0 0 0 h 0
-MouseMoveEvent 536 80 0 0 0 h 0
-MouseMoveEvent 536 79 0 0 0 h 0
-MouseMoveEvent 536 78 0 0 0 h 0
-TimerEvent 536 78 0 0 0 h 0
-MouseMoveEvent 536 77 0 0 0 h 0
-MouseMoveEvent 536 76 0 0 0 h 0
-MouseMoveEvent 537 76 0 0 0 h 0
-MouseMoveEvent 537 75 0 0 0 h 0
-TimerEvent 537 75 0 0 0 h 0
-MouseMoveEvent 537 74 0 0 0 h 0
-MouseMoveEvent 536 74 0 0 0 h 0
-MouseMoveEvent 536 73 0 0 0 h 0
-MouseMoveEvent 536 72 0 0 0 h 0
-TimerEvent 536 72 0 0 0 h 0
-MouseMoveEvent 536 71 0 0 0 h 0
-MouseMoveEvent 536 70 0 0 0 h 0
-MouseMoveEvent 537 70 0 0 0 h 0
-TimerEvent 537 70 0 0 0 h 0
-MouseMoveEvent 537 69 0 0 0 h 0
-TimerEvent 537 69 0 0 0 h 0
-TimerEvent 537 69 0 0 0 h 0
-TimerEvent 537 69 0 0 0 h 0
-MouseMoveEvent 537 69 0 0 0 h 0
-MouseMoveEvent 537 68 0 0 0 h 0
-TimerEvent 537 68 0 0 0 h 0
-MouseMoveEvent 537 67 0 0 0 h 0
-MouseMoveEvent 537 66 0 0 0 h 0
-MouseMoveEvent 537 65 0 0 0 h 0
-MouseMoveEvent 537 66 0 0 0 h 0
-MouseMoveEvent 537 65 0 0 0 h 0
-TimerEvent 537 65 0 0 0 h 0
-MouseMoveEvent 537 64 0 0 0 h 0
-MouseMoveEvent 537 63 0 0 0 h 0
-MouseMoveEvent 537 62 0 0 0 h 0
-MouseMoveEvent 537 61 0 0 0 h 0
-MouseMoveEvent 536 61 0 0 0 h 0
-TimerEvent 536 61 0 0 0 h 0
-MouseMoveEvent 536 60 0 0 0 h 0
-MouseMoveEvent 536 59 0 0 0 h 0
-MouseMoveEvent 536 58 0 0 0 h 0
-MouseMoveEvent 536 57 0 0 0 h 0
-MouseMoveEvent 536 56 0 0 0 h 0
-TimerEvent 536 56 0 0 0 h 0
-MouseMoveEvent 536 55 0 0 0 h 0
-TimerEvent 536 55 0 0 0 h 0
-MouseMoveEvent 536 55 0 0 0 h 0
-MouseMoveEvent 536 54 0 0 0 h 0
-TimerEvent 536 54 0 0 0 h 0
-TimerEvent 536 54 0 0 0 h 0
-MouseMoveEvent 535 54 0 0 0 h 0
-MouseMoveEvent 535 53 0 0 0 h 0
-MouseMoveEvent 535 54 0 0 0 h 0
-MouseMoveEvent 534 54 0 0 0 h 0
-TimerEvent 534 54 0 0 0 h 0
-MouseMoveEvent 534 53 0 0 0 h 0
-MouseMoveEvent 534 52 0 0 0 h 0
-MouseMoveEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-LeftButtonReleaseEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-TimerEvent 534 53 0 0 0 h 0
-MouseMoveEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-MouseMoveEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
-TimerEvent 533 53 0 0 0 h 0
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+
+# H
+KeyPressEvent 411 310 0 104 1 h 0
+CharEvent 411 310 0 104 1 h 0
+KeyReleaseEvent 411 310 0 104 1 h 0
+
+# Drag scrollbar to bottom
+MouseMoveEvent 815 421 0 0 0 h 0
+TimerEvent 815 421 0 0 0 h 0
+LeftButtonPressEvent 815 421 0 0 0 h 0
+MouseMoveEvent 801 -3 0 0 0 h 0
+TimerEvent 801 -3 0 0 0 h 0
+LeftButtonReleaseEvent 801 -3 0 0 0 h 0
+LeaveEvent 765 -60 0 0 0 h 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionNoModelScrollWheel.log f3d-3.4.1/testing/recordings/TestInteractionNoModelScrollWheel.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionNoModelScrollWheel.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionNoModelScrollWheel.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,696 +1,32 @@
 # StreamVersion 1.2
-ExposeEvent 0 599 0 0 0 0 0
-RenderEvent 0 599 0 0 0 0 0
-KeyReleaseEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-TimerEvent 4482 62 0 13 1 Return 0
-EnterEvent 995 167 0 0 0 Return 0
-MouseMoveEvent 995 167 0 0 0 Return 0
-MouseMoveEvent 991 169 0 0 0 Return 0
-MouseMoveEvent 983 173 0 0 0 Return 0
-MouseMoveEvent 975 173 0 0 0 Return 0
-MouseMoveEvent 967 177 0 0 0 Return 0
-TimerEvent 967 177 0 0 0 Return 0
-MouseMoveEvent 951 185 0 0 0 Return 0
-MouseMoveEvent 941 189 0 0 0 Return 0
-MouseMoveEvent 933 193 0 0 0 Return 0
-MouseMoveEvent 927 197 0 0 0 Return 0
-MouseMoveEvent 919 199 0 0 0 Return 0
-MouseMoveEvent 913 203 0 0 0 Return 0
-MouseMoveEvent 907 207 0 0 0 Return 0
-MouseMoveEvent 901 211 0 0 0 Return 0
-MouseMoveEvent 895 213 0 0 0 Return 0
-MouseMoveEvent 889 217 0 0 0 Return 0
-MouseMoveEvent 885 219 0 0 0 Return 0
-MouseMoveEvent 877 221 0 0 0 Return 0
-MouseMoveEvent 871 223 0 0 0 Return 0
-MouseMoveEvent 865 225 0 0 0 Return 0
-MouseMoveEvent 859 225 0 0 0 Return 0
-MouseMoveEvent 853 227 0 0 0 Return 0
-TimerEvent 853 227 0 0 0 Return 0
-MouseMoveEvent 850 227 0 0 0 Return 0
-MouseMoveEvent 846 228 0 0 0 Return 0
-MouseMoveEvent 840 228 0 0 0 Return 0
-MouseMoveEvent 836 228 0 0 0 Return 0
-MouseMoveEvent 833 230 0 0 0 Return 0
-MouseMoveEvent 829 230 0 0 0 Return 0
-MouseMoveEvent 826 230 0 0 0 Return 0
-MouseMoveEvent 823 232 0 0 0 Return 0
-MouseMoveEvent 821 232 0 0 0 Return 0
-MouseMoveEvent 820 232 0 0 0 Return 0
-MouseMoveEvent 815 232 0 0 0 Return 0
-MouseMoveEvent 814 234 0 0 0 Return 0
-MouseMoveEvent 812 234 0 0 0 Return 0
-MouseMoveEvent 810 234 0 0 0 Return 0
-MouseMoveEvent 807 234 0 0 0 Return 0
-TimerEvent 807 234 0 0 0 Return 0
-MouseMoveEvent 804 235 0 0 0 Return 0
-MouseMoveEvent 803 235 0 0 0 Return 0
-MouseMoveEvent 802 235 0 0 0 Return 0
-MouseMoveEvent 801 236 0 0 0 Return 0
-TimerEvent 801 236 0 0 0 Return 0
-TimerEvent 801 236 0 0 0 Return 0
-MouseMoveEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-TimerEvent 800 236 0 0 0 Return 0
-MouseMoveEvent 800 236 0 0 0 Return 0
-MouseMoveEvent 799 237 0 0 0 Return 0
-MouseMoveEvent 798 237 0 0 0 Return 0
-MouseMoveEvent 798 238 0 0 0 Return 0
-MouseMoveEvent 797 240 0 0 0 Return 0
-MouseMoveEvent 796 240 0 0 0 Return 0
-MouseMoveEvent 795 240 0 0 0 Return 0
-MouseMoveEvent 794 241 0 0 0 Return 0
-MouseMoveEvent 794 242 0 0 0 Return 0
-MouseMoveEvent 793 242 0 0 0 Return 0
-TimerEvent 793 242 0 0 0 Return 0
-MouseMoveEvent 791 243 0 0 0 Return 0
-MouseMoveEvent 789 244 0 0 0 Return 0
-MouseMoveEvent 787 244 0 0 0 Return 0
-MouseMoveEvent 786 246 0 0 0 Return 0
-MouseMoveEvent 786 247 0 0 0 Return 0
-MouseMoveEvent 785 248 0 0 0 Return 0
-MouseMoveEvent 784 248 0 0 0 Return 0
-MouseMoveEvent 783 248 0 0 0 Return 0
-MouseMoveEvent 782 248 0 0 0 Return 0
-MouseMoveEvent 782 249 0 0 0 Return 0
-MouseMoveEvent 780 250 0 0 0 Return 0
-MouseMoveEvent 779 250 0 0 0 Return 0
-TimerEvent 779 250 0 0 0 Return 0
-MouseMoveEvent 777 250 0 0 0 Return 0
-MouseMoveEvent 776 250 0 0 0 Return 0
-MouseMoveEvent 776 252 0 0 0 Return 0
-MouseMoveEvent 775 253 0 0 0 Return 0
-MouseMoveEvent 774 253 0 0 0 Return 0
-MouseMoveEvent 773 253 0 0 0 Return 0
-MouseMoveEvent 772 253 0 0 0 Return 0
-MouseMoveEvent 769 254 0 0 0 Return 0
-MouseMoveEvent 768 254 0 0 0 Return 0
-MouseMoveEvent 767 255 0 0 0 Return 0
-TimerEvent 767 255 0 0 0 Return 0
-MouseMoveEvent 765 255 0 0 0 Return 0
-MouseMoveEvent 764 255 0 0 0 Return 0
-MouseMoveEvent 763 256 0 0 0 Return 0
-MouseMoveEvent 762 256 0 0 0 Return 0
-MouseMoveEvent 761 257 0 0 0 Return 0
-MouseMoveEvent 760 257 0 0 0 Return 0
-MouseMoveEvent 759 257 0 0 0 Return 0
-MouseMoveEvent 758 257 0 0 0 Return 0
-MouseMoveEvent 757 257 0 0 0 Return 0
-MouseMoveEvent 755 257 0 0 0 Return 0
-TimerEvent 755 257 0 0 0 Return 0
-MouseMoveEvent 755 258 0 0 0 Return 0
-MouseMoveEvent 754 258 0 0 0 Return 0
-MouseMoveEvent 753 258 0 0 0 Return 0
-MouseMoveEvent 752 259 0 0 0 Return 0
-TimerEvent 752 259 0 0 0 Return 0
-TimerEvent 752 259 0 0 0 Return 0
-MouseMoveEvent 751 259 0 0 0 Return 0
-MouseMoveEvent 751 259 0 0 0 Return 0
-TimerEvent 751 259 0 0 0 Return 0
-MouseMoveEvent 751 260 0 0 0 Return 0
-TimerEvent 751 260 0 0 0 Return 0
-MouseMoveEvent 750 260 0 0 0 Return 0
-TimerEvent 750 260 0 0 0 Return 0
-MouseMoveEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-TimerEvent 749 261 0 0 0 Return 0
-KeyPressEvent 749 261 0 104 1 h 0
-CharEvent 749 261 0 104 1 h 0
-TimerEvent 749 261 0 104 1 h 0
-MouseMoveEvent 749 262 0 0 0 h 0
-MouseMoveEvent 749 262 0 0 0 h 0
-TimerEvent 749 262 0 0 0 h 0
-MouseMoveEvent 749 262 0 0 0 h 0
-TimerEvent 749 262 0 0 0 h 0
-KeyReleaseEvent 749 262 0 104 1 h 0
-TimerEvent 749 262 0 104 1 h 0
-MouseMoveEvent 748 262 0 0 0 h 0
-TimerEvent 748 262 0 0 0 h 0
-MouseMoveEvent 748 263 0 0 0 h 0
-TimerEvent 748 263 0 0 0 h 0
-MouseMoveEvent 747 263 0 0 0 h 0
-MouseMoveEvent 746 263 0 0 0 h 0
-MouseMoveEvent 745 264 0 0 0 h 0
-MouseMoveEvent 744 264 0 0 0 h 0
-MouseMoveEvent 743 265 0 0 0 h 0
-MouseMoveEvent 742 265 0 0 0 h 0
-MouseMoveEvent 741 266 0 0 0 h 0
-TimerEvent 741 266 0 0 0 h 0
-MouseMoveEvent 739 266 0 0 0 h 0
-MouseMoveEvent 738 268 0 0 0 h 0
-MouseMoveEvent 735 268 0 0 0 h 0
-MouseMoveEvent 734 269 0 0 0 h 0
-MouseMoveEvent 732 269 0 0 0 h 0
-MouseMoveEvent 730 270 0 0 0 h 0
-MouseMoveEvent 726 272 0 0 0 h 0
-MouseMoveEvent 723 272 0 0 0 h 0
-MouseMoveEvent 721 272 0 0 0 h 0
-MouseMoveEvent 718 272 0 0 0 h 0
-MouseMoveEvent 717 272 0 0 0 h 0
-MouseMoveEvent 714 274 0 0 0 h 0
-MouseMoveEvent 710 276 0 0 0 h 0
-MouseMoveEvent 708 276 0 0 0 h 0
-MouseMoveEvent 706 276 0 0 0 h 0
-TimerEvent 706 276 0 0 0 h 0
-MouseMoveEvent 705 276 0 0 0 h 0
-MouseMoveEvent 704 276 0 0 0 h 0
-MouseMoveEvent 703 276 0 0 0 h 0
-MouseMoveEvent 704 276 0 0 0 h 0
-TimerEvent 704 276 0 0 0 h 0
-MouseMoveEvent 704 276 0 0 0 h 0
-MouseMoveEvent 704 277 0 0 0 h 0
-TimerEvent 704 277 0 0 0 h 0
-MouseMoveEvent 706 277 0 0 0 h 0
-MouseMoveEvent 708 277 0 0 0 h 0
-MouseMoveEvent 707 277 0 0 0 h 0
-MouseMoveEvent 708 279 0 0 0 h 0
-MouseMoveEvent 709 279 0 0 0 h 0
-MouseMoveEvent 710 279 0 0 0 h 0
-MouseMoveEvent 709 279 0 0 0 h 0
-TimerEvent 709 279 0 0 0 h 0
-MouseMoveEvent 709 280 0 0 0 h 0
-MouseMoveEvent 707 280 0 0 0 h 0
-MouseMoveEvent 704 280 0 0 0 h 0
-MouseMoveEvent 704 279 0 0 0 h 0
-MouseMoveEvent 704 278 0 0 0 h 0
-TimerEvent 704 278 0 0 0 h 0
-MouseMoveEvent 704 277 0 0 0 h 0
-MouseMoveEvent 702 277 0 0 0 h 0
-MouseMoveEvent 700 277 0 0 0 h 0
-MouseMoveEvent 699 276 0 0 0 h 0
-MouseMoveEvent 694 276 0 0 0 h 0
-MouseMoveEvent 690 276 0 0 0 h 0
-MouseMoveEvent 682 276 0 0 0 h 0
-TimerEvent 682 276 0 0 0 h 0
-MouseMoveEvent 674 274 0 0 0 h 0
-MouseMoveEvent 666 272 0 0 0 h 0
-MouseMoveEvent 666 270 0 0 0 h 0
-MouseMoveEvent 664 270 0 0 0 h 0
-MouseMoveEvent 662 270 0 0 0 h 0
-MouseMoveEvent 657 270 0 0 0 h 0
-MouseMoveEvent 651 274 0 0 0 h 0
-MouseMoveEvent 650 276 0 0 0 h 0
-MouseMoveEvent 646 278 0 0 0 h 0
-MouseMoveEvent 643 282 0 0 0 h 0
-MouseMoveEvent 639 286 0 0 0 h 0
-MouseMoveEvent 633 286 0 0 0 h 0
-MouseMoveEvent 629 286 0 0 0 h 0
-MouseMoveEvent 625 286 0 0 0 h 0
-MouseMoveEvent 622 289 0 0 0 h 0
-MouseMoveEvent 616 291 0 0 0 h 0
-TimerEvent 616 291 0 0 0 h 0
-MouseMoveEvent 606 295 0 0 0 h 0
-MouseMoveEvent 602 295 0 0 0 h 0
-MouseMoveEvent 597 295 0 0 0 h 0
-MouseMoveEvent 591 295 0 0 0 h 0
-MouseMoveEvent 587 297 0 0 0 h 0
-MouseMoveEvent 583 299 0 0 0 h 0
-MouseMoveEvent 580 299 0 0 0 h 0
-MouseMoveEvent 576 299 0 0 0 h 0
-MouseMoveEvent 571 299 0 0 0 h 0
-MouseMoveEvent 567 299 0 0 0 h 0
-MouseMoveEvent 561 301 0 0 0 h 0
-MouseMoveEvent 557 301 0 0 0 h 0
-MouseMoveEvent 551 303 0 0 0 h 0
-MouseMoveEvent 548 303 0 0 0 h 0
-MouseMoveEvent 544 303 0 0 0 h 0
-MouseMoveEvent 541 303 0 0 0 h 0
-TimerEvent 541 303 0 0 0 h 0
-MouseMoveEvent 535 303 0 0 0 h 0
-MouseMoveEvent 531 303 0 0 0 h 0
-MouseMoveEvent 526 303 0 0 0 h 0
-MouseMoveEvent 522 303 0 0 0 h 0
-MouseMoveEvent 519 303 0 0 0 h 0
-MouseMoveEvent 515 303 0 0 0 h 0
-MouseMoveEvent 510 303 0 0 0 h 0
-MouseMoveEvent 506 303 0 0 0 h 0
-MouseMoveEvent 503 303 0 0 0 h 0
-MouseMoveEvent 499 303 0 0 0 h 0
-MouseMoveEvent 496 305 0 0 0 h 0
-MouseMoveEvent 492 305 0 0 0 h 0
-MouseMoveEvent 491 305 0 0 0 h 0
-MouseMoveEvent 488 305 0 0 0 h 0
-MouseMoveEvent 485 305 0 0 0 h 0
-MouseMoveEvent 482 305 0 0 0 h 0
-TimerEvent 482 305 0 0 0 h 0
-MouseMoveEvent 477 306 0 0 0 h 0
-MouseMoveEvent 474 306 0 0 0 h 0
-MouseMoveEvent 470 306 0 0 0 h 0
-MouseMoveEvent 468 306 0 0 0 h 0
-MouseMoveEvent 465 306 0 0 0 h 0
-MouseMoveEvent 463 306 0 0 0 h 0
-MouseMoveEvent 462 306 0 0 0 h 0
-MouseMoveEvent 461 306 0 0 0 h 0
-MouseMoveEvent 458 306 0 0 0 h 0
-MouseMoveEvent 457 306 0 0 0 h 0
-MouseMoveEvent 456 306 0 0 0 h 0
-MouseMoveEvent 453 306 0 0 0 h 0
-MouseMoveEvent 452 306 0 0 0 h 0
-MouseMoveEvent 451 306 0 0 0 h 0
-TimerEvent 451 306 0 0 0 h 0
-MouseMoveEvent 450 306 0 0 0 h 0
-MouseMoveEvent 449 306 0 0 0 h 0
-MouseMoveEvent 448 306 0 0 0 h 0
-MouseMoveEvent 446 306 0 0 0 h 0
-MouseMoveEvent 445 306 0 0 0 h 0
-MouseMoveEvent 444 306 0 0 0 h 0
-MouseMoveEvent 443 306 0 0 0 h 0
-MouseMoveEvent 442 306 0 0 0 h 0
-MouseMoveEvent 441 306 0 0 0 h 0
-MouseMoveEvent 440 306 0 0 0 h 0
-MouseMoveEvent 439 306 0 0 0 h 0
-MouseMoveEvent 438 306 0 0 0 h 0
-MouseMoveEvent 437 306 0 0 0 h 0
-MouseMoveEvent 436 306 0 0 0 h 0
-TimerEvent 436 306 0 0 0 h 0
-MouseMoveEvent 433 306 0 0 0 h 0
-MouseMoveEvent 432 306 0 0 0 h 0
-MouseMoveEvent 431 306 0 0 0 h 0
-MouseMoveEvent 430 306 0 0 0 h 0
-MouseMoveEvent 429 306 0 0 0 h 0
-MouseMoveEvent 428 306 0 0 0 h 0
-MouseMoveEvent 427 306 0 0 0 h 0
-TimerEvent 427 306 0 0 0 h 0
-MouseMoveEvent 426 306 0 0 0 h 0
-MouseMoveEvent 425 306 0 0 0 h 0
-MouseMoveEvent 424 306 0 0 0 h 0
-MouseMoveEvent 423 306 0 0 0 h 0
-MouseMoveEvent 422 306 0 0 0 h 0
-MouseMoveEvent 421 306 0 0 0 h 0
-MouseMoveEvent 421 307 0 0 0 h 0
-MouseMoveEvent 420 307 0 0 0 h 0
-TimerEvent 420 307 0 0 0 h 0
-MouseMoveEvent 419 307 0 0 0 h 0
-MouseMoveEvent 418 307 0 0 0 h 0
-TimerEvent 418 307 0 0 0 h 0
-TimerEvent 418 307 0 0 0 h 0
-MouseMoveEvent 418 306 0 0 0 h 0
-MouseMoveEvent 418 305 0 0 0 h 0
-TimerEvent 418 305 0 0 0 h 0
-TimerEvent 418 305 0 0 0 h 0
-MouseMoveEvent 418 305 0 0 0 h 0
-MouseMoveEvent 418 304 0 0 0 h 0
-MouseMoveEvent 417 304 0 0 0 h 0
-TimerEvent 417 304 0 0 0 h 0
-MouseMoveEvent 416 304 0 0 0 h 0
-MouseMoveEvent 416 303 0 0 0 h 0
-MouseMoveEvent 415 302 0 0 0 h 0
-MouseMoveEvent 414 302 0 0 0 h 0
-TimerEvent 414 302 0 0 0 h 0
-MouseMoveEvent 412 302 0 0 0 h 0
-MouseMoveEvent 411 302 0 0 0 h 0
-MouseMoveEvent 410 302 0 0 0 h 0
-MouseMoveEvent 409 302 0 0 0 h 0
-MouseMoveEvent 408 302 0 0 0 h 0
-MouseMoveEvent 407 302 0 0 0 h 0
-MouseMoveEvent 406 302 0 0 0 h 0
-MouseMoveEvent 405 302 0 0 0 h 0
-MouseMoveEvent 404 302 0 0 0 h 0
-MouseMoveEvent 403 302 0 0 0 h 0
-MouseMoveEvent 402 302 0 0 0 h 0
-TimerEvent 402 302 0 0 0 h 0
-MouseMoveEvent 401 302 0 0 0 h 0
-MouseMoveEvent 400 302 0 0 0 h 0
-MouseMoveEvent 399 302 0 0 0 h 0
-MouseMoveEvent 398 302 0 0 0 h 0
-MouseMoveEvent 397 302 0 0 0 h 0
-MouseMoveEvent 396 302 0 0 0 h 0
-MouseMoveEvent 394 302 0 0 0 h 0
-MouseMoveEvent 391 302 0 0 0 h 0
-MouseMoveEvent 390 302 0 0 0 h 0
-MouseMoveEvent 388 302 0 0 0 h 0
-TimerEvent 388 302 0 0 0 h 0
-MouseMoveEvent 385 302 0 0 0 h 0
-MouseMoveEvent 384 302 0 0 0 h 0
-MouseMoveEvent 383 302 0 0 0 h 0
-MouseMoveEvent 382 302 0 0 0 h 0
-MouseMoveEvent 381 302 0 0 0 h 0
-MouseMoveEvent 380 302 0 0 0 h 0
-MouseMoveEvent 379 302 0 0 0 h 0
-MouseMoveEvent 378 302 0 0 0 h 0
-MouseMoveEvent 377 302 0 0 0 h 0
-MouseMoveEvent 375 302 0 0 0 h 0
-MouseMoveEvent 374 302 0 0 0 h 0
-MouseMoveEvent 373 302 0 0 0 h 0
-MouseMoveEvent 372 302 0 0 0 h 0
-MouseMoveEvent 371 302 0 0 0 h 0
-MouseMoveEvent 370 302 0 0 0 h 0
-TimerEvent 370 302 0 0 0 h 0
-MouseMoveEvent 368 302 0 0 0 h 0
-MouseMoveEvent 367 302 0 0 0 h 0
-MouseMoveEvent 366 302 0 0 0 h 0
-MouseMoveEvent 365 302 0 0 0 h 0
-MouseMoveEvent 364 302 0 0 0 h 0
-MouseMoveEvent 363 302 0 0 0 h 0
-MouseMoveEvent 362 302 0 0 0 h 0
-MouseMoveEvent 361 302 0 0 0 h 0
-MouseMoveEvent 359 302 0 0 0 h 0
-MouseMoveEvent 358 302 0 0 0 h 0
-MouseMoveEvent 357 302 0 0 0 h 0
-TimerEvent 357 302 0 0 0 h 0
-MouseMoveEvent 356 302 0 0 0 h 0
-MouseMoveEvent 355 302 0 0 0 h 0
-MouseMoveEvent 354 302 0 0 0 h 0
-MouseMoveEvent 353 302 0 0 0 h 0
-MouseMoveEvent 352 302 0 0 0 h 0
-MouseMoveEvent 351 302 0 0 0 h 0
-TimerEvent 351 302 0 0 0 h 0
-MouseMoveEvent 350 302 0 0 0 h 0
-MouseMoveEvent 349 302 0 0 0 h 0
-MouseMoveEvent 349 301 0 0 0 h 0
-MouseMoveEvent 348 301 0 0 0 h 0
-MouseMoveEvent 348 300 0 0 0 h 0
-MouseMoveEvent 348 299 0 0 0 h 0
-MouseWheelBackwardEvent 348 299 0 0 0 h 0
-TimerEvent 348 299 0 0 0 h 0
-MouseMoveEvent 347 298 0 0 0 h 0
-MouseMoveEvent 347 297 0 0 0 h 0
-MouseMoveEvent 346 297 0 0 0 h 0
-MouseWheelBackwardEvent 346 297 0 0 1 h 0
-MouseMoveEvent 346 296 0 0 0 h 0
-MouseMoveEvent 346 295 0 0 0 h 0
-TimerEvent 346 295 0 0 0 h 0
-MouseMoveEvent 345 295 0 0 0 h 0
-MouseWheelBackwardEvent 345 295 0 0 0 h 0
-MouseWheelBackwardEvent 345 295 0 0 1 h 0
-TimerEvent 345 295 0 0 0 h 0
-MouseWheelBackwardEvent 345 295 0 0 0 h 0
-MouseMoveEvent 345 294 0 0 0 h 0
-TimerEvent 345 294 0 0 0 h 0
-TimerEvent 345 294 0 0 0 h 0
-TimerEvent 345 294 0 0 0 h 0
-TimerEvent 345 294 0 0 0 h 0
-TimerEvent 345 294 0 0 0 h 0
-MouseMoveEvent 345 293 0 0 0 h 0
-TimerEvent 345 293 0 0 0 h 0
-TimerEvent 345 293 0 0 0 h 0
-TimerEvent 345 293 0 0 0 h 0
-MouseMoveEvent 345 293 0 0 0 h 0
-MouseMoveEvent 345 293 0 0 0 h 0
-TimerEvent 345 293 0 0 0 h 0
-MouseMoveEvent 345 292 0 0 0 h 0
-MouseWheelBackwardEvent 345 292 0 0 1 h 0
-MouseMoveEvent 345 291 0 0 0 h 0
-MouseMoveEvent 344 290 0 0 0 h 0
-MouseMoveEvent 343 290 0 0 0 h 0
-MouseWheelBackwardEvent 343 290 0 0 0 h 0
-MouseMoveEvent 343 289 0 0 0 h 0
-TimerEvent 343 289 0 0 0 h 0
-MouseWheelBackwardEvent 343 289 0 0 1 h 0
-MouseWheelBackwardEvent 343 289 0 0 0 h 0
-MouseWheelBackwardEvent 343 289 0 0 1 h 0
-TimerEvent 343 289 0 0 0 h 0
-MouseWheelBackwardEvent 343 289 0 0 0 h 0
-TimerEvent 343 289 0 0 0 h 0
-MouseMoveEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-TimerEvent 343 288 0 0 0 h 0
-MouseMoveEvent 343 288 0 0 0 h 0
-MouseMoveEvent 343 289 0 0 0 h 0
-MouseMoveEvent 343 290 0 0 0 h 0
-MouseMoveEvent 344 290 0 0 0 h 0
-MouseMoveEvent 344 291 0 0 0 h 0
-TimerEvent 344 291 0 0 0 h 0
-MouseMoveEvent 344 292 0 0 0 h 0
-MouseMoveEvent 344 293 0 0 0 h 0
-MouseMoveEvent 345 293 0 0 0 h 0
-MouseMoveEvent 346 294 0 0 0 h 0
-MouseMoveEvent 346 297 0 0 0 h 0
-MouseMoveEvent 347 297 0 0 0 h 0
-MouseMoveEvent 348 297 0 0 0 h 0
-MouseMoveEvent 348 298 0 0 0 h 0
-MouseMoveEvent 350 301 0 0 0 h 0
-MouseMoveEvent 351 301 0 0 0 h 0
-MouseMoveEvent 352 302 0 0 0 h 0
-MouseMoveEvent 355 303 0 0 0 h 0
-MouseMoveEvent 359 303 0 0 0 h 0
-MouseMoveEvent 361 307 0 0 0 h 0
-TimerEvent 361 307 0 0 0 h 0
-MouseMoveEvent 365 310 0 0 0 h 0
-MouseMoveEvent 369 312 0 0 0 h 0
-MouseMoveEvent 375 314 0 0 0 h 0
-MouseMoveEvent 381 318 0 0 0 h 0
-MouseMoveEvent 385 320 0 0 0 h 0
-MouseMoveEvent 393 324 0 0 0 h 0
-MouseMoveEvent 401 326 0 0 0 h 0
-MouseMoveEvent 407 330 0 0 0 h 0
-MouseMoveEvent 415 332 0 0 0 h 0
-MouseMoveEvent 423 336 0 0 0 h 0
-MouseMoveEvent 435 342 0 0 0 h 0
-MouseMoveEvent 443 346 0 0 0 h 0
-MouseMoveEvent 453 350 0 0 0 h 0
-MouseMoveEvent 465 352 0 0 0 h 0
-MouseMoveEvent 475 358 0 0 0 h 0
-TimerEvent 475 358 0 0 0 h 0
-MouseMoveEvent 497 364 0 0 0 h 0
-MouseMoveEvent 509 368 0 0 0 h 0
-MouseMoveEvent 521 372 0 0 0 h 0
-MouseMoveEvent 533 376 0 0 0 h 0
-MouseMoveEvent 545 380 0 0 0 h 0
-MouseMoveEvent 555 384 0 0 0 h 0
-MouseMoveEvent 567 388 0 0 0 h 0
-MouseMoveEvent 579 392 0 0 0 h 0
-MouseMoveEvent 593 396 0 0 0 h 0
-MouseMoveEvent 603 400 0 0 0 h 0
-MouseMoveEvent 617 404 0 0 0 h 0
-MouseMoveEvent 627 408 0 0 0 h 0
-MouseMoveEvent 637 412 0 0 0 h 0
-MouseMoveEvent 649 416 0 0 0 h 0
-MouseMoveEvent 659 420 0 0 0 h 0
-MouseMoveEvent 669 422 0 0 0 h 0
-TimerEvent 669 422 0 0 0 h 0
-MouseMoveEvent 679 426 0 0 0 h 0
-MouseMoveEvent 691 430 0 0 0 h 0
-MouseMoveEvent 699 434 0 0 0 h 0
-MouseMoveEvent 709 438 0 0 0 h 0
-MouseMoveEvent 719 442 0 0 0 h 0
-MouseMoveEvent 729 446 0 0 0 h 0
-MouseMoveEvent 739 452 0 0 0 h 0
-MouseMoveEvent 749 456 0 0 0 h 0
-MouseMoveEvent 757 462 0 0 0 h 0
-MouseMoveEvent 765 466 0 0 0 h 0
-MouseMoveEvent 775 472 0 0 0 h 0
-MouseMoveEvent 783 478 0 0 0 h 0
-MouseMoveEvent 793 482 0 0 0 h 0
-MouseMoveEvent 801 486 0 0 0 h 0
-MouseMoveEvent 809 492 0 0 0 h 0
-MouseMoveEvent 817 500 0 0 0 h 0
-TimerEvent 817 500 0 0 0 h 0
-MouseMoveEvent 831 510 0 0 0 h 0
-MouseMoveEvent 839 516 0 0 0 h 0
-MouseMoveEvent 847 520 0 0 0 h 0
-MouseMoveEvent 853 526 0 0 0 h 0
-MouseMoveEvent 861 530 0 0 0 h 0
-MouseMoveEvent 867 536 0 0 0 h 0
-MouseMoveEvent 873 540 0 0 0 h 0
-MouseMoveEvent 881 544 0 0 0 h 0
-MouseMoveEvent 885 548 0 0 0 h 0
-MouseMoveEvent 891 552 0 0 0 h 0
-MouseMoveEvent 897 554 0 0 0 h 0
-MouseMoveEvent 901 556 0 0 0 h 0
-MouseMoveEvent 904 560 0 0 0 h 0
-MouseMoveEvent 910 562 0 0 0 h 0
-MouseMoveEvent 912 563 0 0 0 h 0
-MouseMoveEvent 915 565 0 0 0 h 0
-TimerEvent 915 565 0 0 0 h 0
-MouseMoveEvent 919 567 0 0 0 h 0
-MouseMoveEvent 922 568 0 0 0 h 0
-MouseMoveEvent 924 568 0 0 0 h 0
-MouseMoveEvent 925 570 0 0 0 h 0
-MouseMoveEvent 928 571 0 0 0 h 0
-MouseMoveEvent 931 571 0 0 0 h 0
-MouseMoveEvent 933 571 0 0 0 h 0
-MouseMoveEvent 933 572 0 0 0 h 0
-MouseMoveEvent 933 573 0 0 0 h 0
-MouseMoveEvent 936 573 0 0 0 h 0
-MouseMoveEvent 938 573 0 0 0 h 0
-TimerEvent 938 573 0 0 0 h 0
-MouseMoveEvent 939 573 0 0 0 h 0
-MouseMoveEvent 940 573 0 0 0 h 0
-MouseMoveEvent 940 574 0 0 0 h 0
-MouseMoveEvent 940 575 0 0 0 h 0
-TimerEvent 940 575 0 0 0 h 0
-MouseMoveEvent 940 575 0 0 0 h 0
-MouseMoveEvent 940 576 0 0 0 h 0
-MouseMoveEvent 941 576 0 0 0 h 0
-MouseMoveEvent 941 577 0 0 0 h 0
-TimerEvent 941 577 0 0 0 h 0
-MouseMoveEvent 941 578 0 0 0 h 0
-MouseMoveEvent 941 579 0 0 0 h 0
-MouseMoveEvent 942 579 0 0 0 h 0
-MouseMoveEvent 944 580 0 0 0 h 0
-MouseMoveEvent 944 581 0 0 0 h 0
-TimerEvent 944 581 0 0 0 h 0
-MouseMoveEvent 944 582 0 0 0 h 0
-MouseMoveEvent 945 582 0 0 0 h 0
-MouseMoveEvent 945 583 0 0 0 h 0
-TimerEvent 945 583 0 0 0 h 0
-MouseMoveEvent 945 584 0 0 0 h 0
-TimerEvent 945 584 0 0 0 h 0
-TimerEvent 945 584 0 0 0 h 0
-MouseMoveEvent 945 584 0 0 0 h 0
-MouseMoveEvent 945 583 0 0 0 h 0
-MouseMoveEvent 945 582 0 0 0 h 0
-TimerEvent 945 582 0 0 0 h 0
-MouseMoveEvent 946 582 0 0 0 h 0
-MouseMoveEvent 946 581 0 0 0 h 0
-MouseMoveEvent 946 580 0 0 0 h 0
-TimerEvent 946 580 0 0 0 h 0
-MouseMoveEvent 947 580 0 0 0 h 0
-MouseMoveEvent 948 579 0 0 0 h 0
-MouseMoveEvent 949 579 0 0 0 h 0
-MouseMoveEvent 950 578 0 0 0 h 0
-TimerEvent 950 578 0 0 0 h 0
-MouseMoveEvent 951 578 0 0 0 h 0
-MouseMoveEvent 952 577 0 0 0 h 0
-MouseMoveEvent 953 577 0 0 0 h 0
-MouseMoveEvent 954 577 0 0 0 h 0
-MouseMoveEvent 955 577 0 0 0 h 0
-MouseMoveEvent 956 577 0 0 0 h 0
-MouseMoveEvent 957 577 0 0 0 h 0
-MouseMoveEvent 958 577 0 0 0 h 0
-MouseMoveEvent 959 577 0 0 0 h 0
-TimerEvent 959 577 0 0 0 h 0
-MouseMoveEvent 960 577 0 0 0 h 0
-MouseMoveEvent 961 577 0 0 0 h 0
-MouseMoveEvent 962 577 0 0 0 h 0
-MouseMoveEvent 964 577 0 0 0 h 0
-MouseMoveEvent 965 577 0 0 0 h 0
-MouseMoveEvent 966 577 0 0 0 h 0
-MouseMoveEvent 968 577 0 0 0 h 0
-MouseMoveEvent 970 577 0 0 0 h 0
-MouseMoveEvent 971 577 0 0 0 h 0
-MouseMoveEvent 971 578 0 0 0 h 0
-TimerEvent 971 578 0 0 0 h 0
-MouseMoveEvent 973 578 0 0 0 h 0
-MouseMoveEvent 973 579 0 0 0 h 0
-MouseMoveEvent 975 580 0 0 0 h 0
-MouseMoveEvent 976 580 0 0 0 h 0
-MouseMoveEvent 976 581 0 0 0 h 0
-MouseMoveEvent 977 581 0 0 0 h 0
-MouseMoveEvent 978 581 0 0 0 h 0
-MouseMoveEvent 978 582 0 0 0 h 0
-MouseMoveEvent 981 584 0 0 0 h 0
-MouseMoveEvent 984 585 0 0 0 h 0
-MouseMoveEvent 986 587 0 0 0 h 0
-TimerEvent 986 587 0 0 0 h 0
-MouseMoveEvent 988 588 0 0 0 h 0
-MouseMoveEvent 989 589 0 0 0 h 0
-MouseMoveEvent 991 590 0 0 0 h 0
-MouseMoveEvent 992 590 0 0 0 h 0
-MouseMoveEvent 993 590 0 0 0 h 0
-MouseMoveEvent 994 590 0 0 0 h 0
-MouseMoveEvent 995 591 0 0 0 h 0
-MouseMoveEvent 998 591 0 0 0 h 0
-LeaveEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
-TimerEvent 1000 593 0 0 0 h 0
+ExposeEvent 0 289 0 0 0 0 0
+RenderEvent 0 289 0 0 0 0 0
+EnterEvent 609 197 0 0 0 0 0
+MouseMoveEvent 609 197 0 0 0 0 0
+
+# H
+KeyPressEvent 700 193 0 104 1 h 0
+CharEvent 700 193 0 104 1 h 0
+TimerEvent 700 193 0 104 1 h 0
+TimerEvent 700 193 0 104 1 h 0
+KeyReleaseEvent 700 193 0 104 1 h 0
+TimerEvent 700 193 0 104 1 h 0
+
+# Scroll to bottom
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+MouseWheelBackwardEvent 700 193 0 0 0 h 0
+MouseWheelBackwardEvent 700 193 0 0 1 h 0
+TimerEvent 700 193 0 0 1 h 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionOpacity.log f3d-3.4.1/testing/recordings/TestInteractionOpacity.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionOpacity.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionOpacity.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,7 @@
 # StreamVersion 1.2
 RenderEvent 0 0 0 0 0 0 0
+
+# Shift + P 21 times
 KeyPressEvent 0 0 1 0 1 Shift_L 0
 KeyPressEvent 0 0 1 80 1 P 0
 CharEvent 0 0 1 80 1 P 0
@@ -64,70 +66,9 @@
 KeyPressEvent 0 0 1 80 1 P 0
 CharEvent 0 0 1 80 1 P 0
 KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
-KeyPressEvent 0 0 1 80 1 P 0
-CharEvent 0 0 1 80 1 P 0
-KeyReleaseEvent 0 0 1 80 1 P 0
 KeyReleaseEvent 0 0 0 0 1 Shift_L 0
+
+# Ctrl + P 5 times
 KeyPressEvent 0 0 2 0 1 Control_L 0
 KeyPressEvent 0 0 2 16 1 p 0
 CharEvent 0 0 2 16 1 p 0
@@ -148,4 +89,3 @@
 CharEvent 0 0 2 16 1 p 0
 KeyReleaseEvent 0 0 2 16 1 p 0
 KeyReleaseEvent 0 0 0 0 1 Control_L 0
-
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionPointCloudTAA.log f3d-3.4.1/testing/recordings/TestInteractionPointCloudTAA.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionPointCloudTAA.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionPointCloudTAA.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,20 @@
+# StreamVersion 1.2
+ConfigureEvent 0 1039 0 0 0 0 0
+ExposeEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionPointSpritesCycle.log f3d-3.4.1/testing/recordings/TestInteractionPointSpritesCycle.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionPointSpritesCycle.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionPointSpritesCycle.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,32 @@
+# StreamVersion 1.1
+ExposeEvent 0 599 0 0 0 0
+
+# O
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
+
+# O
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
+
+# O
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
+
+# O
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
+
+# O
+RenderEvent 0 599 0 0 0 0
+KeyPressEvent 752 51 0 111 1 o
+CharEvent 752 51 0 111 1 o
+KeyReleaseEvent 752 51 0 111 1 o
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionResetCameraWithCameraIndex.log f3d-3.4.1/testing/recordings/TestInteractionResetCameraWithCameraIndex.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionResetCameraWithCameraIndex.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionResetCameraWithCameraIndex.log	2026-01-08 08:41:55.000000000 +0200
@@ -2,37 +2,6 @@
 ExposeEvent 0 599 0 0 0 0
 RenderEvent 0 599 0 0 0 0
 MouseMoveEvent 443 279 0 0 0 0
-MouseMoveEvent 442 279 0 0 0 0
-MouseMoveEvent 441 280 0 0 0 0
-MouseMoveEvent 439 281 0 0 0 0
-MouseMoveEvent 436 283 0 0 0 0
-MouseMoveEvent 430 286 0 0 0 0
-MouseMoveEvent 421 290 0 0 0 0
-MouseMoveEvent 413 293 0 0 0 0
-MouseMoveEvent 405 298 0 0 0 0
-MouseMoveEvent 397 304 0 0 0 0
-MouseMoveEvent 392 311 0 0 0 0
-MouseMoveEvent 382 319 0 0 0 0
-MouseMoveEvent 375 325 0 0 0 0
-MouseMoveEvent 369 332 0 0 0 0
-MouseMoveEvent 366 336 0 0 0 0
-MouseMoveEvent 364 339 0 0 0 0
-MouseMoveEvent 362 341 0 0 0 0
-MouseMoveEvent 360 343 0 0 0 0
-MouseMoveEvent 358 346 0 0 0 0
-MouseMoveEvent 356 351 0 0 0 0
-MouseMoveEvent 354 355 0 0 0 0
-MouseMoveEvent 351 360 0 0 0 0
-MouseMoveEvent 348 365 0 0 0 0
-MouseMoveEvent 345 370 0 0 0 0
-MouseMoveEvent 343 372 0 0 0 0
-MouseMoveEvent 342 374 0 0 0 0
-MouseMoveEvent 342 375 0 0 0 0
-MouseMoveEvent 341 375 0 0 0 0
-MouseMoveEvent 341 374 0 0 0 0
-MouseMoveEvent 341 372 0 0 0 0
-MouseMoveEvent 342 371 0 0 0 0
-MouseMoveEvent 344 368 0 0 0 0
 LeftButtonPressEvent 344 368 0 0 0 0
 StartInteractionEvent 344 368 0 0 0 0
 MouseMoveEvent 346 364 0 0 0 0
@@ -50,64 +19,6 @@
 MouseMoveEvent 376 344 0 0 0 0
 RenderEvent 376 344 0 0 0 0
 InteractionEvent 376 344 0 0 0 0
-MouseMoveEvent 385 339 0 0 0 0
-RenderEvent 385 339 0 0 0 0
-InteractionEvent 385 339 0 0 0 0
-MouseMoveEvent 410 329 0 0 0 0
-RenderEvent 410 329 0 0 0 0
-InteractionEvent 410 329 0 0 0 0
-MouseMoveEvent 428 323 0 0 0 0
-RenderEvent 428 323 0 0 0 0
-InteractionEvent 428 323 0 0 0 0
-MouseMoveEvent 445 317 0 0 0 0
-RenderEvent 445 317 0 0 0 0
-InteractionEvent 445 317 0 0 0 0
-MouseMoveEvent 464 311 0 0 0 0
-RenderEvent 464 311 0 0 0 0
-InteractionEvent 464 311 0 0 0 0
-MouseMoveEvent 483 305 0 0 0 0
-RenderEvent 483 305 0 0 0 0
-InteractionEvent 483 305 0 0 0 0
-MouseMoveEvent 507 299 0 0 0 0
-RenderEvent 507 299 0 0 0 0
-InteractionEvent 507 299 0 0 0 0
-MouseMoveEvent 543 289 0 0 0 0
-RenderEvent 543 289 0 0 0 0
-InteractionEvent 543 289 0 0 0 0
-MouseMoveEvent 559 285 0 0 0 0
-RenderEvent 559 285 0 0 0 0
-InteractionEvent 559 285 0 0 0 0
-MouseMoveEvent 572 280 0 0 0 0
-RenderEvent 572 280 0 0 0 0
-InteractionEvent 572 280 0 0 0 0
-MouseMoveEvent 582 277 0 0 0 0
-RenderEvent 582 277 0 0 0 0
-InteractionEvent 582 277 0 0 0 0
-MouseMoveEvent 588 275 0 0 0 0
-RenderEvent 588 275 0 0 0 0
-InteractionEvent 588 275 0 0 0 0
-MouseMoveEvent 592 272 0 0 0 0
-RenderEvent 592 272 0 0 0 0
-InteractionEvent 592 272 0 0 0 0
-MouseMoveEvent 596 270 0 0 0 0
-RenderEvent 596 270 0 0 0 0
-InteractionEvent 596 270 0 0 0 0
-MouseMoveEvent 599 270 0 0 0 0
-RenderEvent 599 270 0 0 0 0
-InteractionEvent 599 270 0 0 0 0
-MouseMoveEvent 600 268 0 0 0 0
-RenderEvent 600 268 0 0 0 0
-InteractionEvent 600 268 0 0 0 0
-MouseMoveEvent 601 267 0 0 0 0
-RenderEvent 601 267 0 0 0 0
-InteractionEvent 601 267 0 0 0 0
-MouseMoveEvent 602 267 0 0 0 0
-RenderEvent 602 267 0 0 0 0
-InteractionEvent 602 267 0 0 0 0
-MouseMoveEvent 603 267 0 0 0 0
-RenderEvent 603 267 0 0 0 0
-InteractionEvent 603 267 0 0 0 0
-MouseMoveEvent 603 267 0 0 0 0
 RenderEvent 603 267 0 0 0 0
 InteractionEvent 603 267 0 0 0 0
 LeftButtonReleaseEvent 603 267 0 0 0 0
@@ -115,57 +26,6 @@
 RenderEvent 603 267 0 0 0 0
 MouseMoveEvent 604 268 0 0 0 0
 MouseMoveEvent 611 275 0 0 0 0
-MouseMoveEvent 613 277 0 0 0 0
-MouseMoveEvent 615 279 0 0 0 0
-MouseMoveEvent 617 281 0 0 0 0
-MouseMoveEvent 619 283 0 0 0 0
-MouseMoveEvent 622 285 0 0 0 0
-MouseMoveEvent 631 289 0 0 0 0
-MouseMoveEvent 637 291 0 0 0 0
-MouseMoveEvent 642 294 0 0 0 0
-MouseMoveEvent 648 295 0 0 0 0
-MouseMoveEvent 656 296 0 0 0 0
-MouseMoveEvent 665 298 0 0 0 0
-MouseMoveEvent 674 299 0 0 0 0
-MouseMoveEvent 683 299 0 0 0 0
-MouseMoveEvent 692 301 0 0 0 0
-MouseMoveEvent 700 302 0 0 0 0
-MouseMoveEvent 707 302 0 0 0 0
-MouseMoveEvent 710 303 0 0 0 0
-MouseMoveEvent 712 303 0 0 0 0
-MouseMoveEvent 713 304 0 0 0 0
-MouseMoveEvent 714 305 0 0 0 0
-MouseMoveEvent 715 306 0 0 0 0
-MouseMoveEvent 716 307 0 0 0 0
-MouseMoveEvent 717 308 0 0 0 0
-MouseMoveEvent 717 309 0 0 0 0
-MouseMoveEvent 716 309 0 0 0 0
-MouseMoveEvent 710 309 0 0 0 0
-MouseMoveEvent 702 309 0 0 0 0
-MouseMoveEvent 689 311 0 0 0 0
-MouseMoveEvent 679 311 0 0 0 0
-MouseMoveEvent 673 311 0 0 0 0
-MouseMoveEvent 665 311 0 0 0 0
-MouseMoveEvent 654 308 0 0 0 0
-MouseMoveEvent 641 305 0 0 0 0
-MouseMoveEvent 628 301 0 0 0 0
-MouseMoveEvent 617 296 0 0 0 0
-MouseMoveEvent 610 288 0 0 0 0
-MouseMoveEvent 605 281 0 0 0 0
-MouseMoveEvent 602 273 0 0 0 0
-MouseMoveEvent 600 267 0 0 0 0
-MouseMoveEvent 597 262 0 0 0 0
-MouseMoveEvent 596 257 0 0 0 0
-MouseMoveEvent 595 253 0 0 0 0
-MouseMoveEvent 594 249 0 0 0 0
-MouseMoveEvent 592 246 0 0 0 0
-MouseMoveEvent 591 243 0 0 0 0
-MouseMoveEvent 589 241 0 0 0 0
-MouseMoveEvent 588 239 0 0 0 0
-MouseMoveEvent 585 237 0 0 0 0
-MouseMoveEvent 584 235 0 0 0 0
-MouseMoveEvent 584 234 0 0 0 0
-MouseMoveEvent 584 234 0 0 0 0
 RightButtonPressEvent 584 234 0 0 0 0
 StartInteractionEvent 584 234 0 0 0 0
 MouseMoveEvent 583 239 0 0 0 0
@@ -175,109 +35,6 @@
 RenderEvent 582 244 0 0 0 0
 InteractionEvent 582 244 0 0 0 0
 MouseMoveEvent 579 252 0 0 0 0
-RenderEvent 579 252 0 0 0 0
-InteractionEvent 579 252 0 0 0 0
-MouseMoveEvent 578 258 0 0 0 0
-RenderEvent 578 258 0 0 0 0
-InteractionEvent 578 258 0 0 0 0
-MouseMoveEvent 576 263 0 0 0 0
-RenderEvent 576 263 0 0 0 0
-InteractionEvent 576 263 0 0 0 0
-MouseMoveEvent 575 271 0 0 0 0
-RenderEvent 575 271 0 0 0 0
-InteractionEvent 575 271 0 0 0 0
-MouseMoveEvent 573 278 0 0 0 0
-RenderEvent 573 278 0 0 0 0
-InteractionEvent 573 278 0 0 0 0
-MouseMoveEvent 571 290 0 0 0 0
-RenderEvent 571 290 0 0 0 0
-InteractionEvent 571 290 0 0 0 0
-MouseMoveEvent 568 296 0 0 0 0
-RenderEvent 568 296 0 0 0 0
-InteractionEvent 568 296 0 0 0 0
-MouseMoveEvent 567 301 0 0 0 0
-RenderEvent 567 301 0 0 0 0
-InteractionEvent 567 301 0 0 0 0
-MouseMoveEvent 566 308 0 0 0 0
-RenderEvent 566 308 0 0 0 0
-InteractionEvent 566 308 0 0 0 0
-MouseMoveEvent 563 316 0 0 0 0
-RenderEvent 563 316 0 0 0 0
-InteractionEvent 563 316 0 0 0 0
-MouseMoveEvent 562 322 0 0 0 0
-RenderEvent 562 322 0 0 0 0
-InteractionEvent 562 322 0 0 0 0
-MouseMoveEvent 560 330 0 0 0 0
-RenderEvent 560 330 0 0 0 0
-InteractionEvent 560 330 0 0 0 0
-MouseMoveEvent 558 337 0 0 0 0
-RenderEvent 558 337 0 0 0 0
-InteractionEvent 558 337 0 0 0 0
-MouseMoveEvent 557 342 0 0 0 0
-RenderEvent 557 342 0 0 0 0
-InteractionEvent 557 342 0 0 0 0
-MouseMoveEvent 555 347 0 0 0 0
-RenderEvent 555 347 0 0 0 0
-InteractionEvent 555 347 0 0 0 0
-MouseMoveEvent 553 354 0 0 0 0
-RenderEvent 553 354 0 0 0 0
-InteractionEvent 553 354 0 0 0 0
-MouseMoveEvent 551 362 0 0 0 0
-RenderEvent 551 362 0 0 0 0
-InteractionEvent 551 362 0 0 0 0
-MouseMoveEvent 551 373 0 0 0 0
-RenderEvent 551 373 0 0 0 0
-InteractionEvent 551 373 0 0 0 0
-MouseMoveEvent 551 380 0 0 0 0
-RenderEvent 551 380 0 0 0 0
-InteractionEvent 551 380 0 0 0 0
-MouseMoveEvent 551 386 0 0 0 0
-RenderEvent 551 386 0 0 0 0
-InteractionEvent 551 386 0 0 0 0
-MouseMoveEvent 551 390 0 0 0 0
-RenderEvent 551 390 0 0 0 0
-InteractionEvent 551 390 0 0 0 0
-MouseMoveEvent 551 393 0 0 0 0
-RenderEvent 551 393 0 0 0 0
-InteractionEvent 551 393 0 0 0 0
-MouseMoveEvent 551 397 0 0 0 0
-RenderEvent 551 397 0 0 0 0
-InteractionEvent 551 397 0 0 0 0
-MouseMoveEvent 551 401 0 0 0 0
-RenderEvent 551 401 0 0 0 0
-InteractionEvent 551 401 0 0 0 0
-MouseMoveEvent 551 404 0 0 0 0
-RenderEvent 551 404 0 0 0 0
-InteractionEvent 551 404 0 0 0 0
-MouseMoveEvent 551 407 0 0 0 0
-RenderEvent 551 407 0 0 0 0
-InteractionEvent 551 407 0 0 0 0
-MouseMoveEvent 551 411 0 0 0 0
-RenderEvent 551 411 0 0 0 0
-InteractionEvent 551 411 0 0 0 0
-MouseMoveEvent 550 415 0 0 0 0
-RenderEvent 550 415 0 0 0 0
-InteractionEvent 550 415 0 0 0 0
-MouseMoveEvent 550 419 0 0 0 0
-RenderEvent 550 419 0 0 0 0
-InteractionEvent 550 419 0 0 0 0
-MouseMoveEvent 550 422 0 0 0 0
-RenderEvent 550 422 0 0 0 0
-InteractionEvent 550 422 0 0 0 0
-MouseMoveEvent 550 424 0 0 0 0
-RenderEvent 550 424 0 0 0 0
-InteractionEvent 550 424 0 0 0 0
-MouseMoveEvent 550 425 0 0 0 0
-RenderEvent 550 425 0 0 0 0
-InteractionEvent 550 425 0 0 0 0
-MouseMoveEvent 550 426 0 0 0 0
-RenderEvent 550 426 0 0 0 0
-InteractionEvent 550 426 0 0 0 0
-MouseMoveEvent 550 426 0 0 0 0
-RenderEvent 550 426 0 0 0 0
-InteractionEvent 550 426 0 0 0 0
-MouseMoveEvent 550 427 0 0 0 0
-RenderEvent 550 427 0 0 0 0
 InteractionEvent 550 427 0 0 0 0
 MouseMoveEvent 550 428 0 0 0 0
 RenderEvent 550 428 0 0 0 0
@@ -286,226 +43,25 @@
 EndInteractionEvent 550 428 0 0 0 0
 RenderEvent 550 428 0 0 0 0
 MouseMoveEvent 549 428 0 0 0 0
-MouseMoveEvent 548 428 0 0 0 0
-MouseMoveEvent 548 428 0 0 0 0
-MouseMoveEvent 547 428 0 0 0 0
-MouseMoveEvent 546 428 0 0 0 0
-MouseMoveEvent 545 428 0 0 0 0
-MouseMoveEvent 544 429 0 0 0 0
-MouseMoveEvent 543 429 0 0 0 0
-MouseMoveEvent 544 430 0 0 0 0
-MouseMoveEvent 544 430 0 0 0 0
 MiddleButtonPressEvent 544 430 0 0 0 0
 StartInteractionEvent 544 430 0 0 0 0
 MouseMoveEvent 543 430 0 0 0 0
 RenderEvent 543 430 0 0 0 0
 InteractionEvent 543 430 0 0 0 0
 MouseMoveEvent 541 429 0 0 0 0
-RenderEvent 541 429 0 0 0 0
-InteractionEvent 541 429 0 0 0 0
-MouseMoveEvent 537 428 0 0 0 0
-RenderEvent 537 428 0 0 0 0
-InteractionEvent 537 428 0 0 0 0
-MouseMoveEvent 530 426 0 0 0 0
-RenderEvent 530 426 0 0 0 0
-InteractionEvent 530 426 0 0 0 0
-MouseMoveEvent 517 425 0 0 0 0
-RenderEvent 517 425 0 0 0 0
-InteractionEvent 517 425 0 0 0 0
-MouseMoveEvent 502 421 0 0 0 0
-RenderEvent 502 421 0 0 0 0
-InteractionEvent 502 421 0 0 0 0
-MouseMoveEvent 454 413 0 0 0 0
-RenderEvent 454 413 0 0 0 0
-InteractionEvent 454 413 0 0 0 0
-MouseMoveEvent 410 405 0 0 0 0
-RenderEvent 410 405 0 0 0 0
-InteractionEvent 410 405 0 0 0 0
-MouseMoveEvent 379 398 0 0 0 0
-RenderEvent 379 398 0 0 0 0
-InteractionEvent 379 398 0 0 0 0
-MouseMoveEvent 365 392 0 0 0 0
-RenderEvent 365 392 0 0 0 0
-InteractionEvent 365 392 0 0 0 0
-MouseMoveEvent 359 388 0 0 0 0
-RenderEvent 359 388 0 0 0 0
-InteractionEvent 359 388 0 0 0 0
-MouseMoveEvent 347 381 0 0 0 0
-RenderEvent 347 381 0 0 0 0
-InteractionEvent 347 381 0 0 0 0
-MouseMoveEvent 332 374 0 0 0 0
-RenderEvent 332 374 0 0 0 0
-InteractionEvent 332 374 0 0 0 0
-MouseMoveEvent 319 369 0 0 0 0
-RenderEvent 319 369 0 0 0 0
-InteractionEvent 319 369 0 0 0 0
-MouseMoveEvent 305 363 0 0 0 0
-RenderEvent 305 363 0 0 0 0
-InteractionEvent 305 363 0 0 0 0
-MouseMoveEvent 287 358 0 0 0 0
-RenderEvent 287 358 0 0 0 0
-InteractionEvent 287 358 0 0 0 0
-MouseMoveEvent 265 351 0 0 0 0
-RenderEvent 265 351 0 0 0 0
-InteractionEvent 265 351 0 0 0 0
-MouseMoveEvent 237 342 0 0 0 0
-RenderEvent 237 342 0 0 0 0
-InteractionEvent 237 342 0 0 0 0
-MouseMoveEvent 220 338 0 0 0 0
-RenderEvent 220 338 0 0 0 0
-InteractionEvent 220 338 0 0 0 0
-MouseMoveEvent 203 332 0 0 0 0
-RenderEvent 203 332 0 0 0 0
-InteractionEvent 203 332 0 0 0 0
-MouseMoveEvent 189 328 0 0 0 0
-RenderEvent 189 328 0 0 0 0
-InteractionEvent 189 328 0 0 0 0
-MouseMoveEvent 184 324 0 0 0 0
-RenderEvent 184 324 0 0 0 0
-InteractionEvent 184 324 0 0 0 0
-MouseMoveEvent 183 323 0 0 0 0
-RenderEvent 183 323 0 0 0 0
-InteractionEvent 183 323 0 0 0 0
-MouseMoveEvent 182 323 0 0 0 0
-RenderEvent 182 323 0 0 0 0
-InteractionEvent 182 323 0 0 0 0
-MouseMoveEvent 180 323 0 0 0 0
-RenderEvent 180 323 0 0 0 0
-InteractionEvent 180 323 0 0 0 0
-MouseMoveEvent 174 321 0 0 0 0
-RenderEvent 174 321 0 0 0 0
-InteractionEvent 174 321 0 0 0 0
-MouseMoveEvent 166 318 0 0 0 0
-RenderEvent 166 318 0 0 0 0
-InteractionEvent 166 318 0 0 0 0
-MouseMoveEvent 155 315 0 0 0 0
-RenderEvent 155 315 0 0 0 0
-InteractionEvent 155 315 0 0 0 0
-MouseMoveEvent 143 311 0 0 0 0
-RenderEvent 143 311 0 0 0 0
-InteractionEvent 143 311 0 0 0 0
-MouseMoveEvent 120 306 0 0 0 0
-RenderEvent 120 306 0 0 0 0
-InteractionEvent 120 306 0 0 0 0
-MouseMoveEvent 111 305 0 0 0 0
-RenderEvent 111 305 0 0 0 0
-InteractionEvent 111 305 0 0 0 0
-MouseMoveEvent 105 304 0 0 0 0
-RenderEvent 105 304 0 0 0 0
-InteractionEvent 105 304 0 0 0 0
-MouseMoveEvent 98 300 0 0 0 0
-RenderEvent 98 300 0 0 0 0
-InteractionEvent 98 300 0 0 0 0
-MouseMoveEvent 92 297 0 0 0 0
-RenderEvent 92 297 0 0 0 0
-InteractionEvent 92 297 0 0 0 0
-MouseMoveEvent 76 292 0 0 0 0
-RenderEvent 76 292 0 0 0 0
-InteractionEvent 76 292 0 0 0 0
-MouseMoveEvent 60 286 0 0 0 0
-RenderEvent 60 286 0 0 0 0
-InteractionEvent 60 286 0 0 0 0
-MouseMoveEvent 47 282 0 0 0 0
-RenderEvent 47 282 0 0 0 0
-InteractionEvent 47 282 0 0 0 0
-MouseMoveEvent 40 281 0 0 0 0
-RenderEvent 40 281 0 0 0 0
-InteractionEvent 40 281 0 0 0 0
-MouseMoveEvent 39 281 0 0 0 0
 RenderEvent 39 281 0 0 0 0
 InteractionEvent 39 281 0 0 0 0
 MiddleButtonReleaseEvent 39 281 0 0 0 0
 EndInteractionEvent 39 281 0 0 0 0
 RenderEvent 39 281 0 0 0 0
-MouseMoveEvent 39 282 0 0 0 0
-MouseMoveEvent 40 284 0 0 0 0
-MouseMoveEvent 42 285 0 0 0 0
-MouseMoveEvent 46 287 0 0 0 0
-MouseMoveEvent 58 290 0 0 0 0
-MouseMoveEvent 72 294 0 0 0 0
-MouseMoveEvent 90 299 0 0 0 0
-MouseMoveEvent 114 305 0 0 0 0
-MouseMoveEvent 140 311 0 0 0 0
-MouseMoveEvent 170 315 0 0 0 0
-MouseMoveEvent 202 319 0 0 0 0
-MouseMoveEvent 234 325 0 0 0 0
-MouseMoveEvent 264 329 0 0 0 0
-MouseMoveEvent 290 333 0 0 0 0
-MouseMoveEvent 316 335 0 0 0 0
-MouseMoveEvent 340 337 0 0 0 0
-MouseMoveEvent 360 337 0 0 0 0
-MouseMoveEvent 371 337 0 0 0 0
-MouseMoveEvent 376 337 0 0 0 0
-MouseMoveEvent 377 337 0 0 0 0
-MouseMoveEvent 376 336 0 0 0 0
-MouseMoveEvent 375 335 0 0 0 0
-MouseMoveEvent 374 334 0 0 0 0
-MouseMoveEvent 373 333 0 0 0 0
-MouseMoveEvent 372 332 0 0 0 0
-MouseMoveEvent 372 331 0 0 0 0
-MouseMoveEvent 372 330 0 0 0 0
 MouseMoveEvent 371 330 0 0 0 0
 MouseWheelBackwardEvent 371 330 0 0 0 0
 StartInteractionEvent 371 330 0 0 0 0
 RenderEvent 371 330 0 0 0 0
 EndInteractionEvent 371 330 0 0 0 0
 RenderEvent 371 330 0 0 0 0
-MouseWheelBackwardEvent 371 330 0 0 1 0
-StartInteractionEvent 371 330 0 0 1 0
-RenderEvent 371 330 0 0 1 0
-EndInteractionEvent 371 330 0 0 1 0
-RenderEvent 371 330 0 0 1 0
-MouseWheelBackwardEvent 371 330 0 0 0 0
-StartInteractionEvent 371 330 0 0 0 0
-RenderEvent 371 330 0 0 0 0
-EndInteractionEvent 371 330 0 0 0 0
-RenderEvent 371 330 0 0 0 0
-MouseWheelBackwardEvent 371 330 0 0 1 0
-StartInteractionEvent 371 330 0 0 1 0
-RenderEvent 371 330 0 0 1 0
-EndInteractionEvent 371 330 0 0 1 0
-RenderEvent 371 330 0 0 1 0
-MouseWheelBackwardEvent 371 330 0 0 0 0
-StartInteractionEvent 371 330 0 0 0 0
-RenderEvent 371 330 0 0 0 0
-EndInteractionEvent 371 330 0 0 0 0
-RenderEvent 371 330 0 0 0 0
 KeyPressEvent 300 277 0 0 1 Control_L
 CharEvent 300 277 0 0 1 Control_L
-LeftButtonPressEvent 300 277 2 0 0 Control_L
-StartInteractionEvent 300 277 2 0 0 Control_L
-MouseMoveEvent 300 278 2 0 0 Control_L
-RenderEvent 300 278 2 0 0 Control_L
-InteractionEvent 300 278 2 0 0 Control_L
-MouseMoveEvent 299 280 2 0 0 Control_L
-RenderEvent 299 280 2 0 0 Control_L
-InteractionEvent 299 280 2 0 0 Control_L
-MouseMoveEvent 299 282 2 0 0 Control_L
-RenderEvent 299 282 2 0 0 Control_L
-InteractionEvent 299 282 2 0 0 Control_L
-MouseMoveEvent 298 284 2 0 0 Control_L
-RenderEvent 298 284 2 0 0 Control_L
-InteractionEvent 298 284 2 0 0 Control_L
-MouseMoveEvent 297 285 2 0 0 Control_L
-RenderEvent 297 285 2 0 0 Control_L
-InteractionEvent 297 285 2 0 0 Control_L
-MouseMoveEvent 297 286 2 0 0 Control_L
-RenderEvent 297 286 2 0 0 Control_L
-InteractionEvent 297 286 2 0 0 Control_L
-MouseMoveEvent 297 287 2 0 0 Control_L
-RenderEvent 297 287 2 0 0 Control_L
-InteractionEvent 297 287 2 0 0 Control_L
-MouseMoveEvent 297 288 2 0 0 Control_L
-RenderEvent 297 288 2 0 0 Control_L
-InteractionEvent 297 288 2 0 0 Control_L
-MouseMoveEvent 296 290 2 0 0 Control_L
-RenderEvent 296 290 2 0 0 Control_L
-InteractionEvent 296 290 2 0 0 Control_L
-MouseMoveEvent 296 291 2 0 0 Control_L
-RenderEvent 296 291 2 0 0 Control_L
-InteractionEvent 296 291 2 0 0 Control_L
-LeftButtonReleaseEvent 296 291 2 0 0 Control_L
-EndInteractionEvent 296 291 2 0 0 Control_L
 RenderEvent 296 291 2 0 0 Control_L
 MouseMoveEvent 295 291 2 0 0 Control_L
 MouseMoveEvent 293 292 2 0 0 Control_L
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionStochasticTAA.log f3d-3.4.1/testing/recordings/TestInteractionStochasticTAA.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionStochasticTAA.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionStochasticTAA.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,57 @@
+# StreamVersion 1.2
+ConfigureEvent 0 1039 0 0 0 0 0
+ExposeEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFileNextCameraKeeping.log f3d-3.4.1/testing/recordings/TestInteractionSwitchFileNextCameraKeeping.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFileNextCameraKeeping.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionSwitchFileNextCameraKeeping.log	2026-01-08 08:41:55.000000000 +0200
@@ -2,272 +2,36 @@
 ExposeEvent 0 599 0 0 0 0 0
 RenderEvent 0 599 0 0 0 0 0
 TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-EnterEvent 15 13 0 0 0 0 0
-MouseMoveEvent 15 13 0 0 0 0 0
-MouseMoveEvent 9 33 0 0 0 0 0
-MouseMoveEvent 4 53 0 0 0 0 0
-TimerEvent 4 53 0 0 0 0 0
-LeaveEvent -2 73 0 0 0 0 0
-TimerEvent -2 73 0 0 0 0 0
-TimerEvent -2 73 0 0 0 0 0
-EnterEvent 0 255 0 0 0 0 0
-MouseMoveEvent 0 255 0 0 0 0 0
-MouseMoveEvent 6 279 0 0 0 0 0
-TimerEvent 6 279 0 0 0 0 0
-MouseMoveEvent 14 305 0 0 0 0 0
-MouseMoveEvent 24 333 0 0 0 0 0
-MouseMoveEvent 36 361 0 0 0 0 0
-MouseMoveEvent 52 393 0 0 0 0 0
-TimerEvent 52 393 0 0 0 0 0
-MouseMoveEvent 74 425 0 0 0 0 0
-MouseMoveEvent 98 457 0 0 0 0 0
-MouseMoveEvent 158 527 0 0 0 0 0
-MouseMoveEvent 190 561 0 0 0 0 0
-MouseMoveEvent 244 627 0 0 0 0 0
-LeaveEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-EnterEvent 212 593 0 0 0 0 0
-MouseMoveEvent 212 593 0 0 0 0 0
-MouseMoveEvent 212 582 0 0 0 0 0
-TimerEvent 212 582 0 0 0 0 0
-MouseMoveEvent 212 570 0 0 0 0 0
-MouseMoveEvent 213 558 0 0 0 0 0
-MouseMoveEvent 216 548 0 0 0 0 0
-MouseMoveEvent 221 539 0 0 0 0 0
-TimerEvent 221 539 0 0 0 0 0
-MouseMoveEvent 225 530 0 0 0 0 0
-MouseMoveEvent 229 524 0 0 0 0 0
-MouseMoveEvent 238 511 0 0 0 0 0
-MouseMoveEvent 242 504 0 0 0 0 0
-MouseMoveEvent 251 482 0 0 0 0 0
-MouseMoveEvent 258 471 0 0 0 0 0
-MouseMoveEvent 268 454 0 0 0 0 0
-MouseMoveEvent 277 445 0 0 0 0 0
-MouseMoveEvent 282 442 0 0 0 0 0
-MouseMoveEvent 283 441 0 0 0 0 0
-MouseMoveEvent 284 441 0 0 0 0 0
-MouseMoveEvent 285 442 0 0 0 0 0
-MouseMoveEvent 286 442 0 0 0 0 0
-MouseMoveEvent 291 441 0 0 0 0 0
-MouseMoveEvent 303 438 0 0 0 0 0
-MouseMoveEvent 312 437 0 0 0 0 0
-MouseMoveEvent 339 430 0 0 0 0 0
-MouseMoveEvent 355 427 0 0 0 0 0
-MouseMoveEvent 394 417 0 0 0 0 0
-MouseMoveEvent 435 409 0 0 0 0 0
-MouseMoveEvent 459 403 0 0 0 0 0
-MouseMoveEvent 511 391 0 0 0 0 0
-MouseMoveEvent 569 375 0 0 0 0 0
-MouseMoveEvent 599 365 0 0 0 0 0
-MouseMoveEvent 655 349 0 0 0 0 0
-MouseMoveEvent 709 335 0 0 0 0 0
-MouseMoveEvent 751 321 0 0 0 0 0
-MouseMoveEvent 766 313 0 0 0 0 0
-MouseMoveEvent 783 304 0 0 0 0 0
-MouseMoveEvent 786 302 0 0 0 0 0
-MouseMoveEvent 788 301 0 0 0 0 0
-MouseMoveEvent 788 300 0 0 0 0 0
-TimerEvent 788 300 0 0 0 0 0
+
+# Interact with the view
 LeftButtonPressEvent 788 300 0 0 0 0 0
 StartInteractionEvent 788 300 0 0 0 0 0
 TimerEvent 788 300 0 0 0 0 0
 MouseMoveEvent 788 301 0 0 0 0 0
 RenderEvent 788 301 0 0 0 0 0
 InteractionEvent 788 301 0 0 0 0 0
-MouseMoveEvent 787 302 0 0 0 0 0
-RenderEvent 787 302 0 0 0 0 0
-InteractionEvent 787 302 0 0 0 0 0
-MouseMoveEvent 786 303 0 0 0 0 0
-RenderEvent 786 303 0 0 0 0 0
-InteractionEvent 786 303 0 0 0 0 0
-MouseMoveEvent 770 304 0 0 0 0 0
-RenderEvent 770 304 0 0 0 0 0
-InteractionEvent 770 304 0 0 0 0 0
-MouseMoveEvent 739 304 0 0 0 0 0
-RenderEvent 739 304 0 0 0 0 0
-InteractionEvent 739 304 0 0 0 0 0
-MouseMoveEvent 687 304 0 0 0 0 0
-RenderEvent 687 304 0 0 0 0 0
-InteractionEvent 687 304 0 0 0 0 0
-MouseMoveEvent 657 304 0 0 0 0 0
-RenderEvent 657 304 0 0 0 0 0
-InteractionEvent 657 304 0 0 0 0 0
-MouseMoveEvent 640 305 0 0 0 0 0
-RenderEvent 640 305 0 0 0 0 0
-InteractionEvent 640 305 0 0 0 0 0
-MouseMoveEvent 622 306 0 0 0 0 0
-RenderEvent 622 306 0 0 0 0 0
-InteractionEvent 622 306 0 0 0 0 0
-MouseMoveEvent 610 306 0 0 0 0 0
-RenderEvent 610 306 0 0 0 0 0
-InteractionEvent 610 306 0 0 0 0 0
-MouseMoveEvent 601 305 0 0 0 0 0
-RenderEvent 601 305 0 0 0 0 0
-InteractionEvent 601 305 0 0 0 0 0
-MouseMoveEvent 599 305 0 0 0 0 0
-RenderEvent 599 305 0 0 0 0 0
-InteractionEvent 599 305 0 0 0 0 0
-MouseMoveEvent 591 303 0 0 0 0 0
-RenderEvent 591 303 0 0 0 0 0
-InteractionEvent 591 303 0 0 0 0 0
-MouseMoveEvent 573 300 0 0 0 0 0
-RenderEvent 573 300 0 0 0 0 0
-InteractionEvent 573 300 0 0 0 0 0
-MouseMoveEvent 568 299 0 0 0 0 0
-RenderEvent 568 299 0 0 0 0 0
-InteractionEvent 568 299 0 0 0 0 0
-TimerEvent 568 299 0 0 0 0 0
-MouseMoveEvent 567 299 0 0 0 0 0
-RenderEvent 567 299 0 0 0 0 0
-InteractionEvent 567 299 0 0 0 0 0
-MouseMoveEvent 563 298 0 0 0 0 0
-RenderEvent 563 298 0 0 0 0 0
-InteractionEvent 563 298 0 0 0 0 0
-MouseMoveEvent 547 297 0 0 0 0 0
-RenderEvent 547 297 0 0 0 0 0
-InteractionEvent 547 297 0 0 0 0 0
-MouseMoveEvent 536 297 0 0 0 0 0
-RenderEvent 536 297 0 0 0 0 0
-InteractionEvent 536 297 0 0 0 0 0
-MouseMoveEvent 530 296 0 0 0 0 0
-RenderEvent 530 296 0 0 0 0 0
-InteractionEvent 530 296 0 0 0 0 0
-MouseMoveEvent 528 296 0 0 0 0 0
-RenderEvent 528 296 0 0 0 0 0
-InteractionEvent 528 296 0 0 0 0 0
-MouseMoveEvent 522 295 0 0 0 0 0
-RenderEvent 522 295 0 0 0 0 0
-InteractionEvent 522 295 0 0 0 0 0
+TimerEvent 788 300 0 0 0 0 0
 MouseMoveEvent 517 294 0 0 0 0 0
 RenderEvent 517 294 0 0 0 0 0
 InteractionEvent 517 294 0 0 0 0 0
 TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
 LeftButtonReleaseEvent 517 294 0 0 0 0 0
 EndInteractionEvent 517 294 0 0 0 0 0
 RenderEvent 517 294 0 0 0 0 0
 TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
+
+# Ctrl + Right
 KeyPressEvent 517 294 0 0 1 Control_L 0
 CharEvent 517 294 0 0 1 Control_L 0
 TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
 MouseMoveEvent 517 293 2 0 0 Control_L 0
 TimerEvent 517 293 2 0 0 Control_L 0
 MouseMoveEvent 517 292 2 0 0 Control_L 0
 TimerEvent 517 292 2 0 0 Control_L 0
-TimerEvent 517 292 2 0 0 Control_L 0
 KeyPressEvent 517 292 2 0 1 Right 0
 CharEvent 517 292 2 0 1 Right 0
 TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
 KeyReleaseEvent 517 292 2 0 1 Right 0
 TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
-TimerEvent 517 292 2 0 1 Right 0
 KeyReleaseEvent 517 292 2 0 1 Control_L 0
 TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFileNoCameraKeeping.log f3d-3.4.1/testing/recordings/TestInteractionSwitchFileNoCameraKeeping.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFileNoCameraKeeping.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionSwitchFileNoCameraKeeping.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,235 +1,45 @@
 # StreamVersion 1.2
 ExposeEvent 0 599 0 0 0 0 0
-RenderEvent 0 599 0 0 0 0 0
 EnterEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-TimerEvent 236 218 0 0 0 0 0
-MouseMoveEvent 239 217 0 0 0 0 0
-TimerEvent 239 217 0 0 0 0 0
-MouseMoveEvent 246 216 0 0 0 0 0
-MouseMoveEvent 261 216 0 0 0 0 0
-MouseMoveEvent 287 220 0 0 0 0 0
-MouseMoveEvent 311 226 0 0 0 0 0
-TimerEvent 311 226 0 0 0 0 0
-MouseMoveEvent 335 234 0 0 0 0 0
-MouseMoveEvent 354 238 0 0 0 0 0
-MouseMoveEvent 369 242 0 0 0 0 0
-MouseMoveEvent 381 245 0 0 0 0 0
-MouseMoveEvent 395 251 0 0 0 0 0
-MouseMoveEvent 399 253 0 0 0 0 0
-MouseMoveEvent 405 256 0 0 0 0 0
-MouseMoveEvent 409 258 0 0 0 0 0
-MouseMoveEvent 410 259 0 0 0 0 0
-MouseMoveEvent 411 260 0 0 0 0 0
-MouseMoveEvent 412 261 0 0 0 0 0
-MouseMoveEvent 414 262 0 0 0 0 0
-MouseMoveEvent 415 262 0 0 0 0 0
-MouseMoveEvent 418 263 0 0 0 0 0
-MouseMoveEvent 423 266 0 0 0 0 0
-MouseMoveEvent 425 268 0 0 0 0 0
-MouseMoveEvent 432 271 0 0 0 0 0
-MouseMoveEvent 444 273 0 0 0 0 0
-MouseMoveEvent 451 274 0 0 0 0 0
-MouseMoveEvent 463 279 0 0 0 0 0
-MouseMoveEvent 468 282 0 0 0 0 0
-MouseMoveEvent 474 285 0 0 0 0 0
 MouseMoveEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
 LeftButtonPressEvent 475 285 0 0 0 0 0
 StartInteractionEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
-TimerEvent 475 285 0 0 0 0 0
 MouseMoveEvent 474 285 0 0 0 0 0
-RenderEvent 474 285 0 0 0 0 0
-InteractionEvent 474 285 0 0 0 0 0
 MouseMoveEvent 471 285 0 0 0 0 0
-RenderEvent 471 285 0 0 0 0 0
-InteractionEvent 471 285 0 0 0 0 0
-TimerEvent 471 285 0 0 0 0 0
 MouseMoveEvent 466 284 0 0 0 0 0
-RenderEvent 466 284 0 0 0 0 0
-InteractionEvent 466 284 0 0 0 0 0
 MouseMoveEvent 451 281 0 0 0 0 0
-RenderEvent 451 281 0 0 0 0 0
-InteractionEvent 451 281 0 0 0 0 0
 MouseMoveEvent 432 279 0 0 0 0 0
-RenderEvent 432 279 0 0 0 0 0
-InteractionEvent 432 279 0 0 0 0 0
 MouseMoveEvent 414 277 0 0 0 0 0
-RenderEvent 414 277 0 0 0 0 0
-InteractionEvent 414 277 0 0 0 0 0
 MouseMoveEvent 399 277 0 0 0 0 0
-RenderEvent 399 277 0 0 0 0 0
-InteractionEvent 399 277 0 0 0 0 0
 MouseMoveEvent 380 275 0 0 0 0 0
-RenderEvent 380 275 0 0 0 0 0
-InteractionEvent 380 275 0 0 0 0 0
 MouseMoveEvent 362 275 0 0 0 0 0
-RenderEvent 362 275 0 0 0 0 0
-InteractionEvent 362 275 0 0 0 0 0
 MouseMoveEvent 346 275 0 0 0 0 0
-RenderEvent 346 275 0 0 0 0 0
-InteractionEvent 346 275 0 0 0 0 0
 MouseMoveEvent 327 275 0 0 0 0 0
-RenderEvent 327 275 0 0 0 0 0
-InteractionEvent 327 275 0 0 0 0 0
 MouseMoveEvent 294 275 0 0 0 0 0
-RenderEvent 294 275 0 0 0 0 0
-InteractionEvent 294 275 0 0 0 0 0
 MouseMoveEvent 269 274 0 0 0 0 0
-RenderEvent 269 274 0 0 0 0 0
-InteractionEvent 269 274 0 0 0 0 0
 MouseMoveEvent 252 272 0 0 0 0 0
-RenderEvent 252 272 0 0 0 0 0
-InteractionEvent 252 272 0 0 0 0 0
 MouseMoveEvent 236 271 0 0 0 0 0
-RenderEvent 236 271 0 0 0 0 0
-InteractionEvent 236 271 0 0 0 0 0
 MouseMoveEvent 232 270 0 0 0 0 0
-RenderEvent 232 270 0 0 0 0 0
-InteractionEvent 232 270 0 0 0 0 0
 MouseMoveEvent 230 270 0 0 0 0 0
-RenderEvent 230 270 0 0 0 0 0
-InteractionEvent 230 270 0 0 0 0 0
-TimerEvent 230 270 0 0 0 0 0
 MouseMoveEvent 230 270 0 0 0 0 0
-RenderEvent 230 270 0 0 0 0 0
-InteractionEvent 230 270 0 0 0 0 0
 MouseMoveEvent 229 271 0 0 0 0 0
-RenderEvent 229 271 0 0 0 0 0
-InteractionEvent 229 271 0 0 0 0 0
 MouseMoveEvent 225 272 0 0 0 0 0
-RenderEvent 225 272 0 0 0 0 0
-InteractionEvent 225 272 0 0 0 0 0
 MouseMoveEvent 212 277 0 0 0 0 0
-RenderEvent 212 277 0 0 0 0 0
-InteractionEvent 212 277 0 0 0 0 0
 MouseMoveEvent 197 281 0 0 0 0 0
-RenderEvent 197 281 0 0 0 0 0
-InteractionEvent 197 281 0 0 0 0 0
 MouseMoveEvent 188 283 0 0 0 0 0
-RenderEvent 188 283 0 0 0 0 0
-InteractionEvent 188 283 0 0 0 0 0
 MouseMoveEvent 177 286 0 0 0 0 0
-RenderEvent 177 286 0 0 0 0 0
-InteractionEvent 177 286 0 0 0 0 0
 MouseMoveEvent 154 291 0 0 0 0 0
-RenderEvent 154 291 0 0 0 0 0
-InteractionEvent 154 291 0 0 0 0 0
 MouseMoveEvent 109 300 0 0 0 0 0
-RenderEvent 109 300 0 0 0 0 0
-InteractionEvent 109 300 0 0 0 0 0
 MouseMoveEvent 76 306 0 0 0 0 0
-RenderEvent 76 306 0 0 0 0 0
-InteractionEvent 76 306 0 0 0 0 0
 MouseMoveEvent 70 308 0 0 0 0 0
-RenderEvent 70 308 0 0 0 0 0
-InteractionEvent 70 308 0 0 0 0 0
 MouseMoveEvent 69 307 0 0 0 0 0
-RenderEvent 69 307 0 0 0 0 0
-InteractionEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
 LeftButtonReleaseEvent 69 307 0 0 0 0 0
 EndInteractionEvent 69 307 0 0 0 0 0
-RenderEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
-TimerEvent 69 307 0 0 0 0 0
-MouseMoveEvent 70 307 0 0 0 0 0
-MouseMoveEvent 71 307 0 0 0 0 0
-MouseMoveEvent 72 307 0 0 0 0 0
-MouseMoveEvent 75 307 0 0 0 0 0
-TimerEvent 75 307 0 0 0 0 0
-MouseMoveEvent 78 307 0 0 0 0 0
-MouseMoveEvent 82 307 0 0 0 0 0
-MouseMoveEvent 87 306 0 0 0 0 0
-MouseMoveEvent 93 306 0 0 0 0 0
-MouseMoveEvent 109 304 0 0 0 0 0
-MouseMoveEvent 125 304 0 0 0 0 0
-MouseMoveEvent 132 304 0 0 0 0 0
-MouseMoveEvent 143 304 0 0 0 0 0
-MouseMoveEvent 151 304 0 0 0 0 0
-MouseMoveEvent 156 304 0 0 0 0 0
-MouseMoveEvent 167 304 0 0 0 0 0
-MouseMoveEvent 181 304 0 0 0 0 0
-MouseMoveEvent 189 304 0 0 0 0 0
-MouseMoveEvent 206 304 0 0 0 0 0
-MouseMoveEvent 220 303 0 0 0 0 0
 MouseMoveEvent 227 303 0 0 0 0 0
 KeyPressEvent 227 303 0 0 1 Right 0
 CharEvent 227 303 0 0 1 Right 0
 MouseMoveEvent 241 302 0 0 0 Right 0
 MouseMoveEvent 242 302 0 0 0 Right 0
-TimerEvent 242 302 0 0 0 Right 0
 MouseMoveEvent 243 302 0 0 0 Right 0
 MouseMoveEvent 244 302 0 0 0 Right 0
 KeyReleaseEvent 244 302 0 0 1 Right 0
-MouseMoveEvent 245 302 0 0 0 Right 0
-TimerEvent 245 302 0 0 0 Right 0
-MouseMoveEvent 245 302 0 0 0 Right 0
-MouseMoveEvent 246 302 0 0 0 Right 0
-TimerEvent 246 302 0 0 0 Right 0
-TimerEvent 246 302 0 0 0 Right 0
-TimerEvent 246 302 0 0 0 Right 0
-MouseMoveEvent 247 302 0 0 0 Right 0
-MouseMoveEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
-TimerEvent 248 302 0 0 0 Right 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFilePrevCameraKeeping.log f3d-3.4.1/testing/recordings/TestInteractionSwitchFilePrevCameraKeeping.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionSwitchFilePrevCameraKeeping.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionSwitchFilePrevCameraKeeping.log	2026-01-08 08:41:55.000000000 +0200
@@ -2,272 +2,32 @@
 ExposeEvent 0 599 0 0 0 0 0
 RenderEvent 0 599 0 0 0 0 0
 TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-EnterEvent 15 13 0 0 0 0 0
-MouseMoveEvent 15 13 0 0 0 0 0
-MouseMoveEvent 9 33 0 0 0 0 0
-MouseMoveEvent 4 53 0 0 0 0 0
-TimerEvent 4 53 0 0 0 0 0
-LeaveEvent -2 73 0 0 0 0 0
-TimerEvent -2 73 0 0 0 0 0
-TimerEvent -2 73 0 0 0 0 0
-EnterEvent 0 255 0 0 0 0 0
-MouseMoveEvent 0 255 0 0 0 0 0
-MouseMoveEvent 6 279 0 0 0 0 0
-TimerEvent 6 279 0 0 0 0 0
-MouseMoveEvent 14 305 0 0 0 0 0
-MouseMoveEvent 24 333 0 0 0 0 0
-MouseMoveEvent 36 361 0 0 0 0 0
-MouseMoveEvent 52 393 0 0 0 0 0
-TimerEvent 52 393 0 0 0 0 0
-MouseMoveEvent 74 425 0 0 0 0 0
-MouseMoveEvent 98 457 0 0 0 0 0
-MouseMoveEvent 158 527 0 0 0 0 0
-MouseMoveEvent 190 561 0 0 0 0 0
-MouseMoveEvent 244 627 0 0 0 0 0
-LeaveEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-TimerEvent 244 627 0 0 0 0 0
-EnterEvent 212 593 0 0 0 0 0
-MouseMoveEvent 212 593 0 0 0 0 0
-MouseMoveEvent 212 582 0 0 0 0 0
-TimerEvent 212 582 0 0 0 0 0
-MouseMoveEvent 212 570 0 0 0 0 0
-MouseMoveEvent 213 558 0 0 0 0 0
-MouseMoveEvent 216 548 0 0 0 0 0
-MouseMoveEvent 221 539 0 0 0 0 0
-TimerEvent 221 539 0 0 0 0 0
-MouseMoveEvent 225 530 0 0 0 0 0
-MouseMoveEvent 229 524 0 0 0 0 0
-MouseMoveEvent 238 511 0 0 0 0 0
-MouseMoveEvent 242 504 0 0 0 0 0
-MouseMoveEvent 251 482 0 0 0 0 0
-MouseMoveEvent 258 471 0 0 0 0 0
-MouseMoveEvent 268 454 0 0 0 0 0
-MouseMoveEvent 277 445 0 0 0 0 0
-MouseMoveEvent 282 442 0 0 0 0 0
-MouseMoveEvent 283 441 0 0 0 0 0
-MouseMoveEvent 284 441 0 0 0 0 0
-MouseMoveEvent 285 442 0 0 0 0 0
-MouseMoveEvent 286 442 0 0 0 0 0
-MouseMoveEvent 291 441 0 0 0 0 0
-MouseMoveEvent 303 438 0 0 0 0 0
-MouseMoveEvent 312 437 0 0 0 0 0
-MouseMoveEvent 339 430 0 0 0 0 0
-MouseMoveEvent 355 427 0 0 0 0 0
-MouseMoveEvent 394 417 0 0 0 0 0
-MouseMoveEvent 435 409 0 0 0 0 0
-MouseMoveEvent 459 403 0 0 0 0 0
-MouseMoveEvent 511 391 0 0 0 0 0
-MouseMoveEvent 569 375 0 0 0 0 0
-MouseMoveEvent 599 365 0 0 0 0 0
-MouseMoveEvent 655 349 0 0 0 0 0
-MouseMoveEvent 709 335 0 0 0 0 0
-MouseMoveEvent 751 321 0 0 0 0 0
-MouseMoveEvent 766 313 0 0 0 0 0
-MouseMoveEvent 783 304 0 0 0 0 0
-MouseMoveEvent 786 302 0 0 0 0 0
-MouseMoveEvent 788 301 0 0 0 0 0
-MouseMoveEvent 788 300 0 0 0 0 0
-TimerEvent 788 300 0 0 0 0 0
+
+# Interact with the view
 LeftButtonPressEvent 788 300 0 0 0 0 0
 StartInteractionEvent 788 300 0 0 0 0 0
 TimerEvent 788 300 0 0 0 0 0
 MouseMoveEvent 788 301 0 0 0 0 0
 RenderEvent 788 301 0 0 0 0 0
 InteractionEvent 788 301 0 0 0 0 0
-MouseMoveEvent 787 302 0 0 0 0 0
-RenderEvent 787 302 0 0 0 0 0
-InteractionEvent 787 302 0 0 0 0 0
-MouseMoveEvent 786 303 0 0 0 0 0
-RenderEvent 786 303 0 0 0 0 0
-InteractionEvent 786 303 0 0 0 0 0
-MouseMoveEvent 770 304 0 0 0 0 0
-RenderEvent 770 304 0 0 0 0 0
-InteractionEvent 770 304 0 0 0 0 0
-MouseMoveEvent 739 304 0 0 0 0 0
-RenderEvent 739 304 0 0 0 0 0
-InteractionEvent 739 304 0 0 0 0 0
-MouseMoveEvent 687 304 0 0 0 0 0
-RenderEvent 687 304 0 0 0 0 0
-InteractionEvent 687 304 0 0 0 0 0
-MouseMoveEvent 657 304 0 0 0 0 0
-RenderEvent 657 304 0 0 0 0 0
-InteractionEvent 657 304 0 0 0 0 0
-MouseMoveEvent 640 305 0 0 0 0 0
-RenderEvent 640 305 0 0 0 0 0
-InteractionEvent 640 305 0 0 0 0 0
-MouseMoveEvent 622 306 0 0 0 0 0
-RenderEvent 622 306 0 0 0 0 0
-InteractionEvent 622 306 0 0 0 0 0
-MouseMoveEvent 610 306 0 0 0 0 0
-RenderEvent 610 306 0 0 0 0 0
-InteractionEvent 610 306 0 0 0 0 0
-MouseMoveEvent 601 305 0 0 0 0 0
-RenderEvent 601 305 0 0 0 0 0
-InteractionEvent 601 305 0 0 0 0 0
-MouseMoveEvent 599 305 0 0 0 0 0
-RenderEvent 599 305 0 0 0 0 0
-InteractionEvent 599 305 0 0 0 0 0
-MouseMoveEvent 591 303 0 0 0 0 0
-RenderEvent 591 303 0 0 0 0 0
-InteractionEvent 591 303 0 0 0 0 0
-MouseMoveEvent 573 300 0 0 0 0 0
-RenderEvent 573 300 0 0 0 0 0
-InteractionEvent 573 300 0 0 0 0 0
-MouseMoveEvent 568 299 0 0 0 0 0
-RenderEvent 568 299 0 0 0 0 0
-InteractionEvent 568 299 0 0 0 0 0
-TimerEvent 568 299 0 0 0 0 0
-MouseMoveEvent 567 299 0 0 0 0 0
-RenderEvent 567 299 0 0 0 0 0
-InteractionEvent 567 299 0 0 0 0 0
-MouseMoveEvent 563 298 0 0 0 0 0
-RenderEvent 563 298 0 0 0 0 0
-InteractionEvent 563 298 0 0 0 0 0
-MouseMoveEvent 547 297 0 0 0 0 0
-RenderEvent 547 297 0 0 0 0 0
-InteractionEvent 547 297 0 0 0 0 0
-MouseMoveEvent 536 297 0 0 0 0 0
-RenderEvent 536 297 0 0 0 0 0
-InteractionEvent 536 297 0 0 0 0 0
-MouseMoveEvent 530 296 0 0 0 0 0
-RenderEvent 530 296 0 0 0 0 0
-InteractionEvent 530 296 0 0 0 0 0
-MouseMoveEvent 528 296 0 0 0 0 0
-RenderEvent 528 296 0 0 0 0 0
-InteractionEvent 528 296 0 0 0 0 0
-MouseMoveEvent 522 295 0 0 0 0 0
-RenderEvent 522 295 0 0 0 0 0
-InteractionEvent 522 295 0 0 0 0 0
 MouseMoveEvent 517 294 0 0 0 0 0
 RenderEvent 517 294 0 0 0 0 0
 InteractionEvent 517 294 0 0 0 0 0
 TimerEvent 517 294 0 0 0 0 0
 TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
 LeftButtonReleaseEvent 517 294 0 0 0 0 0
 EndInteractionEvent 517 294 0 0 0 0 0
+
+# Ctrl + Left
 RenderEvent 517 294 0 0 0 0 0
 TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
-TimerEvent 517 294 0 0 0 0 0
 KeyPressEvent 517 294 0 0 1 Control_L 0
 CharEvent 517 294 0 0 1 Control_L 0
 TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-TimerEvent 517 294 0 0 1 Control_L 0
-MouseMoveEvent 517 293 2 0 0 Control_L 0
-TimerEvent 517 293 2 0 0 Control_L 0
-MouseMoveEvent 517 292 2 0 0 Control_L 0
-TimerEvent 517 292 2 0 0 Control_L 0
-TimerEvent 517 292 2 0 0 Control_L 0
 KeyPressEvent 517 292 2 0 1 Left 0
 CharEvent 517 292 2 0 1 Left 0
 TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
 KeyReleaseEvent 517 292 2 0 1 Left 0
 TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
-TimerEvent 517 292 2 0 1 Left 0
 KeyReleaseEvent 517 292 2 0 1 Control_L 0
 TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
-TimerEvent 517 292 2 0 1 Control_L 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAADolly.log f3d-3.4.1/testing/recordings/TestInteractionTAADolly.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAADolly.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionTAADolly.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,11 @@
+# StreamVersion 1.2
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+RenderEvent 595 321 0 0 0 0 0
+MouseWheelBackwardEvent 595 321 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAA.log f3d-3.4.1/testing/recordings/TestInteractionTAA.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAA.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionTAA.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,20 @@
+# StreamVersion 1.2
+ConfigureEvent 0 1039 0 0 0 0 0
+ExposeEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAAMiddleClick.log f3d-3.4.1/testing/recordings/TestInteractionTAAMiddleClick.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionTAAMiddleClick.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionTAAMiddleClick.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,22 @@
+# StreamVersion 1.2
+ConfigureEvent 0 1039 0 0 0 0 0
+ExposeEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+MiddleButtonPressEvent 200 150 0 0 0 0
+MiddleButtonReleaseEvent 200 150 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
+RenderEvent 0 1039 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionTrackballBind.log f3d-3.4.1/testing/recordings/TestInteractionTrackballBind.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionTrackballBind.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionTrackballBind.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,703 +1,157 @@
 # StreamVersion 1.2
 ExposeEvent 0 299 0 0 0 0 0
-RenderEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
-TimerEvent 0 299 0 0 0 0 0
 EnterEvent 298 276 0 0 0 0 0
-MouseMoveEvent 298 276 0 0 0 0 0
-MouseMoveEvent 290 276 0 0 0 0 0
-MouseMoveEvent 282 275 0 0 0 0 0
-MouseMoveEvent 274 275 0 0 0 0 0
-TimerEvent 274 275 0 0 0 0 0
-MouseMoveEvent 266 273 0 0 0 0 0
-MouseMoveEvent 258 271 0 0 0 0 0
-MouseMoveEvent 251 268 0 0 0 0 0
-MouseMoveEvent 246 267 0 0 0 0 0
-MouseMoveEvent 242 264 0 0 0 0 0
-TimerEvent 242 264 0 0 0 0 0
-MouseMoveEvent 237 262 0 0 0 0 0
-MouseMoveEvent 233 260 0 0 0 0 0
-MouseMoveEvent 230 259 0 0 0 0 0
-MouseMoveEvent 227 258 0 0 0 0 0
-MouseMoveEvent 224 257 0 0 0 0 0
-TimerEvent 224 257 0 0 0 0 0
-MouseMoveEvent 222 256 0 0 0 0 0
-MouseMoveEvent 220 256 0 0 0 0 0
-MouseMoveEvent 218 256 0 0 0 0 0
-MouseMoveEvent 216 256 0 0 0 0 0
-TimerEvent 216 256 0 0 0 0 0
-MouseMoveEvent 215 256 0 0 0 0 0
-MouseMoveEvent 214 256 0 0 0 0 0
-MouseMoveEvent 213 256 0 0 0 0 0
-MouseMoveEvent 212 256 0 0 0 0 0
-TimerEvent 212 256 0 0 0 0 0
-MouseMoveEvent 211 255 0 0 0 0 0
-MouseMoveEvent 210 254 0 0 0 0 0
-MouseMoveEvent 209 253 0 0 0 0 0
-TimerEvent 209 253 0 0 0 0 0
-MouseMoveEvent 208 253 0 0 0 0 0
-MouseMoveEvent 208 252 0 0 0 0 0
-MouseMoveEvent 206 252 0 0 0 0 0
-MouseMoveEvent 205 252 0 0 0 0 0
-TimerEvent 205 252 0 0 0 0 0
-MouseMoveEvent 203 252 0 0 0 0 0
-MouseMoveEvent 201 252 0 0 0 0 0
-MouseMoveEvent 201 252 0 0 0 0 0
-MouseMoveEvent 199 252 0 0 0 0 0
-MouseMoveEvent 198 252 0 0 0 0 0
-TimerEvent 198 252 0 0 0 0 0
-MouseMoveEvent 197 252 0 0 0 0 0
-MouseMoveEvent 196 251 0 0 0 0 0
-MouseMoveEvent 195 250 0 0 0 0 0
-MouseMoveEvent 194 249 0 0 0 0 0
-TimerEvent 194 249 0 0 0 0 0
-MouseMoveEvent 193 248 0 0 0 0 0
-MouseMoveEvent 192 247 0 0 0 0 0
-MouseMoveEvent 190 246 0 0 0 0 0
-MouseMoveEvent 187 244 0 0 0 0 0
-MouseMoveEvent 184 243 0 0 0 0 0
-TimerEvent 184 243 0 0 0 0 0
-MouseMoveEvent 179 242 0 0 0 0 0
-MouseMoveEvent 176 241 0 0 0 0 0
-MouseMoveEvent 172 241 0 0 0 0 0
-MouseMoveEvent 170 241 0 0 0 0 0
-MouseMoveEvent 167 241 0 0 0 0 0
-TimerEvent 167 241 0 0 0 0 0
-MouseMoveEvent 164 241 0 0 0 0 0
-MouseMoveEvent 162 241 0 0 0 0 0
-MouseMoveEvent 160 241 0 0 0 0 0
-MouseMoveEvent 158 241 0 0 0 0 0
-TimerEvent 158 241 0 0 0 0 0
-MouseMoveEvent 157 241 0 0 0 0 0
-MouseMoveEvent 156 240 0 0 0 0 0
-MouseMoveEvent 155 239 0 0 0 0 0
-MouseMoveEvent 154 239 0 0 0 0 0
-MouseMoveEvent 153 238 0 0 0 0 0
-TimerEvent 153 238 0 0 0 0 0
-MouseMoveEvent 151 238 0 0 0 0 0
-MouseMoveEvent 150 237 0 0 0 0 0
-MouseMoveEvent 148 236 0 0 0 0 0
-MouseMoveEvent 146 236 0 0 0 0 0
-MouseMoveEvent 145 236 0 0 0 0 0
-TimerEvent 145 236 0 0 0 0 0
-MouseMoveEvent 143 236 0 0 0 0 0
-MouseMoveEvent 143 236 0 0 0 0 0
-MouseMoveEvent 142 236 0 0 0 0 0
-MouseMoveEvent 141 236 0 0 0 0 0
-TimerEvent 141 236 0 0 0 0 0
-MouseMoveEvent 140 236 0 0 0 0 0
-MouseMoveEvent 139 236 0 0 0 0 0
 MouseMoveEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
-TimerEvent 138 236 0 0 0 0 0
 KeyPressEvent 138 236 0 107 1 k 0
 CharEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
 KeyReleaseEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-TimerEvent 138 236 0 107 1 k 0
-MouseMoveEvent 137 234 0 0 0 k 0
-MouseMoveEvent 136 232 0 0 0 k 0
-MouseMoveEvent 136 231 0 0 0 k 0
-MouseMoveEvent 136 230 0 0 0 k 0
-MouseMoveEvent 135 229 0 0 0 k 0
-TimerEvent 135 229 0 0 0 k 0
-MouseMoveEvent 135 228 0 0 0 k 0
-MouseMoveEvent 135 227 0 0 0 k 0
 MouseMoveEvent 135 226 0 0 0 k 0
-TimerEvent 135 226 0 0 0 k 0
-TimerEvent 135 226 0 0 0 k 0
 LeftButtonPressEvent 135 226 0 0 0 k 0
 StartInteractionEvent 135 226 0 0 0 k 0
-TimerEvent 135 226 0 0 0 k 0
-TimerEvent 135 226 0 0 0 k 0
 MouseMoveEvent 135 225 0 0 0 k 0
-RenderEvent 135 225 0 0 0 k 0
-InteractionEvent 135 225 0 0 0 k 0
-TimerEvent 135 225 0 0 0 k 0
 MouseMoveEvent 134 222 0 0 0 k 0
-RenderEvent 134 222 0 0 0 k 0
-InteractionEvent 134 222 0 0 0 k 0
 MouseMoveEvent 133 219 0 0 0 k 0
-RenderEvent 133 219 0 0 0 k 0
-InteractionEvent 133 219 0 0 0 k 0
 MouseMoveEvent 132 217 0 0 0 k 0
-RenderEvent 132 217 0 0 0 k 0
-InteractionEvent 132 217 0 0 0 k 0
 MouseMoveEvent 132 215 0 0 0 k 0
-RenderEvent 132 215 0 0 0 k 0
-InteractionEvent 132 215 0 0 0 k 0
 MouseMoveEvent 132 212 0 0 0 k 0
-RenderEvent 132 212 0 0 0 k 0
-InteractionEvent 132 212 0 0 0 k 0
-TimerEvent 132 212 0 0 0 k 0
 MouseMoveEvent 132 210 0 0 0 k 0
-RenderEvent 132 210 0 0 0 k 0
-InteractionEvent 132 210 0 0 0 k 0
 MouseMoveEvent 132 209 0 0 0 k 0
-RenderEvent 132 209 0 0 0 k 0
-InteractionEvent 132 209 0 0 0 k 0
 MouseMoveEvent 133 206 0 0 0 k 0
-RenderEvent 133 206 0 0 0 k 0
-InteractionEvent 133 206 0 0 0 k 0
 MouseMoveEvent 134 203 0 0 0 k 0
-RenderEvent 134 203 0 0 0 k 0
-InteractionEvent 134 203 0 0 0 k 0
-TimerEvent 134 203 0 0 0 k 0
 MouseMoveEvent 135 201 0 0 0 k 0
-RenderEvent 135 201 0 0 0 k 0
-InteractionEvent 135 201 0 0 0 k 0
 MouseMoveEvent 135 196 0 0 0 k 0
-RenderEvent 135 196 0 0 0 k 0
-InteractionEvent 135 196 0 0 0 k 0
 MouseMoveEvent 135 194 0 0 0 k 0
-RenderEvent 135 194 0 0 0 k 0
-InteractionEvent 135 194 0 0 0 k 0
 MouseMoveEvent 136 191 0 0 0 k 0
-RenderEvent 136 191 0 0 0 k 0
-InteractionEvent 136 191 0 0 0 k 0
 MouseMoveEvent 136 188 0 0 0 k 0
-RenderEvent 136 188 0 0 0 k 0
-InteractionEvent 136 188 0 0 0 k 0
-TimerEvent 136 188 0 0 0 k 0
 MouseMoveEvent 136 187 0 0 0 k 0
-RenderEvent 136 187 0 0 0 k 0
-InteractionEvent 136 187 0 0 0 k 0
 MouseMoveEvent 136 185 0 0 0 k 0
-RenderEvent 136 185 0 0 0 k 0
-InteractionEvent 136 185 0 0 0 k 0
 MouseMoveEvent 136 183 0 0 0 k 0
-RenderEvent 136 183 0 0 0 k 0
-InteractionEvent 136 183 0 0 0 k 0
 MouseMoveEvent 136 181 0 0 0 k 0
-RenderEvent 136 181 0 0 0 k 0
-InteractionEvent 136 181 0 0 0 k 0
 MouseMoveEvent 136 180 0 0 0 k 0
-RenderEvent 136 180 0 0 0 k 0
-InteractionEvent 136 180 0 0 0 k 0
-TimerEvent 136 180 0 0 0 k 0
 MouseMoveEvent 136 178 0 0 0 k 0
-RenderEvent 136 178 0 0 0 k 0
-InteractionEvent 136 178 0 0 0 k 0
 MouseMoveEvent 137 176 0 0 0 k 0
-RenderEvent 137 176 0 0 0 k 0
-InteractionEvent 137 176 0 0 0 k 0
 MouseMoveEvent 138 174 0 0 0 k 0
-RenderEvent 138 174 0 0 0 k 0
-InteractionEvent 138 174 0 0 0 k 0
 MouseMoveEvent 139 171 0 0 0 k 0
-RenderEvent 139 171 0 0 0 k 0
-InteractionEvent 139 171 0 0 0 k 0
-TimerEvent 139 171 0 0 0 k 0
 MouseMoveEvent 139 167 0 0 0 k 0
-RenderEvent 139 167 0 0 0 k 0
-InteractionEvent 139 167 0 0 0 k 0
 MouseMoveEvent 140 163 0 0 0 k 0
-RenderEvent 140 163 0 0 0 k 0
-InteractionEvent 140 163 0 0 0 k 0
 MouseMoveEvent 140 159 0 0 0 k 0
-RenderEvent 140 159 0 0 0 k 0
-InteractionEvent 140 159 0 0 0 k 0
 MouseMoveEvent 140 153 0 0 0 k 0
-RenderEvent 140 153 0 0 0 k 0
-InteractionEvent 140 153 0 0 0 k 0
 MouseMoveEvent 140 149 0 0 0 k 0
-RenderEvent 140 149 0 0 0 k 0
-InteractionEvent 140 149 0 0 0 k 0
-TimerEvent 140 149 0 0 0 k 0
 MouseMoveEvent 140 145 0 0 0 k 0
-RenderEvent 140 145 0 0 0 k 0
-InteractionEvent 140 145 0 0 0 k 0
 MouseMoveEvent 140 143 0 0 0 k 0
-RenderEvent 140 143 0 0 0 k 0
-InteractionEvent 140 143 0 0 0 k 0
 MouseMoveEvent 140 140 0 0 0 k 0
-RenderEvent 140 140 0 0 0 k 0
-InteractionEvent 140 140 0 0 0 k 0
 MouseMoveEvent 140 138 0 0 0 k 0
-RenderEvent 140 138 0 0 0 k 0
-InteractionEvent 140 138 0 0 0 k 0
 MouseMoveEvent 140 135 0 0 0 k 0
-RenderEvent 140 135 0 0 0 k 0
-InteractionEvent 140 135 0 0 0 k 0
-TimerEvent 140 135 0 0 0 k 0
 MouseMoveEvent 140 133 0 0 0 k 0
-RenderEvent 140 133 0 0 0 k 0
-InteractionEvent 140 133 0 0 0 k 0
 MouseMoveEvent 140 131 0 0 0 k 0
-RenderEvent 140 131 0 0 0 k 0
-InteractionEvent 140 131 0 0 0 k 0
 MouseMoveEvent 140 130 0 0 0 k 0
-RenderEvent 140 130 0 0 0 k 0
-InteractionEvent 140 130 0 0 0 k 0
 MouseMoveEvent 140 128 0 0 0 k 0
-RenderEvent 140 128 0 0 0 k 0
-InteractionEvent 140 128 0 0 0 k 0
-TimerEvent 140 128 0 0 0 k 0
 MouseMoveEvent 140 126 0 0 0 k 0
-RenderEvent 140 126 0 0 0 k 0
-InteractionEvent 140 126 0 0 0 k 0
 MouseMoveEvent 140 124 0 0 0 k 0
-RenderEvent 140 124 0 0 0 k 0
-InteractionEvent 140 124 0 0 0 k 0
 MouseMoveEvent 140 122 0 0 0 k 0
-RenderEvent 140 122 0 0 0 k 0
-InteractionEvent 140 122 0 0 0 k 0
 MouseMoveEvent 140 118 0 0 0 k 0
-RenderEvent 140 118 0 0 0 k 0
-InteractionEvent 140 118 0 0 0 k 0
 MouseMoveEvent 140 114 0 0 0 k 0
-RenderEvent 140 114 0 0 0 k 0
-InteractionEvent 140 114 0 0 0 k 0
-TimerEvent 140 114 0 0 0 k 0
 MouseMoveEvent 139 109 0 0 0 k 0
-RenderEvent 139 109 0 0 0 k 0
-InteractionEvent 139 109 0 0 0 k 0
 MouseMoveEvent 139 106 0 0 0 k 0
-RenderEvent 139 106 0 0 0 k 0
-InteractionEvent 139 106 0 0 0 k 0
 MouseMoveEvent 138 101 0 0 0 k 0
-RenderEvent 138 101 0 0 0 k 0
-InteractionEvent 138 101 0 0 0 k 0
 MouseMoveEvent 138 99 0 0 0 k 0
-RenderEvent 138 99 0 0 0 k 0
-InteractionEvent 138 99 0 0 0 k 0
 MouseMoveEvent 138 95 0 0 0 k 0
-RenderEvent 138 95 0 0 0 k 0
-InteractionEvent 138 95 0 0 0 k 0
-TimerEvent 138 95 0 0 0 k 0
 MouseMoveEvent 138 93 0 0 0 k 0
-RenderEvent 138 93 0 0 0 k 0
-InteractionEvent 138 93 0 0 0 k 0
 MouseMoveEvent 138 90 0 0 0 k 0
-RenderEvent 138 90 0 0 0 k 0
-InteractionEvent 138 90 0 0 0 k 0
 MouseMoveEvent 138 87 0 0 0 k 0
-RenderEvent 138 87 0 0 0 k 0
-InteractionEvent 138 87 0 0 0 k 0
 MouseMoveEvent 138 85 0 0 0 k 0
-RenderEvent 138 85 0 0 0 k 0
-InteractionEvent 138 85 0 0 0 k 0
-TimerEvent 138 85 0 0 0 k 0
 MouseMoveEvent 138 80 0 0 0 k 0
-RenderEvent 138 80 0 0 0 k 0
-InteractionEvent 138 80 0 0 0 k 0
 MouseMoveEvent 138 77 0 0 0 k 0
-RenderEvent 138 77 0 0 0 k 0
-InteractionEvent 138 77 0 0 0 k 0
 MouseMoveEvent 138 74 0 0 0 k 0
-RenderEvent 138 74 0 0 0 k 0
-InteractionEvent 138 74 0 0 0 k 0
 MouseMoveEvent 138 72 0 0 0 k 0
-RenderEvent 138 72 0 0 0 k 0
-InteractionEvent 138 72 0 0 0 k 0
-TimerEvent 138 72 0 0 0 k 0
 MouseMoveEvent 138 68 0 0 0 k 0
-RenderEvent 138 68 0 0 0 k 0
-InteractionEvent 138 68 0 0 0 k 0
 MouseMoveEvent 138 65 0 0 0 k 0
-RenderEvent 138 65 0 0 0 k 0
-InteractionEvent 138 65 0 0 0 k 0
 MouseMoveEvent 138 63 0 0 0 k 0
-RenderEvent 138 63 0 0 0 k 0
-InteractionEvent 138 63 0 0 0 k 0
 MouseMoveEvent 138 60 0 0 0 k 0
-RenderEvent 138 60 0 0 0 k 0
-InteractionEvent 138 60 0 0 0 k 0
 MouseMoveEvent 138 58 0 0 0 k 0
-RenderEvent 138 58 0 0 0 k 0
-InteractionEvent 138 58 0 0 0 k 0
-TimerEvent 138 58 0 0 0 k 0
 MouseMoveEvent 137 56 0 0 0 k 0
-RenderEvent 137 56 0 0 0 k 0
-InteractionEvent 137 56 0 0 0 k 0
 MouseMoveEvent 137 54 0 0 0 k 0
-RenderEvent 137 54 0 0 0 k 0
-InteractionEvent 137 54 0 0 0 k 0
 MouseMoveEvent 137 51 0 0 0 k 0
-RenderEvent 137 51 0 0 0 k 0
-InteractionEvent 137 51 0 0 0 k 0
 MouseMoveEvent 137 50 0 0 0 k 0
-RenderEvent 137 50 0 0 0 k 0
-InteractionEvent 137 50 0 0 0 k 0
-TimerEvent 137 50 0 0 0 k 0
 MouseMoveEvent 137 48 0 0 0 k 0
-RenderEvent 137 48 0 0 0 k 0
-InteractionEvent 137 48 0 0 0 k 0
 MouseMoveEvent 137 46 0 0 0 k 0
-RenderEvent 137 46 0 0 0 k 0
-InteractionEvent 137 46 0 0 0 k 0
 MouseMoveEvent 137 44 0 0 0 k 0
-RenderEvent 137 44 0 0 0 k 0
-InteractionEvent 137 44 0 0 0 k 0
 MouseMoveEvent 137 43 0 0 0 k 0
-RenderEvent 137 43 0 0 0 k 0
-InteractionEvent 137 43 0 0 0 k 0
 MouseMoveEvent 137 41 0 0 0 k 0
-RenderEvent 137 41 0 0 0 k 0
-InteractionEvent 137 41 0 0 0 k 0
-TimerEvent 137 41 0 0 0 k 0
 MouseMoveEvent 137 40 0 0 0 k 0
-RenderEvent 137 40 0 0 0 k 0
-InteractionEvent 137 40 0 0 0 k 0
 MouseMoveEvent 137 38 0 0 0 k 0
-RenderEvent 137 38 0 0 0 k 0
-InteractionEvent 137 38 0 0 0 k 0
 MouseMoveEvent 137 37 0 0 0 k 0
-RenderEvent 137 37 0 0 0 k 0
-InteractionEvent 137 37 0 0 0 k 0
 MouseMoveEvent 137 36 0 0 0 k 0
-RenderEvent 137 36 0 0 0 k 0
-InteractionEvent 137 36 0 0 0 k 0
 MouseMoveEvent 137 35 0 0 0 k 0
-RenderEvent 137 35 0 0 0 k 0
-InteractionEvent 137 35 0 0 0 k 0
-TimerEvent 137 35 0 0 0 k 0
 MouseMoveEvent 137 33 0 0 0 k 0
-RenderEvent 137 33 0 0 0 k 0
-InteractionEvent 137 33 0 0 0 k 0
 MouseMoveEvent 137 31 0 0 0 k 0
-RenderEvent 137 31 0 0 0 k 0
-InteractionEvent 137 31 0 0 0 k 0
 MouseMoveEvent 137 29 0 0 0 k 0
-RenderEvent 137 29 0 0 0 k 0
-InteractionEvent 137 29 0 0 0 k 0
 MouseMoveEvent 137 28 0 0 0 k 0
-RenderEvent 137 28 0 0 0 k 0
-InteractionEvent 137 28 0 0 0 k 0
-TimerEvent 137 28 0 0 0 k 0
 MouseMoveEvent 137 24 0 0 0 k 0
-RenderEvent 137 24 0 0 0 k 0
-InteractionEvent 137 24 0 0 0 k 0
 MouseMoveEvent 137 22 0 0 0 k 0
-RenderEvent 137 22 0 0 0 k 0
-InteractionEvent 137 22 0 0 0 k 0
 MouseMoveEvent 137 21 0 0 0 k 0
-RenderEvent 137 21 0 0 0 k 0
-InteractionEvent 137 21 0 0 0 k 0
 MouseMoveEvent 137 19 0 0 0 k 0
-RenderEvent 137 19 0 0 0 k 0
-InteractionEvent 137 19 0 0 0 k 0
-TimerEvent 137 19 0 0 0 k 0
 MouseMoveEvent 137 18 0 0 0 k 0
-RenderEvent 137 18 0 0 0 k 0
-InteractionEvent 137 18 0 0 0 k 0
 MouseMoveEvent 137 17 0 0 0 k 0
-RenderEvent 137 17 0 0 0 k 0
-InteractionEvent 137 17 0 0 0 k 0
 MouseMoveEvent 136 16 0 0 0 k 0
-RenderEvent 136 16 0 0 0 k 0
-InteractionEvent 136 16 0 0 0 k 0
 MouseMoveEvent 136 15 0 0 0 k 0
-RenderEvent 136 15 0 0 0 k 0
-InteractionEvent 136 15 0 0 0 k 0
 MouseMoveEvent 136 14 0 0 0 k 0
-RenderEvent 136 14 0 0 0 k 0
-InteractionEvent 136 14 0 0 0 k 0
-TimerEvent 136 14 0 0 0 k 0
 MouseMoveEvent 136 14 0 0 0 k 0
-RenderEvent 136 14 0 0 0 k 0
-InteractionEvent 136 14 0 0 0 k 0
 MouseMoveEvent 136 13 0 0 0 k 0
-RenderEvent 136 13 0 0 0 k 0
-InteractionEvent 136 13 0 0 0 k 0
 MouseMoveEvent 136 12 0 0 0 k 0
-RenderEvent 136 12 0 0 0 k 0
-InteractionEvent 136 12 0 0 0 k 0
-TimerEvent 136 12 0 0 0 k 0
 MouseMoveEvent 136 11 0 0 0 k 0
-RenderEvent 136 11 0 0 0 k 0
-InteractionEvent 136 11 0 0 0 k 0
 MouseMoveEvent 136 10 0 0 0 k 0
-RenderEvent 136 10 0 0 0 k 0
-InteractionEvent 136 10 0 0 0 k 0
 MouseMoveEvent 136 9 0 0 0 k 0
-RenderEvent 136 9 0 0 0 k 0
-InteractionEvent 136 9 0 0 0 k 0
 MouseMoveEvent 136 8 0 0 0 k 0
-RenderEvent 136 8 0 0 0 k 0
-InteractionEvent 136 8 0 0 0 k 0
 MouseMoveEvent 136 7 0 0 0 k 0
-RenderEvent 136 7 0 0 0 k 0
-InteractionEvent 136 7 0 0 0 k 0
-TimerEvent 136 7 0 0 0 k 0
 MouseMoveEvent 136 7 0 0 0 k 0
-RenderEvent 136 7 0 0 0 k 0
-InteractionEvent 136 7 0 0 0 k 0
 MouseMoveEvent 136 6 0 0 0 k 0
-RenderEvent 136 6 0 0 0 k 0
-InteractionEvent 136 6 0 0 0 k 0
 MouseMoveEvent 136 5 0 0 0 k 0
-RenderEvent 136 5 0 0 0 k 0
-InteractionEvent 136 5 0 0 0 k 0
 MouseMoveEvent 136 4 0 0 0 k 0
-RenderEvent 136 4 0 0 0 k 0
-InteractionEvent 136 4 0 0 0 k 0
 MouseMoveEvent 136 3 0 0 0 k 0
-RenderEvent 136 3 0 0 0 k 0
-InteractionEvent 136 3 0 0 0 k 0
-TimerEvent 136 3 0 0 0 k 0
 MouseMoveEvent 136 2 0 0 0 k 0
-RenderEvent 136 2 0 0 0 k 0
-InteractionEvent 136 2 0 0 0 k 0
 MouseMoveEvent 136 1 0 0 0 k 0
-RenderEvent 136 1 0 0 0 k 0
-InteractionEvent 136 1 0 0 0 k 0
 MouseMoveEvent 136 0 0 0 0 k 0
-RenderEvent 136 0 0 0 0 k 0
-InteractionEvent 136 0 0 0 0 k 0
 MouseMoveEvent 136 0 0 0 0 k 0
-RenderEvent 136 0 0 0 0 k 0
-InteractionEvent 136 0 0 0 0 k 0
 LeaveEvent 136 -1 0 0 0 k 0
 MouseMoveEvent 136 -1 0 0 0 k 0
-RenderEvent 136 -1 0 0 0 k 0
-InteractionEvent 136 -1 0 0 0 k 0
-TimerEvent 136 -1 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
-TimerEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 135 -2 0 0 0 k 0
-RenderEvent 135 -2 0 0 0 k 0
-InteractionEvent 135 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 136 -2 0 0 0 k 0
-RenderEvent 136 -2 0 0 0 k 0
-InteractionEvent 136 -2 0 0 0 k 0
-TimerEvent 136 -2 0 0 0 k 0
 MouseMoveEvent 137 -2 0 0 0 k 0
-RenderEvent 137 -2 0 0 0 k 0
-InteractionEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
 LeftButtonReleaseEvent 137 -2 0 0 0 k 0
 EndInteractionEvent 137 -2 0 0 0 k 0
-RenderEvent 137 -2 0 0 0 k 0
 LeaveEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
-TimerEvent 137 -2 0 0 0 k 0
 EnterEvent 137 1 0 0 0 k 0
 MouseMoveEvent 137 1 0 0 0 k 0
-MouseMoveEvent 137 5 0 0 0 k 0
-MouseMoveEvent 137 8 0 0 0 k 0
-MouseMoveEvent 137 12 0 0 0 k 0
-MouseMoveEvent 137 18 0 0 0 k 0
-TimerEvent 137 18 0 0 0 k 0
-MouseMoveEvent 137 24 0 0 0 k 0
-MouseMoveEvent 137 31 0 0 0 k 0
-MouseMoveEvent 137 37 0 0 0 k 0
-MouseMoveEvent 137 41 0 0 0 k 0
-TimerEvent 137 41 0 0 0 k 0
-MouseMoveEvent 137 45 0 0 0 k 0
-MouseMoveEvent 137 48 0 0 0 k 0
-MouseMoveEvent 137 51 0 0 0 k 0
-MouseMoveEvent 137 53 0 0 0 k 0
-MouseMoveEvent 137 54 0 0 0 k 0
-TimerEvent 137 54 0 0 0 k 0
-MouseMoveEvent 137 55 0 0 0 k 0
-MouseMoveEvent 137 56 0 0 0 k 0
-MouseMoveEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
-TimerEvent 137 56 0 0 0 k 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionTrackball.log f3d-3.4.1/testing/recordings/TestInteractionTrackball.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionTrackball.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionTrackball.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,609 +1,125 @@
 # StreamVersion 1.2
 ExposeEvent 0 299 0 0 0 0 0
-RenderEvent 0 299 0 0 0 0 0
 EnterEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-TimerEvent 118 55 0 0 0 0 0
-MouseMoveEvent 106 52 0 0 0 0 0
-MouseMoveEvent 75 46 0 0 0 0 0
-TimerEvent 75 46 0 0 0 0 0
-MouseMoveEvent 46 39 0 0 0 0 0
-MouseMoveEvent 32 35 0 0 0 0 0
-MouseMoveEvent 18 33 0 0 0 0 0
-MouseMoveEvent 7 32 0 0 0 0 0
-LeaveEvent -1 32 0 0 0 0 0
-TimerEvent -1 32 0 0 0 0 0
-TimerEvent -1 32 0 0 0 0 0
-TimerEvent -1 32 0 0 0 0 0
-TimerEvent -1 32 0 0 0 0 0
-EnterEvent 1 97 0 0 0 0 0
-MouseMoveEvent 1 97 0 0 0 0 0
-MouseMoveEvent 5 104 0 0 0 0 0
-MouseMoveEvent 9 110 0 0 0 0 0
-TimerEvent 9 110 0 0 0 0 0
-MouseMoveEvent 13 115 0 0 0 0 0
-MouseMoveEvent 20 121 0 0 0 0 0
-MouseMoveEvent 29 126 0 0 0 0 0
-MouseMoveEvent 39 132 0 0 0 0 0
-MouseMoveEvent 47 136 0 0 0 0 0
-TimerEvent 47 136 0 0 0 0 0
-MouseMoveEvent 54 137 0 0 0 0 0
-MouseMoveEvent 60 139 0 0 0 0 0
-MouseMoveEvent 67 140 0 0 0 0 0
-MouseMoveEvent 75 141 0 0 0 0 0
-MouseMoveEvent 83 143 0 0 0 0 0
-TimerEvent 83 143 0 0 0 0 0
-MouseMoveEvent 92 144 0 0 0 0 0
-MouseMoveEvent 100 146 0 0 0 0 0
-MouseMoveEvent 109 148 0 0 0 0 0
-MouseMoveEvent 118 150 0 0 0 0 0
-TimerEvent 118 150 0 0 0 0 0
-MouseMoveEvent 127 152 0 0 0 0 0
-MouseMoveEvent 135 155 0 0 0 0 0
-MouseMoveEvent 140 157 0 0 0 0 0
-MouseMoveEvent 145 159 0 0 0 0 0
-MouseMoveEvent 149 162 0 0 0 0 0
-TimerEvent 149 162 0 0 0 0 0
-MouseMoveEvent 152 165 0 0 0 0 0
-MouseMoveEvent 154 169 0 0 0 0 0
-MouseMoveEvent 157 172 0 0 0 0 0
-MouseMoveEvent 159 176 0 0 0 0 0
-MouseMoveEvent 160 178 0 0 0 0 0
-TimerEvent 160 178 0 0 0 0 0
-MouseMoveEvent 161 179 0 0 0 0 0
-MouseMoveEvent 161 180 0 0 0 0 0
-MouseMoveEvent 161 181 0 0 0 0 0
-MouseMoveEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-TimerEvent 161 182 0 0 0 0 0
-MouseMoveEvent 161 182 0 0 0 0 0
-MouseMoveEvent 159 183 0 0 0 0 0
-TimerEvent 159 183 0 0 0 0 0
-MouseMoveEvent 155 186 0 0 0 0 0
-MouseMoveEvent 153 190 0 0 0 0 0
-MouseMoveEvent 150 195 0 0 0 0 0
-MouseMoveEvent 148 201 0 0 0 0 0
-MouseMoveEvent 147 207 0 0 0 0 0
-TimerEvent 147 207 0 0 0 0 0
-MouseMoveEvent 147 212 0 0 0 0 0
-MouseMoveEvent 147 216 0 0 0 0 0
-MouseMoveEvent 147 221 0 0 0 0 0
-MouseMoveEvent 148 224 0 0 0 0 0
-MouseMoveEvent 149 228 0 0 0 0 0
-TimerEvent 149 228 0 0 0 0 0
-MouseMoveEvent 150 231 0 0 0 0 0
-MouseMoveEvent 152 235 0 0 0 0 0
-MouseMoveEvent 153 237 0 0 0 0 0
-MouseMoveEvent 154 240 0 0 0 0 0
-TimerEvent 154 240 0 0 0 0 0
-MouseMoveEvent 156 244 0 0 0 0 0
-MouseMoveEvent 157 245 0 0 0 0 0
-MouseMoveEvent 158 247 0 0 0 0 0
-MouseMoveEvent 159 248 0 0 0 0 0
-MouseMoveEvent 160 249 0 0 0 0 0
-TimerEvent 160 249 0 0 0 0 0
-MouseMoveEvent 161 250 0 0 0 0 0
-MouseMoveEvent 161 251 0 0 0 0 0
-MouseMoveEvent 161 252 0 0 0 0 0
-MouseMoveEvent 161 252 0 0 0 0 0
-TimerEvent 161 252 0 0 0 0 0
-MouseMoveEvent 161 253 0 0 0 0 0
-MouseMoveEvent 161 254 0 0 0 0 0
-MouseMoveEvent 161 255 0 0 0 0 0
-MouseMoveEvent 161 256 0 0 0 0 0
-TimerEvent 161 256 0 0 0 0 0
-MouseMoveEvent 161 257 0 0 0 0 0
-MouseMoveEvent 161 259 0 0 0 0 0
-MouseMoveEvent 160 261 0 0 0 0 0
-MouseMoveEvent 159 263 0 0 0 0 0
-MouseMoveEvent 159 265 0 0 0 0 0
-TimerEvent 159 265 0 0 0 0 0
-MouseMoveEvent 159 266 0 0 0 0 0
-MouseMoveEvent 159 266 0 0 0 0 0
-MouseMoveEvent 158 267 0 0 0 0 0
-MouseMoveEvent 158 268 0 0 0 0 0
-TimerEvent 158 268 0 0 0 0 0
-MouseMoveEvent 157 268 0 0 0 0 0
-MouseMoveEvent 156 268 0 0 0 0 0
-MouseMoveEvent 155 269 0 0 0 0 0
-TimerEvent 155 269 0 0 0 0 0
-MouseMoveEvent 154 269 0 0 0 0 0
-MouseMoveEvent 154 270 0 0 0 0 0
-MouseMoveEvent 153 270 0 0 0 0 0
-MouseMoveEvent 152 271 0 0 0 0 0
-MouseMoveEvent 151 271 0 0 0 0 0
-TimerEvent 151 271 0 0 0 0 0
-MouseMoveEvent 150 272 0 0 0 0 0
-MouseMoveEvent 149 272 0 0 0 0 0
-MouseMoveEvent 148 273 0 0 0 0 0
-MouseMoveEvent 147 273 0 0 0 0 0
-TimerEvent 147 273 0 0 0 0 0
-MouseMoveEvent 147 273 0 0 0 0 0
-TimerEvent 147 273 0 0 0 0 0
 MouseMoveEvent 146 273 0 0 0 0 0
 MouseMoveEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
 LeftButtonPressEvent 145 273 0 0 0 0 0
 StartInteractionEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
-TimerEvent 145 273 0 0 0 0 0
 MouseMoveEvent 144 271 0 0 0 0 0
-RenderEvent 144 271 0 0 0 0 0
-InteractionEvent 144 271 0 0 0 0 0
 MouseMoveEvent 143 269 0 0 0 0 0
-RenderEvent 143 269 0 0 0 0 0
-InteractionEvent 143 269 0 0 0 0 0
 MouseMoveEvent 142 267 0 0 0 0 0
-RenderEvent 142 267 0 0 0 0 0
-InteractionEvent 142 267 0 0 0 0 0
-TimerEvent 142 267 0 0 0 0 0
 MouseMoveEvent 142 266 0 0 0 0 0
-RenderEvent 142 266 0 0 0 0 0
-InteractionEvent 142 266 0 0 0 0 0
 MouseMoveEvent 141 263 0 0 0 0 0
-RenderEvent 141 263 0 0 0 0 0
-InteractionEvent 141 263 0 0 0 0 0
 MouseMoveEvent 141 259 0 0 0 0 0
-RenderEvent 141 259 0 0 0 0 0
-InteractionEvent 141 259 0 0 0 0 0
 MouseMoveEvent 140 255 0 0 0 0 0
-RenderEvent 140 255 0 0 0 0 0
-InteractionEvent 140 255 0 0 0 0 0
-TimerEvent 140 255 0 0 0 0 0
 MouseMoveEvent 140 251 0 0 0 0 0
-RenderEvent 140 251 0 0 0 0 0
-InteractionEvent 140 251 0 0 0 0 0
 MouseMoveEvent 140 246 0 0 0 0 0
-RenderEvent 140 246 0 0 0 0 0
-InteractionEvent 140 246 0 0 0 0 0
 MouseMoveEvent 140 242 0 0 0 0 0
-RenderEvent 140 242 0 0 0 0 0
-InteractionEvent 140 242 0 0 0 0 0
 MouseMoveEvent 140 238 0 0 0 0 0
-RenderEvent 140 238 0 0 0 0 0
-InteractionEvent 140 238 0 0 0 0 0
 MouseMoveEvent 140 234 0 0 0 0 0
-RenderEvent 140 234 0 0 0 0 0
-InteractionEvent 140 234 0 0 0 0 0
-TimerEvent 140 234 0 0 0 0 0
 MouseMoveEvent 140 230 0 0 0 0 0
-RenderEvent 140 230 0 0 0 0 0
-InteractionEvent 140 230 0 0 0 0 0
 MouseMoveEvent 141 225 0 0 0 0 0
-RenderEvent 141 225 0 0 0 0 0
-InteractionEvent 141 225 0 0 0 0 0
 MouseMoveEvent 142 221 0 0 0 0 0
-RenderEvent 142 221 0 0 0 0 0
-InteractionEvent 142 221 0 0 0 0 0
 MouseMoveEvent 142 216 0 0 0 0 0
-RenderEvent 142 216 0 0 0 0 0
-InteractionEvent 142 216 0 0 0 0 0
 MouseMoveEvent 142 213 0 0 0 0 0
-RenderEvent 142 213 0 0 0 0 0
-InteractionEvent 142 213 0 0 0 0 0
-TimerEvent 142 213 0 0 0 0 0
 MouseMoveEvent 143 209 0 0 0 0 0
-RenderEvent 143 209 0 0 0 0 0
-InteractionEvent 143 209 0 0 0 0 0
 MouseMoveEvent 144 206 0 0 0 0 0
-RenderEvent 144 206 0 0 0 0 0
-InteractionEvent 144 206 0 0 0 0 0
 MouseMoveEvent 145 203 0 0 0 0 0
-RenderEvent 145 203 0 0 0 0 0
-InteractionEvent 145 203 0 0 0 0 0
 MouseMoveEvent 146 201 0 0 0 0 0
-RenderEvent 146 201 0 0 0 0 0
-InteractionEvent 146 201 0 0 0 0 0
-TimerEvent 146 201 0 0 0 0 0
 MouseMoveEvent 147 198 0 0 0 0 0
-RenderEvent 147 198 0 0 0 0 0
-InteractionEvent 147 198 0 0 0 0 0
 MouseMoveEvent 148 194 0 0 0 0 0
-RenderEvent 148 194 0 0 0 0 0
-InteractionEvent 148 194 0 0 0 0 0
 MouseMoveEvent 149 190 0 0 0 0 0
-RenderEvent 149 190 0 0 0 0 0
-InteractionEvent 149 190 0 0 0 0 0
 MouseMoveEvent 149 186 0 0 0 0 0
-RenderEvent 149 186 0 0 0 0 0
-InteractionEvent 149 186 0 0 0 0 0
 MouseMoveEvent 150 182 0 0 0 0 0
-RenderEvent 150 182 0 0 0 0 0
-InteractionEvent 150 182 0 0 0 0 0
-TimerEvent 150 182 0 0 0 0 0
 MouseMoveEvent 151 179 0 0 0 0 0
-RenderEvent 151 179 0 0 0 0 0
-InteractionEvent 151 179 0 0 0 0 0
 MouseMoveEvent 151 175 0 0 0 0 0
-RenderEvent 151 175 0 0 0 0 0
-InteractionEvent 151 175 0 0 0 0 0
 MouseMoveEvent 152 172 0 0 0 0 0
-RenderEvent 152 172 0 0 0 0 0
-InteractionEvent 152 172 0 0 0 0 0
 MouseMoveEvent 152 170 0 0 0 0 0
-RenderEvent 152 170 0 0 0 0 0
-InteractionEvent 152 170 0 0 0 0 0
 MouseMoveEvent 152 168 0 0 0 0 0
-RenderEvent 152 168 0 0 0 0 0
-InteractionEvent 152 168 0 0 0 0 0
-TimerEvent 152 168 0 0 0 0 0
 MouseMoveEvent 152 166 0 0 0 0 0
-RenderEvent 152 166 0 0 0 0 0
-InteractionEvent 152 166 0 0 0 0 0
 MouseMoveEvent 152 164 0 0 0 0 0
-RenderEvent 152 164 0 0 0 0 0
-InteractionEvent 152 164 0 0 0 0 0
 MouseMoveEvent 152 162 0 0 0 0 0
-RenderEvent 152 162 0 0 0 0 0
-InteractionEvent 152 162 0 0 0 0 0
 MouseMoveEvent 152 160 0 0 0 0 0
-RenderEvent 152 160 0 0 0 0 0
-InteractionEvent 152 160 0 0 0 0 0
-TimerEvent 152 160 0 0 0 0 0
 MouseMoveEvent 152 157 0 0 0 0 0
-RenderEvent 152 157 0 0 0 0 0
-InteractionEvent 152 157 0 0 0 0 0
 MouseMoveEvent 152 155 0 0 0 0 0
-RenderEvent 152 155 0 0 0 0 0
-InteractionEvent 152 155 0 0 0 0 0
 MouseMoveEvent 152 152 0 0 0 0 0
-RenderEvent 152 152 0 0 0 0 0
-InteractionEvent 152 152 0 0 0 0 0
 MouseMoveEvent 152 149 0 0 0 0 0
-RenderEvent 152 149 0 0 0 0 0
-InteractionEvent 152 149 0 0 0 0 0
 MouseMoveEvent 152 144 0 0 0 0 0
-RenderEvent 152 144 0 0 0 0 0
-InteractionEvent 152 144 0 0 0 0 0
-TimerEvent 152 144 0 0 0 0 0
 MouseMoveEvent 151 136 0 0 0 0 0
-RenderEvent 151 136 0 0 0 0 0
-InteractionEvent 151 136 0 0 0 0 0
 MouseMoveEvent 151 131 0 0 0 0 0
-RenderEvent 151 131 0 0 0 0 0
-InteractionEvent 151 131 0 0 0 0 0
 MouseMoveEvent 151 127 0 0 0 0 0
-RenderEvent 151 127 0 0 0 0 0
-InteractionEvent 151 127 0 0 0 0 0
 MouseMoveEvent 151 122 0 0 0 0 0
-RenderEvent 151 122 0 0 0 0 0
-InteractionEvent 151 122 0 0 0 0 0
-TimerEvent 151 122 0 0 0 0 0
 MouseMoveEvent 151 120 0 0 0 0 0
-RenderEvent 151 120 0 0 0 0 0
-InteractionEvent 151 120 0 0 0 0 0
 MouseMoveEvent 151 117 0 0 0 0 0
-RenderEvent 151 117 0 0 0 0 0
-InteractionEvent 151 117 0 0 0 0 0
 MouseMoveEvent 151 114 0 0 0 0 0
-RenderEvent 151 114 0 0 0 0 0
-InteractionEvent 151 114 0 0 0 0 0
 MouseMoveEvent 151 112 0 0 0 0 0
-RenderEvent 151 112 0 0 0 0 0
-InteractionEvent 151 112 0 0 0 0 0
-TimerEvent 151 112 0 0 0 0 0
 MouseMoveEvent 151 110 0 0 0 0 0
-RenderEvent 151 110 0 0 0 0 0
-InteractionEvent 151 110 0 0 0 0 0
 MouseMoveEvent 151 108 0 0 0 0 0
-RenderEvent 151 108 0 0 0 0 0
-InteractionEvent 151 108 0 0 0 0 0
 MouseMoveEvent 151 107 0 0 0 0 0
-RenderEvent 151 107 0 0 0 0 0
-InteractionEvent 151 107 0 0 0 0 0
 MouseMoveEvent 151 105 0 0 0 0 0
-RenderEvent 151 105 0 0 0 0 0
-InteractionEvent 151 105 0 0 0 0 0
 MouseMoveEvent 151 103 0 0 0 0 0
-RenderEvent 151 103 0 0 0 0 0
-InteractionEvent 151 103 0 0 0 0 0
-TimerEvent 151 103 0 0 0 0 0
 MouseMoveEvent 151 102 0 0 0 0 0
-RenderEvent 151 102 0 0 0 0 0
-InteractionEvent 151 102 0 0 0 0 0
 MouseMoveEvent 151 100 0 0 0 0 0
-RenderEvent 151 100 0 0 0 0 0
-InteractionEvent 151 100 0 0 0 0 0
 MouseMoveEvent 151 99 0 0 0 0 0
-RenderEvent 151 99 0 0 0 0 0
-InteractionEvent 151 99 0 0 0 0 0
 MouseMoveEvent 151 98 0 0 0 0 0
-RenderEvent 151 98 0 0 0 0 0
-InteractionEvent 151 98 0 0 0 0 0
 MouseMoveEvent 151 97 0 0 0 0 0
-RenderEvent 151 97 0 0 0 0 0
-InteractionEvent 151 97 0 0 0 0 0
-TimerEvent 151 97 0 0 0 0 0
 MouseMoveEvent 151 95 0 0 0 0 0
-RenderEvent 151 95 0 0 0 0 0
-InteractionEvent 151 95 0 0 0 0 0
 MouseMoveEvent 151 93 0 0 0 0 0
-RenderEvent 151 93 0 0 0 0 0
-InteractionEvent 151 93 0 0 0 0 0
 MouseMoveEvent 151 92 0 0 0 0 0
-RenderEvent 151 92 0 0 0 0 0
-InteractionEvent 151 92 0 0 0 0 0
 MouseMoveEvent 151 89 0 0 0 0 0
-RenderEvent 151 89 0 0 0 0 0
-InteractionEvent 151 89 0 0 0 0 0
-TimerEvent 151 89 0 0 0 0 0
 MouseMoveEvent 152 86 0 0 0 0 0
-RenderEvent 152 86 0 0 0 0 0
-InteractionEvent 152 86 0 0 0 0 0
 MouseMoveEvent 152 84 0 0 0 0 0
-RenderEvent 152 84 0 0 0 0 0
-InteractionEvent 152 84 0 0 0 0 0
 MouseMoveEvent 152 82 0 0 0 0 0
-RenderEvent 152 82 0 0 0 0 0
-InteractionEvent 152 82 0 0 0 0 0
 MouseMoveEvent 152 80 0 0 0 0 0
-RenderEvent 152 80 0 0 0 0 0
-InteractionEvent 152 80 0 0 0 0 0
 MouseMoveEvent 152 78 0 0 0 0 0
-RenderEvent 152 78 0 0 0 0 0
-InteractionEvent 152 78 0 0 0 0 0
-TimerEvent 152 78 0 0 0 0 0
 MouseMoveEvent 152 74 0 0 0 0 0
-RenderEvent 152 74 0 0 0 0 0
-InteractionEvent 152 74 0 0 0 0 0
 MouseMoveEvent 152 72 0 0 0 0 0
-RenderEvent 152 72 0 0 0 0 0
-InteractionEvent 152 72 0 0 0 0 0
 MouseMoveEvent 152 70 0 0 0 0 0
-RenderEvent 152 70 0 0 0 0 0
-InteractionEvent 152 70 0 0 0 0 0
 MouseMoveEvent 152 69 0 0 0 0 0
-RenderEvent 152 69 0 0 0 0 0
-InteractionEvent 152 69 0 0 0 0 0
-TimerEvent 152 69 0 0 0 0 0
 MouseMoveEvent 152 67 0 0 0 0 0
-RenderEvent 152 67 0 0 0 0 0
-InteractionEvent 152 67 0 0 0 0 0
 MouseMoveEvent 152 65 0 0 0 0 0
-RenderEvent 152 65 0 0 0 0 0
-InteractionEvent 152 65 0 0 0 0 0
 MouseMoveEvent 152 63 0 0 0 0 0
-RenderEvent 152 63 0 0 0 0 0
-InteractionEvent 152 63 0 0 0 0 0
 MouseMoveEvent 152 60 0 0 0 0 0
-RenderEvent 152 60 0 0 0 0 0
-InteractionEvent 152 60 0 0 0 0 0
-TimerEvent 152 60 0 0 0 0 0
 MouseMoveEvent 152 57 0 0 0 0 0
-RenderEvent 152 57 0 0 0 0 0
-InteractionEvent 152 57 0 0 0 0 0
 MouseMoveEvent 152 55 0 0 0 0 0
-RenderEvent 152 55 0 0 0 0 0
-InteractionEvent 152 55 0 0 0 0 0
 MouseMoveEvent 151 52 0 0 0 0 0
-RenderEvent 151 52 0 0 0 0 0
-InteractionEvent 151 52 0 0 0 0 0
 MouseMoveEvent 151 50 0 0 0 0 0
-RenderEvent 151 50 0 0 0 0 0
-InteractionEvent 151 50 0 0 0 0 0
 MouseMoveEvent 151 48 0 0 0 0 0
-RenderEvent 151 48 0 0 0 0 0
-InteractionEvent 151 48 0 0 0 0 0
-TimerEvent 151 48 0 0 0 0 0
 MouseMoveEvent 151 46 0 0 0 0 0
-RenderEvent 151 46 0 0 0 0 0
-InteractionEvent 151 46 0 0 0 0 0
 MouseMoveEvent 151 44 0 0 0 0 0
-RenderEvent 151 44 0 0 0 0 0
-InteractionEvent 151 44 0 0 0 0 0
 MouseMoveEvent 151 42 0 0 0 0 0
-RenderEvent 151 42 0 0 0 0 0
-InteractionEvent 151 42 0 0 0 0 0
 MouseMoveEvent 151 41 0 0 0 0 0
-RenderEvent 151 41 0 0 0 0 0
-InteractionEvent 151 41 0 0 0 0 0
 MouseMoveEvent 151 39 0 0 0 0 0
-RenderEvent 151 39 0 0 0 0 0
-InteractionEvent 151 39 0 0 0 0 0
-TimerEvent 151 39 0 0 0 0 0
 MouseMoveEvent 151 38 0 0 0 0 0
-RenderEvent 151 38 0 0 0 0 0
-InteractionEvent 151 38 0 0 0 0 0
 MouseMoveEvent 151 36 0 0 0 0 0
-RenderEvent 151 36 0 0 0 0 0
-InteractionEvent 151 36 0 0 0 0 0
 MouseMoveEvent 151 34 0 0 0 0 0
-RenderEvent 151 34 0 0 0 0 0
-InteractionEvent 151 34 0 0 0 0 0
 MouseMoveEvent 151 34 0 0 0 0 0
-RenderEvent 151 34 0 0 0 0 0
-InteractionEvent 151 34 0 0 0 0 0
-TimerEvent 151 34 0 0 0 0 0
 MouseMoveEvent 151 33 0 0 0 0 0
-RenderEvent 151 33 0 0 0 0 0
-InteractionEvent 151 33 0 0 0 0 0
 MouseMoveEvent 151 31 0 0 0 0 0
-RenderEvent 151 31 0 0 0 0 0
-InteractionEvent 151 31 0 0 0 0 0
 MouseMoveEvent 151 30 0 0 0 0 0
-RenderEvent 151 30 0 0 0 0 0
-InteractionEvent 151 30 0 0 0 0 0
 MouseMoveEvent 151 29 0 0 0 0 0
-RenderEvent 151 29 0 0 0 0 0
-InteractionEvent 151 29 0 0 0 0 0
 MouseMoveEvent 151 28 0 0 0 0 0
-RenderEvent 151 28 0 0 0 0 0
-InteractionEvent 151 28 0 0 0 0 0
-TimerEvent 151 28 0 0 0 0 0
 MouseMoveEvent 151 27 0 0 0 0 0
-RenderEvent 151 27 0 0 0 0 0
-InteractionEvent 151 27 0 0 0 0 0
 MouseMoveEvent 151 27 0 0 0 0 0
-RenderEvent 151 27 0 0 0 0 0
-InteractionEvent 151 27 0 0 0 0 0
 MouseMoveEvent 151 26 0 0 0 0 0
-RenderEvent 151 26 0 0 0 0 0
-InteractionEvent 151 26 0 0 0 0 0
 MouseMoveEvent 151 25 0 0 0 0 0
-RenderEvent 151 25 0 0 0 0 0
-InteractionEvent 151 25 0 0 0 0 0
 MouseMoveEvent 151 24 0 0 0 0 0
-RenderEvent 151 24 0 0 0 0 0
-InteractionEvent 151 24 0 0 0 0 0
-TimerEvent 151 24 0 0 0 0 0
 MouseMoveEvent 151 20 0 0 0 0 0
-RenderEvent 151 20 0 0 0 0 0
-InteractionEvent 151 20 0 0 0 0 0
 MouseMoveEvent 151 20 0 0 0 0 0
-RenderEvent 151 20 0 0 0 0 0
-InteractionEvent 151 20 0 0 0 0 0
 MouseMoveEvent 151 18 0 0 0 0 0
-RenderEvent 151 18 0 0 0 0 0
-InteractionEvent 151 18 0 0 0 0 0
-TimerEvent 151 18 0 0 0 0 0
 MouseMoveEvent 151 17 0 0 0 0 0
-RenderEvent 151 17 0 0 0 0 0
-InteractionEvent 151 17 0 0 0 0 0
 MouseMoveEvent 151 15 0 0 0 0 0
-RenderEvent 151 15 0 0 0 0 0
-InteractionEvent 151 15 0 0 0 0 0
 MouseMoveEvent 150 13 0 0 0 0 0
-RenderEvent 150 13 0 0 0 0 0
-InteractionEvent 150 13 0 0 0 0 0
 MouseMoveEvent 150 12 0 0 0 0 0
-RenderEvent 150 12 0 0 0 0 0
-InteractionEvent 150 12 0 0 0 0 0
 MouseMoveEvent 150 12 0 0 0 0 0
-RenderEvent 150 12 0 0 0 0 0
-InteractionEvent 150 12 0 0 0 0 0
-TimerEvent 150 12 0 0 0 0 0
 MouseMoveEvent 149 11 0 0 0 0 0
-RenderEvent 149 11 0 0 0 0 0
-InteractionEvent 149 11 0 0 0 0 0
 MouseMoveEvent 149 10 0 0 0 0 0
-RenderEvent 149 10 0 0 0 0 0
-InteractionEvent 149 10 0 0 0 0 0
 MouseMoveEvent 149 9 0 0 0 0 0
-RenderEvent 149 9 0 0 0 0 0
-InteractionEvent 149 9 0 0 0 0 0
 MouseMoveEvent 149 8 0 0 0 0 0
-RenderEvent 149 8 0 0 0 0 0
-InteractionEvent 149 8 0 0 0 0 0
 MouseMoveEvent 149 7 0 0 0 0 0
-RenderEvent 149 7 0 0 0 0 0
-InteractionEvent 149 7 0 0 0 0 0
-TimerEvent 149 7 0 0 0 0 0
-TimerEvent 149 7 0 0 0 0 0
-TimerEvent 149 7 0 0 0 0 0
-TimerEvent 149 7 0 0 0 0 0
-TimerEvent 149 7 0 0 0 0 0
 MouseMoveEvent 149 6 0 0 0 0 0
-RenderEvent 149 6 0 0 0 0 0
-InteractionEvent 149 6 0 0 0 0 0
-TimerEvent 149 6 0 0 0 0 0
 MouseMoveEvent 149 6 0 0 0 0 0
-RenderEvent 149 6 0 0 0 0 0
-InteractionEvent 149 6 0 0 0 0 0
 MouseMoveEvent 149 5 0 0 0 0 0
-RenderEvent 149 5 0 0 0 0 0
-InteractionEvent 149 5 0 0 0 0 0
 MouseMoveEvent 150 4 0 0 0 0 0
-RenderEvent 150 4 0 0 0 0 0
-InteractionEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
 LeftButtonReleaseEvent 150 4 0 0 0 0 0
 EndInteractionEvent 150 4 0 0 0 0 0
-RenderEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
-TimerEvent 150 4 0 0 0 0 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionVerboseWatchUnwatch.log f3d-3.4.1/testing/recordings/TestInteractionVerboseWatchUnwatch.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionVerboseWatchUnwatch.log	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/recordings/TestInteractionVerboseWatchUnwatch.log	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,15 @@
+# StreamVersion 1.2
+ExposeEvent 0 299 0 0 0 0 0
+RenderEvent 0 299 0 0 0 0 0
+EnterEvent 290 186 0 0 0 0 0
+LeftButtonPressEvent 90 130 0 0 0 0 0
+StartInteractionEvent 90 130 0 0 0 0 0
+MouseMoveEvent 190 130 0 0 0 0 0
+RenderEvent 190 130 0 0 0 0 0
+InteractionEvent 190 130 0 0 0 0 0
+LeftButtonReleaseEvent 190 130 0 0 0 0 0
+EndInteractionEvent 190 130 0 0 0 0 0
+RenderEvent 190 130 0 0 0 0 0
+KeyPressEvent 190 130 0 0 1 Right 0
+CharEvent 190 130 0 0 1 Right 0
+KeyReleaseEvent 190 130 0 0 1 Right 0
diff -Nru f3d-3.2.0+dfsg/testing/recordings/TestInteractionVolumeCycle.log f3d-3.4.1/testing/recordings/TestInteractionVolumeCycle.log
--- f3d-3.2.0+dfsg/testing/recordings/TestInteractionVolumeCycle.log	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/recordings/TestInteractionVolumeCycle.log	2026-01-08 08:41:55.000000000 +0200
@@ -1,23 +1,11 @@
 # StreamVersion 1.2
 ExposeEvent 0 599 0 0 0 0 0
-RenderEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 0 599 0 0 0 0 0
-TimerEvent 363 259 0 0 0 0 0
 KeyPressEvent 363 259 0 118 1 v 0
 CharEvent 363 259 0 118 1 v 0
-TimerEvent 363 259 0 118 1 v 0
 KeyReleaseEvent 363 259 0 118 1 v 0
-TimerEvent 363 259 0 118 1 v 0
-TimerEvent 363 259 0 118 1 v 0
 KeyPressEvent 363 259 0 115 1 s 0
 CharEvent 363 259 0 115 1 s 0
-TimerEvent 363 259 0 115 1 s 0
 KeyReleaseEvent 363 259 0 115 1 s 0
-TimerEvent 363 259 0 115 1 s 0
-TimerEvent 363 259 0 115 1 s 0
 KeyPressEvent 363 259 0 115 1 s 0
 CharEvent 363 259 0 115 1 s 0
-TimerEvent 363 259 0 115 1 s 0
 KeyReleaseEvent 363 259 0 115 1 s 0
-TimerEvent 363 259 0 115 1 s 0
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptHelpInvalid.txt f3d-3.4.1/testing/scripts/TestCommandScriptHelpInvalid.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptHelpInvalid.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptHelpInvalid.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+help invalid
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptHelp.txt f3d-3.4.1/testing/scripts/TestCommandScriptHelp.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptHelp.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptHelp.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+help set
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToFirstFrame.txt f3d-3.4.1/testing/scripts/TestCommandScriptJumpToFirstFrame.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToFirstFrame.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptJumpToFirstFrame.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+jump_to_frame 0 false
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToLastFrame.txt f3d-3.4.1/testing/scripts/TestCommandScriptJumpToLastFrame.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToLastFrame.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptJumpToLastFrame.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+jump_to_frame -1 false
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToMiddleFrame.txt f3d-3.4.1/testing/scripts/TestCommandScriptJumpToMiddleFrame.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToMiddleFrame.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptJumpToMiddleFrame.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+jump_to_frame 20 false
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToNextFrame.txt f3d-3.4.1/testing/scripts/TestCommandScriptJumpToNextFrame.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToNextFrame.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptJumpToNextFrame.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,4 @@
+jump_to_frame 1 true
+jump_to_frame 1 true
+jump_to_frame 1 true
+jump_to_frame 1 true
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToPreviousFrame.txt f3d-3.4.1/testing/scripts/TestCommandScriptJumpToPreviousFrame.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptJumpToPreviousFrame.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptJumpToPreviousFrame.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+jump_to_frame -1 true
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptParseOptionalBoolExtraArg.txt f3d-3.4.1/testing/scripts/TestCommandScriptParseOptionalBoolExtraArg.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptParseOptionalBoolExtraArg.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptParseOptionalBoolExtraArg.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+load_previous_file_group true extra
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptRemoveCurrentFileGroup.txt f3d-3.4.1/testing/scripts/TestCommandScriptRemoveCurrentFileGroup.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptRemoveCurrentFileGroup.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptRemoveCurrentFileGroup.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+remove_current_file_group
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptRemoveFileGroups.txt f3d-3.4.1/testing/scripts/TestCommandScriptRemoveFileGroups.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestCommandScriptRemoveFileGroups.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/testing/scripts/TestCommandScriptRemoveFileGroups.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1 @@
+remove_file_groups
\ No newline at end of file
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestParseOptionalBoolExtraArg.txt f3d-3.4.1/testing/scripts/TestParseOptionalBoolExtraArg.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestParseOptionalBoolExtraArg.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/scripts/TestParseOptionalBoolExtraArg.txt	1970-01-01 02:00:00.000000000 +0200
@@ -1 +0,0 @@
-load_previous_file_group true extra
diff -Nru f3d-3.2.0+dfsg/testing/scripts/TestRemoveFileGroups.txt f3d-3.4.1/testing/scripts/TestRemoveFileGroups.txt
--- f3d-3.2.0+dfsg/testing/scripts/TestRemoveFileGroups.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/testing/scripts/TestRemoveFileGroups.txt	1970-01-01 02:00:00.000000000 +0200
@@ -1 +0,0 @@
-remove_file_groups
\ No newline at end of file
diff -Nru f3d-3.2.0+dfsg/THIRD_PARTY_LICENSES.md f3d-3.4.1/THIRD_PARTY_LICENSES.md
--- f3d-3.2.0+dfsg/THIRD_PARTY_LICENSES.md	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/THIRD_PARTY_LICENSES.md	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,268 @@
+# Third Party Copyrights and License within F3D application source
+
+## cxxopts.hpp:
+
+```
+Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck
+
+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.
+```
+
+## json.hpp:
+
+```
+    __ _____ _____ _____
+ __|  |   __|     |   | |  JSON for Modern C++
+|  |  |__   |  |  | | | |  version 3.10.5
+|_____|_____|_____|_|___|  https://github.com/nlohmann/json
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2013-2022 Niels Lohmann <http://nlohmann.me>.
+
+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.
+```
+
+## dmon.h:
+
+```
+BSD 2-Clause License
+
+Copyright (c) 2019, Sepehr Taghdisian (septag@protonmail.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
+
+## ImGui
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2014-2024 Omar Cornut
+
+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.
+```
+
+## MonaspaceNeon-Regular.0tf
+
+```
+Copyright (c) 2023, GitHub https://github.com/githubnext/monaspace
+with Reserved Font Name "Monaspace", including subfamilies: "Argon", "Neon", "Xenon", "Radon", and "Krypton"
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+```
+
+### tinyfiledialogs
+
+```
+SPDX-License-Identifier: Zlib
+Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
+	 ________________________________________________________________
+	|                                                                |
+	| 100% compatible C C++  ->  You can rename this .c file as .cpp |
+	|________________________________________________________________|
+
+********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
+  _________
+ /         \ tinyfiledialogs.c v3.18.2 [Jun 8, 2024] zlib licence
+ |tiny file| Unique code file created [November 9, 2014]
+ | dialogs |
+ \____  ___/ http://tinyfiledialogs.sourceforge.net
+	  \|     git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
+			  ____________________________________________
+			 |                                            |
+			 |   email: tinyfiledialogs at ysengrin.com   |
+			 |____________________________________________|
+  _________________________________________________________________________________
+ |                                                                                 |
+ | the windows only wchar_t UTF-16 prototypes are at the bottom of the header file |
+ |_________________________________________________________________________________|
+  _________________________________________________________
+ |                                                         |
+ | on windows: - since v3.6 char is UTF-8 by default       |
+ |             - if you want MBCS set tinyfd_winUtf8 to 0  |
+ |             - functions like fopen expect MBCS          |
+ |_________________________________________________________|
+
+If you like tinyfiledialogs, please upvote my stackoverflow answer
+https://stackoverflow.com/a/47651444
+
+- License -
+This software is provided 'as-is', without any express or implied
+warranty.  In no event will the authors be held liable for any damages
+arising from the use of this software.
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software.  If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+     __________________________________________
+    |  ______________________________________  |
+    | |                                      | |
+    | | DO NOT USE USER INPUT IN THE DIALOGS | |
+    | |______________________________________| |
+    |__________________________________________|
+```
diff -Nru f3d-3.2.0+dfsg/.tsan.supp f3d-3.4.1/.tsan.supp
--- f3d-3.2.0+dfsg/.tsan.supp	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/.tsan.supp	2026-01-08 08:41:55.000000000 +0200
@@ -11,5 +11,6 @@
 # OpenEXR
 race:libOpenEXR
 
-# OSMesa
+# Mesa
 race:libOSMesa
+race:libgallium
diff -Nru f3d-3.2.0+dfsg/vtkext/private/CMakeLists.txt f3d-3.4.1/vtkext/private/CMakeLists.txt
--- f3d-3.2.0+dfsg/vtkext/private/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -8,6 +8,7 @@
 
 vtk_module_build(
   MODULES ${modules}
+  ENABLE_WRAPPING OFF
   INSTALL_HEADERS OFF
   PACKAGE "f3d_vtkext_private")
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/CMakeLists.txt f3d-3.4.1/vtkext/private/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/vtkext/private/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -7,7 +7,7 @@
   BINARY)
 
 set(sources ${CMAKE_CURRENT_BINARY_DIR}/F3DDefaultHDRI.cxx)
-set(private_headers F3DImguiStyle.h ${CMAKE_CURRENT_BINARY_DIR}/F3DDefaultHDRI.h)
+set(private_headers ${CMAKE_CURRENT_BINARY_DIR}/F3DDefaultHDRI.h)
 
 if(F3D_MODULE_UI)
   set(F3D_FONT_FILE "${F3D_SOURCE_DIR}/resources/MonaspaceNeon-Regular.otf" CACHE STRING "Font used in UI widgets")
@@ -19,10 +19,25 @@
     BINARY)
 
   list(APPEND sources ${CMAKE_CURRENT_BINARY_DIR}/F3DFontBuffer.cxx)
-  list(APPEND private_headers ${CMAKE_CURRENT_BINARY_DIR}/F3DFontBuffer.h)
+  list(APPEND private_headers F3DStyle.h ${CMAKE_CURRENT_BINARY_DIR}/F3DFontBuffer.h)
+
+  set(F3D_DEFAULT_LOGO "${F3D_SOURCE_DIR}/resources/logo-black.png" CACHE STRING "logo")
+  mark_as_advanced(F3D_DEFAULT_LOGO)
+
+  f3d_embed_file(
+    INPUT "${F3D_DEFAULT_LOGO}"
+    NAME F3DDefaultLogo
+    BINARY)
+
+  list(APPEND sources ${CMAKE_CURRENT_BINARY_DIR}/F3DDefaultLogo.cxx)
+  list(APPEND private_headers ${CMAKE_CURRENT_BINARY_DIR}/F3DDefaultLogo.h)
 endif()
 
-set(shader_files glsl/vtkF3DComputeDepthCS.glsl)
+set(shader_files glsl/vtkF3DRandomFS.glsl glsl/vtkF3DPointSplatVS.glsl)
+
+if (NOT ANDROID AND NOT EMSCRIPTEN)
+  list(APPEND shader_files glsl/vtkF3DComputeDepthCS.glsl)
+endif()
 
 if(F3D_MODULE_UI)
   list(APPEND shader_files glsl/vtkF3DImguiFS.glsl glsl/vtkF3DImguiVS.glsl)
@@ -61,24 +76,22 @@
   vtkF3DRenderPass
   vtkF3DRenderer
   vtkF3DSolidBackgroundPass
+  vtkF3DStochasticTransparentPass
   vtkF3DUIObserver
   vtkF3DUIActor
   vtkF3DUserRenderPass
+  vtkF3DTAAResolvePass
   )
 
 if (F3D_MODULE_UI)
-  set(classes ${classes} vtkF3DImguiActor vtkF3DImguiConsole vtkF3DImguiObserver)
+  set(classes ${classes} vtkF3DImguiActor vtkF3DImguiConsole vtkF3DImguiObserver F3DStyle)
 endif ()
 
 # Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10675
-if(NOT ANDROID AND NOT EMSCRIPTEN AND VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240203)
+if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20240203)
   set(classes ${classes} vtkF3DPointSplatMapper)
 endif()
 
-if(NOT VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220907)
-  set(classes ${classes} vtkF3DOrientationMarkerWidget)
-endif()
-
 if(WIN32)
   list(APPEND classes vtkF3DWGLRenderWindow)
 endif()
@@ -102,6 +115,11 @@
   list(APPEND classes vtkF3DEXRReader)
 endif()
 
+if(F3D_MODULE_WEBP)
+  find_package(WebP REQUIRED)
+  list(APPEND classes vtkF3DWebPReader)
+endif()
+
 if(F3D_MODULE_UI AND NOT F3D_USE_EXTERNAL_IMGUI)
   list(APPEND sources
     "${F3D_SOURCE_DIR}/external/imgui/imgui.cpp"
@@ -111,13 +129,8 @@
     "${F3D_SOURCE_DIR}/external/imgui/imgui_widgets.cpp")
 endif()
 
-set(_no_install "")
-if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-  set(_no_install "NO_INSTALL")
-endif()
-
 vtk_module_add_module(f3d::vtkextPrivate
-  ${_no_install}
+  NO_INSTALL
   FORCE_STATIC
   CLASSES ${classes}
   SOURCES ${sources}
@@ -133,6 +146,11 @@
   vtk_module_link(f3d::vtkextPrivate PRIVATE OpenEXR::OpenEXR)
 endif()
 
+# webp
+if(F3D_MODULE_WEBP)
+  vtk_module_link(f3d::vtkextPrivate PRIVATE WebP::webp)
+endif()
+
 # imgui
 if (F3D_MODULE_UI)
   vtk_module_definitions(f3d::vtkextPrivate PRIVATE F3D_MODULE_UI)
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DColoringInfoHandler.cxx f3d-3.4.1/vtkext/private/module/F3DColoringInfoHandler.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DColoringInfoHandler.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/F3DColoringInfoHandler.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -161,7 +161,7 @@
 //----------------------------------------------------------------------------
 void F3DColoringInfoHandler::CycleColoringArray(bool cycleToNonColoring)
 {
-  auto& data =
+  const auto& data =
     this->CurrentUsingCellData ? this->CellDataColoringInfo : this->PointDataColoringInfo;
   if (!this->CurrentColoringIter.has_value())
   {
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DColoringInfoHandler.h f3d-3.4.1/vtkext/private/module/F3DColoringInfoHandler.h
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DColoringInfoHandler.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/F3DColoringInfoHandler.h	2026-01-08 08:41:55.000000000 +0200
@@ -75,7 +75,7 @@
 
   // Current coloring state
   bool CurrentUsingCellData = false;
-  std::optional<ColoringMap::iterator> CurrentColoringIter;
+  std::optional<ColoringMap::const_iterator> CurrentColoringIter;
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DImguiStyle.h f3d-3.4.1/vtkext/private/module/F3DImguiStyle.h
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DImguiStyle.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/F3DImguiStyle.h	1970-01-01 02:00:00.000000000 +0200
@@ -1,68 +0,0 @@
-/**
- * @class   F3DImguiStyle
- * @brief   Namespace containing method to recover color for styling F3D ImGui UI
- *
- * Provide methods to use when recovering colors for F3D ImGui UI
- */
-
-#ifndef F3DImguiStyle_h
-#define F3DImguiStyle_h
-
-#include <imgui.h>
-
-namespace F3DImguiStyle
-{
-constexpr ImVec4 DecomposeImv4(int val)
-{
-  return {
-    ((val >> 16) & 0xff) / 255.f,
-    ((val >> 8) & 0xff) / 255.f,
-    (val & 0xff) / 255.f,
-    1.f,
-  };
-}
-
-constexpr ImVec4 GetErrorColor()
-{
-  // red with the same saturation (95%) and luminance (50%) as F3D yellow
-  return DecomposeImv4(0xf94306);
-}
-
-constexpr ImVec4 GetWarningColor()
-{
-  // F3D yellow
-  return DecomposeImv4(0xf9b208);
-}
-
-constexpr ImVec4 GetHighlightColor()
-{
-  // F3D blue
-  return DecomposeImv4(0x788bff);
-}
-
-static constexpr ImVec4 GetCompletionColor()
-{
-  // green with the same saturation (100%) and luminance (74%) as F3D blue
-  return DecomposeImv4(0x7aff7a);
-}
-
-constexpr ImVec4 GetTextColor()
-{
-  // F3D white
-  return DecomposeImv4(0xf4f4f4);
-}
-
-constexpr ImVec4 GetMidColor()
-{
-  // grey between F3D black and F3D white
-  return DecomposeImv4(0x545454);
-}
-
-constexpr ImVec4 GetBackgroundColor()
-{
-  // F3D black
-  return DecomposeImv4(0x141414);
-}
-};
-
-#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DLog.cxx f3d-3.4.1/vtkext/private/module/F3DLog.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DLog.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/F3DLog.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,11 +2,21 @@
 
 #include "vtkF3DConsoleOutputWindow.h"
 
+#include <vtkCallbackCommand.h>
+#include <vtkNew.h>
+
+// extern variables
 F3DLog::Severity F3DLog::VerboseLevel = F3DLog::Severity::Info;
+std::function<void(F3DLog::Severity, const std::string&)> F3DLog::Forwarder;
 
 //----------------------------------------------------------------------------
 void F3DLog::Print(Severity sev, const std::string& str)
 {
+  if (F3DLog::Forwarder)
+  {
+    F3DLog::Forwarder(sev, str);
+  }
+
   vtkOutputWindow* win = vtkOutputWindow::GetInstance();
   switch (sev)
   {
@@ -68,3 +78,9 @@
       break;
   }
 }
+
+//----------------------------------------------------------------------------
+void F3DLog::Forward(std::function<void(Severity, const std::string&)> userCallback)
+{
+  F3DLog::Forwarder = std::move(userCallback);
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DLog.h f3d-3.4.1/vtkext/private/module/F3DLog.h
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DLog.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/F3DLog.h	2026-01-08 08:41:55.000000000 +0200
@@ -11,6 +11,7 @@
 #ifndef F3DLog_h
 #define F3DLog_h
 
+#include <functional>
 #include <string>
 
 namespace F3DLog
@@ -56,6 +57,18 @@
  * are written to stdout.
  */
 void SetStandardStream(StandardStream mode);
+
+/**
+ * Callback function for forwarding log messages.
+ * If set, it will be invoked with the message string whenever a log message is printed.
+ */
+extern std::function<void(Severity, const std::string&)> Forwarder;
+
+/**
+ * Set a callback function to forward log messages.
+ * The callback will be invoked with the message string whenever a log message is printed.
+ */
+void Forward(std::function<void(Severity, const std::string&)> userCallback);
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DStyle.cxx f3d-3.4.1/vtkext/private/module/F3DStyle.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DStyle.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/F3DStyle.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,52 @@
+#include "F3DStyle.h"
+
+#ifdef F3D_MODULE_UI
+
+#include <imgui.h>
+
+const inline ImVec4 intToImVec4(int rgb, int alpha = 255)
+{
+  return ImVec4{
+    ((rgb >> 16) & 0xff) / 255.f,
+    ((rgb >> 8) & 0xff) / 255.f,
+    (rgb & 0xff) / 255.f,
+    alpha / 255.f,
+  };
+}
+
+const ImVec4 F3DStyle::imgui::GetErrorColor()
+{
+  return intToImVec4(F3DStyle::F3D_RED);
+}
+
+const ImVec4 F3DStyle::imgui::GetWarningColor()
+{
+  return intToImVec4(F3DStyle::F3D_YELLOW);
+}
+
+const ImVec4 F3DStyle::imgui::GetHighlightColor()
+{
+  return intToImVec4(F3DStyle::F3D_BLUE);
+}
+
+const ImVec4 F3DStyle::imgui::GetCompletionColor()
+{
+  return intToImVec4(F3DStyle::F3D_GREEN);
+}
+
+const ImVec4 F3DStyle::imgui::GetTextColor()
+{
+  return intToImVec4(F3DStyle::F3D_WHITE);
+}
+
+const ImVec4 F3DStyle::imgui::GetMidColor()
+{
+  return intToImVec4(F3DStyle::F3D_GREY);
+}
+
+const ImVec4 F3DStyle::imgui::GetBackgroundColor()
+{
+  return intToImVec4(F3DStyle::F3D_BLACK);
+}
+
+#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/F3DStyle.h f3d-3.4.1/vtkext/private/module/F3DStyle.h
--- f3d-3.2.0+dfsg/vtkext/private/module/F3DStyle.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/F3DStyle.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,92 @@
+/**
+ * @class   F3DStyle
+ * @brief   Namespace containing method to recover color for styling F3D UI
+ *
+ * Provide methods to use when recovering colors for F3D UI
+ */
+
+#ifndef F3DStyle_h
+#define F3DStyle_h
+
+#include <tuple>
+
+struct ImVec4;
+
+namespace F3DStyle
+{
+
+// red with the same saturation (95%) and luminance (50%) as F3D yellow
+constexpr int F3D_RED = 0xf94306;
+// green with the same saturation (100%) and luminance (74%) as F3D blue
+constexpr int F3D_GREEN = 0x7aff7a;
+constexpr int F3D_BLUE = 0x788bff;
+constexpr int F3D_YELLOW = 0xf9b208;
+constexpr int F3D_WHITE = 0xf4f4f4;
+// grey between F3D black and F3D white
+constexpr int F3D_GREY = 0x545454;
+constexpr int F3D_BLACK = 0x141414;
+
+constexpr std::tuple<float, float, float> DecomposeFloatTuple(int val)
+{
+  return std::make_tuple(
+    ((val >> 16) & 0xff) / 255.f, ((val >> 8) & 0xff) / 255.f, (val & 0xff) / 255.f);
+}
+
+constexpr std::tuple<float, float, float> GetF3DRed()
+{
+  return DecomposeFloatTuple(F3D_RED);
+}
+
+constexpr std::tuple<float, float, float> GetF3DGreen()
+{
+  return DecomposeFloatTuple(F3D_GREEN);
+}
+
+constexpr std::tuple<float, float, float> GetF3DBlue()
+{
+  return DecomposeFloatTuple(F3D_BLUE);
+}
+
+constexpr std::tuple<float, float, float> GetF3DYellow()
+{
+  return DecomposeFloatTuple(F3D_YELLOW);
+}
+
+constexpr std::tuple<float, float, float> GetF3DWhite()
+{
+  return DecomposeFloatTuple(F3D_WHITE);
+}
+
+constexpr std::tuple<float, float, float> GetF3DGrey()
+{
+  return DecomposeFloatTuple(F3D_GREY);
+}
+
+constexpr std::tuple<float, float, float> GetF3DBlack()
+{
+  return DecomposeFloatTuple(F3D_BLACK);
+}
+
+constexpr float GetDefaultMargin()
+{
+  return 5.f;
+}
+
+#ifdef F3D_MODULE_UI
+
+namespace imgui
+{
+
+const ImVec4 GetErrorColor();
+const ImVec4 GetWarningColor();
+const ImVec4 GetHighlightColor();
+const ImVec4 GetCompletionColor();
+const ImVec4 GetTextColor();
+const ImVec4 GetMidColor();
+const ImVec4 GetBackgroundColor();
+
+};
+#endif
+};
+
+#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/glsl/vtkF3DPointSplatVS.glsl f3d-3.4.1/vtkext/private/module/glsl/vtkF3DPointSplatVS.glsl
--- f3d-3.2.0+dfsg/vtkext/private/module/glsl/vtkF3DPointSplatVS.glsl	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/glsl/vtkF3DPointSplatVS.glsl	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,153 @@
+//VTK::System::Dec
+
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+uniform float scaleFactor;
+uniform int cameraParallel;
+
+// low-pass filtering matrix
+// stored as a vector since it's a 2x2 symmetric matrix
+uniform vec3 lowpassMatrix;
+
+in vec4 vertexMC;
+
+//VTK::Covariance::Dec
+
+//VTK::PositionVC::Dec
+
+// optional normal declaration
+//VTK::Normal::Dec
+
+// Texture coordinates
+//VTK::TCoord::Dec
+
+// material property values
+//VTK::Color::Dec
+
+// clipping plane vars
+//VTK::Clip::Dec
+
+// camera and actor matrix values
+//VTK::Camera::Dec
+
+// picking support
+//VTK::Picking::Dec
+
+mat3 quaternionToMatrix(vec4 quat)
+{
+  // Normalize quaternion (required?)
+  vec4 q = quat / length(quat);
+  float r = q.x;
+  float x = q.y;
+  float y = q.z;
+  float z = q.w;
+
+  return mat3(1. - 2. * (y * y + z * z), 2. * (x * y + r * z), 2. * (x * z - r * y),
+    2. * (x * y - r * z), 1. - 2. * (x * x + z * z), 2. * (y * z + r * x),
+    2. * (x * z + r * y), 2. * (y * z - r * x), 1. - 2. * (x * x + y * y));
+}
+
+mat3 computeCov3D(vec3 scale, vec4 rotation)
+{
+  // Create scaling matrix
+  mat3 S = mat3(
+    scaleFactor * scale.x, 0., 0.,
+    0., scaleFactor * scale.y, 0.,
+    0., 0., scaleFactor * scale.z);
+
+  // Compute rotation matrix from quaternion
+  mat3 R = quaternionToMatrix(rotation);
+
+  mat3 M = R * S;
+
+  // Compute 3D world covariance matrix
+  return M * transpose(M);
+}
+
+mat3 getProjectionMatrix(vec3 positionVC)
+{
+  // orthographic projection
+  if (cameraParallel != 0)
+  {
+    return mat3(
+      VCDCMatrix[0][0], 0., 0.,
+      0., VCDCMatrix[1][1], 0.,
+      0., 0., 0.);
+  }
+
+  // affine approximation of the perspective projection
+  // Refer to eq 29 and 31 in "EWA Splatting" (Zwicker et al., 2002).
+  return mat3(
+    VCDCMatrix[0][0] / positionVC.z, 0., 0.,
+    0., VCDCMatrix[1][1] / positionVC.z, 0.,
+    -(VCDCMatrix[0][0] * positionVC.x) / (positionVC.z * positionVC.z), -(VCDCMatrix[1][1] * positionVC.y) / (positionVC.z * positionVC.z), 0.);
+}
+
+void main()
+{
+  //VTK::Color::Impl
+
+  //VTK::Normal::Impl
+
+  //VTK::TCoord::Impl
+
+  //VTK::Clip::Impl
+
+  vec4 posVC = MCVCMatrix * vertexMC;
+
+  mat3 J = getProjectionMatrix(posVC.xyz);
+
+  mat3 W = mat3(MCVCMatrix);
+
+  mat3 T = J * W;
+
+  //VTK::Covariance::Impl
+
+  // discard third column/row and store the matrix in a vector
+  mat2 cov2d = mat2(cov);
+
+  // It is possible to apply a convolution here by adding a custom 2D covariance matrix
+  // For example, it can be useful for making sure the point is at least 1 pixel wide
+  cov2d += mat2(lowpassMatrix.x, lowpassMatrix.y, lowpassMatrix.y, lowpassMatrix.z);
+
+  mat2 transform;
+
+  // eigen vectors of the covariance matrix
+  if (abs(cov2d[0][1]) > 1e-6)
+  {
+    // compute basis transformation based on eigen decomposition of the 2D covariance
+    // half of the covariance matrix trace
+    float halfTrace = 0.5 * (cov2d[0][0] + cov2d[1][1]);
+
+    // eigen values of the covariance matrix
+    float term = sqrt(halfTrace * halfTrace - determinant(cov2d));
+    float eigenValue1 = halfTrace + term;
+    float eigenValue2 = halfTrace - term;
+
+    vec2 eigenVector1 = normalize(vec2(eigenValue1 - cov2d[1][1], cov2d[0][1]));
+    vec2 eigenVector2 = vec2(eigenVector1.y, -eigenVector1.x);
+
+    transform = mat2(sqrt(eigenValue1) * eigenVector1, sqrt(eigenValue2) * eigenVector2);
+  }
+  else
+  {
+    // the covariance is a diagonal matrix, so axis aligned
+    // we just need to extract the scaling matrix (the square root of the covariance in this case)
+    transform = mat2(sqrt(cov2d[0][0]), 0.0, 0.0, sqrt(cov2d[1][1]));
+  }
+
+  // filter out points outside of the frustum inflated by 30% because the approximation may stretch them too much
+  // see https://github.com/graphdeco-inria/gaussian-splatting/issues/191
+  // this is a hand-tuned parameter but it should work on most cases (if not, it may be useful to expose the value as uniform)
+  vec4 posDC = VCDCMatrix * posVC;
+
+  if (posDC.x < -1.3*posDC.w || posDC.x > 1.3*posDC.w || posDC.y < -1.3*posDC.w || posDC.y > 1.3*posDC.w)
+  {
+    transform = mat2(0);
+  }
+
+  //VTK::PositionVC::Impl
+
+  //VTK::Picking::Impl
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/glsl/vtkF3DRandomFS.glsl f3d-3.4.1/vtkext/private/module/glsl/vtkF3DRandomFS.glsl
--- f3d-3.2.0+dfsg/vtkext/private/module/glsl/vtkF3DRandomFS.glsl	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/glsl/vtkF3DRandomFS.glsl	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,19 @@
+//VTK::Color::Dec
+
+// A single iteration of Bob Jenkins' One-At-A-Time hashing algorithm.
+uint hash( uint x ) {
+    x += ( x << 10u );
+    x ^= ( x >>  6u );
+    x += ( x <<  3u );
+    x ^= ( x >> 11u );
+    x += ( x << 15u );
+    return x;
+}
+
+uint hash( uvec3 v ) { return hash( v.x ^ hash(v.y ^ hash(v.z))); }
+
+float random_ign(vec2 coord, uint index)
+{
+    coord = floor(coord) + 5.588238 * vec2(index % 4096u);
+    return fract(fract(dot(coord, vec2(0.06711056, 0.00583715))) * 52.9829189);
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/CMakeLists.txt f3d-3.4.1/vtkext/private/module/Testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -19,6 +19,13 @@
        TestF3DEXRMemReader.cxx)
 endif()
 
+if(F3D_MODULE_WEBP)
+  list(APPEND test_sources
+       TestF3DWebPReader.cxx
+       TestF3DWebPReaderInvalid.cxx
+       TestF3DWebPMemReader.cxx)
+endif()
+
 vtk_add_test_cxx(vtkextPrivateTests tests
   NO_DATA NO_VALID NO_OUTPUT
   ${test_sources}
@@ -40,6 +47,12 @@
     PROPERTIES
     FAIL_REGULAR_EXPRESSION "")
 endif()
+
+if(F3D_MODULE_WEBP)
+  set_tests_properties(f3d::vtkextPrivateCxx-TestF3DWebPReaderInvalid f3d::vtkextPrivateCxx-TestF3DWebPMemReader
+    PROPERTIES
+    FAIL_REGULAR_EXPRESSION "")
+endif()
 
 if (F3D_MODULE_UI)
   target_compile_definitions(vtkextPrivateTests PRIVATE F3D_MODULE_UI)
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DBitonicSort.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DBitonicSort.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DBitonicSort.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DBitonicSort.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -54,40 +54,40 @@
   // check invalid workgroup size
   if (sorter->Initialize(-1, VTK_FLOAT, VTK_FLOAT))
   {
-    std::cerr << "The invalid workgroup size is not failing" << std::endl;
+    std::cerr << "The invalid workgroup size is not failing\n";
     return EXIT_FAILURE;
   }
 
   // check invalid types
   if (sorter->Initialize(128, VTK_CHAR, VTK_FLOAT))
   {
-    std::cerr << "The invalid key type is not failing" << std::endl;
+    std::cerr << "The invalid key type is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (sorter->Initialize(128, VTK_FLOAT, VTK_CHAR))
   {
-    std::cerr << "The invalid key type is not failing" << std::endl;
+    std::cerr << "The invalid key type is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (sorter->Run(
         vtkOpenGLRenderWindow::SafeDownCast(renWin), nbElements, bufferKeys, bufferValues))
   {
-    std::cerr << "Uninitialized run is not failing" << std::endl;
+    std::cerr << "Uninitialized run is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (!sorter->Initialize(128, VTK_DOUBLE, VTK_INT))
   {
-    std::cerr << "Valid Initialize call failed" << std::endl;
+    std::cerr << "Valid Initialize call failed\n";
     return EXIT_FAILURE;
   }
 
   if (!sorter->Run(
         vtkOpenGLRenderWindow::SafeDownCast(renWin), nbElements, bufferKeys, bufferValues))
   {
-    std::cerr << "Sorter Run call failed" << std::endl;
+    std::cerr << "Sorter Run call failed\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DCachedTexturesPrint.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DCachedTexturesPrint.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DCachedTexturesPrint.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DCachedTexturesPrint.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,13 +4,15 @@
 #include "vtkF3DCachedLUTTexture.h"
 #include "vtkF3DCachedSpecularTexture.h"
 
+#include <iostream>
+
 int TestF3DCachedTexturesPrint(int argc, char* argv[])
 {
   vtkNew<vtkF3DCachedLUTTexture> lut;
   vtkNew<vtkF3DCachedSpecularTexture> specular;
 
-  lut->Print(cout);
-  specular->Print(cout);
+  lut->Print(std::cout);
+  specular->Print(std::cout);
 
   return EXIT_SUCCESS;
 }
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DEXRMemReader.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DEXRMemReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DEXRMemReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DEXRMemReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,5 +1,7 @@
 #include <vtkImageData.h>
+#include <vtkMemoryResourceStream.h>
 #include <vtkNew.h>
+#include <vtkVersion.h>
 
 #include "vtkF3DEXRReader.h"
 
@@ -13,8 +15,14 @@
   reader->SetFileName("readFromMem.exr");
 
   // check failure
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  vtkNew<vtkMemoryResourceStream> nullStream;
+  nullStream->SetBuffer(nullptr, 0);
+  reader->SetStream(nullStream);
+#else
   reader->SetMemoryBuffer("foo");
   reader->SetMemoryBufferLength(0);
+#endif
   reader->Update();
 
   // read file to buffer
@@ -34,19 +42,25 @@
   }
 
   // set buffer and read
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  vtkNew<vtkMemoryResourceStream> stream;
+  stream->SetBuffer(buff.data(), buff.size());
+  reader->SetStream(stream);
+#else
   reader->SetMemoryBuffer(buff.data());
   reader->SetMemoryBufferLength(buff.size());
+#endif
   reader->Update();
 
-  reader->Print(cout);
+  reader->Print(std::cout);
 
   vtkImageData* img = reader->GetOutput();
 
-  int* dims = img->GetDimensions();
+  const int* dims = img->GetDimensions();
 
   if (dims[0] != 610 && dims[1] != 406)
   {
-    std::cerr << "Incorrect EXR image size." << dims[0] << ":" << dims[1] << std::endl;
+    std::cerr << "Incorrect EXR image size: " << dims[0] << "x" << dims[1] << "\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DEXRReader.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DEXRReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DEXRReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DEXRReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -13,15 +13,15 @@
   reader->SetFileName(filename.c_str());
   reader->Update();
 
-  reader->Print(cout);
+  reader->Print(std::cout);
 
   vtkImageData* img = reader->GetOutput();
 
-  int* dims = img->GetDimensions();
+  const int* dims = img->GetDimensions();
 
   if (dims[0] != 1024 && dims[1] != 512)
   {
-    std::cerr << "Incorrect EXR image size." << std::endl;
+    std::cerr << "Incorrect EXR image size.\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DFpsCounter.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DFpsCounter.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DFpsCounter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DFpsCounter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -41,21 +41,21 @@
   // make sure only the 100 last records are kept
   if (uiActor->GetNumberOfFrameTimes() > 100)
   {
-    std::cerr << "Number of frame times must be at most 100" << std::endl;
+    std::cerr << "Number of frame times must be at most 100\n";
     return EXIT_FAILURE;
   }
 
   // make sure only the total time kept doesn't exceed 1 second
   if (uiActor->GetTotalFrameTimes() > 1.0)
   {
-    std::cerr << "Number of total frame times must be at most 1.0" << std::endl;
+    std::cerr << "Number of total frame times must be at most 1.0\n";
     return EXIT_FAILURE;
   }
 
   // make sure the FPS value is exactly 100
   if (uiActor->GetFpsValue() != 100)
   {
-    std::cerr << "Number of FPS value must be exactly 100" << std::endl;
+    std::cerr << "Number of FPS value must be exactly 100\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DGenericImporter.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DGenericImporter.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DGenericImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DGenericImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,9 +1,19 @@
+#include <vtkConeSource.h>
+#include <vtkDataAssembly.h>
 #include <vtkDoubleArray.h>
 #include <vtkGLTFReader.h>
+#include <vtkInformation.h>
+#include <vtkMultiBlockDataSet.h>
+#include <vtkMultiPieceDataSet.h>
 #include <vtkNew.h>
-#include <vtkPLYReader.h>
-#include <vtkTestUtilities.h>
+#include <vtkPartitionedDataSet.h>
+#include <vtkPartitionedDataSetCollection.h>
+#include <vtkPolyData.h>
+#include <vtkSphereSource.h>
+#include <vtkTable.h>
+#include <vtkTrivialProducer.h>
 #include <vtkVersion.h>
+#include <vtkXMLMultiBlockDataReader.h>
 
 #include "vtkF3DGenericImporter.h"
 
@@ -11,79 +21,426 @@
 
 int TestF3DGenericImporter(int argc, char* argv[])
 {
-  vtkNew<vtkF3DGenericImporter> importer;
-  if (importer->GetAnimationSupportLevel() != vtkF3DImporter::AnimationSupportLevel::UNIQUE)
+  // Create reusable test data
+  vtkNew<vtkSphereSource> sphere;
+  sphere->Update();
+  vtkNew<vtkConeSource> cone;
+  cone->Update();
+
+  // Test basic importer state
   {
-    std::cerr << "Unexpected animation support level" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkF3DGenericImporter> importer;
+    if (importer->GetAnimationSupportLevel() != vtkF3DImporter::AnimationSupportLevel::UNIQUE)
+    {
+      std::cerr << "Unexpected animation support level\n";
+      return EXIT_FAILURE;
+    }
+    if (importer->GetNumberOfAnimations() != 0)
+    {
+      std::cerr << "Unexpected number of animations\n";
+      return EXIT_FAILURE;
+    }
+
+    int nbTimeSteps;
+    double timeRange[2];
+    vtkNew<vtkDoubleArray> timeSteps;
+    if (importer->GetTemporalInformation(0, timeRange, nbTimeSteps, timeSteps))
+    {
+      std::cerr << "Unexpected return value with GetTemporalInformation\n";
+      return EXIT_FAILURE;
+    }
   }
-  if (importer->GetNumberOfAnimations() != 0)
+
+  // Test animation with GLTF reader
   {
-    std::cerr << "Unexpected number of animations" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkGLTFReader> reader;
+    std::string filename = std::string(argv[1]) + "data/BoxAnimated.gltf";
+    reader->SetFileName(filename.c_str());
+    reader->UpdateInformation();
+    reader->EnableAnimation(0);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(reader);
+    importer->Update();
+    importer->Print(std::cout);
+
+    if (importer->GetNumberOfAnimations() != 1)
+    {
+      std::cerr << "Unexpected number of animations\n";
+      return EXIT_FAILURE;
+    }
+
+    importer->EnableAnimation(0);
+    if (!importer->IsAnimationEnabled(0))
+    {
+      std::cerr << "Unexpected not enabled animation\n";
+      return EXIT_FAILURE;
+    }
+
+    importer->DisableAnimation(0);
+    if (importer->IsAnimationEnabled(0))
+    {
+      std::cerr << "Unexpected enabled animation\n";
+      return EXIT_FAILURE;
+    }
   }
 
-  int nbTimeSteps;
-  double timeRange[2];
-  vtkNew<vtkDoubleArray> timeSteps;
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240910)
+  // Test UpdateAtTimeValue with invalid animation
+  {
+    vtkNew<vtkGLTFReader> reader;
+    std::string filename = std::string(argv[1]) + "data/BoxAnimated_invalid_animation.gltf";
+    reader->SetFileName(filename.c_str());
+    reader->UpdateInformation();
+    reader->EnableAnimation(0);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(reader);
+    importer->Update();
+    if (!importer->UpdateAtTimeValue(0.1))
+    {
+      std::cerr << "Unexpected UpdateAtTimeValue failure\n";
+      return EXIT_FAILURE;
+    }
+  }
+#endif
 
-  if (importer->GetTemporalInformation(0, 60, nbTimeSteps, timeRange, timeSteps))
+  // Test UpdateAtTimeValue failure path
   {
-    std::cerr << "Unexpected return value with GetTemporalInformation" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkGLTFReader> reader;
+    std::string filename = std::string(argv[1]) + "data/BoxAnimated.gltf";
+    reader->SetFileName(filename.c_str());
+    reader->UpdateInformation();
+    reader->EnableAnimation(0);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(reader);
+    importer->Update();
+    importer->EnableAnimation(0);
+
+    reader->SetFileName("/nonexistent/path/file.gltf");
+    if (importer->UpdateAtTimeValue(0.5))
+    {
+      std::cerr << "UpdateAtTimeValue should fail with invalid reader\n";
+      return EXIT_FAILURE;
+    }
   }
 
-  // Test with reader
-  vtkNew<vtkGLTFReader> reader;
+  // Test GetDataObjectDescription
+  {
+    if (vtkF3DGenericImporter::GetDataObjectDescription(nullptr) != "")
+    {
+      std::cerr << "GetDataObjectDescription(nullptr) should return empty\n";
+      return EXIT_FAILURE;
+    }
+
+    vtkNew<vtkPartitionedDataSet> pds;
+    pds->SetNumberOfPartitions(1);
+    pds->SetPartition(0, sphere->GetOutput());
+
+    vtkNew<vtkPartitionedDataSetCollection> pdc;
+    pdc->SetNumberOfPartitionedDataSets(1);
+    pdc->SetPartitionedDataSet(0, pds);
+
+    if (vtkF3DGenericImporter::GetDataObjectDescription(pdc).empty())
+    {
+      std::cerr << "GetDataObjectDescription(pdc) should not be empty\n";
+      return EXIT_FAILURE;
+    }
+  }
 
-  std::string filename = std::string(argv[1]) + "data/BoxAnimated.gltf";
-  reader->SetFileName(filename.c_str());
-  reader->UpdateInformation();
-  reader->EnableAnimation(0);
+  // Test failure path with unsupported data type (vtkTable)
+  {
+    vtkNew<vtkTable> table;
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(table);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetNumberOfBlocks() != 0)
+    {
+      std::cerr << "Importer with vtkTable should have 0 blocks\n";
+      return EXIT_FAILURE;
+    }
+  }
 
-  importer->SetInternalReader(reader);
-  importer->Update();
-  importer->Print(cout);
-  if (importer->GetNumberOfAnimations() != 1)
+  // Test MultiBlock from file (covers null blocks, nested structures)
   {
-    std::cerr << "Unexpected number of animations" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkXMLMultiBlockDataReader> reader;
+    std::string filename = std::string(argv[1]) + "data/mb.vtm";
+    reader->SetFileName(filename.c_str());
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(reader);
+    importer->Update();
+
+    // mb.vtm contains 3 dataset blocks, vtkTable is skipped
+    if (importer->GetNumberOfBlocks() != 3)
+    {
+      std::cerr << "MB file: Expected 3 blocks, got " << importer->GetNumberOfBlocks() << "\n";
+      return EXIT_FAILURE;
+    }
+
+    // Test out-of-bounds access
+    if (importer->GetImportedPoints(-1) != nullptr || importer->GetImportedPoints(3) != nullptr ||
+      importer->GetImportedImage(-1) != nullptr || importer->GetImportedImage(3) != nullptr ||
+      !importer->GetBlockName(-1).empty() || !importer->GetBlockName(3).empty())
+    {
+      std::cerr << "Out-of-bounds access should return nullptr/empty\n";
+      return EXIT_FAILURE;
+    }
   }
 
-  importer->EnableAnimation(0);
-  if (!importer->IsAnimationEnabled(0))
+  // Test MultiBlock with metadata names
   {
-    std::cerr << "Unexpected not enabled animation" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkMultiBlockDataSet> mb;
+    mb->SetNumberOfBlocks(2);
+    mb->SetBlock(0, sphere->GetOutput());
+    mb->SetBlock(1, cone->GetOutput());
+    mb->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "MySphere");
+    mb->GetMetaData(1u)->Set(vtkCompositeDataSet::NAME(), "MyCone");
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(mb);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetBlockName(0) != "MySphere" || importer->GetBlockName(1) != "MyCone")
+    {
+      std::cerr << "MB metadata names not preserved\n";
+      return EXIT_FAILURE;
+    }
   }
 
-  importer->DisableAnimation(0);
-  if (importer->IsAnimationEnabled(0))
+  // Test MultiBlock without metadata (Block_N fallback)
   {
-    std::cerr << "Unexpected enabled animation" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkMultiBlockDataSet> mb;
+    mb->SetNumberOfBlocks(2);
+    mb->SetBlock(0, sphere->GetOutput());
+    mb->SetBlock(1, cone->GetOutput());
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(mb);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetBlockName(0) != "Block_0" || importer->GetBlockName(1) != "Block_1")
+    {
+      std::cerr << "MB without metadata: Expected 'Block_N', got '" << importer->GetBlockName(0)
+                << "', '" << importer->GetBlockName(1) << "'\n";
+      return EXIT_FAILURE;
+    }
   }
 
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240910)
-  filename = std::string(argv[1]) + "data/BoxAnimated_invalid_animation.gltf";
-  reader->SetFileName(filename.c_str());
-  reader->UpdateInformation();
-  reader->EnableAnimation(0);
-
-  importer->SetInternalReader(reader);
-  importer->Update();
-  if (!importer->UpdateAtTimeValue(0.1))
+  // Test nested MultiBlock with hierarchical names
   {
-    std::cerr << "Unexpected UpdateAtTimeValue failure" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkMultiBlockDataSet> innerMB;
+    innerMB->SetNumberOfBlocks(2);
+    innerMB->SetBlock(0, sphere->GetOutput());
+    innerMB->SetBlock(1, cone->GetOutput());
+    innerMB->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "InnerSphere");
+    innerMB->GetMetaData(1u)->Set(vtkCompositeDataSet::NAME(), "InnerCone");
+
+    vtkNew<vtkMultiBlockDataSet> outerMB;
+    outerMB->SetNumberOfBlocks(1);
+    outerMB->SetBlock(0, innerMB);
+    outerMB->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "OuterBlock");
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(outerMB);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetBlockName(0) != "OuterBlock/InnerSphere" ||
+      importer->GetBlockName(1) != "OuterBlock/InnerCone")
+    {
+      std::cerr << "Nested MB: Expected hierarchical names\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test MultiPieceDataSet nested inside MultiBlock (generic composite fallback)
+  {
+    vtkNew<vtkMultiPieceDataSet> mp;
+    mp->SetNumberOfPieces(2);
+    mp->SetPiece(0, sphere->GetOutput());
+    mp->SetPiece(1, cone->GetOutput());
+    mp->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "SpherePiece");
+    mp->GetMetaData(1u)->Set(vtkCompositeDataSet::NAME(), "ConePiece");
+
+    vtkNew<vtkMultiBlockDataSet> mb;
+    mb->SetNumberOfBlocks(1);
+    mb->SetBlock(0, mp);
+    mb->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "NestedMP");
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(mb);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetNumberOfBlocks() != 2)
+    {
+      std::cerr << "Nested MP: Expected 2 blocks\n";
+      return EXIT_FAILURE;
+    }
+
+    std::string name0 = importer->GetBlockName(0);
+    std::string name1 = importer->GetBlockName(1);
+    if (name0.find("NestedMP") == std::string::npos ||
+      name0.find("SpherePiece") == std::string::npos ||
+      name1.find("NestedMP") == std::string::npos || name1.find("ConePiece") == std::string::npos)
+    {
+      std::cerr << "Nested MP: Expected hierarchical names with piece metadata\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test PartitionedDataSet with metadata names
+  {
+    vtkNew<vtkPartitionedDataSet> pds;
+    pds->SetNumberOfPartitions(2);
+    pds->SetPartition(0, sphere->GetOutput());
+    pds->SetPartition(1, cone->GetOutput());
+    pds->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "SpherePartition");
+    pds->GetMetaData(1u)->Set(vtkCompositeDataSet::NAME(), "ConePartition");
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(pds);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    std::string name0 = importer->GetBlockName(0);
+    std::string name1 = importer->GetBlockName(1);
+    if (name0.find("SpherePartition") == std::string::npos ||
+      name1.find("ConePartition") == std::string::npos)
+    {
+      std::cerr << "PDS: Expected partition metadata names\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test PartitionedDataSet without metadata (Partition_N fallback)
+  {
+    vtkNew<vtkPartitionedDataSet> pds;
+    pds->SetNumberOfPartitions(2);
+    pds->SetPartition(0, sphere->GetOutput());
+    pds->SetPartition(1, cone->GetOutput());
+    // No metadata
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(pds);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    std::string name0 = importer->GetBlockName(0);
+    std::string name1 = importer->GetBlockName(1);
+    if (name0.find("Partition_0") == std::string::npos ||
+      name1.find("Partition_1") == std::string::npos)
+    {
+      std::cerr << "PDS without metadata: Expected 'Partition_N', got '" << name0 << "', '" << name1
+                << "'\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test PartitionedDataSet with null partition
+  {
+    vtkNew<vtkPartitionedDataSet> pds;
+    pds->SetNumberOfPartitions(3);
+    pds->SetPartition(0, sphere->GetOutput());
+    pds->SetPartition(2, cone->GetOutput());
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(pds);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetNumberOfBlocks() != 2)
+    {
+      std::cerr << "PDS with null: Expected 2 blocks, got " << importer->GetNumberOfBlocks()
+                << "\n";
+      return EXIT_FAILURE;
+    }
+  }
+
+  // Test PartitionedDataSetCollection with assembly
+  {
+    vtkNew<vtkPartitionedDataSet> pds0;
+    pds0->SetNumberOfPartitions(1);
+    pds0->SetPartition(0, sphere->GetOutput());
+
+    vtkNew<vtkPartitionedDataSet> pds1;
+    pds1->SetNumberOfPartitions(1);
+    pds1->SetPartition(0, cone->GetOutput());
+
+    vtkNew<vtkPartitionedDataSetCollection> pdc;
+    pdc->SetNumberOfPartitionedDataSets(2);
+    pdc->SetPartitionedDataSet(0, pds0);
+    pdc->SetPartitionedDataSet(1, pds1);
+
+    vtkNew<vtkDataAssembly> assembly;
+    assembly->Initialize();
+    int sphereNode = assembly->AddNode("SphereFromAssembly", assembly->GetRootNode());
+    int coneNode = assembly->AddNode("ConeFromAssembly", assembly->GetRootNode());
+    assembly->AddDataSetIndex(sphereNode, 0);
+    assembly->AddDataSetIndex(coneNode, 1);
+    pdc->SetDataAssembly(assembly);
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(pdc);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetBlockName(0) != "SphereFromAssembly" ||
+      importer->GetBlockName(1) != "ConeFromAssembly")
+    {
+      std::cerr << "PDC with assembly: Expected assembly names\n";
+      return EXIT_FAILURE;
+    }
   }
-#endif
 
-  // Static method testing
-  if (vtkF3DGenericImporter::GetDataObjectDescription(nullptr) != "")
+  // Test PartitionedDataSetCollection with metadata (no assembly)
   {
-    std::cerr << "Unexpected data object description with null input" << std::endl;
-    return EXIT_FAILURE;
+    vtkNew<vtkPartitionedDataSet> pds0;
+    pds0->SetNumberOfPartitions(1);
+    pds0->SetPartition(0, sphere->GetOutput());
+
+    vtkNew<vtkPartitionedDataSetCollection> pdc;
+    pdc->SetNumberOfPartitionedDataSets(1);
+    pdc->SetPartitionedDataSet(0, pds0);
+    pdc->GetMetaData(0u)->Set(vtkCompositeDataSet::NAME(), "MetadataName");
+
+    vtkNew<vtkTrivialProducer> producer;
+    producer->SetOutput(pdc);
+
+    vtkNew<vtkF3DGenericImporter> importer;
+    importer->SetInternalReader(producer);
+    importer->Update();
+
+    if (importer->GetBlockName(0) != "MetadataName")
+    {
+      std::cerr << "PDC with metadata: Expected 'MetadataName', got '" << importer->GetBlockName(0)
+                << "'\n";
+      return EXIT_FAILURE;
+    }
   }
 
   return EXIT_SUCCESS;
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DMetaImporterAnimation.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DMetaImporterAnimation.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DMetaImporterAnimation.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DMetaImporterAnimation.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -15,20 +15,20 @@
   // Check importer error code path
   if (!importer->GetAnimationName(0).empty())
   {
-    std::cerr << "Unexpected animation name that should be empty" << std::endl;
+    std::cerr << "Unexpected animation name that should be empty\n";
     return EXIT_FAILURE;
   }
   if (importer->IsAnimationEnabled(0))
   {
-    std::cerr << "Unexpected enabled animation that should not be" << std::endl;
+    std::cerr << "Unexpected enabled animation that should not be\n";
     return EXIT_FAILURE;
   }
 
   int nbTimeSteps;
   double timeRange[2];
-  if (importer->GetTemporalInformation(0, 60, nbTimeSteps, timeRange, nullptr))
+  if (importer->GetTemporalInformation(0, timeRange, nbTimeSteps, nullptr))
   {
-    std::cerr << "Unexpected enabled animation that should not be" << std::endl;
+    std::cerr << "Unexpected enabled animation that should not be\n";
     return EXIT_FAILURE;
   }
 
@@ -48,29 +48,27 @@
 
   if (importer->GetNumberOfAnimations() != 0)
   {
-    std::cerr << "Unexpected number of animations" << std::endl;
+    std::cerr << "Unexpected number of animations\n";
     return EXIT_FAILURE;
   }
 
   if (importer->GetAnimationName(0) != "")
   {
-    std::cerr << "Unexpected non-empty animation name" << std::endl;
+    std::cerr << "Unexpected non-empty animation name\n";
     return EXIT_FAILURE;
   }
 
   importer->EnableAnimation(0);
   if (importer->IsAnimationEnabled(0))
   {
-    std::cerr << "Unexpected enable animation name after enabling without animation available"
-              << std::endl;
+    std::cerr << "Unexpected enable animation name after enabling without animation available\n";
     return EXIT_FAILURE;
   }
 
   importer->DisableAnimation(0);
   if (importer->IsAnimationEnabled(0))
   {
-    std::cerr << "Unexpected enable animation name after disabling without animation available"
-              << std::endl;
+    std::cerr << "Unexpected enable animation name after disabling without animation available\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DMetaImporterMultiColoring.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DMetaImporterMultiColoring.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DMetaImporterMultiColoring.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DMetaImporterMultiColoring.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -17,7 +17,7 @@
   // Check importer error code path
   if (!importer->GetCameraName(0).empty())
   {
-    std::cerr << "Unexpected camera name that should be empty" << std::endl;
+    std::cerr << "Unexpected camera name that should be empty\n";
     return EXIT_FAILURE;
   }
 
@@ -50,44 +50,44 @@
   auto info = coloringHandler.SetCurrentColoring(true, false, "Momentum", false);
   if (!info.has_value())
   {
-    std::cerr << "Coloring handler unable to set coloring as expected" << std::endl;
+    std::cerr << "Coloring handler unable to set coloring as expected\n";
     return EXIT_FAILURE;
   }
   if (info.value().Name != "Momentum")
   {
-    std::cerr << "Unexpected coloring name: " << info.value().Name << std::endl;
+    std::cerr << "Unexpected coloring name: " << info.value().Name << "\n";
     return EXIT_FAILURE;
   }
   if (info.value().MaximumNumberOfComponents != 3)
   {
-    std::cerr << "Unexpected coloring nComp" << std::endl;
+    std::cerr << "Unexpected coloring nComp\n";
     return EXIT_FAILURE;
   }
   if (info.value().ComponentNames[0] != "LX Momentum")
   {
-    std::cerr << "Unexpected coloring component name 0" << std::endl;
+    std::cerr << "Unexpected coloring component name 0\n";
     return EXIT_FAILURE;
   }
   if (info.value().ComponentNames[1] != "LY Momentum_t")
   {
-    std::cerr << "Unexpected coloring component name 1" << std::endl;
+    std::cerr << "Unexpected coloring component name 1\n";
     return EXIT_FAILURE;
   }
   if (info.value().ComponentNames[2] != "")
   {
-    std::cerr << "Unexpected coloring component name 2" << std::endl;
+    std::cerr << "Unexpected coloring component name 2\n";
     return EXIT_FAILURE;
   }
   if (!vtkMathUtilities::FuzzyCompare(info.value().ComponentRanges[0][0], -5.49586, 1e-5) ||
     !vtkMathUtilities::FuzzyCompare(info.value().ComponentRanges[0][1], 5.79029, 1e-5))
   {
-    std::cerr << "Unexpected coloring component range" << std::endl;
+    std::cerr << "Unexpected coloring component range\n";
     return EXIT_FAILURE;
   }
   if (!vtkMathUtilities::FuzzyCompare(info.value().MagnitudeRange[0], 0., 1e-5) ||
     !vtkMathUtilities::FuzzyCompare(info.value().MagnitudeRange[1], 6.25568, 1e-5))
   {
-    std::cerr << "Unexpected coloring magnitude range" << std::endl;
+    std::cerr << "Unexpected coloring magnitude range\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DObjectFactory.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DObjectFactory.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DObjectFactory.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DObjectFactory.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -16,6 +16,8 @@
 #include "vtkF3DPointSplatMapper.h"
 #endif
 
+#include <iostream>
+
 int TestF3DObjectFactory(int argc, char* argv[])
 {
   vtkNew<vtkF3DObjectFactory> factory;
@@ -29,45 +31,44 @@
   // Check factory utility methods
   if (strcmp(factory->GetVTKSourceVersion(), VTK_SOURCE_VERSION) != 0)
   {
-    std::cerr << "vtkF3DObjectFactory failed to provide correct VTK_SOURCE_VERSION" << std::endl;
+    std::cerr << "vtkF3DObjectFactory failed to provide correct VTK_SOURCE_VERSION\n";
     return EXIT_FAILURE;
   }
   if (strcmp(factory->GetDescription(), "F3D factory overrides.") != 0)
   {
-    std::cerr << "vtkF3DObjectFactory failed to provide expected description" << std::endl;
+    std::cerr << "vtkF3DObjectFactory failed to provide expected description\n";
     return EXIT_FAILURE;
   }
 
   // Check actual factory mechanism
   vtkNew<vtkPolyDataMapper> mapper;
-  vtkF3DPolyDataMapper* mapperPtr = vtkF3DPolyDataMapper::SafeDownCast(mapper);
+  const vtkF3DPolyDataMapper* mapperPtr = vtkF3DPolyDataMapper::SafeDownCast(mapper);
   if (mapperPtr == nullptr)
   {
-    std::cerr << "vtkF3DObjectFactory failed to create a vtkF3DPolyDataMapper" << std::endl;
+    std::cerr << "vtkF3DObjectFactory failed to create a vtkF3DPolyDataMapper\n";
     return EXIT_FAILURE;
   }
 
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
   vtkNew<vtkPointGaussianMapper> pointMapper;
-  pointMapper->Print(cout);
+  pointMapper->Print(std::cout);
   vtkF3DPointSplatMapper* pointMapperPtr = vtkF3DPointSplatMapper::SafeDownCast(pointMapper);
   if (pointMapperPtr == nullptr)
   {
-    std::cerr << "vtkF3DObjectFactory failed to create a vtkF3DPointSplatMapper" << std::endl;
+    std::cerr << "vtkF3DObjectFactory failed to create a vtkF3DPointSplatMapper\n";
     return EXIT_FAILURE;
   }
 #endif
 
   vtkNew<vtkOutputWindow> window;
 #if F3D_MODULE_UI
-  vtkF3DImguiConsole* windowPtr = vtkF3DImguiConsole::SafeDownCast(window);
+  const vtkF3DImguiConsole* windowPtr = vtkF3DImguiConsole::SafeDownCast(window);
 #else
   vtkF3DConsoleOutputWindow* windowPtr = vtkF3DConsoleOutputWindow::SafeDownCast(window);
 #endif
   if (windowPtr == nullptr)
   {
-    std::cerr << "vtkF3DObjectFactory failed to create the correct type of vtkOutputWindow"
-              << std::endl;
+    std::cerr << "vtkF3DObjectFactory failed to create the correct type of vtkOutputWindow\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DOpenGLGridMapper.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -31,9 +31,9 @@
   const std::array<double, 6> expected = { x0, x1, y0, y1, z0, z1 };
   if (actual != expected)
   {
-    std::cerr << "wrong bounds for " << name << " failed:" << std::endl;
-    std::cerr << "  got " << actual << std::endl;
-    std::cerr << "  expected " << expected << std::endl;
+    std::cerr << "wrong bounds for " << name << " failed:\n";
+    std::cerr << "  got " << actual << "\n";
+    std::cerr << "  expected " << expected << "\n";
     return false;
   }
   return true;
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DRendererWithColoring.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DRendererWithColoring.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DRendererWithColoring.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DRendererWithColoring.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -6,6 +6,8 @@
 #include "vtkF3DGenericImporter.h"
 #include "vtkF3DRenderer.h"
 
+#include <iostream>
+
 int TestF3DRendererWithColoring(int argc, char* argv[])
 {
   vtkNew<vtkF3DRenderer> renderer;
@@ -40,7 +42,7 @@
 
   if (renderer->GetArrayNameForColoring() != "Invalid" || renderer->GetComponentForColoring() != -1)
   {
-    std::cerr << "Unexpected coloring information with invalid array" << std::endl;
+    std::cerr << "Unexpected coloring information with invalid array\n";
     return EXIT_FAILURE;
   }
 
@@ -53,14 +55,14 @@
 
   if (renderer->GetArrayNameForColoring() != "Momentum" || renderer->GetComponentForColoring() != 5)
   {
-    std::cerr << "Unexpected coloring information with invalid component" << std::endl;
+    std::cerr << "Unexpected coloring information with invalid component\n";
     return EXIT_FAILURE;
   }
 
   renderer->CycleComponentForColoring();
   if (renderer->GetArrayNameForColoring() != "Momentum" || renderer->GetComponentForColoring() != 1)
   {
-    std::cerr << "Unexpected coloring information after cycling component" << std::endl;
+    std::cerr << "Unexpected coloring information after cycling component\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DRenderPass.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DRenderPass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DRenderPass.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DRenderPass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,6 +5,7 @@
 
 #include "vtkF3DHexagonalBokehBlurPass.h"
 #include "vtkF3DRenderPass.h"
+#include "vtkF3DTAAResolvePass.h"
 #include "vtkF3DUserRenderPass.h"
 
 #include <iostream>
@@ -14,8 +15,12 @@
   vtkNew<vtkF3DRenderPass> pass;
   pass->Print(std::cout);
 
+  vtkNew<vtkF3DTAAResolvePass> taaP;
+  taaP->SetDelegatePass(pass);
+  taaP->Print(std::cout);
+
   vtkNew<vtkF3DHexagonalBokehBlurPass> bokeh;
-  bokeh->SetDelegatePass(pass);
+  bokeh->SetDelegatePass(taaP);
   bokeh->Print(std::cout);
 
   bokeh->SetCircleOfConfusionRadius(30.f);
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPMemReader.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPMemReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPMemReader.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPMemReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,70 @@
+#include <vtkImageData.h>
+#include <vtkMemoryResourceStream.h>
+#include <vtkNew.h>
+#include <vtkVersion.h>
+
+#include "vtkF3DWebPReader.h"
+
+#include <fstream>
+#include <iostream>
+#include <vector>
+
+int TestF3DWebPMemReader(int argc, char* argv[])
+{
+  vtkNew<vtkF3DWebPReader> reader;
+  reader->SetFileName("image.webp");
+
+  // check failure
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  vtkNew<vtkMemoryResourceStream> nullStream;
+  nullStream->SetBuffer(nullptr, 0);
+  reader->SetStream(nullStream);
+#else
+  reader->SetMemoryBuffer("foo");
+  reader->SetMemoryBufferLength(0);
+#endif
+  reader->Update();
+
+  // read file to buffer
+  std::ifstream file(std::string(argv[1]) + "data/image.webp", std::ios::binary | std::ios::ate);
+  if (!file)
+  {
+    std::cerr << "Cannot open the file\n";
+    return EXIT_FAILURE;
+  }
+
+  std::streamsize size = file.tellg();
+  file.seekg(0, std::ios::beg);
+
+  std::vector<char> buff(size);
+  if (!file.read(buff.data(), size))
+  {
+    std::cerr << "Cannot read the file\n";
+    return EXIT_FAILURE;
+  }
+
+  // set buffer and read
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  vtkNew<vtkMemoryResourceStream> stream;
+  stream->SetBuffer(buff.data(), buff.size());
+  reader->SetStream(stream);
+#else
+  reader->SetMemoryBuffer(buff.data());
+  reader->SetMemoryBufferLength(buff.size());
+#endif
+  reader->Update();
+
+  reader->Print(std::cout);
+
+  vtkImageData* img = reader->GetOutput();
+
+  const int* dims = img->GetDimensions();
+
+  if (dims[0] != 1024 && dims[1] != 1024)
+  {
+    std::cerr << "Incorrect WebP image size." << dims[0] << ":" << dims[1] << "\n";
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPReader.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPReader.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,29 @@
+#include <vtkImageData.h>
+#include <vtkNew.h>
+
+#include "vtkF3DWebPReader.h"
+
+#include <iostream>
+
+int TestF3DWebPReader(int argc, char* argv[])
+{
+  vtkNew<vtkF3DWebPReader> reader;
+
+  std::string filename = std::string(argv[1]) + "data/image.webp";
+  reader->SetFileName(filename.c_str());
+  reader->Update();
+
+  std::cout << "Reader Name: " << reader->GetDescriptiveName() << '\n';
+
+  vtkImageData* img = reader->GetOutput();
+
+  const int* dims = img->GetDimensions();
+
+  if (dims[0] != 1024 && dims[1] != 1024)
+  {
+    std::cerr << "Incorrect WebP image size.\n";
+    return EXIT_FAILURE;
+  }
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPReaderInvalid.cxx f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPReaderInvalid.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/Testing/TestF3DWebPReaderInvalid.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/Testing/TestF3DWebPReaderInvalid.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,27 @@
+#include <vtkNew.h>
+
+#include "vtkF3DWebPReader.h"
+
+#include <iostream>
+
+int TestF3DWebPReaderInvalid(int argc, char* argv[])
+{
+  vtkNew<vtkF3DWebPReader> reader;
+  reader->Update();
+
+  // Do not create a dummy.webp
+  std::string filename = std::string(argv[1]) + "data/dummy.webp";
+  reader->CanReadFile(filename.c_str());
+
+  filename = std::string(argv[1]) + "data/invalid.webp";
+  reader->SetFileName(filename.c_str());
+  reader->CanReadFile(filename.c_str());
+  reader->Update();
+
+  filename = std::string(argv[1]) + "data/invalid2.webp";
+  reader->SetFileName(filename.c_str());
+  reader->CanReadFile(filename.c_str());
+  reader->Update();
+
+  return EXIT_SUCCESS;
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DCachedLUTTexture.cxx f3d-3.4.1/vtkext/private/module/vtkF3DCachedLUTTexture.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DCachedLUTTexture.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DCachedLUTTexture.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -20,7 +20,7 @@
 void vtkF3DCachedLUTTexture::PrintSelf(ostream& os, vtkIndent indent)
 {
   this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << endl;
+  os << indent << "FileName: " << this->FileName << "\n";
 }
 
 //------------------------------------------------------------------------------
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DCachedSpecularTexture.cxx f3d-3.4.1/vtkext/private/module/vtkF3DCachedSpecularTexture.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DCachedSpecularTexture.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DCachedSpecularTexture.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -21,7 +21,7 @@
 void vtkF3DCachedSpecularTexture::PrintSelf(ostream& os, vtkIndent indent)
 {
   this->Superclass::PrintSelf(os, indent);
-  os << indent << "FileName: " << this->FileName << endl;
+  os << indent << "FileName: " << this->FileName << "\n";
 }
 
 //------------------------------------------------------------------------------
@@ -72,7 +72,7 @@
       data[i] = firstImg->GetScalarPointer(0, 0, i);
     }
 
-    int* firstDims = firstImg->GetDimensions();
+    const int* firstDims = firstImg->GetDimensions();
     if (firstDims[0] != firstDims[1])
     {
       vtkWarningMacro("Specular cache has unexpected dimensions");
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DConsoleOutputWindow.cxx f3d-3.4.1/vtkext/private/module/vtkF3DConsoleOutputWindow.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DConsoleOutputWindow.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DConsoleOutputWindow.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,44 +1,45 @@
 #include "vtkF3DConsoleOutputWindow.h"
 
 #include <vtkObjectFactory.h>
-#include <vtkWindows.h>
+
+#include <iostream>
 
 vtkStandardNewMacro(vtkF3DConsoleOutputWindow);
 
 //----------------------------------------------------------------------------
-#ifdef WIN32
-vtkF3DConsoleOutputWindow::vtkF3DConsoleOutputWindow()
-{
-  // enable formatting on windows terminal
-  HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
-  DWORD dwMode;
-  GetConsoleMode(hOut, &dwMode);
-  SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
-}
-#else
 vtkF3DConsoleOutputWindow::vtkF3DConsoleOutputWindow() = default;
-#endif
 
 //----------------------------------------------------------------------------
 void vtkF3DConsoleOutputWindow::DisplayText(const char* txt)
 {
   std::string fmtText;
-  switch (this->GetCurrentMessageType())
+
+  if (this->UseColoring)
   {
-    case vtkOutputWindow::MESSAGE_TYPE_ERROR:
-      fmtText = this->UseColoring ? "\033[31;1m" : "";
-      break;
-    case vtkOutputWindow::MESSAGE_TYPE_WARNING:
-    case vtkOutputWindow::MESSAGE_TYPE_GENERIC_WARNING:
-      fmtText = this->UseColoring ? "\033[33m" : "";
-      break;
-    default:
-      break;
+    switch (this->GetCurrentMessageType())
+    {
+      case vtkOutputWindow::MESSAGE_TYPE_ERROR:
+        fmtText = "\033[31;1m";
+        fmtText += txt;
+        fmtText += "\033[0m";
+        break;
+      case vtkOutputWindow::MESSAGE_TYPE_WARNING:
+      case vtkOutputWindow::MESSAGE_TYPE_GENERIC_WARNING:
+        fmtText = "\033[33m";
+        fmtText += txt;
+        fmtText += "\033[0m";
+        break;
+      default:
+        fmtText = txt;
+        break;
+    }
+  }
+  else
+  {
+    fmtText = txt;
   }
-  fmtText += txt;
-
-  fmtText += this->UseColoring ? "\033[0m\n" : "\n";
 
+  fmtText += "\n";
   this->Superclass::DisplayText(fmtText.c_str());
 
   switch (this->GetDisplayStream(this->GetCurrentMessageType()))
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DEXRReader.cxx f3d-3.4.1/vtkext/private/module/vtkF3DEXRReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DEXRReader.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DEXRReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,9 +2,11 @@
 
 #include "vtkFloatArray.h"
 #include "vtkImageData.h"
+#include "vtkMemoryResourceStream.h"
 #include "vtkNew.h"
 #include "vtkObjectFactory.h"
 #include "vtkPointData.h"
+#include "vtkVersion.h"
 #include "vtksys/FStream.hxx"
 
 #include <ImfArray.h>
@@ -85,10 +87,16 @@
 
   // Setup filename to read the header
   this->ComputeInternalFileName(this->DataExtent[4]);
-  if ((this->InternalFileName == nullptr || this->InternalFileName[0] == '\0') &&
-    !this->MemoryBuffer)
+  if ((this->InternalFileName == nullptr || this->InternalFileName[0] == '\0'))
   {
-    return;
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+    if (!this->GetStream())
+#else
+    if (!this->GetMemoryBuffer())
+#endif
+    {
+      return;
+    }
   }
 
   auto checkChannels = [&](Imf::RgbaInputFile& file)
@@ -108,12 +116,26 @@
 
   try
   {
-    if (this->MemoryBuffer)
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+    if (this->GetStream())
     {
-      MemStream memoryStream("EXRmemoryStream", this->MemoryBuffer, this->MemoryBufferLength);
+      // F3D only uses stream from memory
+      vtkMemoryResourceStream* stream = vtkMemoryResourceStream::SafeDownCast(this->GetStream());
+      assert(stream);
+
+      MemStream memoryStream("EXRmemoryStream", stream->GetBuffer(), stream->GetSize());
       Imf::RgbaInputFile file = Imf::RgbaInputFile(memoryStream);
       checkChannels(file);
     }
+#else
+    if (this->GetMemoryBuffer())
+    {
+      MemStream memoryStream(
+        "EXRmemoryStream", this->GetMemoryBuffer(), this->GetMemoryBufferLength());
+      Imf::RgbaInputFile file = Imf::RgbaInputFile(memoryStream);
+      checkChannels(file);
+    }
+#endif
     else
     {
       Imf::RgbaInputFile file(this->InternalFileName);
@@ -199,12 +221,26 @@
   {
     Imf::setGlobalThreadCount(std::thread::hardware_concurrency());
 
-    if (this->MemoryBuffer)
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+    if (this->GetStream())
+    {
+      // F3D only uses stream from memory
+      vtkMemoryResourceStream* stream = vtkMemoryResourceStream::SafeDownCast(this->GetStream());
+      assert(stream);
+
+      MemStream memoryStream("EXRmemoryStream", stream->GetBuffer(), stream->GetSize());
+      Imf::RgbaInputFile file = Imf::RgbaInputFile(memoryStream);
+      readContent(file);
+    }
+#else
+    if (this->GetMemoryBuffer())
     {
-      MemStream memoryStream("EXRmemoryStream", this->MemoryBuffer, this->MemoryBufferLength);
+      MemStream memoryStream(
+        "EXRmemoryStream", this->GetMemoryBuffer(), this->GetMemoryBufferLength());
       Imf::RgbaInputFile file = Imf::RgbaInputFile(memoryStream);
       readContent(file);
     }
+#endif
     else
     {
       Imf::RgbaInputFile file(this->InternalFileName);
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DExternalRenderWindow.cxx f3d-3.4.1/vtkext/private/module/vtkF3DExternalRenderWindow.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DExternalRenderWindow.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DExternalRenderWindow.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,13 @@
 #include <vtkObjectFactory.h>
 #include <vtkOpenGLFramebufferObject.h>
 #include <vtkOpenGLState.h>
+#include <vtkVersion.h>
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240914)
+#include <vtk_glad.h>
+#else
+#include <vtk_glew.h>
+#endif
 
 #include "vtkF3DExternalRenderWindow.h"
 
@@ -18,6 +25,10 @@
 //------------------------------------------------------------------------------
 void vtkF3DExternalRenderWindow::Start()
 {
+  this->GetState()->vtkglEnable(GL_BLEND);
+  this->GetState()->vtkglBlendFuncSeparate(
+    GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
   // creates or resizes the framebuffer
   this->Size[0] = (this->Size[0] > 0 ? this->Size[0] : 300);
   this->Size[1] = (this->Size[1] > 0 ? this->Size[1] : 300);
@@ -36,6 +47,12 @@
   {
     // skip vtkGenericOpenGLRenderWindow because it calls OpenGL before it's even initialized
     this->vtkOpenGLRenderWindow::OpenGLInit();
+
+    if (!this->Initialized)
+    {
+      // The context can fails to initialize if the OpenGL symbol loader is invalid.
+      return;
+    }
   }
 
   this->Superclass::Render();
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGenericImporter.cxx f3d-3.4.1/vtkext/private/module/vtkF3DGenericImporter.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGenericImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DGenericImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,6 +4,8 @@
 #include "vtkF3DPostProcessFilter.h"
 
 #include <vtkActor.h>
+#include <vtkCompositeDataIterator.h>
+#include <vtkDataAssembly.h>
 #include <vtkEventForwarderCommand.h>
 #include <vtkImageData.h>
 #include <vtkInformation.h>
@@ -24,18 +26,33 @@
 
 struct vtkF3DGenericImporter::Internals
 {
+  // Data structure for each block in a composite dataset
+  struct BlockData
+  {
+    vtkNew<vtkF3DPostProcessFilter> PostPro;
+    vtkNew<vtkActor> Actor;
+    vtkNew<vtkPolyDataMapper> Mapper;
+    vtkPolyData* Points = nullptr;
+    vtkImageData* Image = nullptr;
+    std::string Name;
+  };
+
   vtkSmartPointer<vtkAlgorithm> Reader = nullptr;
-  vtkNew<vtkF3DPostProcessFilter> PostPro;
-  vtkNew<vtkActor> GeometryActor;
-  vtkNew<vtkPolyDataMapper> PolyDataMapper;
+  std::vector<BlockData> Blocks;
   std::string OutputDescription;
 
-  vtkPolyData* ImportedPoints = nullptr;
-  vtkImageData* ImportedImage = nullptr;
-
   bool HasAnimation = false;
   bool AnimationEnabled = false;
   std::array<double, 2> TimeRange;
+
+  void UpdateBlock(BlockData& bd, vtkDataSet* dataset)
+  {
+    bd.PostPro->SetInputDataObject(dataset);
+    bd.PostPro->Update();
+    bd.Points = vtkPolyData::SafeDownCast(bd.PostPro->GetOutput(1));
+    vtkImageData* image = vtkImageData::SafeDownCast(bd.PostPro->GetOutput(2));
+    bd.Image = image && image->GetNumberOfCells() > 0 ? image : nullptr;
+  }
 };
 
 vtkStandardNewMacro(vtkF3DGenericImporter);
@@ -55,7 +72,7 @@
   vtkInformation* readerInfo = this->Pimpl->Reader->GetOutputInformation(0);
   if (readerInfo->Has(vtkStreamingDemandDrivenPipeline::TIME_RANGE()))
   {
-    double* readerTimeRange = readerInfo->Get(vtkStreamingDemandDrivenPipeline::TIME_RANGE());
+    const double* readerTimeRange = readerInfo->Get(vtkStreamingDemandDrivenPipeline::TIME_RANGE());
     this->Pimpl->TimeRange[0] = readerTimeRange[0];
     this->Pimpl->TimeRange[1] = readerTimeRange[1];
     this->Pimpl->HasAnimation = true;
@@ -104,8 +121,7 @@
 
 //----------------------------------------------------------------------------
 bool vtkF3DGenericImporter::GetTemporalInformation([[maybe_unused]] vtkIdType animationIndex,
-  double vtkNotUsed(frameRate), int& vtkNotUsed(nbTimeSteps), double timeRange[2],
-  vtkDoubleArray* vtkNotUsed(timeSteps))
+  double timeRange[2], int& vtkNotUsed(nbTimeSteps), vtkDoubleArray* vtkNotUsed(timeSteps))
 {
   assert(animationIndex == 0);
   // F3D do not care about timesteps
@@ -119,54 +135,111 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DGenericImporter::CreateActorForBlock(
+  vtkDataSet* block, vtkRenderer* ren, const std::string& blockName)
+{
+  this->Pimpl->Blocks.emplace_back();
+  Internals::BlockData& bd = this->Pimpl->Blocks.back();
+  bd.Name = blockName;
+
+  this->Pimpl->UpdateBlock(bd, block);
+
+  bd.Mapper->SetInputConnection(bd.PostPro->GetOutputPort(0));
+  bd.Mapper->ScalarVisibilityOff();
+
+  bd.Actor->SetMapper(bd.Mapper);
+  bd.Actor->GetProperty()->SetPointSize(10.0);
+  bd.Actor->GetProperty()->SetLineWidth(1.0);
+  bd.Actor->GetProperty()->SetRoughness(0.3);
+  bd.Actor->GetProperty()->SetBaseIOR(1.5);
+  bd.Actor->GetProperty()->SetInterpolationToPBR();
+
+  ren->AddActor(bd.Actor);
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240707)
+  this->ActorCollection->AddItem(bd.Actor);
+#endif
+
+  bd.Actor->VisibilityOn();
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DGenericImporter::ImportActors(vtkRenderer* ren)
 {
   assert(this->Pimpl->Reader);
 
+  // Clear any previous blocks
+  this->Pimpl->Blocks.clear();
+
   // Read file and forward progress
   vtkNew<vtkEventForwarderCommand> progressForwarder;
   progressForwarder->SetTarget(this);
   this->Pimpl->Reader->AddObserver(vtkCommand::ProgressEvent, progressForwarder);
-  bool status = this->Pimpl->PostPro->GetExecutive()->Update();
-  if (!status || !this->Pimpl->Reader->GetOutputDataObject(0))
+  bool status = this->Pimpl->Reader->GetExecutive()->Update();
+
+  vtkDataObject* output = this->Pimpl->Reader->GetOutputDataObject(0);
+  if (!status || !output)
   {
     this->SetFailureStatus();
     return;
   }
 
-  // Cast to dataset types
-  this->Pimpl->ImportedPoints = vtkPolyData::SafeDownCast(this->Pimpl->PostPro->GetOutput(1));
-  vtkImageData* image = vtkImageData::SafeDownCast(this->Pimpl->PostPro->GetOutput(2));
-  this->Pimpl->ImportedImage = image->GetNumberOfCells() > 0 ? image : nullptr;
-
-  // Recover output description from the reader
-  this->Pimpl->OutputDescription =
-    vtkF3DGenericImporter::GetDataObjectDescription(this->Pimpl->Reader->GetOutputDataObject(0));
-
-  // Add filter outputs to mapper inputs
-  this->Pimpl->PolyDataMapper->SetInputConnection(this->Pimpl->PostPro->GetOutputPort(0));
-  this->Pimpl->PolyDataMapper->ScalarVisibilityOff();
-
-  // Set geometry actor default properties
-  // Rely on vtkProperty default for other properties
-  this->Pimpl->GeometryActor->GetProperty()->SetPointSize(10.0);
-  this->Pimpl->GeometryActor->GetProperty()->SetLineWidth(1.0);
-  this->Pimpl->GeometryActor->GetProperty()->SetRoughness(0.3);
-  this->Pimpl->GeometryActor->GetProperty()->SetBaseIOR(1.5);
-  this->Pimpl->GeometryActor->GetProperty()->SetInterpolationToPBR();
-
-  // add mappers
-  // TODO implement proper composite support
-  this->Pimpl->GeometryActor->SetMapper(this->Pimpl->PolyDataMapper);
+  this->Pimpl->OutputDescription = this->GetDataObjectDescription(output);
 
-  // add props
-  ren->AddActor(this->Pimpl->GeometryActor);
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240707)
-  this->ActorCollection->AddItem(this->Pimpl->GeometryActor);
-#endif
+  vtkMultiBlockDataSet* mb = vtkMultiBlockDataSet::SafeDownCast(output);
+  vtkPartitionedDataSetCollection* pdc = vtkPartitionedDataSetCollection::SafeDownCast(output);
+  vtkPartitionedDataSet* pds = vtkPartitionedDataSet::SafeDownCast(output);
 
-  // Set visibilities
-  this->Pimpl->GeometryActor->VisibilityOn();
+  if (mb)
+  {
+    this->ImportMultiBlock(mb, ren);
+  }
+  else if (pdc)
+  {
+    this->ImportPartitionedDataSetCollection(pdc, ren);
+  }
+  else if (pds)
+  {
+    this->ImportPartitionedDataSet(pds, ren);
+  }
+#if VTK_VERSION_NUMBER <= VTK_VERSION_CHECK(9, 5, 2)
+  // Handle other composite types (e.g., AMR) using generic iterator
+  else if (vtkCompositeDataSet* composite = vtkCompositeDataSet::SafeDownCast(output))
+  {
+    auto iter = vtkSmartPointer<vtkCompositeDataIterator>::Take(composite->NewIterator());
+    iter->SkipEmptyNodesOn();
+    int idx = 0;
+    for (iter->InitTraversal(); !iter->IsDoneWithTraversal(); iter->GoToNextItem(), idx++)
+    {
+      vtkDataSet* ds = vtkDataSet::SafeDownCast(iter->GetCurrentDataObject());
+      if (ds)
+      {
+        std::string blockName = "Block_" + std::to_string(idx);
+        if (iter->HasCurrentMetaData())
+        {
+          const char* name = iter->GetCurrentMetaData()->Get(vtkCompositeDataSet::NAME());
+          if (name)
+          {
+            blockName = name;
+          }
+        }
+        this->CreateActorForBlock(ds, ren, blockName);
+      }
+    }
+  }
+#endif
+  else
+  {
+    vtkDataSet* dataset = vtkDataSet::SafeDownCast(output);
+    if (dataset)
+    {
+      this->CreateActorForBlock(dataset, ren, "");
+    }
+    else
+    {
+      this->SetFailureStatus();
+      return;
+    }
+  }
 
   this->UpdateTemporalInformation();
 }
@@ -177,7 +250,6 @@
   if (reader)
   {
     this->Pimpl->Reader = reader;
-    this->Pimpl->PostPro->SetInputConnection(this->Pimpl->Reader->GetOutputPort());
   }
 }
 
@@ -261,7 +333,6 @@
 //----------------------------------------------------------------------------
 bool vtkF3DGenericImporter::UpdateAtTimeValue(double timeValue)
 {
-
   if (!this->Pimpl->AnimationEnabled)
   {
     // Animation is not enabled, nothing to do
@@ -269,13 +340,46 @@
   }
 
   assert(this->Pimpl->Reader);
-  if (!this->Pimpl->PostPro->UpdateTimeStep(timeValue) ||
-    !this->Pimpl->Reader->GetOutputDataObject(0))
+
+  vtkInformation* info = this->Pimpl->Reader->GetOutputInformation(0);
+  info->Set(vtkStreamingDemandDrivenPipeline::UPDATE_TIME_STEP(), timeValue);
+  bool status = this->Pimpl->Reader->GetExecutive()->Update();
+
+  vtkDataObject* output = this->Pimpl->Reader->GetOutputDataObject(0);
+  if (!status || !output)
   {
     F3DLog::Print(F3DLog::Severity::Warning, "A reader failed to update at a timeValue");
     return false;
   }
 
+  vtkCompositeDataSet* composite = vtkCompositeDataSet::SafeDownCast(output);
+
+  if (composite)
+  {
+    auto iter = vtkSmartPointer<vtkCompositeDataIterator>::Take(composite->NewIterator());
+    iter->SkipEmptyNodesOn();
+
+    size_t blockIdx = 0;
+    for (iter->InitTraversal();
+         !iter->IsDoneWithTraversal() && blockIdx < this->Pimpl->Blocks.size();
+         iter->GoToNextItem(), blockIdx++)
+    {
+      vtkDataSet* block = vtkDataSet::SafeDownCast(iter->GetCurrentDataObject());
+      if (block)
+      {
+        this->Pimpl->UpdateBlock(this->Pimpl->Blocks[blockIdx], block);
+      }
+    }
+  }
+  else if (!this->Pimpl->Blocks.empty())
+  {
+    vtkDataSet* dataset = vtkDataSet::SafeDownCast(output);
+    if (dataset)
+    {
+      this->Pimpl->UpdateBlock(this->Pimpl->Blocks[0], dataset);
+    }
+  }
+
   this->UpdateOutputDescriptions();
   return true;
 }
@@ -290,13 +394,197 @@
 }
 
 //----------------------------------------------------------------------------
-vtkPolyData* vtkF3DGenericImporter::GetImportedPoints()
+vtkPolyData* vtkF3DGenericImporter::GetImportedPoints(vtkIdType actorIndex)
+{
+  if (actorIndex >= 0 && actorIndex < static_cast<vtkIdType>(this->Pimpl->Blocks.size()))
+  {
+    return this->Pimpl->Blocks[actorIndex].Points;
+  }
+  return nullptr;
+}
+
+//----------------------------------------------------------------------------
+vtkImageData* vtkF3DGenericImporter::GetImportedImage(vtkIdType actorIndex)
+{
+  if (actorIndex >= 0 && actorIndex < static_cast<vtkIdType>(this->Pimpl->Blocks.size()))
+  {
+    return this->Pimpl->Blocks[actorIndex].Image;
+  }
+  return nullptr;
+}
+
+//----------------------------------------------------------------------------
+std::string vtkF3DGenericImporter::GetBlockName(vtkIdType actorIndex)
+{
+  if (actorIndex >= 0 && actorIndex < static_cast<vtkIdType>(this->Pimpl->Blocks.size()))
+  {
+    return this->Pimpl->Blocks[actorIndex].Name;
+  }
+  return "";
+}
+
+//----------------------------------------------------------------------------
+vtkIdType vtkF3DGenericImporter::GetNumberOfBlocks()
 {
-  return this->Pimpl->ImportedPoints;
+  return static_cast<vtkIdType>(this->Pimpl->Blocks.size());
 }
 
 //----------------------------------------------------------------------------
-vtkImageData* vtkF3DGenericImporter::GetImportedImage()
+void vtkF3DGenericImporter::ImportMultiBlock(
+  vtkMultiBlockDataSet* mb, vtkRenderer* ren, const std::string& parentName)
 {
-  return this->Pimpl->ImportedImage;
+  for (unsigned int i = 0; i < mb->GetNumberOfBlocks(); i++)
+  {
+    vtkDataObject* obj = mb->GetBlock(i);
+    if (!obj)
+    {
+      continue;
+    }
+
+    std::string blockName = parentName;
+    if (!blockName.empty())
+    {
+      blockName += "/";
+    }
+
+    if (const char* name =
+          mb->HasMetaData(i) ? mb->GetMetaData(i)->Get(vtkCompositeDataSet::NAME()) : nullptr)
+    {
+      blockName += name;
+    }
+    else
+    {
+      blockName += "Block_" + std::to_string(i);
+    }
+
+    vtkMultiBlockDataSet* childMB = vtkMultiBlockDataSet::SafeDownCast(obj);
+    vtkCompositeDataSet* childComposite = vtkCompositeDataSet::SafeDownCast(obj);
+    vtkDataSet* ds = vtkDataSet::SafeDownCast(obj);
+
+    if (childMB)
+    {
+      this->ImportMultiBlock(childMB, ren, blockName);
+    }
+    else if (childComposite)
+    {
+      auto iter = vtkSmartPointer<vtkCompositeDataIterator>::Take(childComposite->NewIterator());
+      iter->SkipEmptyNodesOn();
+      int subIdx = 0;
+      for (iter->InitTraversal(); !iter->IsDoneWithTraversal(); iter->GoToNextItem(), subIdx++)
+      {
+        vtkDataSet* subDs = vtkDataSet::SafeDownCast(iter->GetCurrentDataObject());
+        if (subDs)
+        {
+          const char* name = iter->HasCurrentMetaData()
+            ? iter->GetCurrentMetaData()->Get(vtkCompositeDataSet::NAME())
+            : "Block_";
+          blockName += "/" + (name ? std::string(name) : "Block_") + std::to_string(subIdx);
+          this->CreateActorForBlock(subDs, ren, blockName);
+        }
+      }
+    }
+    else if (ds)
+    {
+      this->CreateActorForBlock(ds, ren, blockName);
+    }
+  }
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DGenericImporter::ImportPartitionedDataSetCollection(
+  vtkPartitionedDataSetCollection* pdc, vtkRenderer* ren)
+{
+  std::map<unsigned int, std::string> datasetIndexToName;
+  vtkDataAssembly* assembly = pdc->GetDataAssembly();
+  if (assembly)
+  {
+    std::vector<int> childNodes = assembly->GetChildNodes(assembly->GetRootNode());
+    for (int nodeId : childNodes)
+    {
+      const char* nodeName = assembly->GetNodeName(nodeId);
+      if (nodeName)
+      {
+        std::vector<unsigned int> indices = assembly->GetDataSetIndices(nodeId, false);
+        for (unsigned int idx : indices)
+        {
+          datasetIndexToName[idx] = nodeName;
+        }
+      }
+    }
+  }
+
+  for (unsigned int i = 0; i < pdc->GetNumberOfPartitionedDataSets(); i++)
+  {
+    vtkPartitionedDataSet* pds = pdc->GetPartitionedDataSet(i);
+    if (!pds)
+    {
+      continue;
+    }
+
+    std::string pdsName;
+
+    auto it = datasetIndexToName.find(i);
+    if (it != datasetIndexToName.end())
+    {
+      pdsName = it->second;
+    }
+    else if (pdc->HasMetaData(i))
+    {
+      const char* name = pdc->GetMetaData(i)->Get(vtkCompositeDataSet::NAME());
+      if (name)
+      {
+        pdsName = name;
+      }
+    }
+    if (pdsName.empty())
+    {
+      pdsName = "PartitionedDataSet_" + std::to_string(i);
+    }
+
+    this->ImportPartitionedDataSet(pds, ren, pdsName);
+  }
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DGenericImporter::ImportPartitionedDataSet(
+  vtkPartitionedDataSet* pds, vtkRenderer* ren, const std::string& pdsName)
+{
+  std::string baseName = pdsName;
+  if (baseName.empty())
+  {
+    baseName = "PartitionedDataSet_" + std::to_string(this->Pimpl->Blocks.size());
+  }
+
+  for (unsigned int j = 0; j < pds->GetNumberOfPartitions(); j++)
+  {
+    vtkDataSet* ds = pds->GetPartition(j);
+    if (!ds)
+    {
+      continue;
+    }
+
+    std::string partitionName;
+    if (pds->HasMetaData(j))
+    {
+      const char* name = pds->GetMetaData(j)->Get(vtkCompositeDataSet::NAME());
+      if (name)
+      {
+        partitionName = name;
+      }
+    }
+
+    std::string blockName = baseName;
+    if (!partitionName.empty())
+    {
+      blockName += "/";
+      blockName += partitionName;
+    }
+    else if (pds->GetNumberOfPartitions() > 1)
+    {
+      blockName += "/Partition_";
+      blockName += std::to_string(j);
+    }
+
+    this->CreateActorForBlock(ds, ren, blockName);
+  }
 }
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGenericImporter.h f3d-3.4.1/vtkext/private/module/vtkF3DGenericImporter.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGenericImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DGenericImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -14,6 +14,7 @@
 class vtkDataObject;
 class vtkImageData;
 class vtkMultiBlockDataSet;
+class vtkPartitionedDataSet;
 class vtkPartitionedDataSetCollection;
 class vtkPolyData;
 class vtkF3DGenericImporter : public vtkF3DImporter
@@ -84,17 +85,29 @@
    * Framerate is ignored in this implementation.
    * Only timerange is defined in this implementation.
    */
-  bool GetTemporalInformation(vtkIdType animationIndex, double frameRate, int& nbTimeSteps,
-    double timeRange[2], vtkDoubleArray* timeSteps) override;
+  bool GetTemporalInformation(vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps,
+    vtkDoubleArray* timeSteps) override;
 
   ///@{
   /**
-   * Direct access to generic importer specific datasets
+   * Direct access to generic importer specific datasets.
+   * Return data for the specified actor/block index.
    */
-  vtkPolyData* GetImportedPoints();
-  vtkImageData* GetImportedImage();
+  vtkPolyData* GetImportedPoints(vtkIdType actorIndex);
+  vtkImageData* GetImportedImage(vtkIdType actorIndex);
   ///@}
 
+  /**
+   * Get the name of a block by its actor index.
+   * Returns an empty string if the index is invalid.
+   */
+  std::string GetBlockName(vtkIdType actorIndex);
+
+  /**
+   * Get the number of blocks/actors created by this importer.
+   */
+  vtkIdType GetNumberOfBlocks();
+
 protected:
   vtkF3DGenericImporter();
   ~vtkF3DGenericImporter() override = default;
@@ -118,6 +131,28 @@
   vtkF3DGenericImporter(const vtkF3DGenericImporter&) = delete;
   void operator=(const vtkF3DGenericImporter&) = delete;
 
+  /**
+   * Create an actor for a single dataset block
+   */
+  void CreateActorForBlock(vtkDataSet* block, vtkRenderer* ren, const std::string& blockName = "");
+
+  /**
+   * Import blocks from a vtkMultiBlockDataSet with proper name extraction
+   */
+  void ImportMultiBlock(
+    vtkMultiBlockDataSet* mb, vtkRenderer* ren, const std::string& parentName = "");
+
+  /**
+   * Import blocks from a vtkPartitionedDataSetCollection with proper name extraction
+   */
+  void ImportPartitionedDataSetCollection(vtkPartitionedDataSetCollection* pdc, vtkRenderer* ren);
+
+  /**
+   * Import blocks from a vtkPartitionedDataSet with proper name extraction
+   */
+  void ImportPartitionedDataSet(
+    vtkPartitionedDataSet* pds, vtkRenderer* ren, const std::string& pdsName = "");
+
   struct Internals;
   std::unique_ptr<Internals> Pimpl;
 };
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGLXRenderWindow.cxx f3d-3.4.1/vtkext/private/module/vtkF3DGLXRenderWindow.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DGLXRenderWindow.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DGLXRenderWindow.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -10,7 +10,11 @@
 
 #include "vtkF3DGLXRenderWindow.h"
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251009)
+#include "vtkX11Functions.h"
+#else
 #include <X11/Xlib.h>
+#endif
 
 //------------------------------------------------------------------------------
 vtkF3DGLXRenderWindow::vtkF3DGLXRenderWindow() = default;
@@ -21,17 +25,32 @@
 //------------------------------------------------------------------------------
 vtkF3DGLXRenderWindow* vtkF3DGLXRenderWindow::New()
 {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251009)
   // Check if the X display is available
+  vtkX11FunctionsInitialize();
+
+  if (vtkXOpenDisplay == nullptr || vtkXCloseDisplay == nullptr)
+  {
+    // libX11 cannot be open
+    return nullptr;
+  }
+
+  Display* dpy = vtkXOpenDisplay(nullptr);
+  if (dpy == nullptr)
+  {
+    // no display available
+    return nullptr;
+  }
+  vtkXCloseDisplay(dpy);
+
+  vtkX11FunctionsFinalize();
+#else
   Display* dpy = XOpenDisplay(nullptr);
   if (dpy == nullptr)
   {
     return nullptr;
   }
   XCloseDisplay(dpy);
-
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240914)
-  // Load core glx functions
-  gladLoaderLoadGLX(nullptr, 0);
 #endif
 
   VTK_STANDARD_NEW_BODY(vtkF3DGLXRenderWindow);
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DHexagonalBokehBlurPass.cxx f3d-3.4.1/vtkext/private/module/vtkF3DHexagonalBokehBlurPass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DHexagonalBokehBlurPass.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DHexagonalBokehBlurPass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -61,7 +61,7 @@
 void vtkF3DHexagonalBokehBlurPass::PrintSelf(ostream& os, vtkIndent indent)
 {
   this->Superclass::PrintSelf(os, indent);
-  os << indent << "CircleOfConfusion: " << this->CircleOfConfusionRadius << std::endl;
+  os << indent << "CircleOfConfusion: " << this->CircleOfConfusionRadius << "\n";
 }
 
 //------------------------------------------------------------------------------
@@ -72,42 +72,42 @@
   {
     this->BackgroundTexture = vtkSmartPointer<vtkTextureObject>::New();
     this->BackgroundTexture->SetContext(renWin);
-    this->BackgroundTexture->SetFormat(GL_RGBA);
-    this->BackgroundTexture->SetInternalFormat(GL_RGBA32F);
-    this->BackgroundTexture->SetDataType(GL_FLOAT);
+    this->BackgroundTexture->SetFormat(GL_RGB);
+    this->BackgroundTexture->SetInternalFormat(GL_RGB16F);
+    this->BackgroundTexture->SetDataType(GL_HALF_FLOAT);
     this->BackgroundTexture->SetMinificationFilter(vtkTextureObject::Linear);
     this->BackgroundTexture->SetMagnificationFilter(vtkTextureObject::Linear);
     this->BackgroundTexture->SetWrapS(vtkTextureObject::ClampToEdge);
     this->BackgroundTexture->SetWrapT(vtkTextureObject::ClampToEdge);
-    this->BackgroundTexture->Allocate2D(width, height, 4, VTK_FLOAT);
+    this->BackgroundTexture->Allocate2D(width, height, 3, VTK_FLOAT);
   }
 
   if (this->VerticalBlurTexture == nullptr)
   {
     this->VerticalBlurTexture = vtkSmartPointer<vtkTextureObject>::New();
     this->VerticalBlurTexture->SetContext(renWin);
-    this->VerticalBlurTexture->SetFormat(GL_RGBA);
-    this->VerticalBlurTexture->SetInternalFormat(GL_RGBA32F);
-    this->VerticalBlurTexture->SetDataType(GL_FLOAT);
+    this->VerticalBlurTexture->SetFormat(GL_RGB);
+    this->VerticalBlurTexture->SetInternalFormat(GL_RGB16F);
+    this->VerticalBlurTexture->SetDataType(GL_HALF_FLOAT);
     this->VerticalBlurTexture->SetMinificationFilter(vtkTextureObject::Linear);
     this->VerticalBlurTexture->SetMagnificationFilter(vtkTextureObject::Linear);
     this->VerticalBlurTexture->SetWrapS(vtkTextureObject::ClampToEdge);
     this->VerticalBlurTexture->SetWrapT(vtkTextureObject::ClampToEdge);
-    this->VerticalBlurTexture->Allocate2D(width, height, 4, VTK_FLOAT);
+    this->VerticalBlurTexture->Allocate2D(width, height, 3, VTK_FLOAT);
   }
 
   if (this->DiagonalBlurTexture == nullptr)
   {
     this->DiagonalBlurTexture = vtkSmartPointer<vtkTextureObject>::New();
     this->DiagonalBlurTexture->SetContext(renWin);
-    this->DiagonalBlurTexture->SetFormat(GL_RGBA);
-    this->DiagonalBlurTexture->SetInternalFormat(GL_RGBA32F);
-    this->DiagonalBlurTexture->SetDataType(GL_FLOAT);
+    this->DiagonalBlurTexture->SetFormat(GL_RGB);
+    this->DiagonalBlurTexture->SetInternalFormat(GL_RGB16F);
+    this->DiagonalBlurTexture->SetDataType(GL_HALF_FLOAT);
     this->DiagonalBlurTexture->SetMinificationFilter(vtkTextureObject::Linear);
     this->DiagonalBlurTexture->SetMagnificationFilter(vtkTextureObject::Linear);
     this->DiagonalBlurTexture->SetWrapS(vtkTextureObject::ClampToEdge);
     this->DiagonalBlurTexture->SetWrapT(vtkTextureObject::ClampToEdge);
-    this->DiagonalBlurTexture->Allocate2D(width, height, 4, VTK_FLOAT);
+    this->DiagonalBlurTexture->Allocate2D(width, height, 3, VTK_FLOAT);
   }
 
   if (this->FrameBufferObject == nullptr)
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiActor.cxx f3d-3.4.1/vtkext/private/module/vtkF3DImguiActor.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiActor.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiActor.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,23 +1,30 @@
 #include "vtkF3DImguiActor.h"
 
+#include "F3DDefaultLogo.h"
 #include "F3DFontBuffer.h"
-#include "F3DImguiStyle.h"
+#include "F3DStyle.h"
 #include "vtkF3DImguiConsole.h"
 #include "vtkF3DImguiFS.h"
 #include "vtkF3DImguiVS.h"
 
+#include <vtkImageData.h>
 #include <vtkObjectFactory.h>
 #include <vtkOpenGLBufferObject.h>
 #include <vtkOpenGLRenderWindow.h>
 #include <vtkOpenGLShaderCache.h>
 #include <vtkOpenGLState.h>
 #include <vtkOpenGLVertexArrayObject.h>
+#include <vtkPNGReader.h>
 #include <vtkRenderWindowInteractor.h>
 #include <vtkShader.h>
 #include <vtkShaderProgram.h>
 #include <vtkTextureObject.h>
 #include <vtkVersion.h>
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+#include <vtkMemoryResourceStream.h>
+#endif
+
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240914)
 #include <vtk_glad.h>
 #else
@@ -25,11 +32,39 @@
 #endif
 
 #include <imgui.h>
-
+#include <numeric>
 #include <optional>
+#include <sstream>
+#include <string>
+
+namespace
+{
+constexpr float LOGO_DISPLAY_WIDTH = 256.f;
+constexpr float LOGO_DISPLAY_HEIGHT = 256.f;
+constexpr float DROPZONE_LOGO_TEXT_PADDING = 20.f;
+constexpr float DROPZONE_MARGIN = 0.5f;
+constexpr float DROPZONE_PADDING_X = 5.0f;
+constexpr float DROPZONE_PADDING_Y = 2.0f;
+
+static std::vector<std::string> SplitBindings(const std::string& s, const char delim)
+{
+  std::vector<std::string> result;
+  std::stringstream ss(s);
+  std::string item;
+
+  while (std::getline(ss, item, delim))
+  {
+    result.push_back(item);
+  }
+
+  return result;
+}
+
+}
 
 struct vtkF3DImguiActor::Internals
 {
+
   void Initialize(vtkOpenGLRenderWindow* renWin)
   {
     if (this->FontTexture == nullptr)
@@ -50,6 +85,29 @@
       // Create VBO
       this->VertexBuffer = vtkSmartPointer<vtkOpenGLBufferObject>::New();
 
+      // Load embedded PNG icon into texture
+      vtkNew<vtkPNGReader> iconReader;
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+      vtkNew<vtkMemoryResourceStream> stream;
+      stream->SetBuffer(F3DDefaultLogo, sizeof(F3DDefaultLogo));
+      iconReader->SetStream(stream);
+#else
+      iconReader->SetMemoryBuffer(F3DDefaultLogo);
+      iconReader->SetMemoryBufferLength(sizeof(F3DDefaultLogo));
+#endif
+      iconReader->Update();
+
+      vtkImageData* imageData = iconReader->GetOutput();
+      int* dims = imageData->GetDimensions();
+
+      unsigned char* logoPixels = static_cast<unsigned char*>(imageData->GetScalarPointer());
+      if (logoPixels)
+      {
+        this->LogoTexture = vtkSmartPointer<vtkTextureObject>::New();
+        this->LogoTexture->SetContext(renWin);
+        this->LogoTexture->Create2DFromRaw(dims[0], dims[1], 4, VTK_UNSIGNED_CHAR, logoPixels);
+      }
+
       // https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10589
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231016)
       this->VertexBuffer->SetUsage(vtkOpenGLBufferObject::StreamDraw);
@@ -94,7 +152,11 @@
         this->FontTexture->ReleaseGraphicsResources(renWin);
         this->FontTexture = nullptr;
       }
-
+      if (this->LogoTexture)
+      {
+        this->LogoTexture->ReleaseGraphicsResources(renWin);
+        this->LogoTexture = nullptr;
+      }
       if (this->VertexBuffer)
       {
         this->VertexBuffer = nullptr;
@@ -149,12 +211,6 @@
     shift[0] = -(2.f * drawData->DisplayPos.x + drawData->DisplaySize.x) / drawData->DisplaySize.x;
     shift[1] = (2.f * drawData->DisplayPos.y + drawData->DisplaySize.y) / drawData->DisplaySize.y;
 
-    this->FontTexture->Activate();
-
-    this->Program->SetUniform2f("Scale", scale);
-    this->Program->SetUniform2f("Shift", shift);
-    this->Program->SetUniformi("Texture", this->FontTexture->GetTextureUnit());
-
     // Render the UI
     this->VertexArray->Bind();
     this->VertexBuffer->Bind();
@@ -176,6 +232,13 @@
       {
         const ImDrawCmd* cmd = &cmdList->CmdBuffer[iCmd];
 
+        // Activate texture and set uniforms per draw command:
+        vtkTextureObject* texObj = reinterpret_cast<vtkTextureObject*>(cmd->GetTexID());
+        texObj->Activate();
+        this->Program->SetUniform2f("Scale", scale);
+        this->Program->SetUniform2f("Shift", shift);
+        this->Program->SetUniformi("Texture", texObj->GetTextureUnit());
+
         // Project scissor/clipping rectangles into framebuffer space
         ImVec2 clipMin(
           (cmd->ClipRect.x - clipOff.x) * clipScale.x, (cmd->ClipRect.y - clipOff.y) * clipScale.y);
@@ -201,6 +264,7 @@
     this->IndexBuffer->Release();
 
     this->FontTexture->Deactivate();
+    this->LogoTexture->Deactivate();
   }
 
   vtkSmartPointer<vtkTextureObject> FontTexture;
@@ -208,6 +272,7 @@
   vtkSmartPointer<vtkOpenGLBufferObject> VertexBuffer;
   vtkSmartPointer<vtkOpenGLBufferObject> IndexBuffer;
   vtkSmartPointer<vtkShaderProgram> Program;
+  vtkSmartPointer<vtkTextureObject> LogoTexture;
 };
 
 namespace
@@ -252,16 +317,24 @@
 
   ImFontConfig fontConfig;
 
+  ImVector<ImWchar> ranges;
+  ImFontGlyphRangesBuilder builder;
+  builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
+  builder.AddChar(0x2264); // Less-Than or Equal To
+  builder.BuildRanges(&ranges);
+
   ImFont* font = nullptr;
   if (this->FontFile.empty())
   {
+    // ImGui API is not very helpful with this
     fontConfig.FontDataOwnedByAtlas = false;
-    font =
-      io.Fonts->AddFontFromMemoryTTF((void*)F3DFontBuffer, sizeof(F3DFontBuffer), 18, &fontConfig);
+    font = io.Fonts->AddFontFromMemoryTTF(
+      const_cast<void*>(reinterpret_cast<const void*>(F3DFontBuffer)), sizeof(F3DFontBuffer), 18,
+      &fontConfig, ranges.Data);
   }
   else
   {
-    font = io.Fonts->AddFontFromFileTTF(this->FontFile.c_str(), 18, &fontConfig);
+    font = io.Fonts->AddFontFromFileTTF(this->FontFile.c_str(), 18, &fontConfig, ranges.Data);
   }
 
   io.Fonts->Build();
@@ -272,22 +345,23 @@
 
   ImGuiStyle* style = &ImGui::GetStyle();
   style->AntiAliasedLines = false;
-  style->GrabRounding = 4.0f;
-  style->WindowPadding = ImVec2(10, 10);
-  style->WindowRounding = 8.f;
-  style->WindowBorderSize = 0.f;
   style->FrameBorderSize = 0.f;
   style->FramePadding = ImVec2(4, 2);
   style->FrameRounding = 2.f;
-  style->Colors[ImGuiCol_Text] = F3DImguiStyle::GetTextColor();
-  style->Colors[ImGuiCol_WindowBg] = F3DImguiStyle::GetBackgroundColor();
+  style->GrabRounding = 4.0f;
+  style->ScrollbarPadding = 2.f;
+  style->WindowBorderSize = 0.f;
+  style->WindowPadding = ImVec2(10, 10);
+  style->WindowRounding = 8.f;
+  style->Colors[ImGuiCol_Text] = F3DStyle::imgui::GetTextColor();
+  style->Colors[ImGuiCol_WindowBg] = F3DStyle::imgui::GetBackgroundColor();
   style->Colors[ImGuiCol_FrameBg] = colTransparent;
   style->Colors[ImGuiCol_FrameBgActive] = colTransparent;
   style->Colors[ImGuiCol_ScrollbarBg] = colTransparent;
-  style->Colors[ImGuiCol_ScrollbarGrab] = F3DImguiStyle::GetMidColor();
-  style->Colors[ImGuiCol_ScrollbarGrabHovered] = F3DImguiStyle::GetHighlightColor();
-  style->Colors[ImGuiCol_ScrollbarGrabActive] = F3DImguiStyle::GetHighlightColor();
-  style->Colors[ImGuiCol_TextSelectedBg] = F3DImguiStyle::GetHighlightColor();
+  style->Colors[ImGuiCol_ScrollbarGrab] = F3DStyle::imgui::GetMidColor();
+  style->Colors[ImGuiCol_ScrollbarGrabHovered] = F3DStyle::imgui::GetHighlightColor();
+  style->Colors[ImGuiCol_ScrollbarGrabActive] = F3DStyle::imgui::GetHighlightColor();
+  style->Colors[ImGuiCol_TextSelectedBg] = F3DStyle::imgui::GetHighlightColor();
 
   // Setup backend name
   io.BackendPlatformName = io.BackendRendererName = "F3D/VTK";
@@ -316,8 +390,8 @@
       return;
     }
 
-    constexpr ImVec4 colorImv = F3DImguiStyle::GetTextColor();
-    constexpr ImU32 color =
+    const ImVec4 colorImv = F3DStyle::imgui::GetTextColor();
+    const ImU32 color =
       IM_COL32(colorImv.x * 255, colorImv.y * 255, colorImv.z * 255, colorImv.w * 255);
 
     const int dropzonePad =
@@ -346,18 +420,35 @@
 
     ImGui::Begin("DropZoneText", nullptr, flags);
     /* Use background draw list to prevent "ignoring" NoBringToFrontOnFocus */
-    ImDrawList* draw_list = ImGui::GetBackgroundDrawList();
+    ImDrawList* drawList = ImGui::GetBackgroundDrawList();
+
+    // Logo rendering
+    if (this->DropZoneLogoVisible && this->Pimpl->LogoTexture)
+    {
+      float logoDisplayWidth = ::LOGO_DISPLAY_WIDTH;
+      float logoDisplayHeight = ::LOGO_DISPLAY_HEIGHT;
+      ImVec2 center = viewport->GetWorkCenter();
+      ImVec2 logoPos(center.x - logoDisplayWidth * ::DROPZONE_MARGIN,
+        center.y - logoDisplayHeight * ::DROPZONE_MARGIN);
+
+      // VTK texture pointer to ImTextureID cast (void*)
+      ImTextureID texID = reinterpret_cast<ImTextureID>(this->Pimpl->LogoTexture.Get());
+
+      drawList->AddImage(texID, logoPos,
+        ImVec2(logoPos.x + logoDisplayWidth, logoPos.y + logoDisplayHeight), ImVec2(0, 1),
+        ImVec2(1, 0));
+    }
 
     const ImVec2 p0(dropzonePad, dropzonePad);
     const ImVec2 p1(dropzonePad + dropZoneW, dropzonePad + dropZoneH);
 
-    // Draw top and bottom line
+    // Border lines
     for (float x = p0.x - 1; x < p1.x; x += tickLength + tickSpaceW)
     {
       const float y0 = p0.y + halfTickThickness;
       const float x1 = std::min(p1.x, x + tickLength);
-      draw_list->AddLine(ImVec2(x, y0), ImVec2(x1, y0), color, tickThickness);
-      draw_list->AddLine(ImVec2(x, p1.y), ImVec2(x1, p1.y), color, tickThickness);
+      drawList->AddLine(ImVec2(x, y0), ImVec2(x1, y0), color, tickThickness);
+      drawList->AddLine(ImVec2(x, p1.y), ImVec2(x1, p1.y), color, tickThickness);
     }
 
     // Draw left and right line
@@ -365,19 +456,113 @@
     {
       const float x1 = p1.x - halfTickThickness;
       const float y1 = std::min(p1.y, y + tickLength);
-      draw_list->AddLine(ImVec2(p0.x, y), ImVec2(p0.x, y1), color, tickThickness);
-      draw_list->AddLine(ImVec2(x1, y), ImVec2(x1, y1), color, tickThickness);
+      drawList->AddLine(ImVec2(p0.x, y), ImVec2(p0.x, y1), color, tickThickness);
+      drawList->AddLine(ImVec2(x1, y), ImVec2(x1, y1), color, tickThickness);
     }
 
     ImGui::End();
 
-    ImVec2 dropTextSize = ImGui::CalcTextSize(this->DropText.c_str());
+    // If DropText is provided, render and skip binds
+    if (!this->DropText.empty())
+    {
+      ImVec2 textSize = ImGui::CalcTextSize(this->DropText.c_str());
+      ImVec2 textPos(viewport->GetWorkCenter().x - textSize.x * ::DROPZONE_MARGIN,
+        viewport->GetWorkCenter().y - ::DROPZONE_MARGIN * textSize.y + ::LOGO_DISPLAY_HEIGHT / 2 +
+          ::DROPZONE_LOGO_TEXT_PADDING);
+      drawList->AddText(textPos, ImColor(F3DStyle::imgui::GetTextColor()), this->DropText.c_str());
+      return;
+    }
 
-    ImGui::Begin("DropZoneText", nullptr, flags);
-    ImGui::SetCursorPos(ImVec2(viewport->GetWorkCenter().x - 0.5f * dropTextSize.x,
-      viewport->GetWorkCenter().y - 0.5f * dropTextSize.y));
-    ImGui::TextUnformatted(this->DropText.c_str());
-    ImGui::End();
+    float maxDescTextWidth = 0.0f;
+    float maxBindingsTextWidth = 0.0f;
+    const float spacingX = ImGui::GetStyle().ItemSpacing.x;
+    const float plusWidth = ImGui::CalcTextSize("+").x;
+
+    // Compute widths
+    for (const auto& pair : this->DropBinds)
+    {
+      const auto& desc = pair.first;
+      const auto& bind = pair.second;
+
+      ImVec2 descSize = ImGui::CalcTextSize(desc.c_str());
+      maxDescTextWidth = std::max(maxDescTextWidth, descSize.x);
+
+      auto keys = ::SplitBindings(bind, '+');
+      float totalBindingsWidth = std::accumulate(keys.begin(), keys.end(),
+        0.0f, // use float init since CalcTextSize returns float
+        [](float sum, const std::string& key)
+        {
+          return sum + ImGui::CalcTextSize(key.c_str()).x +
+            ::DROPZONE_MARGIN * ::DROPZONE_LOGO_TEXT_PADDING;
+        });
+
+      if (keys.size() > 1)
+      {
+        totalBindingsWidth += (keys.size() - 1) * (spacingX + plusWidth + spacingX);
+      }
+
+      maxBindingsTextWidth = std::max(maxBindingsTextWidth, totalBindingsWidth);
+    }
+
+    const ImColor descTextColor = F3DStyle::imgui::GetTextColor();
+    const ImColor bindingRectColor = F3DStyle::imgui::GetMidColor();
+    const ImColor bindingTextColor = F3DStyle::imgui::GetTextColor();
+
+    float tableWidth =
+      maxDescTextWidth + maxBindingsTextWidth + ::DROPZONE_LOGO_TEXT_PADDING + spacingX;
+
+    // Position table below logo if needed
+    ImVec2 startPos;
+    if (this->DropZoneLogoVisible && this->Pimpl->LogoTexture)
+    {
+      startPos = ImVec2(viewport->GetWorkCenter().x - tableWidth * ::DROPZONE_MARGIN,
+        viewport->GetWorkCenter().y + ::LOGO_DISPLAY_HEIGHT / 2 + ::DROPZONE_MARGIN);
+    }
+    else
+    {
+      startPos = ImVec2(
+        viewport->GetWorkCenter().x - tableWidth * ::DROPZONE_MARGIN, viewport->GetWorkCenter().y);
+    }
+
+    ImVec2 cursor = startPos;
+
+    for (const auto& pair : this->DropBinds)
+    {
+      const auto& desc = pair.first;
+      const auto& bind = pair.second;
+
+      drawList->AddText(cursor, descTextColor, desc.c_str());
+      float rowHeight =
+        ImGui::GetTextLineHeightWithSpacing() + ::DROPZONE_MARGIN * ::DROPZONE_LOGO_TEXT_PADDING;
+
+      float xBindings = cursor.x + maxDescTextWidth + ::DROPZONE_LOGO_TEXT_PADDING;
+      ImVec2 bindingPos(xBindings, cursor.y);
+
+      auto keys = ::SplitBindings(bind, '+');
+      for (size_t k = 0; k < keys.size(); ++k)
+      {
+        const std::string& key = keys[k];
+        ImVec2 textSize = ImGui::CalcTextSize(key.c_str());
+        ImVec2 padding(::DROPZONE_PADDING_X, ::DROPZONE_PADDING_Y);
+
+        ImVec2 rectMin = ImVec2(bindingPos.x, bindingPos.y);
+        ImVec2 rectMax =
+          ImVec2(rectMin.x + textSize.x + padding.x * 2, rectMin.y + textSize.y + padding.y * 2);
+
+        drawList->AddRectFilled(rectMin, rectMax, bindingRectColor, 4.0f);
+        drawList->AddText(
+          ImVec2(rectMin.x + padding.x, rectMin.y + padding.y), bindingTextColor, key.c_str());
+
+        bindingPos.x = rectMax.x + spacingX;
+
+        if (k < keys.size() - 1)
+        {
+          drawList->AddText(bindingPos, descTextColor, "+");
+          bindingPos.x += plusWidth + spacingX;
+        }
+      }
+      cursor.y += rowHeight;
+    }
   }
 }
 
@@ -386,15 +571,24 @@
 {
   if (!this->FileName.empty())
   {
-    ImGuiViewport* viewport = ImGui::GetMainViewport();
+    const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
-    constexpr float marginTop = 5.f;
+    constexpr float margin = F3DStyle::GetDefaultMargin();
     ImVec2 winSize = ImGui::CalcTextSize(this->FileName.c_str());
     winSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
     winSize.y += 2.f * ImGui::GetStyle().WindowPadding.y;
 
-    ::SetupNextWindow(ImVec2(viewport->GetWorkCenter().x - 0.5f * winSize.x, marginTop), winSize);
-    ImGui::SetNextWindowBgAlpha(0.9f);
+    // Adjust position if HDRIFileName is also visible
+    float totalWidth = winSize.x;
+    if (this->HDRIFileNameVisible && !this->HDRIFileName.empty())
+    {
+      ImVec2 hdriWinSize = ImGui::CalcTextSize(this->HDRIFileName.c_str());
+      hdriWinSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
+      totalWidth += hdriWinSize.x + ImGui::GetStyle().WindowPadding.x;
+    }
+
+    ::SetupNextWindow(ImVec2(viewport->GetWorkCenter().x - 0.5f * totalWidth, margin), winSize);
+    ImGui::SetNextWindowBgAlpha(this->BackdropOpacity);
 
     ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
       ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove;
@@ -408,18 +602,18 @@
 //----------------------------------------------------------------------------
 void vtkF3DImguiActor::RenderMetaData()
 {
-  ImGuiViewport* viewport = ImGui::GetMainViewport();
+  const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
-  constexpr float marginRight = 5.f;
+  constexpr float margin = F3DStyle::GetDefaultMargin();
 
   ImVec2 winSize = ImGui::CalcTextSize(this->MetaData.c_str());
   winSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
   winSize.y += 2.f * ImGui::GetStyle().WindowPadding.y;
 
-  ::SetupNextWindow(ImVec2(viewport->WorkSize.x - winSize.x - marginRight,
+  ::SetupNextWindow(ImVec2(viewport->WorkSize.x - winSize.x - margin,
                       viewport->GetWorkCenter().y - 0.5f * winSize.y),
     winSize);
-  ImGui::SetNextWindowBgAlpha(0.9f);
+  ImGui::SetNextWindowBgAlpha(this->BackdropOpacity);
 
   ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
     ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove;
@@ -430,44 +624,100 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DImguiActor::RenderHDRIFileName()
+{
+  if (!this->HDRIFileName.empty())
+  {
+    const ImGuiViewport* viewport = ImGui::GetMainViewport();
+
+    constexpr float margin = F3DStyle::GetDefaultMargin();
+    ImVec2 winSize = ImGui::CalcTextSize(this->HDRIFileName.c_str());
+    winSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
+    winSize.y += 2.f * ImGui::GetStyle().WindowPadding.y;
+
+    // Adjust position if FileName is also visible
+    float totalWidth = winSize.x;
+    float winOffsetX = 0.f;
+    if (this->FileNameVisible && !this->FileName.empty())
+    {
+      ImVec2 fileWinSize = ImGui::CalcTextSize(this->FileName.c_str());
+      fileWinSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
+      totalWidth += fileWinSize.x + ImGui::GetStyle().WindowPadding.x;
+      winOffsetX = fileWinSize.x + ImGui::GetStyle().WindowPadding.x;
+    }
+
+    ::SetupNextWindow(
+      ImVec2(viewport->GetWorkCenter().x - 0.5f * totalWidth + winOffsetX, margin), winSize);
+    ImGui::SetNextWindowBgAlpha(this->BackdropOpacity);
+
+    ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
+      ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove;
+
+    ImGui::Begin("HDRIFileName", nullptr, flags);
+    ImGui::TextUnformatted(this->HDRIFileName.c_str());
+    ImGui::End();
+  }
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DImguiActor::RenderCheatSheet()
 {
-  ImGuiViewport* viewport = ImGui::GetMainViewport();
+  const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
-  constexpr float marginLeft = 5.f;
-  constexpr float marginTopBottom = 5.f;
+  constexpr float margin = F3DStyle::GetDefaultMargin();
+  constexpr float padding = 16.f;
+  const float plusWidth = ImGui::CalcTextSize("+").x;
+  const float spacingX = ImGui::GetStyle().ItemSpacing.x;
 
   float textHeight = 0.f;
   float winWidth = 0.f;
 
+  // Use to create all rect with same size
+  float maxBindingTextWidth = 0.f;
+  float maxDescTextWidth = 0.f;
+  float maxValueTextWidth = 0.f;
+
   for (const auto& [group, content] : this->CheatSheet)
   {
     textHeight +=
       ImGui::GetTextLineHeightWithSpacing() + 2 * ImGui::GetStyle().SeparatorTextPadding.y;
-    for (const auto& [bind, desc, val] : content)
+    for (const auto& [bind, desc, val, type] : content)
     {
-      std::string line = bind;
-      line += ": ";
-      line += desc;
-      if (!val.empty())
+      textHeight += ImGui::GetTextLineHeightWithSpacing();
+
+      auto keys = ::SplitBindings(bind, '+');
+
+      float bindingLineWidth = std::accumulate(keys.begin(), keys.end(),
+        0.0f, // use float init since CalcTextSize returns float
+        [](float sum, const std::string& key) { return sum + ImGui::CalcTextSize(key.c_str()).x; });
+
+      if (keys.size() > 1)
       {
-        line += " [" + val + "]";
+        bindingLineWidth += (keys.size() - 1) * (spacingX + plusWidth + spacingX);
       }
-      ImVec2 currentLine = ImGui::CalcTextSize(line.c_str());
+      maxBindingTextWidth = std::max(maxBindingTextWidth, bindingLineWidth);
 
-      winWidth = std::max(winWidth, currentLine.x);
-      textHeight += ImGui::GetTextLineHeightWithSpacing();
+      ImVec2 descriptionLineSize = ImGui::CalcTextSize(desc.c_str());
+      maxDescTextWidth = std::max(maxDescTextWidth, descriptionLineSize.x);
+
+      std::string cyclingValue = "< " + val + " >";
+      ImVec2 valueLineSize = ImGui::CalcTextSize(cyclingValue.c_str());
+      maxValueTextWidth = std::max(maxValueTextWidth, valueLineSize.x);
+
+      winWidth = maxBindingTextWidth + maxDescTextWidth + maxValueTextWidth;
     }
   }
 
-  winWidth += 2.f * ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().ScrollbarSize;
+  winWidth += ImGui::GetStyle().ScrollbarSize + 4.f * padding;
   textHeight += 2.f * ImGui::GetStyle().WindowPadding.y;
 
-  const float winTop = std::max(marginTopBottom, (viewport->WorkSize.y - textHeight) * 0.5f);
+  const float winTop = std::max(margin, (viewport->WorkSize.y - textHeight) * 0.5f);
+
+  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
 
-  ::SetupNextWindow(ImVec2(marginLeft, winTop),
-    ImVec2(winWidth, std::min(viewport->WorkSize.y - (2 * marginTopBottom), textHeight)));
-  ImGui::SetNextWindowBgAlpha(0.9f);
+  ::SetupNextWindow(ImVec2(margin, winTop),
+    ImVec2(winWidth, std::min(viewport->WorkSize.y - (2 * margin), textHeight)));
+  ImGui::SetNextWindowBgAlpha(this->BackdropOpacity);
 
   ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
     ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings |
@@ -479,29 +729,88 @@
   for (const auto& [group, list] : this->CheatSheet)
   {
     ImGui::SeparatorText(group.c_str());
-    for (const auto& [bind, desc, val] : list)
+    ImGui::BeginTable("BindingsTable", 3);
+    ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthFixed, maxDescTextWidth);
+    ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, maxValueTextWidth);
+    ImGui::TableSetupColumn("Bindings", ImGuiTableColumnFlags_WidthStretch, maxBindingTextWidth);
+    for (const auto& [bind, desc, val, type] : list)
     {
-      if (!val.empty())
+      ImVec4 bindingTextColor, bindingRectColor, descTextColor, valueTextColor;
+
+      if (type == CheatSheetBindingType::TOGGLE && val == "ON")
       {
-        ImGui::Text("%s: %s [%s]", bind.c_str(), desc.c_str(), val.c_str());
+        bindingTextColor = F3DStyle::imgui::GetBackgroundColor();
+        bindingRectColor = F3DStyle::imgui::GetWarningColor();
+        descTextColor = F3DStyle::imgui::GetWarningColor();
+        valueTextColor = F3DStyle::imgui::GetWarningColor();
       }
       else
       {
-        ImGui::Text("%s: %s", bind.c_str(), desc.c_str());
+        bindingTextColor = F3DStyle::imgui::GetTextColor();
+        bindingRectColor = F3DStyle::imgui::GetMidColor();
+        descTextColor = F3DStyle::imgui::GetTextColor();
+        valueTextColor = F3DStyle::imgui::GetHighlightColor();
+      }
+
+      ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() + margin);
+
+      ImGui::TableNextColumn();
+      ImGui::TextColored(descTextColor, "%s", desc.c_str());
+
+      ImGui::TableNextColumn();
+      if (type == CheatSheetBindingType::CYCLIC)
+      {
+        ImGui::TextColored(valueTextColor, "< %s >", val.c_str());
+      }
+      else if (type == CheatSheetBindingType::NUMERICAL || type == CheatSheetBindingType::OTHER)
+      {
+        ImGui::TextColored(valueTextColor, "%s", val.c_str());
+      }
+
+      ImGui::TableNextColumn();
+
+      ImVec2 topBindingCorner, bottomBindingCorner;
+      std::vector<std::string> splittedBinding = ::SplitBindings(bind, '+');
+      const float maxCursorPosX = ImGui::GetCursorPosX() + ImGui::GetColumnWidth();
+      float posX = maxCursorPosX - ImGui::CalcTextSize(bind.c_str()).x - ImGui::GetScrollX() -
+        ((splittedBinding.size() * 2) - 1) * spacingX;
+      ImGui::SetCursorPosX(posX);
+      for (const std::string& key : splittedBinding)
+      {
+        ImDrawList* drawList = ImGui::GetWindowDrawList();
+        drawList->ChannelsSplit(2);
+        drawList->ChannelsSetCurrent(1);
+        ImGui::TextColored(bindingTextColor, "%s", key.c_str());
+        drawList->ChannelsSetCurrent(0);
+        topBindingCorner =
+          ImVec2(ImGui::GetItemRectMin().x - margin, ImGui::GetItemRectMin().y - (margin * .5f));
+        bottomBindingCorner =
+          ImVec2(ImGui::GetItemRectMax().x + margin, ImGui::GetItemRectMax().y + (margin * .5f));
+        drawList->AddRectFilled(
+          topBindingCorner, bottomBindingCorner, ImColor(bindingRectColor), 5.f);
+        drawList->ChannelsMerge();
+        if (key != splittedBinding.back())
+        {
+          ImGui::SameLine();
+          ImGui::Text("+");
+        }
+        ImGui::SameLine();
       }
     }
+
+    ImGui::EndTable();
   }
 
   ImGui::End();
+  ImGui::PopStyleVar();
 }
 
 //----------------------------------------------------------------------------
 void vtkF3DImguiActor::RenderFpsCounter()
 {
-  ImGuiViewport* viewport = ImGui::GetMainViewport();
+  const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
-  constexpr float marginRight = 3.f;
-  constexpr float marginBottom = 3.f;
+  constexpr float margin = F3DStyle::GetDefaultMargin();
 
   std::string fpsString = std::to_string(this->FpsValue);
   fpsString += " fps";
@@ -510,11 +819,11 @@
   winSize.x += 2.f * ImGui::GetStyle().WindowPadding.x;
   winSize.y += 2.f * ImGui::GetStyle().WindowPadding.y;
 
-  ImVec2 position(viewport->WorkSize.x - winSize.x - marginRight,
-    viewport->WorkSize.y - winSize.y - marginBottom);
+  ImVec2 position(
+    viewport->WorkSize.x - winSize.x - margin, viewport->WorkSize.y - winSize.y - margin);
 
   ::SetupNextWindow(position, winSize);
-  ImGui::SetNextWindowBgAlpha(0.9f);
+  ImGui::SetNextWindowBgAlpha(this->BackdropOpacity);
 
   ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
     ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove;
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiActor.h f3d-3.4.1/vtkext/private/module/vtkF3DImguiActor.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiActor.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiActor.h	2026-01-08 08:41:55.000000000 +0200
@@ -72,6 +72,11 @@
   void RenderMetaData() override;
 
   /**
+   * Render the HDRI filename UI widget
+   */
+  void RenderHDRIFileName() override;
+
+  /**
    * Render the cheatsheet UI widget
    */
   void RenderCheatSheet() override;
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiConsole.cxx f3d-3.4.1/vtkext/private/module/vtkF3DImguiConsole.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiConsole.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiConsole.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,6 +1,6 @@
 #include "vtkF3DImguiConsole.h"
 
-#include "F3DImguiStyle.h"
+#include "F3DStyle.h"
 
 #include <vtkCallbackCommand.h>
 #include <vtkCommand.h>
@@ -11,10 +11,11 @@
 
 #include <algorithm>
 #include <array>
+#include <cstdint>
 
 struct vtkF3DImguiConsole::Internals
 {
-  enum class LogType
+  enum class LogType : std::uint8_t
   {
     Log,
     Warning,
@@ -24,13 +25,13 @@
   };
 
   std::vector<std::pair<LogType, std::string>> Logs;
-  std::array<char, 256> CurrentInput = {};
+  std::array<char, 2048> CurrentInput = {};
   bool NewError = false;
   bool NewWarning = false;
   std::pair<size_t, size_t> Completions{ 0,
     0 }; // Index for start and length of completions in Logs
   std::function<std::vector<std::string>(const std::string& pattern)>
-    GetCommandsMatchCallback; // Callback to get the list of commands matching pattern
+    CompletionCallback; // Callback to get the list of commands matching pattern
   std::vector<std::string> CommandHistory;
   std::pair<std::string, int> LastInput; // Last input before navigating history
   int CommandHistoryIndexInv = -1;       // Current inverted index in command history navigation
@@ -57,10 +58,10 @@
     {
       case ImGuiInputTextFlags_CallbackCompletion:
       {
-        assert(this->GetCommandsMatchCallback);
+        assert(this->CompletionCallback);
         std::string pattern{ data->Buf };
         std::vector<std::string> candidates =
-          this->GetCommandsMatchCallback(pattern); // List of supported commands
+          this->CompletionCallback(pattern); // List of candidates completion
 
         if (candidates.size() == 1)
         {
@@ -68,31 +69,66 @@
           // nice casing.
           data->DeleteChars(0, static_cast<int>(pattern.size()));
           data->InsertChars(data->CursorPos, candidates[0].c_str());
-          data->InsertChars(data->CursorPos, " ");
         }
         else if (candidates.size() > 1)
         {
+          std::string_view bestCandidate = candidates[0];
+#if defined(_WIN32) || defined(__APPLE__)
+          // Find which candidate matches the casing of the pattern the best
+          int bestPatternMatchLen = 0;
+          for (const auto& candidate : candidates)
+          {
+            int patternMatchLen = 0;
+            for (unsigned i = 0; i < pattern.size() && i < candidate.size(); i++)
+            {
+              if (pattern[i] == candidate[i])
+              {
+                patternMatchLen++;
+              }
+              else
+              {
+                break;
+              }
+            }
+            if (patternMatchLen > bestPatternMatchLen)
+            {
+              bestCandidate = candidate;
+              bestPatternMatchLen = patternMatchLen;
+            }
+          }
+#endif
+
           // Multiple matches. Complete as much as we can.
           // So inputting "C"+Tab will complete to "CL" then display "CLEAR" and "CLASSIFY" as
           // matches.
-          size_t matchLen = pattern.size();
+          size_t matchLen = 0;
           bool allCandidatesMatches = true;
           // Find the common prefix to all candidates
           while (allCandidatesMatches)
           {
-            const std::string& first = candidates[0];
-            if (first.size() <= matchLen)
+            if (bestCandidate.size() <= matchLen)
             {
-              // The first candidate is shorter than the current match length
+              // The best candidate is shorter than the current match length
               allCandidatesMatches = false;
             }
             else
             {
               // Check if all candidates match the current character
-              const char target = first[matchLen];
+              const char target = bestCandidate[matchLen];
               allCandidatesMatches = std::all_of(candidates.begin(), candidates.end(),
                 [matchLen, target](const std::string& s)
-                { return s.size() > matchLen && s[matchLen] == target; });
+                {
+                  return s.size() > matchLen &&
+#if defined(_WIN32) || defined(__APPLE__)
+                    // Windows and Mac filesystems are case-insensitive by default
+                    // Perform a case-insensitive comparison in case this char is part of a file
+                    // path
+                    std::tolower(s[matchLen]) == std::tolower(target);
+#else
+                    // Linux filesystems are typically case-sensitive
+                    s[matchLen] == target;
+#endif
+                });
             }
             if (allCandidatesMatches)
             {
@@ -106,7 +142,7 @@
             // (possibly just pattern itself in the worst case)
             data->DeleteChars(0, static_cast<int>(pattern.size()));
             data->InsertChars(
-              data->CursorPos, candidates[0].c_str(), candidates[0].c_str() + matchLen);
+              data->CursorPos, bestCandidate.data(), bestCandidate.data() + matchLen);
           }
 
           this->Completions.first = this->Logs.size();
@@ -208,28 +244,23 @@
 //----------------------------------------------------------------------------
 void vtkF3DImguiConsole::ShowConsole(bool minimal)
 {
-  ImGuiViewport* viewport = ImGui::GetMainViewport();
+  const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
-  constexpr float marginConsole = 30.f;
-  constexpr float marginTopRight = 5.f;
+  constexpr float margin = F3DStyle::GetDefaultMargin();
   const float padding = ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().FramePadding.x;
+  float windowWidth = viewport->WorkSize.x - 2.f * margin;
 
+  ImGui::SetNextWindowPos(ImVec2(margin, margin));
   // explicitly calculate size of minimal console to avoid extra flashing frame
   if (minimal)
   {
     if (this->Pimpl->NewError || this->Pimpl->NewWarning)
     {
       // prevent overlap with console badge in minimal console
-      ImGui::SetNextWindowPos(ImVec2(marginTopRight, marginTopRight));
-      ImGui::SetNextWindowSize(ImVec2(
-        viewport->WorkSize.x - 2.f * marginConsole, ImGui::CalcTextSize(">").y + 2.f * padding));
-    }
-    else
-    {
-      ImGui::SetNextWindowPos(ImVec2(marginTopRight, marginTopRight));
-      ImGui::SetNextWindowSize(ImVec2(
-        viewport->WorkSize.x - 2.f * marginTopRight, ImGui::CalcTextSize(">").y + 2.f * padding));
+      const ImVec2 badgeSize = this->GetBadgeSize();
+      windowWidth = viewport->WorkSize.x - badgeSize.x - 3.f * margin;
     }
+    ImGui::SetNextWindowSize(ImVec2(windowWidth, ImGui::CalcTextSize(">").y + 2.f * padding));
   }
   else
   {
@@ -237,9 +268,7 @@
     this->Pimpl->NewError = false;
     this->Pimpl->NewWarning = false;
 
-    ImGui::SetNextWindowPos(ImVec2(marginConsole, marginConsole));
-    ImGui::SetNextWindowSize(ImVec2(
-      viewport->WorkSize.x - 2.f * marginConsole, viewport->WorkSize.y - 2.f * marginConsole));
+    ImGui::SetNextWindowSize(ImVec2(windowWidth, viewport->WorkSize.y - 2.f * margin));
   }
 
   ImGui::SetNextWindowBgAlpha(0.9f);
@@ -276,16 +305,16 @@
           switch (severity)
           {
             case Internals::LogType::Error:
-              ImGui::PushStyleColor(ImGuiCol_Text, F3DImguiStyle::GetErrorColor());
+              ImGui::PushStyleColor(ImGuiCol_Text, F3DStyle::imgui::GetErrorColor());
               break;
             case Internals::LogType::Warning:
-              ImGui::PushStyleColor(ImGuiCol_Text, F3DImguiStyle::GetWarningColor());
+              ImGui::PushStyleColor(ImGuiCol_Text, F3DStyle::imgui::GetWarningColor());
               break;
             case Internals::LogType::Typed:
-              ImGui::PushStyleColor(ImGuiCol_Text, F3DImguiStyle::GetHighlightColor());
+              ImGui::PushStyleColor(ImGuiCol_Text, F3DStyle::imgui::GetHighlightColor());
               break;
             case Internals::LogType::Completion:
-              ImGui::PushStyleColor(ImGuiCol_Text, F3DImguiStyle::GetCompletionColor());
+              ImGui::PushStyleColor(ImGuiCol_Text, F3DStyle::imgui::GetCompletionColor());
               break;
             default:
               hasColor = false;
@@ -373,19 +402,15 @@
 //----------------------------------------------------------------------------
 void vtkF3DImguiConsole::ShowBadge()
 {
-  ImGuiViewport* viewport = ImGui::GetMainViewport();
+  const ImGuiViewport* viewport = ImGui::GetMainViewport();
 
   if (this->Pimpl->NewError || this->Pimpl->NewWarning)
   {
-    constexpr float marginTopRight = 5.f;
-    const float padding = ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().FramePadding.x;
-    ImVec2 winSize = ImGui::CalcTextSize("!");
-    winSize.x += 2.f * padding;
-    winSize.y += 2.f * padding;
-
-    ImGui::SetNextWindowPos(
-      ImVec2(viewport->WorkSize.x - winSize.x - marginTopRight, marginTopRight));
-    ImGui::SetNextWindowSize(winSize);
+    constexpr float margin = F3DStyle::GetDefaultMargin();
+    ImVec2 badgeSize = this->GetBadgeSize();
+
+    ImGui::SetNextWindowPos(ImVec2(viewport->WorkSize.x - badgeSize.x - margin, margin));
+    ImGui::SetNextWindowSize(badgeSize);
     ImGui::SetNextWindowBgAlpha(0.9f);
 
     ImGuiWindowFlags winFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
@@ -394,10 +419,11 @@
     ImGui::Begin("ConsoleAlert", nullptr, winFlags);
 
     ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
-    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, F3DImguiStyle::GetHighlightColor());
+    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, F3DStyle::imgui::GetHighlightColor());
 
     ImGui::PushStyleColor(ImGuiCol_Text,
-      this->Pimpl->NewError ? F3DImguiStyle::GetErrorColor() : F3DImguiStyle::GetWarningColor());
+      this->Pimpl->NewError ? F3DStyle::imgui::GetErrorColor()
+                            : F3DStyle::imgui::GetWarningColor());
 
     if (ImGui::Button("!"))
     {
@@ -411,6 +437,16 @@
 }
 
 //----------------------------------------------------------------------------
+ImVec2 vtkF3DImguiConsole::GetBadgeSize()
+{
+  const float padding = ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().FramePadding.x;
+  ImVec2 badgeSize = ImGui::CalcTextSize("!");
+  badgeSize.x += 2.f * padding;
+  badgeSize.y += 2.f * padding;
+  return badgeSize;
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DImguiConsole::Clear()
 {
   this->Pimpl->Logs.clear();
@@ -419,8 +455,8 @@
 }
 
 //----------------------------------------------------------------------------
-void vtkF3DImguiConsole::SetCommandsMatchCallback(
+void vtkF3DImguiConsole::SetCompletionCallback(
   std::function<std::vector<std::string>(const std::string& pattern)> callback)
 {
-  this->Pimpl->GetCommandsMatchCallback = std::move(callback);
+  this->Pimpl->CompletionCallback = std::move(callback);
 }
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiConsole.h f3d-3.4.1/vtkext/private/module/vtkF3DImguiConsole.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiConsole.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiConsole.h	2026-01-08 08:41:55.000000000 +0200
@@ -22,6 +22,7 @@
 
 class vtkOpenGLRenderWindow;
 class vtkWindow;
+struct ImVec2;
 
 class vtkF3DImguiConsole : public vtkF3DConsoleOutputWindow
 {
@@ -50,9 +51,14 @@
   void Clear();
 
   /**
-   * Set the callback to get the list of commands matching pattern
+   * Get Badge window size
    */
-  void SetCommandsMatchCallback(
+  ImVec2 GetBadgeSize();
+
+  /**
+   * Set the callback to get completion candidates
+   */
+  void SetCompletionCallback(
     std::function<std::vector<std::string>(const std::string& pattern)> callback);
 
 protected:
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiObserver.cxx f3d-3.4.1/vtkext/private/module/vtkF3DImguiObserver.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DImguiObserver.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DImguiObserver.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -192,110 +192,150 @@
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseMove(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  int* p = that->GetEventPosition();
-  int* sz = that->GetRenderWindow()->GetSize();
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMousePosEvent(static_cast<float>(p[0]), static_cast<float>(sz[1] - p[1] - 1));
-  // RenderUI is not called here on purpose to avoid too frequent UI draw
-  // The event loop is taking care of it
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    int* p = that->GetEventPosition();
+    int* sz = that->GetRenderWindow()->GetSize();
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMousePosEvent(static_cast<float>(p[0]), static_cast<float>(sz[1] - p[1] - 1));
+    // RenderUI is not called here on purpose to avoid too frequent UI draw
+    // The event loop is taking care of it
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseLeftPress(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseButtonEvent(ImGuiMouseButton_Left, true);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseButtonEvent(ImGuiMouseButton_Left, true);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseLeftRelease(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseRightPress(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseButtonEvent(ImGuiMouseButton_Right, true);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseButtonEvent(ImGuiMouseButton_Right, true);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseRightRelease(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseWheelForward(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseWheelEvent(0.f, 1.f);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseWheelEvent(0.f, 1.f);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::MouseWheelBackward(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddMouseWheelEvent(0.f, -1.f);
-  this->RenderUI(that);
-  return io.WantCaptureMouse;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddMouseWheelEvent(0.f, -1.f);
+    this->RenderUI(that);
+    return io.WantCaptureMouse;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::Char(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddInputCharacter(that->GetKeyCode());
-  this->RenderUI(that);
-  return io.WantCaptureKeyboard;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddInputCharacter(that->GetKeyCode());
+    this->RenderUI(that);
+    return io.WantCaptureKeyboard;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::KeyPress(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddKeyEvent(ImGuiMod_Ctrl, that->GetControlKey() == 1);
-  io.AddKeyEvent(ImGuiMod_Shift, that->GetShiftKey() == 1);
-  io.AddKeyEvent(ImGuiMod_Alt, that->GetAltKey() == 1);
-  io.AddKeyEvent(::GetImGuiKeyFromKeySym(that->GetKeySym()), true);
-  this->RenderUI(that);
-  return io.WantCaptureKeyboard;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddKeyEvent(ImGuiMod_Ctrl, that->GetControlKey() == 1);
+    io.AddKeyEvent(ImGuiMod_Shift, that->GetShiftKey() == 1);
+    io.AddKeyEvent(ImGuiMod_Alt, that->GetAltKey() == 1);
+    io.AddKeyEvent(::GetImGuiKeyFromKeySym(that->GetKeySym()), true);
+    this->RenderUI(that);
+    return io.WantCaptureKeyboard;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
 bool vtkF3DImguiObserver::KeyRelease(vtkObject* caller, unsigned long, void*)
 {
-  vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
-  ImGuiIO& io = ImGui::GetIO();
-  io.AddKeyEvent(ImGuiMod_Ctrl, that->GetControlKey() == 1);
-  io.AddKeyEvent(ImGuiMod_Shift, that->GetShiftKey() == 1);
-  io.AddKeyEvent(ImGuiMod_Alt, that->GetAltKey() == 1);
-  io.AddKeyEvent(::GetImGuiKeyFromKeySym(that->GetKeySym()), false);
-  this->RenderUI(that);
-  return io.WantCaptureKeyboard;
+  if (ImGui::GetCurrentContext() != nullptr)
+  {
+    vtkRenderWindowInteractor* that = static_cast<vtkRenderWindowInteractor*>(caller);
+    ImGuiIO& io = ImGui::GetIO();
+    io.AddKeyEvent(ImGuiMod_Ctrl, that->GetControlKey() == 1);
+    io.AddKeyEvent(ImGuiMod_Shift, that->GetShiftKey() == 1);
+    io.AddKeyEvent(ImGuiMod_Alt, that->GetAltKey() == 1);
+    io.AddKeyEvent(::GetImGuiKeyFromKeySym(that->GetKeySym()), false);
+    this->RenderUI(that);
+    return io.WantCaptureKeyboard;
+  }
+  return false;
 }
 
 //----------------------------------------------------------------------------
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DInteractorEventRecorder.cxx f3d-3.4.1/vtkext/private/module/vtkF3DInteractorEventRecorder.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DInteractorEventRecorder.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DInteractorEventRecorder.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,7 @@
 
 #include <vtkCallbackCommand.h>
 #include <vtkObjectFactory.h>
+#include <vtkRenderWindow.h>
 #include <vtkRenderWindowInteractor.h>
 #include <vtkVersion.h>
 
@@ -29,6 +30,23 @@
   }
 
   this->Interactor = interactor;
+
+  if (this->Interactor)
+  {
+    // Make sure RenderEvent triggers renders
+    // TODO: Ideally, RenderEvent should be triggered from VTK side
+    vtkNew<vtkCallbackCommand> renderCallback;
+    renderCallback->SetClientData(this->Interactor->GetRenderWindow());
+    renderCallback->SetCallback(
+      [](vtkObject*, unsigned long, void* clientData, void*)
+      {
+        vtkRenderWindow* window = static_cast<vtkRenderWindow*>(clientData);
+        window->Render();
+      });
+
+    this->Interactor->AddObserver(vtkCommand::RenderEvent, renderCallback);
+  }
+
   this->Modified();
 }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DInteractorStyle.cxx f3d-3.4.1/vtkext/private/module/vtkF3DInteractorStyle.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DInteractorStyle.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DInteractorStyle.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -156,7 +156,7 @@
   if (!ren->GetUseTrackball())
   {
     double up[3];
-    this->InterpolateTemporaryUp(0.1, ren->GetUpVector(), up);
+    this->InterpolateTemporaryUp(0.1, ren->GetUpDirection(), up);
 
     double envUpCamDirCross[3];
     vtkMath::Cross(up, camera->GetDirectionOfProjection(), envUpCamDirCross);
@@ -166,7 +166,7 @@
       // Keep setting the temporary up to the camera's up vector until the interpolated up vector
       // and the camera direction vector are not collinear
       this->SetTemporaryUp(camera->GetViewUp());
-      this->InterpolateTemporaryUp(0.1, ren->GetUpVector(), up);
+      this->InterpolateTemporaryUp(0.1, ren->GetUpDirection(), up);
     }
 
     // Rotate camera around the focal point about the environment's up vector
@@ -174,7 +174,7 @@
     Transform->Identity();
     const double* fp = camera->GetFocalPoint();
     Transform->Translate(+fp[0], +fp[1], +fp[2]);
-    Transform->RotateWXYZ(rxf, ren->GetUpVector());
+    Transform->RotateWXYZ(rxf, ren->GetUpDirection());
     Transform->Translate(-fp[0], -fp[1], -fp[2]);
     Transform->TransformPoint(camera->GetPosition(), camera->GetPosition());
 
@@ -183,7 +183,7 @@
     // Clamp parameter to `camera->Elevation()` to maintain -90 < elevation < +90
     constexpr double maxAbsElevation = 90 - 1e-10;
     const double elevation = vtkMath::DegreesFromRadians(
-      vtkMath::AngleBetweenVectors(ren->GetUpVector(), camera->GetDirectionOfProjection()) -
+      vtkMath::AngleBetweenVectors(ren->GetUpDirection(), camera->GetDirectionOfProjection()) -
       vtkMath::Pi() / 2);
     camera->Elevation(std::clamp(ryf, -maxAbsElevation - elevation, +maxAbsElevation - elevation));
 
@@ -264,6 +264,7 @@
   {
     this->Superclass::Dolly(factor);
   }
+  this->InvokeEvent(vtkCommand::InteractionEvent, nullptr);
 }
 
 //----------------------------------------------------------------------------
@@ -360,7 +361,7 @@
   if (this->CurrentRenderer)
   {
     vtkF3DRenderer* ren = vtkF3DRenderer::SafeDownCast(this->CurrentRenderer);
-    SetTemporaryUp(ren->GetUpVector());
+    SetTemporaryUp(ren->GetUpDirection());
   }
 }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DMetaImporter.cxx f3d-3.4.1/vtkext/private/module/vtkF3DMetaImporter.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DMetaImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DMetaImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,7 @@
 
 #include "F3DLog.h"
 #include "vtkF3DGenericImporter.h"
+#include "vtkF3DImporter.h"
 
 #include <vtkActorCollection.h>
 #include <vtkCallbackCommand.h>
@@ -225,6 +226,11 @@
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240707)
     vtkActorCollection* actorCollection = importer->GetImportedActors();
 #endif
+
+    // Recover generic importer if any (for indexed access to points/image)
+    vtkF3DGenericImporter* genericImporter = vtkF3DGenericImporter::SafeDownCast(importer);
+    vtkIdType actorIndex = 0;
+
     vtkCollectionSimpleIterator ait;
     actorCollection->InitTraversal(ait);
     while (auto* actor = actorCollection->GetNextActor(ait))
@@ -240,9 +246,6 @@
       surface->GetBounds(bounds);
       this->Pimpl->GeometryBoundingBox.AddBounds(bounds);
 
-      // Recover generic importer if any
-      vtkF3DGenericImporter* genericImporter = vtkF3DGenericImporter::SafeDownCast(importer);
-
       // Create and configure coloring actors
       this->Pimpl->ColoringActorsAndMappers.emplace_back(vtkF3DMetaImporter::ColoringStruct(actor));
       vtkF3DMetaImporter::ColoringStruct& cs = this->Pimpl->ColoringActorsAndMappers.back();
@@ -259,9 +262,8 @@
       vtkPolyData* points = surface;
       if (genericImporter)
       {
-        // For generic importer, use the single imported points
-        // TODO when supporting composite, handle with an actor based index
-        points = genericImporter->GetImportedPoints();
+        // Use indexed accessor for composite support
+        points = genericImporter->GetImportedPoints(actorIndex);
       }
       pss.Mapper->SetInputData(points);
       this->Renderer->AddActor(pss.Actor);
@@ -270,7 +272,7 @@
       // Create and configure volume props
       if (genericImporter)
       {
-        vtkImageData* image = genericImporter->GetImportedImage();
+        vtkImageData* image = genericImporter->GetImportedImage(actorIndex);
         if (image)
         {
           // XXX: Note that creating this struct takes some time
@@ -281,6 +283,8 @@
           vs.Prop->VisibilityOff();
         }
       }
+
+      actorIndex++;
     }
 
     importerPair.Updated = true;
@@ -515,8 +519,8 @@
 }
 
 //----------------------------------------------------------------------------
-bool vtkF3DMetaImporter::GetTemporalInformation(vtkIdType animationIndex, double frameRate,
-  int& nbTimeSteps, double timeRange[2], vtkDoubleArray* timeSteps)
+bool vtkF3DMetaImporter::GetTemporalInformation(
+  vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps, vtkDoubleArray* timeSteps)
 {
   vtkIdType localAnimationIndex = animationIndex;
   for (const auto& importerPair : this->Pimpl->Importers)
@@ -529,8 +533,21 @@
 
     if (localAnimationIndex < nAnim)
     {
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20251210)
+      vtkF3DImporter* f3dImporter = vtkF3DImporter::SafeDownCast(importerPair.Importer);
+      if (f3dImporter)
+      {
+        f3dImporter->GetTemporalInformation(localAnimationIndex, timeRange, nbTimeSteps, timeSteps);
+      }
+      else
+      {
+        return importerPair.Importer->GetTemporalInformation(
+          localAnimationIndex, 0, nbTimeSteps, timeRange, timeSteps);
+      }
+#else
       return importerPair.Importer->GetTemporalInformation(
-        localAnimationIndex, frameRate, nbTimeSteps, timeRange, timeSteps);
+        localAnimationIndex, timeRange, nbTimeSteps, timeSteps);
+#endif
     }
     else
     {
@@ -592,6 +609,12 @@
       vtkActorCollection* actorCollection =
         this->Pimpl->ActorsForImporterMap.at(importerPair.Importer).Get();
 #endif
+
+      // Recover generic importer if any (for indexed access to points/image)
+      vtkF3DGenericImporter* genericImporter =
+        vtkF3DGenericImporter::SafeDownCast(importerPair.Importer);
+      vtkIdType actorIndex = 0;
+
       vtkCollectionSimpleIterator ait;
       actorCollection->InitTraversal(ait);
       while (auto* actor = actorCollection->GetNextActor(ait))
@@ -601,23 +624,22 @@
 
         // Update coloring vectors, with a dedicated logic for generic importer
         vtkDataSet* datasetForColoring = pdMapper->GetInput();
-        vtkF3DGenericImporter* genericImporter =
-          vtkF3DGenericImporter::SafeDownCast(importerPair.Importer);
         if (genericImporter)
         {
-          // TODO This will be improved with proper composite support
-          // Currently generic importer always has a single actor
-          if (genericImporter->GetImportedImage())
+          // Use indexed accessor for composite support
+          if (genericImporter->GetImportedImage(actorIndex))
           {
-            datasetForColoring = genericImporter->GetImportedImage();
+            datasetForColoring = genericImporter->GetImportedImage(actorIndex);
           }
-          else if (genericImporter->GetImportedPoints())
+          else if (genericImporter->GetImportedPoints(actorIndex))
           {
-            datasetForColoring = genericImporter->GetImportedPoints();
+            datasetForColoring = genericImporter->GetImportedPoints(actorIndex);
           }
         }
         this->Pimpl->ColoringInfoHandler.UpdateColoringInfo(datasetForColoring, false);
         this->Pimpl->ColoringInfoHandler.UpdateColoringInfo(datasetForColoring, true);
+
+        actorIndex++;
       }
     }
     this->Pimpl->ColoringInfoTime.Modified();
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DMetaImporter.h f3d-3.4.1/vtkext/private/module/vtkF3DMetaImporter.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DMetaImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DMetaImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -141,8 +141,8 @@
   void EnableAnimation(vtkIdType animationIndex) override;
   void DisableAnimation(vtkIdType animationIndex) override;
   bool IsAnimationEnabled(vtkIdType animationIndex) override;
-  bool GetTemporalInformation(vtkIdType animationIndex, double frameRate, int& nbTimeSteps,
-    double timeRange[2], vtkDoubleArray* timeSteps) override;
+  bool GetTemporalInformation(vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps,
+    vtkDoubleArray* timeSteps) override;
   ///@}
 
   ///@{
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DObjectFactory.cxx f3d-3.4.1/vtkext/private/module/vtkF3DObjectFactory.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DObjectFactory.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DObjectFactory.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -4,8 +4,7 @@
 
 #include "vtkF3DPolyDataMapper.h"
 
-#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__) &&                                           \
-  VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
 #include <vtkF3DPointSplatMapper.h>
 #endif
 
@@ -39,8 +38,7 @@
   this->RegisterOverride("vtkPolyDataMapper", "vtkF3DPolyDataMapper",
     "vtkPolyDataMapper override for F3D", 1, ::Factory<vtkF3DPolyDataMapper>);
 
-#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__) &&                                           \
-  VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
   this->RegisterOverride("vtkPointGaussianMapper", "vtkF3DPointSplatMapper",
     "vtkPointGaussianMapper override for F3D", 1, ::Factory<vtkF3DPointSplatMapper>);
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOrientationMarkerWidget.cxx f3d-3.4.1/vtkext/private/module/vtkF3DOrientationMarkerWidget.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOrientationMarkerWidget.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DOrientationMarkerWidget.cxx	1970-01-01 02:00:00.000000000 +0200
@@ -1,10 +0,0 @@
-#include "vtkF3DOrientationMarkerWidget.h"
-
-#include <vtkObjectFactory.h>
-
-vtkStandardNewMacro(vtkF3DOrientationMarkerWidget);
-
-vtkF3DOrientationMarkerWidget::vtkF3DOrientationMarkerWidget()
-{
-  this->Interactive = 0;
-}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOrientationMarkerWidget.h f3d-3.4.1/vtkext/private/module/vtkF3DOrientationMarkerWidget.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOrientationMarkerWidget.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DOrientationMarkerWidget.h	1970-01-01 02:00:00.000000000 +0200
@@ -1,31 +0,0 @@
-/**
- * @class   vtkF3DOrientationMarkerWidget
- * @brief   A vtkF3DOrientationMarkerWidget which is non interactive by default
- *
- * In VTK, calling vtkF3DOrientationMarkerWidget::InteractiveOff triggers a render
- * which we do not want as the renderer may not be ready to render when setting up the
- * widget. This initialize Interactive to false, removing the need to call the setter.
- * See https://gitlab.kitware.com/vtk/vtk/-/issues/18660.
- */
-
-#ifndef vtkF3DOrientationMarkerWidget_h
-#define vtkF3DOrientationMarkerWidget_h
-
-#include "vtkOrientationMarkerWidget.h"
-
-class vtkF3DOrientationMarkerWidget : public vtkOrientationMarkerWidget
-{
-public:
-  static vtkF3DOrientationMarkerWidget* New();
-  vtkTypeMacro(vtkF3DOrientationMarkerWidget, vtkOrientationMarkerWidget);
-
-protected:
-  vtkF3DOrientationMarkerWidget();
-  ~vtkF3DOrientationMarkerWidget() override = default;
-
-private:
-  vtkF3DOrientationMarkerWidget(const vtkF3DOrientationMarkerWidget&) = delete;
-  void operator=(const vtkF3DOrientationMarkerWidget&) = delete;
-};
-
-#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOverlayRenderPass.cxx f3d-3.4.1/vtkext/private/module/vtkF3DOverlayRenderPass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DOverlayRenderPass.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DOverlayRenderPass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -99,7 +99,6 @@
 
   this->OverlayPass = vtkSmartPointer<vtkFramebufferPass>::New();
   this->OverlayPass->SetDelegatePass(overlayCamP);
-  this->OverlayPass->SetColorFormat(vtkTextureObject::Float32);
 }
 
 // ----------------------------------------------------------------------------
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPointSplatMapper.cxx f3d-3.4.1/vtkext/private/module/vtkF3DPointSplatMapper.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPointSplatMapper.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DPointSplatMapper.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,7 +1,11 @@
 #include "vtkF3DPointSplatMapper.h"
 
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
 #include "vtkF3DBitonicSort.h"
 #include "vtkF3DComputeDepthCS.h"
+#endif
+#include "vtkF3DPointSplatVS.h"
+#include "vtkF3DRenderer.h"
 
 #include <vtkCamera.h>
 #include <vtkObjectFactory.h>
@@ -12,13 +16,16 @@
 #include <vtkOpenGLRenderer.h>
 #include <vtkOpenGLShaderCache.h>
 #include <vtkOpenGLState.h>
+#include <vtkOpenGLVertexArrayObject.h>
 #include <vtkOpenGLVertexBufferObject.h>
 #include <vtkOpenGLVertexBufferObjectGroup.h>
 #include <vtkPointData.h>
 #include <vtkPolyData.h>
 #include <vtkShader.h>
 #include <vtkShaderProgram.h>
+#include <vtkShaderProperty.h>
 #include <vtkTextureObject.h>
+#include <vtkUniforms.h>
 #include <vtkVersion.h>
 
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240914)
@@ -42,12 +49,19 @@
 protected:
   vtkF3DSplatMapperHelper();
 
+  void GetShaderTemplate(
+    std::map<vtkShader::Type, vtkShader*> shaders, vtkRenderer* ren, vtkActor* actor) override;
+
   // overridden to create the OpenGL depth buffer
   void BuildBufferObjects(vtkRenderer* ren, vtkActor* act) override;
 
   // overridden to sort splats
   void RenderPieceDraw(vtkRenderer* ren, vtkActor* act) override;
 
+  // add instance id and TAA to fragment shader
+  void ReplaceShaderPositionVC(
+    std::map<vtkShader::Type, vtkShader*> shaders, vtkRenderer* ren, vtkActor* actor) override;
+
   // add spherical harmonics
   void ReplaceShaderColor(
     std::map<vtkShader::Type, vtkShader*> shaders, vtkRenderer* ren, vtkActor* actor) override;
@@ -61,6 +75,7 @@
     vtkOpenGLHelper& cellBO, vtkRenderer* ren, vtkActor* actor) override;
 
 private:
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
   void SortSplats(vtkRenderer* ren);
 
   vtkNew<vtkShader> DepthComputeShader;
@@ -71,6 +86,9 @@
 
   double DirectionThreshold = 0.999;
   double LastDirection[3] = { 0.0, 0.0, 0.0 };
+#endif
+
+  bool OwnerUseInstancing();
 
   int MaxTextureSize = 0;
   vtkNew<vtkTextureObject> SphericalHarmonicsTexture;
@@ -83,11 +101,30 @@
 //----------------------------------------------------------------------------
 vtkF3DSplatMapperHelper::vtkF3DSplatMapperHelper()
 {
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
   this->DepthComputeShader->SetType(vtkShader::Compute);
   this->DepthComputeShader->SetSource(vtkF3DComputeDepthCS);
   this->DepthProgram->SetComputeShader(this->DepthComputeShader);
 
   this->Sorter->Initialize(512, VTK_FLOAT, VTK_UNSIGNED_INT);
+#endif
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DSplatMapperHelper::GetShaderTemplate(
+  std::map<vtkShader::Type, vtkShader*> shaders, vtkRenderer* ren, vtkActor* actor)
+{
+  if (this->OwnerUseInstancing())
+  {
+    this->vtkOpenGLPolyDataMapper::GetShaderTemplate(shaders, ren, actor);
+
+    this->UsingPoints = false;
+    shaders[vtkShader::Vertex]->SetSource(vtkF3DPointSplatVS);
+  }
+  else
+  {
+    this->Superclass::GetShaderTemplate(shaders, ren, actor);
+  }
 }
 
 //----------------------------------------------------------------------------
@@ -104,9 +141,11 @@
 
   vtkOpenGLPointGaussianMapperHelper::BuildBufferObjects(ren, act);
 
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
   // allocate a buffer of depths used for sorting splats
   this->DepthBuffer->Allocate(splatCount * sizeof(float), vtkOpenGLBufferObject::ArrayBuffer,
     vtkOpenGLBufferObject::DynamicCopy);
+#endif
 
   this->SphericalHarmonicsDegree = 0;
 
@@ -252,6 +291,10 @@
       "sphericalHarmonics", this->SphericalHarmonicsTexture->GetTextureUnit());
   }
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251120)
+  this->VBOs->SetInstancing(this->OwnerUseInstancing());
+#endif
+
   this->Superclass::SetMapperShaderParameters(cellBO, ren, actor);
 }
 
@@ -268,6 +311,7 @@
   this->Superclass::SetCameraShaderParameters(cellBO, ren, actor);
 }
 
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
 //----------------------------------------------------------------------------
 void vtkF3DSplatMapperHelper::SortSplats(vtkRenderer* ren)
 {
@@ -275,8 +319,8 @@
 
   if (numVerts)
   {
-    double* focalPoint = ren->GetActiveCamera()->GetFocalPoint();
-    double* origin = ren->GetActiveCamera()->GetPosition();
+    const double* focalPoint = ren->GetActiveCamera()->GetFocalPoint();
+    const double* origin = ren->GetActiveCamera()->GetPosition();
     double direction[3];
 
     for (int i = 0; i < 3; ++i)
@@ -318,16 +362,118 @@
     }
   }
 }
+#endif
 
 //----------------------------------------------------------------------------
 void vtkF3DSplatMapperHelper::RenderPieceDraw(vtkRenderer* ren, vtkActor* actor)
 {
-  if (vtkShader::IsComputeShaderSupported() && actor->GetForceTranslucent())
+#if !defined(__ANDROID__) && !defined(__EMSCRIPTEN__)
+  const vtkF3DRenderer* renderer = vtkF3DRenderer::SafeDownCast(ren);
+
+  if (renderer->GetBlendingMode() == vtkF3DRenderer::BlendingMode::SORT &&
+    vtkShader::IsComputeShaderSupported() && actor->HasTranslucentPolygonalGeometry())
   {
     this->SortSplats(ren);
   }
+#endif
+
+  if (this->OwnerUseInstancing())
+  {
+    int numVerts = this->VBOs->GetNumberOfTuples("vertexMC");
+    if (numVerts)
+    {
+      this->UpdateShaders(this->Primitives[PrimitivePoints], ren, actor);
 
-  vtkOpenGLPointGaussianMapperHelper::RenderPieceDraw(ren, actor);
+      this->Primitives[PrimitivePoints].VAO->Bind();
+      glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, numVerts);
+      this->Primitives[PrimitivePoints].VAO->Release();
+    }
+  }
+  else
+  {
+    // Use VTK geometry shader based rendering
+    vtkOpenGLPointGaussianMapperHelper::RenderPieceDraw(ren, actor);
+  }
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DSplatMapperHelper::ReplaceShaderPositionVC(
+  std::map<vtkShader::Type, vtkShader*> shaders, vtkRenderer* ren, vtkActor* actor)
+{
+  vtkUniforms* uniforms = actor->GetShaderProperty()->GetVertexCustomUniforms();
+
+  // TAA
+  vtkUniforms::TupleType type = uniforms->GetUniformTupleType("jitter");
+  if (type != vtkUniforms::TupleTypeInvalid)
+  {
+    std::string VSSource = shaders[vtkShader::Vertex]->GetSource();
+
+    vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Dec",
+      "//VTK::PositionVC::Dec\n"
+      "  uniform vec2 jitter;\n",
+      false);
+
+    vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Impl",
+      "//VTK::PositionVC::Impl\n"
+      "  // apply temporal jittering for TAA\n"
+      "  gl_Position.xy += jitter * gl_Position.w;\n",
+      false);
+
+    shaders[vtkShader::Vertex]->SetSource(VSSource);
+  }
+
+  if (this->OwnerUseInstancing())
+  {
+    std::string FSSource = shaders[vtkShader::Fragment]->GetSource();
+
+    vtkShaderProgram::Substitute(FSSource, "//VTK::PositionVC::Dec\n",
+      "//VTK::PositionVC::Dec\n"
+      "flat in int instanceId;\n");
+
+    shaders[vtkShader::Fragment]->SetSource(FSSource);
+  }
+
+  std::string VSSource = shaders[vtkShader::Vertex]->GetSource();
+
+  if (this->OwnerUseInstancing())
+  {
+    vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Dec\n",
+      "uniform float boundScale;\n"
+      "out vec2 offsetVCVSOutput;\n"
+      "flat out int instanceId;\n");
+
+    vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Impl\n",
+      R"(
+  vec2 offsets[4] = vec2[](vec2(-boundScale, -boundScale),
+                           vec2(boundScale, -boundScale),
+                           vec2(-boundScale, boundScale),
+                           vec2(boundScale, boundScale));
+
+  offsetVCVSOutput = offsets[gl_VertexID % 4];
+
+  vec4 posNDC = posDC;
+  posNDC = posNDC / posNDC.w;
+
+  gl_Position = vec4(posNDC.xy + transform * offsetVCVSOutput, posNDC.zw);
+
+  instanceId = gl_InstanceID;
+)");
+  }
+  else
+  {
+    vtkShaderProgram::Substitute(
+      VSSource, "//VTK::PositionVC::Dec\n", "out mat2 transformVCVSOutput;\n");
+
+    vtkShaderProgram::Substitute(VSSource, "//VTK::PositionVC::Impl\n",
+      R"(
+  transformVCVSOutput = transform;
+  gl_Position = posDC;
+)");
+  }
+
+  shaders[vtkShader::Vertex]->SetSource(VSSource);
+
+  this->Superclass::ReplaceShaderPositionVC(shaders, ren, actor);
 }
 
 //----------------------------------------------------------------------------
@@ -338,16 +484,19 @@
   {
     std::string VSSource = shaders[vtkShader::Vertex]->GetSource();
 
-    vtkShaderProgram::Substitute(VSSource, "//VTK::Color::Dec",
-      "//VTK::Color::Dec\n\n"
-      "uniform sampler2DArray sphericalHarmonics;\n"
-      "uniform vec3 cameraDirection;\n"
-      "vec3 decode(ivec3 texelIndex)\n"
-      "{\n"
-      "  vec3 texel = texelFetch(sphericalHarmonics, texelIndex, 0).rgb;\n"
-      "  return texel * 2.0 - 1.0;\n"
-      "}\n\n",
-      false);
+    if (this->SphericalHarmonicsDegree > 0)
+    {
+      vtkShaderProgram::Substitute(VSSource, "//VTK::Color::Dec",
+        "//VTK::Color::Dec\n\n"
+        "uniform sampler2DArray sphericalHarmonics;\n"
+        "uniform vec3 cameraDirection;\n"
+        "vec3 decode(ivec3 texelIndex)\n"
+        "{\n"
+        "  vec3 texel = texelFetch(sphericalHarmonics, texelIndex, 0).rgb;\n"
+        "  return texel * 2.0 - 1.0;\n"
+        "}\n\n",
+        false);
+    }
 
     std::stringstream shStr;
     shStr << "//VTK::Color::Impl\n";
@@ -355,8 +504,18 @@
     if (this->SphericalHarmonicsDegree >= 1)
     {
       shStr << "  vec3 sh1 = vec3(0);\n";
-      shStr << "  ivec2 texelIndex = ivec2(gl_VertexID % " << this->MaxTextureSize
-            << ", gl_VertexID / " << this->MaxTextureSize << ");\n";
+
+      if (this->OwnerUseInstancing())
+      {
+        shStr << "  ivec2 texelIndex = ivec2(gl_InstanceID % " << this->MaxTextureSize
+              << ", gl_InstanceID / " << this->MaxTextureSize << ");\n";
+      }
+      else
+      {
+        shStr << "  ivec2 texelIndex = ivec2(gl_VertexID % " << this->MaxTextureSize
+              << ", gl_VertexID / " << this->MaxTextureSize << ");\n";
+      }
+
       shStr << "  sh1 -= 0.48860251 * decode(ivec3(texelIndex, 0)) * cameraDirection.y;\n";
       shStr << "  sh1 += 0.48860251 * decode(ivec3(texelIndex, 1)) * cameraDirection.z;\n";
       shStr << "  sh1 -= 0.48860251 * decode(ivec3(texelIndex, 2)) * cameraDirection.x;\n";
@@ -417,6 +576,18 @@
 }
 
 //----------------------------------------------------------------------------
+bool vtkF3DSplatMapperHelper::OwnerUseInstancing()
+{
+  bool useInstancing = false;
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251120)
+  useInstancing = vtkF3DPointSplatMapper::SafeDownCast(this->Owner)->GetUseInstancing();
+#endif
+
+  return useInstancing;
+}
+
+//----------------------------------------------------------------------------
 vtkOpenGLPointGaussianMapperHelper* vtkF3DPointSplatMapper::CreateHelper()
 {
   auto helper = vtkF3DSplatMapperHelper::New();
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPointSplatMapper.h f3d-3.4.1/vtkext/private/module/vtkF3DPointSplatMapper.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPointSplatMapper.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DPointSplatMapper.h	2026-01-08 08:41:55.000000000 +0200
@@ -16,8 +16,21 @@
   static vtkF3DPointSplatMapper* New();
   vtkTypeMacro(vtkF3DPointSplatMapper, vtkOpenGLPointGaussianMapper);
 
+  //@{
+  /**
+   * Use instancing or VTK geometry shader based pipeline.
+   * Instancing works on GLES devices but doesn't support depth sorting.
+   * Default is true.
+   */
+  vtkGetMacro(UseInstancing, bool);
+  vtkSetMacro(UseInstancing, bool);
+  //@}
+
 protected:
   vtkOpenGLPointGaussianMapperHelper* CreateHelper() override;
+
+private:
+  bool UseInstancing = true;
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPolyDataMapper.cxx f3d-3.4.1/vtkext/private/module/vtkF3DPolyDataMapper.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPolyDataMapper.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DPolyDataMapper.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -24,11 +24,7 @@
 //-----------------------------------------------------------------------------
 vtkF3DPolyDataMapper::vtkF3DPolyDataMapper()
 {
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20230518)
   this->SetVBOShiftScaleMethod(vtkPolyDataMapper::ShiftScaleMethodType::DISABLE_SHIFT_SCALE);
-#else
-  this->SetVBOShiftScaleMethod(vtkOpenGLVertexBufferObject::DISABLE_SHIFT_SCALE);
-#endif
 
   // map glTF arrays to GPU VBOs
   this->MapDataArrayToVertexAttribute(
@@ -217,6 +213,13 @@
 
   posImpl += "  gl_Position = MCDCMatrix * posMC;\n";
 
+  // TAA
+  type = uniforms->GetUniformTupleType("jitter");
+  if (type)
+  {
+    posImpl += "  gl_Position.xy += jitter * gl_Position.w;\n";
+  }
+
   if (this->PrimitiveInfo[this->LastBoundBO].LastLightComplexity > 0)
   {
     posImpl += "  vertexVCVSOutput = MCVCMatrix * posMC;\n";
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPolyDataMapper.h f3d-3.4.1/vtkext/private/module/vtkF3DPolyDataMapper.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPolyDataMapper.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DPolyDataMapper.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,7 +1,11 @@
 /**
  * @class   vtkF3DPolyDataMapper
- * @brief   Custom surface mapper used to include skinning and morphing for glTF format
+ * @brief   Custom surface mapper used to include F3D features
  *
+ * This mapper is used to add many F3D custom features:
+ * - skinning and morphing capabilities
+ * - support for MatCap rendering
+ * - support for TAA jittering
  */
 
 #ifndef vtkF3DPolyDataMapper_h
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPostProcessFilter.cxx f3d-3.4.1/vtkext/private/module/vtkF3DPostProcessFilter.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DPostProcessFilter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DPostProcessFilter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,10 +1,6 @@
 #include "vtkF3DPostProcessFilter.h"
 
-#include "F3DLog.h"
-
 #include <vtkAppendPolyData.h>
-#include <vtkCompositeDataIterator.h>
-#include <vtkCompositeDataSet.h>
 #include <vtkDataObject.h>
 #include <vtkDataSetSurfaceFilter.h>
 #include <vtkImageData.h>
@@ -38,54 +34,8 @@
   vtkPolyData* outputPoints = vtkPolyData::GetData(outputVector, 1);
   vtkImageData* outputImage = vtkImageData::GetData(outputVector, 2);
 
-  vtkCompositeDataSet* composite = vtkCompositeDataSet::SafeDownCast(dataObject);
   vtkSmartPointer<vtkDataSet> dataset = vtkDataSet::SafeDownCast(dataObject);
 
-  // Extract data from a composite dataset
-  if (composite)
-  {
-    auto iter = vtkSmartPointer<vtkCompositeDataIterator>::Take(composite->NewIterator());
-    iter->SkipEmptyNodesOn();
-
-    // If it contains a single leaf, extract it as is
-    int nLeaf = 0;
-    for (iter->InitTraversal(); !iter->IsDoneWithTraversal(); iter->GoToNextItem())
-    {
-      dataset = vtkDataSet::SafeDownCast(iter->GetCurrentDataObject());
-      nLeaf++;
-    }
-
-    // If multiple leaves, extract all surfaces and append them together
-    if (nLeaf > 1)
-    {
-      vtkNew<vtkAppendPolyData> append;
-      for (iter->InitTraversal(); !iter->IsDoneWithTraversal(); iter->GoToNextItem())
-      {
-        vtkDataSet* leafDS = vtkDataSet::SafeDownCast(iter->GetCurrentDataObject());
-        vtkSmartPointer<vtkPolyData> leafPD = vtkPolyData::SafeDownCast(leafDS);
-        if (!leafDS)
-        {
-          F3DLog::Print(F3DLog::Severity::Warning,
-            "A non data set block was ignored while reading a composite.");
-        }
-        else
-        {
-          if (!leafPD)
-          {
-            vtkNew<vtkDataSetSurfaceFilter> geom;
-            geom->SetInputData(iter->GetCurrentDataObject());
-            geom->Update();
-            leafPD = vtkPolyData::SafeDownCast(geom->GetOutput());
-          }
-          append->AddInputData(leafPD);
-        }
-      }
-
-      append->Update();
-      dataset = append->GetOutput();
-    }
-  }
-
   // If the input is a polydata or an unstructured grid without cells, add a polyvertex cell
   vtkPolyData* pd = vtkPolyData::SafeDownCast(dataset);
   vtkUnstructuredGrid* ug = vtkUnstructuredGrid::SafeDownCast(dataset);
@@ -158,7 +108,6 @@
 int vtkF3DPostProcessFilter::FillInputPortInformation(int vtkNotUsed(port), vtkInformation* info)
 {
   info->Set(vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet");
-  info->Append(vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE(), "vtkCompositeDataSet");
   return 1;
 }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderer.cxx f3d-3.4.1/vtkext/private/module/vtkF3DRenderer.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderer.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DRenderer.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -5,16 +5,20 @@
 #include "F3DLog.h"
 #include "vtkF3DCachedLUTTexture.h"
 #include "vtkF3DCachedSpecularTexture.h"
+#include "vtkF3DInteractorStyle.h"
 #include "vtkF3DOpenGLGridMapper.h"
 #include "vtkF3DOverlayRenderPass.h"
 #include "vtkF3DPolyDataMapper.h"
 #include "vtkF3DRenderPass.h"
 #include "vtkF3DSolidBackgroundPass.h"
+#include "vtkF3DTAAResolvePass.h"
 #include "vtkF3DUserRenderPass.h"
 
 #include <vtkAxesActor.h>
 #include <vtkBoundingBox.h>
 #include <vtkCamera.h>
+#include <vtkCameraOrientationRepresentation.h>
+#include <vtkCameraOrientationWidget.h>
 #include <vtkCellData.h>
 #include <vtkCornerAnnotation.h>
 #include <vtkCullerCollection.h>
@@ -27,6 +31,7 @@
 #include <vtkLightCollection.h>
 #include <vtkLightKit.h>
 #include <vtkMath.h>
+#include <vtkMathUtilities.h>
 #include <vtkMatrix4x4.h>
 #include <vtkMultiBlockDataSet.h>
 #include <vtkObjectFactory.h>
@@ -34,6 +39,7 @@
 #include <vtkOpenGLRenderWindow.h>
 #include <vtkOpenGLRenderer.h>
 #include <vtkOpenGLTexture.h>
+#include <vtkOrientationMarkerWidget.h>
 #include <vtkPBRLUTTexture.h>
 #include <vtkPNGReader.h>
 #include <vtkPiecewiseFunction.h>
@@ -42,15 +48,19 @@
 #include <vtkPolyData.h>
 #include <vtkProperty.h>
 #include <vtkRenderWindow.h>
+#include <vtkRenderWindowInteractor.h>
 #include <vtkSSAAPass.h>
 #include <vtkScalarBarActor.h>
+#include <vtkShaderProperty.h>
 #include <vtkSkybox.h>
+#include <vtkSphericalHarmonics.h>
 #include <vtkTable.h>
 #include <vtkTextActor.h>
 #include <vtkTextProperty.h>
 #include <vtkTextureObject.h>
 #include <vtkToneMappingPass.h>
 #include <vtkTransform.h>
+#include <vtkUniforms.h>
 #include <vtkVersion.h>
 #include <vtkVolumeProperty.h>
 #include <vtkXMLImageDataReader.h>
@@ -62,14 +72,20 @@
 #include <vtksys/MD5.h>
 #include <vtksys/SystemTools.hxx>
 
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
-#include <vtkSphericalHarmonics.h>
+#if F3D_MODULE_UI
+#include "F3DStyle.h"
 #endif
 
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20220907)
-#include <vtkOrientationMarkerWidget.h>
-#else
-#include "vtkF3DOrientationMarkerWidget.h"
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
+#include "vtkF3DPointSplatMapper.h"
+#endif
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+#include <vtkMemoryResourceStream.h>
+#endif
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+#include <vtkGridAxesActor3D.h>
 #endif
 
 #if F3D_MODULE_RAYTRACING
@@ -108,7 +124,6 @@
   return collapsed;
 }
 
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
 //----------------------------------------------------------------------------
 // Compute the MD5 hash of an existing file on disk
 std::string ComputeFileHash(const std::string& filepath)
@@ -161,7 +176,6 @@
   return img;
 }
 #endif
-#endif
 
 //----------------------------------------------------------------------------
 // TODO : add this function in a utils file for rendering in VTK directly
@@ -202,6 +216,21 @@
 
   return texture;
 }
+
+template<typename F>
+void ExecFuncOnAllPolyDataUniforms(vtkActorCollection* actors, F&& func)
+{
+  actors->InitTraversal();
+  vtkActor* actor = nullptr;
+
+  while ((actor = actors->GetNextActor()))
+  {
+    if (actor->GetMapper() && actor->GetMapper()->IsA("vtkPolyDataMapper"))
+    {
+      func(actor->GetShaderProperty()->GetVertexCustomUniforms());
+    }
+  }
+}
 }
 
 //----------------------------------------------------------------------------
@@ -222,9 +251,7 @@
   this->EnvMapLookupTable = vtkF3DCachedLUTTexture::New();
   this->EnvMapPrefiltered = vtkF3DCachedSpecularTexture::New();
 #endif
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
   this->EnvMapPrefiltered->HalfPrecisionOff();
-#endif
 
   this->SkyboxActor->SetProjection(vtkSkybox::Sphere);
   this->SkyboxActor->GammaCorrectOn();
@@ -267,6 +294,11 @@
   this->AddActor(this->SkyboxActor);
   this->AddActor(this->UIActor);
 
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+  this->AddActor(this->GridAxesActor);
+  this->GridAxesActor->SetUseBounds(false);
+#endif
+
   this->GridConfigured = false;
   this->CheatSheetConfigured = false;
   this->ActorsPropertiesConfigured = false;
@@ -288,7 +320,7 @@
 
   this->ColorTransferFunctionConfigured = false;
   this->ColoringMappersConfigured = false;
-  this->PointSpritesMappersConfigured = false;
+  this->ColoringPointSpritesMappersConfigured = false;
   this->VolumePropsAndMappersConfigured = false;
   this->ScalarBarActorConfigured = false;
   this->CheatSheetConfigured = false;
@@ -296,68 +328,131 @@
 
   // create ImGui context if F3D_MODULE_UI is enabled
   this->UIActor->Initialize(vtkOpenGLRenderWindow::SafeDownCast(this->RenderWindow));
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251001)
+  // create a window resize callback for axis
+  this->ModernAxisWidgetResizeCallback = vtkSmartPointer<vtkCallbackCommand>::New();
+  this->ModernAxisWidgetResizeCallback->SetClientData(this);
+  this->ModernAxisWidgetResizeCallback->SetCallback(
+    [](vtkObject* const, unsigned long, void* clientData, void*)
+    {
+      vtkF3DRenderer* self = static_cast<vtkF3DRenderer*>(clientData);
+      self->UpdateAxisWidgetSize();
+    });
+  this->RenderWindow->AddObserver(
+    vtkCommand::WindowResizeEvent, this->ModernAxisWidgetResizeCallback);
+#endif
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::ApplyUpDirection(const std::array<double, 3>& up)
+{
+  std::array<double, 3> right = { 1, 0, 0 };
+  if (std::abs(vtkMath::Dot(right.data(), up.data())) > 0.999)
+  {
+    right = { 0, 1, 0 };
+  }
+
+  std::array<double, 3> front;
+  vtkMath::Cross(right.data(), up.data(), front.data());
+  vtkMath::Normalize(front.data());
+
+  std::array<double, 3> orthRight;
+  vtkMath::Cross(up.data(), front.data(), orthRight.data());
+  vtkMath::Normalize(orthRight.data());
+
+  std::copy(up.begin(), up.end(), this->UpDirection);
+  std::copy(orthRight.begin(), orthRight.end(), this->RightDirection);
+
+  this->SkyboxActor->SetFloorPlane(up[0], up[1], up[2], 0.0);
+  this->SkyboxActor->SetFloorRight(front[0], front[1], front[2]);
+
+  this->SetEnvironmentUp(this->UpDirection);
+  this->SetEnvironmentRight(this->RightDirection);
 }
 
 //----------------------------------------------------------------------------
-void vtkF3DRenderer::InitializeUpVector(const std::vector<double>& upVec)
+void vtkF3DRenderer::InitializeUpDirection(const std::vector<double>& upVec)
 {
   assert(upVec.size() == 3);
 
-  const auto isNullVector = [](const std::array<double, 3>& v)
-  {
-    constexpr double e = 1e-8;
-    return ::abs(v[0]) < e && ::abs(v[1]) < e && ::abs(v[2]) < e;
-  };
+  this->SetPendingUpDirection(upVec);
+  this->ConfigureUpDirection();
+
+  double pos[3];
+  vtkMath::Cross(this->UpDirection, this->RightDirection, pos);
+  vtkMath::MultiplyScalar(pos, -1.0);
+
+  vtkCamera* cam = this->GetActiveCamera();
+  cam->SetFocalPoint(0.0, 0.0, 0.0);
+  cam->SetPosition(pos);
+  cam->SetViewUp(this->UpDirection);
+  this->UpDirectionConfigured = true;
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::SetPendingUpDirection(const std::vector<double>& upVec)
+{
+  assert(upVec.size() == 3);
 
   std::array<double, 3> up = { upVec[0], upVec[1], upVec[2] };
-  std::array<double, 3> right = { 1, 0, 0 };
 
-  /* if `up` is `(0,0,0)` make it `(0,1,0)` */
-  if (isNullVector(up))
+  constexpr double e = 1e-8;
+  if (std::abs(up[0]) < e && std::abs(up[1]) < e && std::abs(up[2]) < e)
   {
-    up[1] = 1.0;
-    F3DLog::Print(F3DLog::Severity::Warning, "null up vector, using (0,0,1) instead");
+    up = { 0.0, 1.0, 0.0 };
   }
+
   vtkMath::Normalize(up.data());
 
-  /* make sure `right` is not colinear with `up` */
-  if (::abs(vtkMath::Dot(right, up)) > 0.999)
+  if (vtkMathUtilities::FuzzyCompare(up[0], this->PendingUpDirection[0]) &&
+    vtkMathUtilities::FuzzyCompare(up[1], this->PendingUpDirection[1]) &&
+    vtkMathUtilities::FuzzyCompare(up[2], this->PendingUpDirection[2]))
   {
-    right = { 0, 1, 0 };
+    return;
   }
 
-  /* make `front` orthogonal */
-  std::array<double, 3> front;
-  vtkMath::Cross(right.data(), up.data(), front.data());
-  vtkMath::Normalize(front.data());
+  std::copy(up.begin(), up.end(), this->PendingUpDirection);
+  this->UpDirectionConfigured = false;
+}
 
-  /* ensure `right` is orthogonal */
-  vtkMath::Cross(up.data(), front.data(), right.data());
-  vtkMath::Normalize(right.data());
-
-  this->UpVector[0] = up[0];
-  this->UpVector[1] = up[1];
-  this->UpVector[2] = up[2];
-  this->RightVector[0] = right[0];
-  this->RightVector[1] = right[1];
-  this->RightVector[2] = right[2];
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::ConfigureUpDirection()
+{
+  std::array<double, 3> newUp = { this->PendingUpDirection[0], this->PendingUpDirection[1],
+    this->PendingUpDirection[2] };
+  std::array<double, 3> oldUp = { this->UpDirection[0], this->UpDirection[1],
+    this->UpDirection[2] };
 
-  double pos[3];
-  vtkMath::Cross(this->UpVector, this->RightVector, pos);
-  vtkMath::MultiplyScalar(pos, -1.0);
+  std::array<double, 3> axis;
+  vtkMath::Cross(oldUp.data(), newUp.data(), axis.data());
+  double sinAngle = vtkMath::Normalize(axis.data());
+  double cosAngle = vtkMath::Dot(oldUp.data(), newUp.data());
+  double angle = std::atan2(sinAngle, cosAngle) * 180.0 / vtkMath::Pi();
 
   vtkCamera* cam = this->GetActiveCamera();
-  cam->SetFocalPoint(0.0, 0.0, 0.0);
+  double foc[3];
+  cam->GetFocalPoint(foc);
+
+  vtkNew<vtkTransform> camTransform;
+  camTransform->Translate(foc[0], foc[1], foc[2]);
+  camTransform->RotateWXYZ(angle, axis.data());
+  camTransform->Translate(-foc[0], -foc[1], -foc[2]);
+
+  double pos[3];
+  cam->GetPosition(pos);
+  camTransform->TransformPoint(pos, pos);
   cam->SetPosition(pos);
-  cam->SetViewUp(this->UpVector);
 
-  // skybox orientation
-  this->SkyboxActor->SetFloorPlane(this->UpVector[0], this->UpVector[1], this->UpVector[2], 0.0);
-  this->SkyboxActor->SetFloorRight(front[0], front[1], front[2]);
+  double viewUp[3];
+  cam->GetViewUp(viewUp);
+  camTransform->TransformVector(viewUp, viewUp);
+  cam->SetViewUp(viewUp);
 
-  // environment orientation
-  this->SetEnvironmentUp(this->UpVector);
-  this->SetEnvironmentRight(this->RightVector);
+  this->ApplyUpDirection(newUp);
+
+  this->GridConfigured = false;
+  this->UpDirectionConfigured = true;
 }
 
 //----------------------------------------------------------------------------
@@ -375,7 +470,6 @@
   newPass->SetUseRaytracing(this->UseRaytracing);
 #endif
   newPass->SetUseSSAOPass(this->UseSSAOPass);
-  newPass->SetUseDepthPeelingPass(this->UseDepthPeelingPass);
   newPass->SetUseBlurBackground(this->UseBlurBackground);
   newPass->SetCircleOfConfusionRadius(this->CircleOfConfusionRadius);
   newPass->SetForceOpaqueBackground(this->HDRISkyboxVisible);
@@ -430,6 +524,18 @@
     renderingPass = fxaaP;
   }
 
+  if (this->AntiAliasingModeEnabled == vtkF3DRenderer::AntiAliasingMode::TAA)
+  {
+    vtkNew<vtkF3DTAAResolvePass> taaP;
+    taaP->SetDelegatePass(renderingPass);
+    renderingPass = taaP;
+
+    this->RenderWindow->AddObserver(
+      vtkCommand::WindowResizeEvent, taaP.Get(), &vtkF3DTAAResolvePass::ResetIterations);
+    this->RenderWindow->GetInteractor()->GetInteractorStyle()->AddObserver(
+      vtkCommand::InteractionEvent, taaP.Get(), &vtkF3DTAAResolvePass::ResetIterations);
+  }
+
   if (this->FinalShader.has_value())
   {
     // basic validation
@@ -486,8 +592,10 @@
   double bounds[6];
   this->ComputeVisiblePropBounds(bounds);
 
-  stream << "Scene bounding box: " << bounds[0] << "," << bounds[1] << "," << bounds[2] << ","
-         << bounds[3] << "," << bounds[4] << "," << bounds[5] << "\n\n";
+  stream << "Scene bounding box: "                                  //
+         << bounds[0] << u8" \u2264 x \u2264 " << bounds[1] << ", " //
+         << bounds[2] << u8" \u2264 y \u2264 " << bounds[3] << ", " //
+         << bounds[4] << u8" \u2264 z \u2264 " << bounds[5] << "\n\n";
 
   // Camera Info
   vtkCamera* cam = this->GetActiveCamera();
@@ -498,9 +606,9 @@
   cam->GetFocalPoint(focal);
   cam->GetViewUp(up);
 
-  stream << "Camera position: " << position[0] << "," << position[1] << "," << position[2] << "\n"
-         << "Camera focal point: " << focal[0] << "," << focal[1] << "," << focal[2] << "\n"
-         << "Camera view up: " << up[0] << "," << up[1] << "," << up[2] << "\n"
+  stream << "Camera position: " << position[0] << ", " << position[1] << ", " << position[2] << "\n"
+         << "Camera focal point: " << focal[0] << ", " << focal[1] << ", " << focal[2] << "\n"
+         << "Camera view up: " << up[0] << ", " << up[1] << ", " << up[2] << "\n"
          << "Camera view angle: " << cam->GetViewAngle() << "\n\n";
   descr += stream.str();
 
@@ -513,6 +621,20 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::UpdateAxisWidgetSize()
+{
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251001)
+  if (this->ModernAxisRepresentation)
+  {
+    int* size = this->GetSize();
+    // Maintain the axis widget size proportional (15%) to the shortest viewport dimension.
+    int widgetSize = static_cast<int>(std::min(size[0], size[1]) * 0.15);
+    this->ModernAxisRepresentation->SetSize(widgetSize, widgetSize);
+  }
+#endif
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ShowAxis(bool show)
 {
   // Dynamic visible axis
@@ -522,24 +644,49 @@
   // care when destructing this renderer
   if (this->AxisVisible != show)
   {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251001)
+    this->ModernAxisWidget = nullptr;
+    this->ModernAxisRepresentation = nullptr;
+    this->AxesActorConfigured = false;
+#else
     this->AxisWidget = nullptr;
+#endif
     if (show)
     {
+      // Needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12489
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251001)
+      this->ModernAxisWidget = vtkSmartPointer<vtkCameraOrientationWidget>::New();
+      this->ModernAxisRepresentation = vtkSmartPointer<vtkCameraOrientationRepresentation>::New();
+      this->ModernAxisRepresentation->SetRenderer(this);
+      this->ModernAxisRepresentation->AnchorToLowerRight();
+      this->ModernAxisRepresentation->ContainerVisibilityOn();
+
+#if F3D_MODULE_UI
+      auto containerProperty = this->ModernAxisRepresentation->GetContainerProperty();
+      containerProperty->SetOpacity(this->ModernAxisBackdropOpacity);
+      const auto [r, g, b] = F3DStyle::GetF3DBlack();
+      containerProperty->SetColor(r, g, b);
+#endif
+
+      this->ModernAxisWidget->SetRepresentation(this->ModernAxisRepresentation);
+
+      this->ModernAxisWidget->SetParentRenderer(this);
+      this->ModernAxisWidget->SetKeyPressActivation(false);
+      this->ModernAxisWidget->SetProcessEvents(false);
+      this->ModernAxisWidget->On();
+
+      this->UpdateAxisWidgetSize();
+#else
       assert(this->RenderWindow->GetInteractor());
       vtkNew<vtkAxesActor> axes;
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20220907)
       this->AxisWidget = vtkSmartPointer<vtkOrientationMarkerWidget>::New();
-#else
-      this->AxisWidget = vtkSmartPointer<vtkF3DOrientationMarkerWidget>::New();
-#endif
       this->AxisWidget->SetOrientationMarker(axes);
       this->AxisWidget->SetInteractor(this->RenderWindow->GetInteractor());
       this->AxisWidget->SetViewport(0.85, 0.0, 1.0, 0.15);
       this->AxisWidget->On();
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20220907)
       this->AxisWidget->InteractiveOff();
-#endif
       this->AxisWidget->SetKeyPressActivation(false);
+#endif
     }
 
     this->AxisVisible = show;
@@ -549,6 +696,17 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::ConfigureAxesActor()
+{
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251001)
+  this->ModernAxisRepresentation->SetXAxisColor(this->ColorAxisX);
+  this->ModernAxisRepresentation->SetYAxisColor(this->ColorAxisY);
+  this->ModernAxisRepresentation->SetZAxisColor(this->ColorAxisZ);
+  this->AxesActorConfigured = true;
+#endif
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::SetGridAbsolute(bool absolute)
 {
   if (this->GridAbsolute != absolute)
@@ -594,6 +752,24 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::SetAxesColor(const std::vector<double>& colorXAxis,
+  const std::vector<double>& colorYAxis, const std::vector<double>& colorZAxis)
+{
+  assert(colorXAxis.size() == 3 && colorYAxis.size() == 3 && colorZAxis.size() == 3);
+
+  if (!std::equal(colorXAxis.begin(), colorXAxis.end(), this->ColorAxisX) ||
+    !std::equal(colorYAxis.begin(), colorYAxis.end(), this->ColorAxisY) ||
+    !std::equal(colorZAxis.begin(), colorZAxis.end(), this->ColorAxisZ))
+  {
+    std::copy(colorXAxis.begin(), colorXAxis.end(), this->ColorAxisX);
+    std::copy(colorYAxis.begin(), colorYAxis.end(), this->ColorAxisY);
+    std::copy(colorZAxis.begin(), colorZAxis.end(), this->ColorAxisZ);
+    this->GridConfigured = false;
+    this->AxesActorConfigured = false;
+  }
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ShowGrid(bool show)
 {
   if (this->GridVisible != show)
@@ -678,14 +854,13 @@
              << "]\n\n";
       this->GridInfo = stream.str();
 
-      vtkNew<vtkF3DOpenGLGridMapper> gridMapper;
-      gridMapper->SetFadeDistance(diag);
-      gridMapper->SetUnitSquare(tmpUnitSquare);
-      gridMapper->SetSubdivisions(this->GridSubdivisions);
+      this->GridMapper->SetFadeDistance(diag);
+      this->GridMapper->SetUnitSquare(tmpUnitSquare);
+      this->GridMapper->SetSubdivisions(this->GridSubdivisions);
 
       if (this->GridAbsolute)
       {
-        gridMapper->SetOriginOffset(-center[0], -center[1], -center[2]);
+        this->GridMapper->SetOriginOffset(-center[0], -center[1], -center[2]);
       }
 
       double orientation[3];
@@ -694,11 +869,34 @@
       this->GridActor->SetPosition(gridPos);
 
       this->GridActor->GetProperty()->SetColor(this->GridColor);
-      gridMapper->SetAxis1Color(::abs(right[0]), ::abs(right[1]), ::abs(right[2]), 1);
-      gridMapper->SetAxis2Color(::abs(front[0]), ::abs(front[1]), ::abs(front[2]), 1);
+
+      const auto factor = [](const double* v, const double x, const double y, const double z)
+      {
+        const double ref[3] = { x, y, z };
+        return std::abs(vtkMath::Dot(v, ref));
+      };
+      const double fX1 = factor(right, 1, 0, 0);
+      const double fY1 = factor(right, 0, 1, 0);
+      const double fZ1 = factor(right, 0, 0, 1);
+      const double fX2 = factor(front, 1, 0, 0);
+      const double fY2 = factor(front, 0, 1, 0);
+      const double fZ2 = factor(front, 0, 0, 1);
+      const double f1 = fX1 + fY1 + fZ1;
+      const double f2 = fX2 + fY2 + fZ2;
+
+      this->GridMapper->SetAxis1Color( //
+        (this->ColorAxisX[0] * fX1 + this->ColorAxisY[0] * fY1 + this->ColorAxisZ[0] * fZ1) / f1,
+        (this->ColorAxisX[1] * fX1 + this->ColorAxisY[1] * fY1 + this->ColorAxisZ[1] * fZ1) / f1,
+        (this->ColorAxisX[2] * fX1 + this->ColorAxisY[2] * fY1 + this->ColorAxisZ[2] * fZ1) / f1,
+        1);
+      this->GridMapper->SetAxis2Color( //
+        (this->ColorAxisX[0] * fX2 + this->ColorAxisY[0] * fY2 + this->ColorAxisZ[0] * fZ2) / f2,
+        (this->ColorAxisX[1] * fX2 + this->ColorAxisY[1] * fY2 + this->ColorAxisZ[1] * fZ2) / f2,
+        (this->ColorAxisX[2] * fX2 + this->ColorAxisY[2] * fY2 + this->ColorAxisZ[2] * fZ2) / f2,
+        1);
 
       this->GridActor->ForceTranslucentOn();
-      this->GridActor->SetMapper(gridMapper);
+      this->GridActor->SetMapper(this->GridMapper);
       this->GridActor->UseBoundsOff();
       this->GridActor->PickableOff();
       this->GridConfigured = true;
@@ -710,6 +908,78 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::ShowAxesGrid([[maybe_unused]] bool show)
+{
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+  if (this->AxesGridVisible != show)
+  {
+    this->AxesGridVisible = show;
+    this->RenderPassesConfigured = false;
+    this->GridAxesConfigured = false;
+    this->CheatSheetConfigured = false;
+  }
+#endif
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::ConfigureGridAxesUsingCurrentActors()
+{
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+  bool show = this->AxesGridVisible;
+  if (show)
+  {
+    double* up = this->GetEnvironmentUp();
+    double* right = this->GetEnvironmentRight();
+    double front[3];
+    vtkMath::Cross(right, up, front);
+
+    vtkNew<vtkMatrix4x4> upMatrix;
+    const double m[16] = {
+      right[0], right[1], right[2], 0, //
+      up[0], up[1], up[2], 0,          //
+      front[0], front[1], front[2], 0, //
+      0, 0, 0, 1,                      //
+    };
+    upMatrix->DeepCopy(m);
+    vtkNew<vtkMatrix4x4> upMatrixInv;
+    upMatrixInv->DeepCopy(upMatrix);
+    upMatrixInv->Transpose();
+
+    double orientation[3];
+    vtkTransform::GetOrientation(orientation, upMatrixInv);
+    const vtkBoundingBox bbox = this->ComputeVisiblePropOrientedBounds(upMatrix);
+
+    if (!bbox.IsValid())
+    {
+      show = false;
+    }
+    else
+    {
+      this->GridAxesActor->SetOrientation(orientation);
+      this->GridAxesActor->SetVisibility(true);
+
+      double center[4] = { 0, 0, 0, 1 };
+      bbox.GetCenter(center);
+
+      this->GridAxesActor->SetPosition(center);
+
+      double a, b, c, x, y, z;
+      bbox.GetBounds(a, b, c, x, y, z);
+      GridAxesActor->SetGridBounds(a, b, c, x, y, z);
+
+      GridAxesActor->SetXTitle("X Axis");
+      GridAxesActor->SetYTitle("Y Axis");
+      GridAxesActor->SetZTitle("Z Axis");
+
+      this->GridAxesConfigured = true;
+    }
+  }
+  this->GridAxesActor->SetVisibility(show);
+
+#endif
+}
+
+//----------------------------------------------------------------------------
 vtkBoundingBox vtkF3DRenderer::ComputeVisiblePropOrientedBounds(const vtkMatrix4x4* matrix)
 {
   const auto isMatrixAxisAligned = [](const vtkMatrix4x4* m, const double tol = 1e-8)
@@ -964,6 +1234,8 @@
         if (this->HDRIReader)
         {
           this->HDRIReader->SetFileName(this->HDRIFile.c_str());
+          std::filesystem::path hdriPath(this->HDRIFile);
+          this->UIActor->SetHDRIFileName(hdriPath.filename().string().c_str());
         }
         else
         {
@@ -978,10 +1250,16 @@
     {
       // No valid HDRI file have been provided, read the default HDRI
       // TODO add support for memory buffer in the vtkHDRReader in VTK
-      // https://github.com/f3d-app/f3d/issues/935
+      // https://github.com/f3d-app/f3d/issues/1100
       this->HDRIReader = vtkSmartPointer<vtkPNGReader>::New();
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+      vtkNew<vtkMemoryResourceStream> stream;
+      stream->SetBuffer(F3DDefaultHDRI, sizeof(F3DDefaultHDRI));
+      this->HDRIReader->SetStream(stream);
+#else
       this->HDRIReader->SetMemoryBuffer(F3DDefaultHDRI);
       this->HDRIReader->SetMemoryBufferLength(sizeof(F3DDefaultHDRI));
+#endif
       this->UseDefaultHDRI = true;
     }
     this->HasValidHDRIReader = true;
@@ -992,7 +1270,6 @@
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureHDRIHash()
 {
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
   if (!this->HasValidHDRIHash && this->GetUseImageBasedLighting() && this->HasValidHDRIReader)
   {
     if (this->UseDefaultHDRI)
@@ -1007,8 +1284,6 @@
     this->HasValidHDRIHash = true;
     this->CreateCacheDirectory();
   }
-#endif
-  this->HDRIHashConfigured = true;
 }
 
 //----------------------------------------------------------------------------
@@ -1018,7 +1293,6 @@
   {
     bool needHDRITexture = this->HDRISkyboxVisible || this->GetUseImageBasedLighting();
 
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
     if (this->HasValidHDRIHash)
     {
       std::string dummy;
@@ -1027,7 +1301,6 @@
           (!this->CheckForSHCache(dummy) || !this->CheckForSpecCache(dummy) ||
             this->UseRaytracing));
     }
-#endif
 
     if (needHDRITexture)
     {
@@ -1070,8 +1343,7 @@
     // No cache support before 20221220
     // IBL without textures has been added in VTK in
     // https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10454
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220) &&                                     \
-  VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 3, 20230902)
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 3, 20230902)
     if (this->SphericalHarmonics)
     {
       this->SphericalHarmonics->Modified();
@@ -1089,7 +1361,6 @@
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureHDRILUT()
 {
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
   if (this->GetUseImageBasedLighting() && !this->HasValidHDRILUT)
   {
     vtkF3DCachedLUTTexture* lut = vtkF3DCachedLUTTexture::SafeDownCast(this->EnvMapLookupTable);
@@ -1126,14 +1397,12 @@
     }
     this->HasValidHDRILUT = true;
   }
-#endif
   this->HDRILUTConfigured = true;
 }
 
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureHDRISphericalHarmonics()
 {
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
   if (this->GetUseImageBasedLighting() && !this->HasValidHDRISH)
   {
     // Check spherical harmonics cache
@@ -1172,14 +1441,12 @@
     }
     this->HasValidHDRISH = true;
   }
-#endif
   this->HDRISphericalHarmonicsConfigured = true;
 }
 
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureHDRISpecular()
 {
-#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 2, 20221220)
   if (this->GetUseImageBasedLighting() && !this->HasValidHDRISpec)
   {
     vtkF3DCachedSpecularTexture* spec =
@@ -1230,7 +1497,6 @@
     }
     this->HasValidHDRISpec = true;
   }
-#endif
 
   this->HDRISpecularConfigured = true;
 }
@@ -1246,17 +1512,6 @@
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureTextActors()
 {
-  // Dynamic text color
-  double textColor[3];
-  if (this->IsBackgroundDark())
-  {
-    textColor[0] = textColor[1] = textColor[2] = 0.9;
-  }
-  else
-  {
-    textColor[0] = textColor[1] = textColor[2] = 0.2;
-  }
-
   // Font
   std::string fontFileStr;
   if (this->FontFile.has_value())
@@ -1321,6 +1576,15 @@
     this->TextActorsConfigured = false;
   }
 }
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::SetBackdropOpacity(const double backdropOpacity)
+{
+  this->UIActor->SetBackdropOpacity(backdropOpacity);
+  // Adjust axis opacity to visually match ImGui background (VTK appears darker)
+  this->ModernAxisBackdropOpacity = backdropOpacity - 0.3;
+}
+
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::SetBackground(const double* color)
 {
@@ -1352,17 +1616,30 @@
 }
 
 //----------------------------------------------------------------------------
-void vtkF3DRenderer::SetUseDepthPeelingPass(bool use)
+void vtkF3DRenderer::SetDropZoneBinds(
+  const std::vector<std::pair<std::string, std::string>>& dropZoneBinds)
+{
+  this->UIActor->SetDropBinds(dropZoneBinds);
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::SetBlendingMode(BlendingMode mode)
 {
-  if (this->UseDepthPeelingPass != use)
+  if (this->BlendingModeEnabled != mode)
   {
-    this->UseDepthPeelingPass = use;
+    this->BlendingModeEnabled = mode;
     this->RenderPassesConfigured = false;
     this->CheatSheetConfigured = false;
   }
 }
 
 //----------------------------------------------------------------------------
+vtkF3DRenderer::BlendingMode vtkF3DRenderer::GetBlendingMode() const
+{
+  return this->BlendingModeEnabled;
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::SetUseBlurBackground(bool use)
 {
   if (this->UseBlurBackground != use)
@@ -1503,6 +1780,17 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::ShowHDRIFilename(bool show)
+{
+  if (this->HDRIFilenameVisible != show)
+  {
+    this->HDRIFilenameVisible = show;
+    this->UIActor->SetHDRIFileNameVisibility(show);
+    this->CheatSheetConfigured = false;
+  }
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureMetaData()
 {
   this->UIActor->SetMetaDataVisibility(this->MetaDataVisible);
@@ -1568,6 +1856,16 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::ShowDropZoneLogo(bool show)
+{
+  if (this->DropZoneLogoVisible != show)
+  {
+    this->DropZoneLogoVisible = show;
+    this->UIActor->SetDropZoneLogoVisibility(show);
+  }
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ShowHDRISkybox(bool show)
 {
   if (this->HDRISkyboxVisible != show)
@@ -1667,22 +1965,23 @@
   {
     this->ActorsPropertiesConfigured = false;
     this->GridConfigured = false;
+    this->MetaDataConfigured = false;
   }
   this->ImporterTimeStamp = importerMTime;
 
   // XXX: Handle animation update in importer, which may have an impact on the colormap
   // We assume animation change do not change the number of actors
   vtkMTimeType importerUpdateMTime = this->Importer->GetUpdateMTime();
-  if (this->UsingExpandingRange && importerUpdateMTime > this->ImporterTimeStamp)
+  if (this->UsingExpandingRange && (importerUpdateMTime > this->ImporterUpdateTimeStamp) &&
+    (this->EnableColoring || (!this->UseRaytracing && this->UseVolume)))
   {
     // XXX: This could be improved further to only configure mappers and actors
     // when the coloring range actually change
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
     this->ScalarBarActorConfigured = false;
-    this->MetaDataConfigured = false;
     this->ColoringConfigured = false;
   }
   this->ImporterUpdateTimeStamp = importerUpdateMTime;
@@ -1692,6 +1991,16 @@
     this->ConfigureActorsProperties();
   }
 
+  if (!this->UpDirectionConfigured)
+  {
+    this->ConfigureUpDirection();
+  }
+
+  if (!this->PointSpritesConfigured)
+  {
+    this->ConfigurePointSprites();
+  }
+
   if (!this->ColoringConfigured)
   {
     this->ConfigureColoring();
@@ -1709,11 +2018,21 @@
     this->ConfigureTextActors();
   }
 
+  if (!this->GridAxesConfigured)
+  {
+    this->ConfigureGridAxesUsingCurrentActors();
+  }
+
   if (!this->RenderPassesConfigured)
   {
     this->ConfigureRenderPasses();
   }
 
+  if (!this->AxesActorConfigured && this->ModernAxisRepresentation)
+  {
+    this->ConfigureAxesActor();
+  }
+
   // Grid need all actors setup to be configured correctly
   if (!this->GridConfigured)
   {
@@ -1724,6 +2043,8 @@
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::Render()
 {
+  this->ConfigureJitter(this->AntiAliasingModeEnabled == vtkF3DRenderer::AntiAliasingMode::TAA);
+
   if (!this->TimerVisible)
   {
     this->Superclass::Render();
@@ -1828,14 +2149,6 @@
 }
 
 //----------------------------------------------------------------------------
-bool vtkF3DRenderer::IsBackgroundDark()
-{
-  double luminance =
-    0.299 * this->Background[0] + 0.587 * this->Background[1] + 0.114 * this->Background[2];
-  return this->HDRISkyboxVisible ? true : luminance < 0.5;
-}
-
-//----------------------------------------------------------------------------
 void vtkF3DRenderer::CreateCacheDirectory()
 {
   assert(this->HasValidHDRIHash);
@@ -1985,6 +2298,38 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::SetPointSpritesType(vtkF3DRenderer::SplatType type)
+{
+  if (this->PointSpritesType != type)
+  {
+    this->PointSpritesType = type;
+    this->PointSpritesConfigured = false;
+    this->CheatSheetConfigured = false;
+  }
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::SetPointSpritesSize(bool absoluteScale, double size)
+{
+  if (this->PointSpritesAbsoluteScale != absoluteScale || this->PointSpritesSize != size)
+  {
+    this->PointSpritesSize = size;
+    this->PointSpritesAbsoluteScale = absoluteScale;
+    this->PointSpritesConfigured = false;
+  }
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DRenderer::SetPointSpritesUseInstancing(bool useInstancing)
+{
+  if (this->PointSpritesUseInstancing != useInstancing)
+  {
+    this->PointSpritesUseInstancing = useInstancing;
+    this->PointSpritesConfigured = false;
+  }
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureActorsProperties()
 {
   assert(this->Importer);
@@ -2088,7 +2433,7 @@
     if (this->TexturesTransform.has_value())
     {
       const std::vector<double> texTransform = this->TexturesTransform.value();
-      double transform[] = {                                    //
+      const double transform[] = {                              //
         texTransform[0], texTransform[1], texTransform[2], 0.0, //
         texTransform[3], texTransform[4], texTransform[5], 0.0, //
         texTransform[6], texTransform[7], texTransform[8], 0.0, //
@@ -2171,11 +2516,14 @@
 }
 
 //----------------------------------------------------------------------------
-void vtkF3DRenderer::SetPointSpritesProperties(SplatType type, double pointSpritesSize)
+void vtkF3DRenderer::ConfigurePointSprites()
 {
-  assert(this->Importer);
+  if (!this->UsePointSprites)
+  {
+    return;
+  }
 
-  if (type == SplatType::GAUSSIAN)
+  if (!this->PointSpritesUseInstancing)
   {
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
     if (!vtkShader::IsComputeShaderSupported())
@@ -2190,46 +2538,67 @@
   const vtkBoundingBox& bbox = this->Importer->GetGeometryBoundingBox();
 
   double scaleFactor = 1.0;
-  if (bbox.IsValid())
+  if (!this->PointSpritesAbsoluteScale && bbox.IsValid())
   {
-    scaleFactor = pointSpritesSize * bbox.GetDiagonalLength() * 0.001;
+    scaleFactor = this->PointSpritesSize * bbox.GetDiagonalLength() * 0.001;
   }
 
   for (const auto& sprites : this->Importer->GetPointSpritesActorsAndMappers())
   {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20240203)
+    vtkF3DPointSplatMapper* splatMapper = vtkF3DPointSplatMapper::SafeDownCast(sprites.Mapper);
+    splatMapper->SetUseInstancing(this->PointSpritesUseInstancing);
+#endif
+
     sprites.Mapper->EmissiveOff();
-    if (type == SplatType::GAUSSIAN)
-    {
-      sprites.Mapper->SetScaleFactor(1.0);
-      sprites.Mapper->SetSplatShaderCode(nullptr); // gaussian is the default VTK shader
-      sprites.Mapper->SetScaleArray("scale");
+    sprites.Mapper->SetScaleFactor(scaleFactor);
 
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
+    // in order to make sure the sprites are at least 1 pixel large we set a lowpass matrix
+    // that will convolve the splat with another isotropic splat
+    // the value 0.3 is coming from the Gaussian Splatting paper
+    // ideally we should expose this as a parameter
+    int* viewport = this->GetSize();
+
+    float lowPass[3] = { 0.3f / (viewport[0] * viewport[0]), 0.f,
+      0.3f / (viewport[1] * viewport[1]) };
+    sprites.Mapper->SetLowpassMatrix(lowPass);
+#endif
+
+    vtkPolyData* polyData = vtkPolyData::SafeDownCast(sprites.Mapper->GetInput());
+    if (polyData && polyData->GetPointData()->HasArray("scale") &&
+      polyData->GetPointData()->HasArray("rotation"))
+    {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
       sprites.Mapper->AnisotropicOn();
-      sprites.Mapper->SetBoundScale(3.0);
       sprites.Mapper->SetRotationArray("rotation");
-
-      int* viewport = this->GetSize();
-
-      float lowPass[3] = { 0.3f / (viewport[0] * viewport[0]), 0.f,
-        0.3f / (viewport[1] * viewport[1]) };
-      sprites.Mapper->SetLowpassMatrix(lowPass);
+      sprites.Mapper->SetScaleArray("scale");
 #else
-      F3DLog::Print(F3DLog::Severity::Warning,
-        "Gaussian splatting selected but VTK <= 9.3 only supports isotropic gaussians");
+      F3DLog::Print(F3DLog::Severity::Warning, "VTK <= 9.3 only supports isotropic point sprites");
 #endif
-
-      sprites.Actor->ForceTranslucentOn();
     }
     else
     {
 #if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
       sprites.Mapper->AnisotropicOff();
-      sprites.Mapper->SetLowpassMatrix(0., 0., 0.);
+      sprites.Mapper->SetRotationArray(nullptr);
+      sprites.Mapper->SetScaleArray(nullptr);
 #endif
+    }
 
-      sprites.Mapper->SetScaleFactor(scaleFactor);
-
+    if (this->PointSpritesType == SplatType::GAUSSIAN)
+    {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
+      sprites.Mapper->SetBoundScale(3.0);
+#endif
+      sprites.Mapper->SetSplatShaderCode(nullptr); // gaussian is the default VTK shader
+      sprites.Actor->ForceTranslucentOn();
+    }
+    else
+    {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 3, 20231102)
+      sprites.Mapper->SetBoundScale(1.0);
+#endif
       sprites.Mapper->SetSplatShaderCode(
         "//VTK::Color::Impl\n"
         "float dist = dot(offsetVCVSOutput.xy, offsetVCVSOutput.xy);\n"
@@ -2265,6 +2634,7 @@
     this->UsePointSprites = use;
     this->CheatSheetConfigured = false;
     this->ColoringConfigured = false;
+    this->PointSpritesConfigured = false;
   }
 }
 
@@ -2317,7 +2687,7 @@
     this->UserScalarBarRange = range;
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
     this->ScalarBarActorConfigured = false;
     this->ColoringConfigured = false;
@@ -2333,7 +2703,7 @@
 
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
 
     this->ScalarBarActorConfigured = false;
@@ -2350,7 +2720,7 @@
 
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
 
     this->ScalarBarActorConfigured = false;
@@ -2377,7 +2747,7 @@
     this->UseCellColoring = useCell;
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
     this->ScalarBarActorConfigured = false;
     this->CheatSheetConfigured = false;
@@ -2394,7 +2764,7 @@
     this->ArrayNameForColoring = arrayName;
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
     this->ScalarBarActorConfigured = false;
     this->CheatSheetConfigured = false;
@@ -2417,7 +2787,7 @@
     this->ComponentForColoring = component;
     this->ColorTransferFunctionConfigured = false;
     this->ColoringMappersConfigured = false;
-    this->PointSpritesMappersConfigured = false;
+    this->ColoringPointSpritesMappersConfigured = false;
     this->VolumePropsAndMappersConfigured = false;
     this->ScalarBarActorConfigured = false;
     this->CheatSheetConfigured = false;
@@ -2485,7 +2855,7 @@
     {
       if (hasColoring)
       {
-        if (!this->PointSpritesMappersConfigured)
+        if (!this->ColoringPointSpritesMappersConfigured)
         {
           vtkF3DRenderer::ConfigureMapperForColoring(sprites.Mapper, info.value().Name,
             this->ComponentForColoring, this->ColorTransferFunction, this->ColorRange,
@@ -2497,7 +2867,7 @@
   }
   if (pointSpritesVisible)
   {
-    this->PointSpritesMappersConfigured = true;
+    this->ColoringPointSpritesMappersConfigured = true;
   }
 
   // Handle Volume prop
@@ -2676,6 +3046,72 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DRenderer::ConfigureJitter(bool enable)
+{
+  // needs https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12534
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251017)
+  if (!enable)
+  {
+    ExecFuncOnAllPolyDataUniforms(
+      this->GetActors(), [](vtkUniforms* uniforms) { uniforms->RemoveUniform("jitter"); });
+    return;
+  }
+#endif
+
+  float jitter[2];
+
+  if (enable)
+  {
+    jitter[0] = this->ConfigureHaltonSequence(0);
+    jitter[1] = this->ConfigureHaltonSequence(1);
+
+    vtkRenderWindow* renderWindow = this->GetRenderWindow();
+    int width = renderWindow->GetSize()[0];
+    int height = renderWindow->GetSize()[1];
+
+    jitter[0] = ((jitter[0] - 0.5f) / width) * 2.0f;
+    jitter[1] = ((jitter[1] - 0.5f) / height) * 2.0f;
+  }
+  else
+  {
+    jitter[0] = 0.0f;
+    jitter[1] = 0.0f;
+  }
+
+  ExecFuncOnAllPolyDataUniforms(
+    this->GetActors(), [&](vtkUniforms* uniforms) { uniforms->SetUniform2f("jitter", jitter); });
+}
+
+//----------------------------------------------------------------------------
+float vtkF3DRenderer::ConfigureHaltonSequence(int direction)
+{
+  assert(direction == 0 || direction == 1);
+
+  int base = 2 + direction;
+  int& numerator = this->TaaHaltonNumerator[direction];
+  int& denominator = this->TaaHaltonDenominator[direction];
+
+  int difference = denominator - numerator;
+  if (difference == 1)
+  {
+    numerator = 1;
+    denominator *= base;
+  }
+  else
+  {
+    int quotient = denominator / base;
+    while (difference <= quotient && quotient > 0)
+    {
+      quotient = quotient / base;
+    }
+
+    numerator = (base + 1) * quotient - difference;
+  }
+
+  return static_cast<float>(numerator) / static_cast<float>(denominator);
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureScalarBarActorForColoring(
   vtkScalarBarActor* scalarBar, std::string arrayName, int component, vtkColorTransferFunction* ctf)
 {
@@ -2690,6 +3126,7 @@
   scalarBar->SetWidth(0.8);
   scalarBar->SetHeight(0.07);
   scalarBar->SetPosition(0.1, 0.01);
+  scalarBar->SetMaximumNumberOfColors(512);
 }
 
 //----------------------------------------------------------------------------
@@ -2817,12 +3254,18 @@
 //----------------------------------------------------------------------------
 void vtkF3DRenderer::ConfigureActorTextureTransform(vtkActor* actorBase, const double* matrix)
 {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20250802)
+  vtkInformationDoubleVectorKey* generalTextureTransformKey = vtkProp::GENERAL_TEXTURE_TRANSFORM();
+#else
+  vtkInformationDoubleVectorKey* generalTextureTransformKey = vtkProp::GeneralTextureTransform();
+#endif
+
   vtkInformation* info = actorBase->GetPropertyKeys();
   if (info)
   {
     /**
      * The actor already has a property key dictionary
-     * Check that GeneralTextureTransform exists and combine,
+     * Check that the general texture transform exists and combine them together,
      * Otherwise set the property to our texture transform
      */
 
@@ -2832,13 +3275,13 @@
       finalTransform[i] = matrix[i];
     }
 
-    if (auto transformPtr = info->Get(vtkProp::GeneralTextureTransform()))
+    if (auto transformPtr = info->Get(generalTextureTransformKey))
     {
       // We need to create 4x4 vtk matrixes from the arrays
       vtkNew<vtkMatrix4x4> matTransform;
       matTransform->Multiply4x4(transformPtr, matrix, finalTransform);
     }
-    info->Set(vtkProp::GeneralTextureTransform(), finalTransform, 16);
+    info->Set(generalTextureTransformKey, finalTransform, 16);
   }
   else
   {
@@ -2846,7 +3289,7 @@
      * No dictionary found, add new dictionary with transform
      */
     vtkNew<vtkInformation> properties;
-    properties->Set(vtkProp::GeneralTextureTransform(), matrix, 16);
+    properties->Set(generalTextureTransformKey, matrix, 16);
     actorBase->SetPropertyKeys(properties);
   }
 }
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderer.h f3d-3.4.1/vtkext/private/module/vtkF3DRenderer.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderer.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DRenderer.h	2026-01-08 08:41:55.000000000 +0200
@@ -11,21 +11,30 @@
 #ifndef vtkF3DRenderer_h
 #define vtkF3DRenderer_h
 
+#include "F3DStyle.h"
+
 #include "vtkF3DMetaImporter.h"
 #include "vtkF3DUIActor.h"
 
+#include <vtkCallbackCommand.h>
 #include <vtkLight.h>
 #include <vtkOpenGLRenderer.h>
+#include <vtkVersion.h>
 
+#include <array>
 #include <filesystem>
 #include <map>
 #include <optional>
 
 namespace fs = std::filesystem;
 
-class vtkDiscretizableColorTransferFunction;
+class vtkCameraOrientationRepresentation;
+class vtkCameraOrientationWidget;
 class vtkColorTransferFunction;
 class vtkCornerAnnotation;
+class vtkDiscretizableColorTransferFunction;
+class vtkF3DOpenGLGridMapper;
+class vtkGridAxesActor3D;
 class vtkImageReader2;
 class vtkOrientationMarkerWidget;
 class vtkScalarBarActor;
@@ -45,7 +54,28 @@
   {
     NONE,
     FXAA,
-    SSAA
+    SSAA,
+    TAA
+  };
+
+  /**
+   * Enum listing possible blending modes.
+   */
+  enum class BlendingMode : unsigned char
+  {
+    NONE,
+    DUAL_DEPTH_PEELING,
+    SORT,
+    STOCHASTIC
+  };
+
+  /**
+   * Enum listing possible splat types.
+   */
+  enum class SplatType : unsigned char
+  {
+    SPHERE,
+    GAUSSIAN
   };
 
   ///@{
@@ -54,14 +84,17 @@
    */
   void ShowAxis(bool show);
   void ShowGrid(bool show);
+  void ShowAxesGrid(bool show);
   void ShowEdge(const std::optional<bool>& show);
   void ShowTimer(bool show);
   void ShowMetaData(bool show);
   void ShowFilename(bool show);
+  void ShowHDRIFilename(bool show);
   void ShowCheatSheet(bool show);
   void ShowConsole(bool show);
   void ShowMinimalConsole(bool show);
   void ShowDropZone(bool show);
+  void ShowDropZoneLogo(bool show);
   void ShowHDRISkybox(bool show);
   void ShowArmature(bool show);
   ///@}
@@ -81,19 +114,28 @@
   void SetLightIntensity(const double intensity);
   void SetFilenameInfo(const std::string& info);
   void SetDropZoneInfo(const std::string& info);
+  void SetDropZoneBinds(const std::vector<std::pair<std::string, std::string>>& dropZoneBinds);
   void SetGridAbsolute(bool absolute);
   void SetGridUnitSquare(const std::optional<double>& unitSquare);
   void SetGridSubdivisions(int subdivisions);
   void SetGridColor(const std::vector<double>& color);
+  void SetAxesColor(const std::vector<double>& colorXAxis, const std::vector<double>& colorYAxis,
+    const std::vector<double>& colorZAxis);
   ///@}
 
+  /**
+   * Set the backdrop opacity
+   * Should be called before ShowAxis
+   */
+  void SetBackdropOpacity(const double backdropOpacity);
+
   ///@{
   /**
    * Set usages and configurations of different render passes
    */
   void SetUseRaytracing(bool use);
   void SetUseRaytracingDenoiser(bool use);
-  void SetUseDepthPeelingPass(bool use);
+  void SetBlendingMode(BlendingMode mode);
   void SetUseSSAOPass(bool use);
   void SetAntiAliasingMode(AntiAliasingMode mode);
   void SetUseToneMappingPass(bool use);
@@ -105,6 +147,11 @@
   ///@}
 
   /**
+   * Get BlendingMode
+   */
+  BlendingMode GetBlendingMode() const;
+
+  /**
    * Set SetUseOrthographicProjection
    */
   void SetUseOrthographicProjection(const std::optional<bool>& use);
@@ -162,10 +209,9 @@
   void Initialize();
 
   /**
-   * Initialize actors properties related to the up vector using the provided upString, including
-   * the camera
+   * Initialize the camera position based on the given up direction.
    */
-  void InitializeUpVector(const std::vector<double>& upVec);
+  void InitializeUpDirection(const std::vector<double>& upVec);
 
   /**
    * Compute bounds of visible props as transformed by given matrix.
@@ -187,12 +233,12 @@
   /**
    * Get up vector
    */
-  vtkGetVector3Macro(UpVector, double);
+  vtkGetVector3Macro(UpDirection, double);
 
   /**
    * Set/Get right vector
    */
-  vtkGetVector3Macro(RightVector, double);
+  vtkGetVector3Macro(RightDirection, double);
 
   /**
    * Set cache path, only used by the HDRI logic
@@ -266,16 +312,21 @@
    */
   void SetTextureNormal(const std::optional<fs::path>& tex);
 
-  enum class SplatType
-  {
-    SPHERE,
-    GAUSSIAN
-  };
+  /**
+   * Set point sprites type
+   */
+  void SetPointSpritesType(SplatType type);
 
   /**
-   * Set the point sprites size and the splat type on the pointGaussianMapper
+   * Set the point sprites size
+   * If absoluteScale is false, the size is scaled by the scene bounding box
    */
-  void SetPointSpritesProperties(SplatType splatType, double pointSpritesSize);
+  void SetPointSpritesSize(bool absoluteScale, double size);
+
+  /**
+   * Set point sprites instancing usage
+   */
+  void SetPointSpritesUseInstancing(bool useInstancing);
 
   /**
    * Set the visibility of the scalar bar.
@@ -359,6 +410,13 @@
   ///@}
 
   /**
+   * Set the pending up direction. Stores the direction and marks
+   * the up vector as needing configuration.
+   * Actual configuration (camera rotation, skybox, environment) happens in UpdateActors.
+   */
+  void SetPendingUpDirection(const std::vector<double>& upVec);
+
+  /**
    * Get information about the current coloring
    * Returns a single line string containing the coloring description
    */
@@ -404,6 +462,11 @@
   void ConfigureCheatSheet(const std::vector<vtkF3DUIActor::CheatSheetGroup>& info);
 
   /**
+   * Configure Axes (X,Y,Z) colors on the widget
+   */
+  void ConfigureAxesActor();
+
+  /**
    * Use this method to flag in the renderer that the cheatsheet needs to be updated
    * This is not required to call when using any of the setter of the renderer
    */
@@ -425,8 +488,6 @@
 
   void ReleaseGraphicsResources(vtkWindow* w) override;
 
-  bool IsBackgroundDark();
-
   /**
    * Configure meta data actor visibility and content
    */
@@ -460,6 +521,12 @@
   ///@}
 
   /**
+   * Apply the given up vector to the scene, computing an appropriate right vector.
+   * Updates UpDirection, RightDirection, skybox floor, and environment orientation.
+   */
+  void ApplyUpDirection(const std::array<double, 3>& up);
+
+  /**
    * Configure all actors properties
    */
   void ConfigureActorsProperties();
@@ -470,11 +537,22 @@
   void ConfigureGridUsingCurrentActors();
 
   /**
+   * Configure the Grid Axes actor
+   */
+  void ConfigureGridAxesUsingCurrentActors();
+
+  /**
    * Configure the different render passes
    */
   void ConfigureRenderPasses();
 
   /**
+   * Rotate camera and apply up direction to scene.
+   * Called from UpdateActors when UpDirectionConfigured is false.
+   */
+  void ConfigureUpDirection();
+
+  /**
    * Create a cache directory if a HDRIHash is set
    */
   void CreateCacheDirectory();
@@ -500,6 +578,17 @@
     bool cellFlag = false, bool inverseOpacityFlag = false);
 
   /**
+   * Configure screen spaced jittering for TAA
+   */
+  void ConfigureJitter(bool enable);
+
+  /**
+   * Configure Halton sequence for TAA. Valid direction values are 0 and 1. Returns a value that is
+   * used for jitter
+   */
+  float ConfigureHaltonSequence(int direction);
+
+  /**
    * Convenience method for configuring a scalar bar actor for coloring
    */
   void ConfigureScalarBarActorForColoring(vtkScalarBarActor* scalarBar, std::string arrayName,
@@ -516,9 +605,29 @@
    */
   void ConfigureActorTextureTransform(vtkActor* actorBase, const double* matrix);
 
+  /**
+   * Configure all properties of the point sprites mapper
+   */
+  void ConfigurePointSprites();
+
+  /**
+   * Updates the axis widget size based on the window size
+   */
+  void UpdateAxisWidgetSize();
+
   vtkSmartPointer<vtkOrientationMarkerWidget> AxisWidget;
+  vtkSmartPointer<vtkCameraOrientationWidget> ModernAxisWidget;
+  vtkSmartPointer<vtkCameraOrientationRepresentation> ModernAxisRepresentation;
+  vtkSmartPointer<vtkCallbackCommand> ModernAxisWidgetResizeCallback;
+  double ModernAxisBackdropOpacity = 0.0;
+
+  // Does vtk version support GridAxesActor
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+  vtkNew<vtkGridAxesActor3D> GridAxesActor;
+#endif
 
   vtkNew<vtkActor> GridActor;
+  vtkNew<vtkF3DOpenGLGridMapper> GridMapper;
   vtkNew<vtkSkybox> SkyboxActor;
   vtkNew<vtkF3DUIActor> UIActor;
 
@@ -526,11 +635,15 @@
 
   bool CheatSheetConfigured = false;
   bool ActorsPropertiesConfigured = false;
+  bool UpDirectionConfigured = false;
   bool GridConfigured = false;
+  bool GridAxesConfigured = false;
+  bool AxesActorConfigured = false;
   bool RenderPassesConfigured = false;
   bool LightIntensitiesConfigured = false;
   bool TextActorsConfigured = false;
   bool MetaDataConfigured = false;
+  bool PointSpritesConfigured = false;
   bool HDRIReaderConfigured = false;
   bool HDRIHashConfigured = false;
   bool HDRITextureConfigured = false;
@@ -542,20 +655,25 @@
   bool GridVisible = false;
   bool GridAbsolute = false;
   bool AxisVisible = false;
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 4, 20250513)
+  bool AxesGridVisible = false;
+#endif
   std::optional<bool> EdgeVisible;
   bool TimerVisible = false;
   bool FilenameVisible = false;
   bool MetaDataVisible = false;
+  bool HDRIFilenameVisible = false;
   bool CheatSheetVisible = false;
   bool ConsoleVisible = false;
   bool MinimalConsoleVisible = false;
   bool DropZoneVisible = false;
+  bool DropZoneLogoVisible = false;
   bool HDRISkyboxVisible = false;
   bool ArmatureVisible = false;
   bool UseRaytracing = false;
   bool UseRaytracingDenoiser = false;
-  bool UseDepthPeelingPass = false;
   AntiAliasingMode AntiAliasingModeEnabled = AntiAliasingMode::NONE;
+  BlendingMode BlendingModeEnabled = BlendingMode::NONE;
   bool UseSSAOPass = false;
   bool UseToneMappingPass = false;
   bool UseBlurBackground = false;
@@ -564,8 +682,9 @@
   bool InvertZoom = false;
 
   int RaytracingSamples = 0;
-  double UpVector[3] = { 0.0, 1.0, 0.0 };
-  double RightVector[3] = { 1.0, 0.0, 0.0 };
+  double UpDirection[3] = { 0.0, 1.0, 0.0 };
+  double RightDirection[3] = { 1.0, 0.0, 0.0 };
+  double PendingUpDirection[3] = { 0.0, 1.0, 0.0 };
   double CircleOfConfusionRadius = 20.0;
   std::optional<double> PointSize;
   std::optional<double> LineWidth;
@@ -573,6 +692,10 @@
   int GridSubdivisions = 10;
   double GridColor[3] = { 0.0, 0.0, 0.0 };
 
+  double ColorAxisX[3] = { 0.0, 0.0, 0.0 };
+  double ColorAxisY[3] = { 0.0, 0.0, 0.0 };
+  double ColorAxisZ[3] = { 0.0, 0.0, 0.0 };
+
   std::string HDRIFile;
   vtkSmartPointer<vtkImageReader2> HDRIReader;
   bool HasValidHDRIReader = false;
@@ -607,7 +730,7 @@
   bool ScalarBarActorConfigured = false;
 
   bool ColoringMappersConfigured = false;
-  bool PointSpritesMappersConfigured = false;
+  bool ColoringPointSpritesMappersConfigured = false;
   bool VolumePropsAndMappersConfigured = false;
   bool ColoringConfigured = false;
 
@@ -644,6 +767,14 @@
   std::optional<std::vector<double>> UserScalarBarRange;
   std::vector<double> Colormap;
   std::optional<int> ColormapDiscretization;
+
+  int TaaHaltonNumerator[2] = { 0, 0 };
+  int TaaHaltonDenominator[2] = { 1, 1 };
+
+  SplatType PointSpritesType = SplatType::SPHERE;
+  double PointSpritesSize = 10;
+  bool PointSpritesAbsoluteScale = false;
+  bool PointSpritesUseInstancing = false;
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderPass.cxx f3d-3.4.1/vtkext/private/module/vtkF3DRenderPass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderPass.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DRenderPass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -2,6 +2,8 @@
 
 #include "vtkF3DHexagonalBokehBlurPass.h"
 #include "vtkF3DImporter.h"
+#include "vtkF3DRenderer.h"
+#include "vtkF3DStochasticTransparentPass.h"
 
 #include <vtkBoundingBox.h>
 #include <vtkCameraPass.h>
@@ -47,7 +49,6 @@
   this->Superclass::PrintSelf(os, indent);
   os << indent << "UseRaytracing: " << this->UseRaytracing << "\n";
   os << indent << "UseSSAOPass: " << this->UseSSAOPass << "\n";
-  os << indent << "UseDepthPeelingPass: " << this->UseDepthPeelingPass << "\n";
   os << indent << "UseBlurBackground: " << this->UseBlurBackground << "\n";
   os << indent << "ForceOpaqueBackground: " << this->ForceOpaqueBackground << "\n";
 }
@@ -121,7 +122,7 @@
   vtkNew<vtkCameraPass> bgCamP;
   bgCamP->SetDelegatePass(bgP);
   this->BackgroundPass = vtkSmartPointer<vtkFramebufferPass>::New();
-  this->BackgroundPass->SetColorFormat(vtkTextureObject::Float32);
+  this->BackgroundPass->SetColorFormat(vtkTextureObject::Float16);
 
   if (this->UseBlurBackground)
   {
@@ -182,14 +183,22 @@
       collection->AddItem(opaqueP);
     }
 
-    // translucent and volumic passes
-    if (this->UseDepthPeelingPass)
+    // translucent and volumic
+    const vtkF3DRenderer* renderer = vtkF3DRenderer::SafeDownCast(s->GetRenderer());
+    if (renderer && renderer->GetBlendingMode() == vtkF3DRenderer::BlendingMode::DUAL_DEPTH_PEELING)
     {
       vtkNew<vtkDualDepthPeelingPass> ddpP;
       ddpP->SetTranslucentPass(translucentP);
       ddpP->SetVolumetricPass(volumeP);
       collection->AddItem(ddpP);
     }
+    else if (renderer && renderer->GetBlendingMode() == vtkF3DRenderer::BlendingMode::STOCHASTIC)
+    {
+      vtkNew<vtkF3DStochasticTransparentPass> stochasticP;
+      stochasticP->SetTranslucentPass(translucentP);
+      stochasticP->SetVolumetricPass(volumeP);
+      collection->AddItem(stochasticP);
+    }
     else
     {
       collection->AddItem(translucentP);
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderPass.h f3d-3.4.1/vtkext/private/module/vtkF3DRenderPass.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DRenderPass.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DRenderPass.h	2026-01-08 08:41:55.000000000 +0200
@@ -38,7 +38,6 @@
   vtkSetMacro(ArmatureVisible, bool);
   vtkSetMacro(UseRaytracing, bool);
   vtkSetMacro(UseSSAOPass, bool);
-  vtkSetMacro(UseDepthPeelingPass, bool);
   vtkSetMacro(UseBlurBackground, bool);
   vtkSetMacro(ForceOpaqueBackground, bool);
   vtkSetVector6Macro(Bounds, double);
@@ -62,7 +61,6 @@
   bool ArmatureVisible = false;
   bool UseRaytracing = false;
   bool UseSSAOPass = false;
-  bool UseDepthPeelingPass = false;
   bool UseBlurBackground = false;
   bool ForceOpaqueBackground = false;
 
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DStochasticTransparentPass.cxx f3d-3.4.1/vtkext/private/module/vtkF3DStochasticTransparentPass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DStochasticTransparentPass.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DStochasticTransparentPass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,145 @@
+#include "vtkF3DStochasticTransparentPass.h"
+
+#include "vtkF3DRandomFS.h"
+
+#include <vtkF3DOpenGLGridMapper.h>
+#include <vtkInformation.h>
+#include <vtkInformationIntegerKey.h>
+#include <vtkObjectFactory.h>
+#include <vtkOpenGLActor.h>
+#include <vtkOpenGLRenderWindow.h>
+#include <vtkRenderState.h>
+#include <vtkRenderer.h>
+#include <vtkShaderProgram.h>
+#include <vtkVersion.h>
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251120)
+#include "vtkF3DPointSplatMapper.h"
+
+#include <vtkOpenGLPointGaussianMapperHelper.h>
+#endif
+
+vtkStandardNewMacro(vtkF3DStochasticTransparentPass);
+vtkCxxSetObjectMacro(vtkF3DStochasticTransparentPass, TranslucentPass, vtkRenderPass);
+vtkCxxSetObjectMacro(vtkF3DStochasticTransparentPass, VolumetricPass, vtkRenderPass);
+
+vtkInformationKeyMacro(vtkF3DStochasticTransparentPass, PropIndex, Integer);
+
+//------------------------------------------------------------------------------
+vtkF3DStochasticTransparentPass::vtkF3DStochasticTransparentPass() = default;
+
+//------------------------------------------------------------------------------
+vtkF3DStochasticTransparentPass::~vtkF3DStochasticTransparentPass()
+{
+  if (this->TranslucentPass)
+  {
+    this->TranslucentPass->Delete();
+  }
+  if (this->VolumetricPass)
+  {
+    this->VolumetricPass->Delete();
+  }
+}
+
+//------------------------------------------------------------------------------
+void vtkF3DStochasticTransparentPass::Render(const vtkRenderState* s)
+{
+  this->RenWin = static_cast<vtkOpenGLRenderWindow*>(s->GetRenderer()->GetRenderWindow());
+
+  // Setup vtkOpenGLRenderPass
+  this->PreRender(s);
+
+  // force usage of depth buffer, even if the actors are translucents
+  int numProps = s->GetPropArrayCount();
+  for (int j = 0; j < numProps; ++j)
+  {
+    vtkProp* prop = s->GetPropArray()[j];
+    vtkInformation* info = prop->GetPropertyKeys();
+    info->Set(vtkOpenGLActor::GLDepthMaskOverride(), 1);
+    info->Set(vtkF3DStochasticTransparentPass::PropIndex(), j);
+  }
+
+  this->TranslucentPass->Render(s);
+  this->VolumetricPass->Render(s);
+
+  this->PostRender(s);
+
+  // remove depth buffer keys
+  for (int j = 0; j < numProps; ++j)
+  {
+    vtkProp* prop = s->GetPropArray()[j];
+    vtkInformation* info = prop->GetPropertyKeys();
+    info->Remove(vtkOpenGLActor::GLDepthMaskOverride());
+  }
+}
+
+//------------------------------------------------------------------------------
+bool vtkF3DStochasticTransparentPass::SetShaderParameters(vtkShaderProgram* program,
+  vtkAbstractMapper* mapper, vtkProp* prop, vtkOpenGLVertexArrayObject* VAO)
+{
+  vtkInformation* info = prop->GetPropertyKeys();
+  program->SetUniformi("propIndex", info->Get(vtkF3DStochasticTransparentPass::PropIndex()));
+  program->SetUniformi("seed", this->Seed++);
+
+  return this->Superclass::SetShaderParameters(program, mapper, prop, VAO);
+}
+
+//------------------------------------------------------------------------------
+bool vtkF3DStochasticTransparentPass::PreReplaceShaderValues(std::string& vtkNotUsed(vertexShader),
+  std::string& vtkNotUsed(geometryShader), std::string& fragmentShader, vtkAbstractMapper* mapper,
+  vtkProp* vtkNotUsed(prop))
+{
+  if (!vtkF3DOpenGLGridMapper::SafeDownCast(mapper))
+  {
+    // add random function utilities
+    std::string dec = vtkF3DRandomFS;
+    dec += "\nuniform int propIndex;\n";
+    dec += "\nuniform int seed;\n";
+
+    std::string primIdDef = "\n#define primId gl_PrimitiveID\n";
+
+    // If we are using instancing, use the instance id instead of the primitive id
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251120)
+    vtkOpenGLPointGaussianMapperHelper* splatHelper =
+      vtkOpenGLPointGaussianMapperHelper::SafeDownCast(mapper);
+
+    if (splatHelper)
+    {
+      vtkF3DPointSplatMapper* splatMapper =
+        vtkF3DPointSplatMapper::SafeDownCast(splatHelper->Owner);
+
+      if (splatMapper && splatMapper->GetUseInstancing())
+      {
+        primIdDef = "\n#define primId instanceId\n";
+      }
+    }
+#endif
+
+    dec += primIdDef;
+
+    vtkShaderProgram::Substitute(fragmentShader, "//VTK::Color::Dec", dec);
+
+    vtkShaderProgram::Substitute(fragmentShader, "  //VTK::Color::Impl",
+      "  //VTK::Color::Impl\n"
+      "  float rd = random_ign(gl_FragCoord.xy, hash(uvec3(seed, propIndex, primId)));\n"
+      "  if (rd >= opacity) discard;\n"
+      "  opacity = 1.0;\n\n");
+  }
+
+  return true;
+}
+
+//------------------------------------------------------------------------------
+void vtkF3DStochasticTransparentPass::ReleaseGraphicsResources(vtkWindow* win)
+{
+  this->Superclass::ReleaseGraphicsResources(win);
+
+  if (this->TranslucentPass)
+  {
+    this->TranslucentPass->ReleaseGraphicsResources(win);
+  }
+  if (this->VolumetricPass)
+  {
+    this->VolumetricPass->ReleaseGraphicsResources(win);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DStochasticTransparentPass.h f3d-3.4.1/vtkext/private/module/vtkF3DStochasticTransparentPass.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DStochasticTransparentPass.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DStochasticTransparentPass.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,63 @@
+/**
+ * @class   vtkF3DStochasticTransparentPass
+ * @brief   Implements a stochastic transparency rendering pass.
+ *
+ * This render pass implements a stochastic transparency technique based on
+ * Enderton et al.'s "Stochastic Transparency". Instead of sorting and
+ * compositing transparent objects, the pass applies a pseudo-random per-fragment
+ * masking strategy so that over many independent samples the correct blended result is converged to
+ * the expected result. This eliminates the need for draw-order sorting for translucent geometry and
+ * can improve performance in complex scenes.
+ *
+ * Reference:
+ * - Enderton, Eric, et al.
+ *   "Stochastic transparency."
+ *   Proceedings of the 2010 ACM SIGGRAPH symposium on Interactive 3D Graphics and Games. 2010.
+ * - Kheradmand, Shakiba, et al.
+ *   "StochasticSplats: Stochastic Rasterization for Sorting-Free 3D Gaussian Splatting."
+ *   arXiv preprint arXiv:2503.24366 (2025).
+ */
+
+#ifndef vtkF3DStochasticTransparentPass_h
+#define vtkF3DStochasticTransparentPass_h
+
+#include "vtkOpenGLRenderPass.h"
+
+class vtkInformationIntegerKey;
+class vtkOpenGLRenderWindow;
+
+class vtkF3DStochasticTransparentPass : public vtkOpenGLRenderPass
+{
+public:
+  static vtkF3DStochasticTransparentPass* New();
+  vtkTypeMacro(vtkF3DStochasticTransparentPass, vtkOpenGLRenderPass);
+
+  void Render(const vtkRenderState* s) override;
+  void ReleaseGraphicsResources(vtkWindow* w) override;
+
+  void SetTranslucentPass(vtkRenderPass* translucentPass);
+  void SetVolumetricPass(vtkRenderPass* volumetricPass);
+
+  bool PreReplaceShaderValues(std::string& vertexShader, std::string& geometryShader,
+    std::string& fragmentShader, vtkAbstractMapper* mapper, vtkProp* prop) override;
+
+  bool SetShaderParameters(vtkShaderProgram* program, vtkAbstractMapper* mapper, vtkProp* prop,
+    vtkOpenGLVertexArrayObject* VAO = nullptr) override;
+
+  static vtkInformationIntegerKey* PropIndex();
+
+private:
+  vtkF3DStochasticTransparentPass();
+  ~vtkF3DStochasticTransparentPass() override;
+
+  vtkRenderPass* TranslucentPass = nullptr;
+  vtkRenderPass* VolumetricPass = nullptr;
+  vtkOpenGLRenderWindow* RenWin = nullptr;
+
+  int Seed = 0;
+
+  vtkF3DStochasticTransparentPass(const vtkF3DStochasticTransparentPass&) = delete;
+  void operator=(const vtkF3DStochasticTransparentPass&) = delete;
+};
+
+#endif // vtkF3DStochasticTransparentPass_h
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DTAAResolvePass.cxx f3d-3.4.1/vtkext/private/module/vtkF3DTAAResolvePass.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DTAAResolvePass.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DTAAResolvePass.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,147 @@
+#include "vtkF3DTAAResolvePass.h"
+
+#include <vtkCamera.h>
+#include <vtkObjectFactory.h>
+#include <vtkOpenGLError.h>
+#include <vtkOpenGLFramebufferObject.h>
+#include <vtkOpenGLQuadHelper.h>
+#include <vtkOpenGLRenderUtilities.h>
+#include <vtkOpenGLRenderWindow.h>
+#include <vtkOpenGLShaderCache.h>
+#include <vtkOpenGLState.h>
+#include <vtkRenderState.h>
+#include <vtkRenderer.h>
+#include <vtkShaderProgram.h>
+#include <vtkTextureObject.h>
+
+vtkStandardNewMacro(vtkF3DTAAResolvePass);
+
+//------------------------------------------------------------------------------
+void vtkF3DTAAResolvePass::Render(const vtkRenderState* state)
+{
+  vtkOpenGLClearErrorMacro();
+  this->NumberOfRenderedProps = 0;
+
+  vtkRenderer* renderer = state->GetRenderer();
+  vtkOpenGLRenderWindow* renWin = vtkOpenGLRenderWindow::SafeDownCast(renderer->GetRenderWindow());
+  vtkOpenGLState* ostate = renWin->GetState();
+
+  vtkOpenGLState::ScopedglEnableDisable bsaver(ostate, GL_BLEND);
+  vtkOpenGLState::ScopedglEnableDisable dsaver(ostate, GL_DEPTH_TEST);
+
+  assert(this->DelegatePass != nullptr);
+
+  // create framebuffer and textures
+  int pos[2];
+  int size[2];
+  renderer->GetTiledSizeAndOrigin(&size[0], &size[1], &pos[0], &pos[1]);
+
+  if (this->HistoryTexture == nullptr)
+  {
+    this->HistoryTexture = vtkSmartPointer<vtkTextureObject>::New();
+    this->HistoryTexture->SetContext(renWin);
+    this->HistoryTexture->SetFormat(GL_RGBA);
+    this->HistoryTexture->SetInternalFormat(GL_RGBA16F);
+    this->HistoryTexture->SetDataType(GL_HALF_FLOAT);
+    this->HistoryTexture->SetMinificationFilter(vtkTextureObject::Linear);
+    this->HistoryTexture->SetMagnificationFilter(vtkTextureObject::Linear);
+    this->HistoryTexture->SetWrapS(vtkTextureObject::ClampToEdge);
+    this->HistoryTexture->SetWrapT(vtkTextureObject::ClampToEdge);
+    renderer->GetTiledSizeAndOrigin(&size[0], &size[1], &pos[0], &pos[1]);
+    this->HistoryTexture->Allocate2D(size[0], size[1], 4, VTK_FLOAT);
+    this->ResetIterations();
+  }
+  this->HistoryTexture->Resize(size[0], size[1]);
+
+  if (this->ColorTexture == nullptr)
+  {
+    this->ColorTexture = vtkSmartPointer<vtkTextureObject>::New();
+    this->ColorTexture->SetContext(renWin);
+    this->ColorTexture->SetFormat(GL_RGBA);
+    this->ColorTexture->SetInternalFormat(GL_RGBA16F);
+    this->ColorTexture->SetDataType(GL_HALF_FLOAT);
+    this->ColorTexture->SetMinificationFilter(vtkTextureObject::Linear);
+    this->ColorTexture->SetMagnificationFilter(vtkTextureObject::Linear);
+    this->ColorTexture->SetWrapS(vtkTextureObject::ClampToEdge);
+    this->ColorTexture->SetWrapT(vtkTextureObject::ClampToEdge);
+    this->ColorTexture->Allocate2D(size[0], size[1], 4, VTK_FLOAT);
+  }
+  this->ColorTexture->Resize(size[0], size[1]);
+
+  if (this->FrameBufferObject == nullptr)
+  {
+    this->FrameBufferObject = vtkSmartPointer<vtkOpenGLFramebufferObject>::New();
+    this->FrameBufferObject->SetContext(renWin);
+  }
+
+  renWin->GetState()->PushFramebufferBindings();
+  this->RenderDelegate(
+    state, size[0], size[1], size[0], size[1], this->FrameBufferObject, this->ColorTexture);
+  renWin->GetState()->PopFramebufferBindings();
+
+  if (!this->QuadHelper)
+  {
+    std::string TAAResolveFS = vtkOpenGLRenderUtilities::GetFullScreenQuadFragmentShaderTemplate();
+    vtkShaderProgram::Substitute(TAAResolveFS, "//VTK::FSQ::Decl",
+      "uniform sampler2D colorTexture;\n"
+      "uniform sampler2D historyTexture;\n"
+      "uniform float blendFactor;\n"
+      "//VTK::FSQ::Decl");
+
+    vtkShaderProgram::Substitute(TAAResolveFS, "//VTK::FSQ::Impl",
+      "vec4 current = texture(colorTexture, texCoord);\n"
+      "vec4 history = texture(historyTexture, texCoord);\n"
+      "gl_FragData[0] = mix(current, history, blendFactor);\n"
+      "//VTK::FSQ::Impl");
+    this->QuadHelper =
+      std::make_shared<vtkOpenGLQuadHelper>(renWin, nullptr, TAAResolveFS.c_str(), nullptr);
+    this->QuadHelper->ShaderChangeValue = this->GetMTime();
+  }
+  else
+  {
+    renWin->GetShaderCache()->ReadyShaderProgram(this->QuadHelper->Program);
+  }
+
+  assert(this->QuadHelper->Program && this->QuadHelper->Program->GetCompiled());
+
+  this->HistoryTexture->Activate();
+  this->ColorTexture->Activate();
+  this->QuadHelper->Program->SetUniformi("colorTexture", this->ColorTexture->GetTextureUnit());
+  this->QuadHelper->Program->SetUniformi("historyTexture", this->HistoryTexture->GetTextureUnit());
+
+  const float blendFactor = this->HistoryIteration / (this->HistoryIteration + 1.0f);
+  this->QuadHelper->Program->SetUniformf("blendFactor", blendFactor);
+  ostate->vtkglDisable(GL_BLEND);
+  ostate->vtkglDisable(GL_DEPTH_TEST);
+  ostate->vtkglClear(GL_DEPTH_BUFFER_BIT);
+  ostate->vtkglViewport(pos[0], pos[1], size[0], size[1]);
+  ostate->vtkglScissor(pos[0], pos[1], size[0], size[1]);
+
+  this->QuadHelper->Render();
+
+  this->ColorTexture->Deactivate();
+  this->HistoryTexture->Deactivate();
+  this->HistoryTexture->CopyFromFrameBuffer(pos[0], pos[1], size[0], size[1], size[0], size[1]);
+  this->HistoryIteration = std::min(this->HistoryIteration + 1, 1024);
+
+  vtkOpenGLCheckErrorMacro("failed after Render");
+}
+
+//------------------------------------------------------------------------------
+void vtkF3DTAAResolvePass::ReleaseGraphicsResources(vtkWindow* window)
+{
+  this->Superclass::ReleaseGraphicsResources(window);
+
+  if (this->FrameBufferObject)
+  {
+    this->FrameBufferObject->ReleaseGraphicsResources(window);
+  }
+  if (this->ColorTexture)
+  {
+    this->ColorTexture->ReleaseGraphicsResources(window);
+  }
+  if (this->HistoryTexture)
+  {
+    this->HistoryTexture->ReleaseGraphicsResources(window);
+  }
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DTAAResolvePass.h f3d-3.4.1/vtkext/private/module/vtkF3DTAAResolvePass.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DTAAResolvePass.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DTAAResolvePass.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,58 @@
+/**
+ * @class   vtkF3DTAAResolvePass
+ * @brief   Temporal Anti-Aliasing resolve pass
+ *
+ * This pass is used to resolve the jittered rendering for Temporal Anti-Aliasing.
+ * Adapted from https://sugulee.wordpress.com/2021/06/21/temporal-anti-aliasingtaa-tutorial/
+ */
+
+#ifndef vtkF3DTAAResolvePass_h
+#define vtkF3DTAAResolvePass_h
+
+#include "vtkImageProcessingPass.h"
+
+#include <vtkSmartPointer.h>
+
+#include <memory>
+
+class vtkOpenGLFramebufferObject;
+class vtkOpenGLQuadHelper;
+class vtkTextureObject;
+
+class vtkF3DTAAResolvePass : public vtkImageProcessingPass
+{
+public:
+  static vtkF3DTAAResolvePass* New();
+  vtkTypeMacro(vtkF3DTAAResolvePass, vtkImageProcessingPass);
+
+  /**
+   * Perform rendering according to a render state.
+   */
+  void Render(const vtkRenderState* state) override;
+
+  /**
+   * Release graphics resources and ask components to release their own resources.
+   */
+  void ReleaseGraphicsResources(vtkWindow* window) override;
+
+  /**
+   * Reset the iterations count.
+   */
+  void ResetIterations()
+  {
+    this->HistoryIteration = 0;
+  }
+
+private:
+  vtkF3DTAAResolvePass() = default;
+  ~vtkF3DTAAResolvePass() override = default;
+
+  vtkSmartPointer<vtkOpenGLFramebufferObject> FrameBufferObject;
+  vtkSmartPointer<vtkTextureObject> ColorTexture;
+  vtkSmartPointer<vtkTextureObject> HistoryTexture;
+
+  std::shared_ptr<vtkOpenGLQuadHelper> QuadHelper;
+
+  int HistoryIteration = 0;
+};
+#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DUIActor.cxx f3d-3.4.1/vtkext/private/module/vtkF3DUIActor.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DUIActor.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DUIActor.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -19,12 +19,25 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DUIActor::SetDropZoneLogoVisibility(bool show)
+{
+  this->DropZoneLogoVisible = show;
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DUIActor::SetDropText(const std::string& info)
 {
   this->DropText = info;
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DUIActor::SetDropBinds(
+  const std::vector<std::pair<std::string, std::string>>& dropZoneBinds)
+{
+  this->DropBinds = dropZoneBinds;
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DUIActor::SetFileNameVisibility(bool show)
 {
   this->FileNameVisible = show;
@@ -37,6 +50,18 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DUIActor::SetHDRIFileNameVisibility(bool show)
+{
+  this->HDRIFileNameVisible = show;
+}
+
+//----------------------------------------------------------------------------
+void vtkF3DUIActor::SetHDRIFileName(const std::string& filename)
+{
+  this->HDRIFileName = filename;
+}
+
+//----------------------------------------------------------------------------
 void vtkF3DUIActor::SetMetaDataVisibility(bool show)
 {
   this->MetaDataVisible = show;
@@ -123,6 +148,16 @@
 }
 
 //----------------------------------------------------------------------------
+void vtkF3DUIActor::SetBackdropOpacity(const double backdropOpacity)
+{
+  if (this->BackdropOpacity != backdropOpacity)
+  {
+    this->BackdropOpacity = backdropOpacity;
+    this->Initialized = false;
+  }
+}
+
+//----------------------------------------------------------------------------
 int vtkF3DUIActor::RenderOverlay(vtkViewport* vp)
 {
   vtkOpenGLRenderWindow* renWin = vtkOpenGLRenderWindow::SafeDownCast(vp->GetVTKWindow());
@@ -140,39 +175,49 @@
     this->RenderDropZone();
   }
 
-  if (this->FileNameVisible)
+  if (this->ConsoleVisible)
   {
-    this->RenderFileName();
+    // To improve user readability when console is visible, all other overlays won't be shown
+    this->RenderConsole(false);
+    this->EndFrame(renWin);
+    return 1;
   }
 
-  if (this->MetaDataVisible)
+  if (this->MinimalConsoleVisible)
   {
-    this->RenderMetaData();
+    // To improve user readability when minimal console is visible cheatsheet and filename
+    // are not shown
+    this->RenderConsole(true);
   }
-
-  if (this->CheatSheetVisible)
+  else
   {
-    this->RenderCheatSheet();
+    if (this->FileNameVisible)
+    {
+      this->RenderFileName();
+    }
+    if (this->HDRIFileNameVisible)
+    {
+      this->RenderHDRIFileName();
+    }
+    if (this->CheatSheetVisible)
+    {
+      this->RenderCheatSheet();
+    }
   }
 
-  if (this->FpsCounterVisible)
+  if (this->ConsoleBadgeEnabled)
   {
-    this->RenderFpsCounter();
+    this->RenderConsoleBadge();
   }
 
-  // Only one console can be visible at a time, console has priority over minimal console
-  if (this->ConsoleVisible)
-  {
-    this->RenderConsole(false);
-  }
-  else if (this->MinimalConsoleVisible)
+  if (this->MetaDataVisible)
   {
-    this->RenderConsole(true);
+    this->RenderMetaData();
   }
 
-  if (this->ConsoleBadgeEnabled)
+  if (this->FpsCounterVisible)
   {
-    this->RenderConsoleBadge();
+    this->RenderFpsCounter();
   }
 
   this->EndFrame(renWin);
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DUIActor.h f3d-3.4.1/vtkext/private/module/vtkF3DUIActor.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DUIActor.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtkF3DUIActor.h	2026-01-08 08:41:55.000000000 +0200
@@ -10,6 +10,7 @@
 
 #include <vtkProp.h>
 
+#include <cstdint>
 #include <deque>
 
 class vtkOpenGLRenderWindow;
@@ -20,7 +21,19 @@
   static vtkF3DUIActor* New();
   vtkTypeMacro(vtkF3DUIActor, vtkProp);
 
-  using CheatSheetTuple = std::tuple<std::string, std::string, std::string>;
+  /**
+   * Enumeration of available cheatsheet binding types.
+   * Duplicate of library/public/interactor.h
+   */
+  enum class CheatSheetBindingType : std::uint8_t
+  {
+    CYCLIC = 0,
+    NUMERICAL = 1,
+    TOGGLE = 2,
+    OTHER = 3,
+  };
+
+  using CheatSheetTuple = std::tuple<std::string, std::string, std::string, CheatSheetBindingType>;
   using CheatSheetGroup = std::pair<std::string, std::vector<CheatSheetTuple>>;
 
   /**
@@ -37,12 +50,25 @@
   void SetDropZoneVisibility(bool show);
 
   /**
+   * Set the logo dropzone visibility
+   * False by default
+   */
+  void SetDropZoneLogoVisibility(bool show);
+
+  /**
    * Set the dropzone string
    * Empty by default
    */
   void SetDropText(const std::string& info);
 
   /**
+   * Set the dropzone binds
+   * Each pair contains a description and its associated interaction bind
+   * Empty by default
+   */
+  void SetDropBinds(const std::vector<std::pair<std::string, std::string>>& dropZoneBinds);
+
+  /**
    * Set the filename visibility
    * False by default
    */
@@ -55,6 +81,18 @@
   void SetFileName(const std::string& filename);
 
   /**
+   * Set the HDRI filename visibility
+   * False by default
+   */
+  void SetHDRIFileNameVisibility(bool show);
+
+  /**
+   * Set the HDRI filename string
+   * Empty by default
+   */
+  void SetHDRIFileName(const std::string& filename);
+
+  /**
    * Set the metadata visibility
    * False by default
    */
@@ -126,6 +164,11 @@
   int RenderOverlay(vtkViewport* vp) override;
 
   /**
+   * Set the backdrop opacity
+   */
+  void SetBackdropOpacity(const double backdropOpacity);
+
+  /**
    * Set the delta time (time between UI frames) in seconds
    */
   virtual void SetDeltaTime(double)
@@ -172,6 +215,13 @@
   }
 
   /**
+   * Render the HDRI filename UI widget
+   */
+  virtual void RenderHDRIFileName()
+  {
+  }
+
+  /**
    * Render the cheatsheet UI widget
    */
   virtual void RenderCheatSheet()
@@ -198,13 +248,17 @@
   virtual void RenderConsoleBadge()
   {
   }
-
+  bool DropZoneLogoVisible = false;
   bool DropZoneVisible = false;
   std::string DropText = "";
+  std::vector<std::pair<std::string, std::string>> DropBinds;
 
   bool FileNameVisible = false;
   std::string FileName = "";
 
+  bool HDRIFileNameVisible = false;
+  std::string HDRIFileName = "";
+
   bool MetaDataVisible = false;
   std::string MetaData = "";
 
@@ -226,6 +280,8 @@
   std::string FontFile = "";
   double FontScale = 1.0;
 
+  double BackdropOpacity = 0.9;
+
 private:
   vtkF3DUIActor(const vtkF3DUIActor&) = delete;
   void operator=(const vtkF3DUIActor&) = delete;
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DWebPReader.cxx f3d-3.4.1/vtkext/private/module/vtkF3DWebPReader.cxx
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DWebPReader.cxx	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DWebPReader.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,146 @@
+#include "vtkF3DWebPReader.h"
+
+#include <vtkFileResourceStream.h>
+#include <vtkImageData.h>
+#include <vtkMemoryResourceStream.h>
+#include <vtkNew.h>
+#include <vtkObjectFactory.h>
+#include <vtkPointData.h>
+#include <vtkUnsignedCharArray.h>
+#include <vtkVersion.h>
+#include <vtksys/FStream.hxx>
+
+#include "webp/decode.h"
+
+#include <algorithm>
+
+vtkStandardNewMacro(vtkF3DWebPReader);
+
+//------------------------------------------------------------------------------
+vtkF3DWebPReader::vtkF3DWebPReader() = default;
+
+//------------------------------------------------------------------------------
+vtkF3DWebPReader::~vtkF3DWebPReader() = default;
+
+//------------------------------------------------------------------------------
+void vtkF3DWebPReader::ExecuteInformation()
+{
+  // XXX: Needed because of VTK initialize file pattern in the constructor for some reasons
+  delete[] this->FilePattern;
+  this->FilePattern = nullptr;
+
+  // Setup filename to read the header
+  this->ComputeInternalFileName(this->DataExtent[4]);
+  if ((this->InternalFileName == nullptr || this->InternalFileName[0] == '\0'))
+  {
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+    if (!this->GetStream())
+#else
+    if (!this->GetMemoryBuffer())
+#endif
+    {
+      return;
+    }
+  }
+
+  vtkResourceStream* stream;
+  vtkNew<vtkFileResourceStream> fileStream;
+
+#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 5, 20251016)
+  if (this->GetStream())
+  {
+    stream = this->GetStream();
+  }
+#else
+  vtkNew<vtkMemoryResourceStream> memStream;
+  if (this->GetMemoryBuffer())
+  {
+    memStream->SetBuffer(this->GetMemoryBuffer(), this->GetMemoryBufferLength());
+    stream = memStream;
+  }
+#endif
+  else
+  {
+    fileStream->Open(this->InternalFileName);
+    stream = fileStream;
+  }
+
+  stream->Seek(0, vtkResourceStream::SeekDirection::End);
+  this->BufferData.resize(stream->Tell());
+
+  stream->Seek(0, vtkResourceStream::SeekDirection::Begin);
+  stream->Read(this->BufferData.data(), this->BufferData.size());
+
+  int width = -1, height = -1;
+
+  if (!WebPGetInfo(this->BufferData.data(), this->BufferData.size(), &width, &height))
+  {
+    vtkErrorMacro(<< "Could get webp infos");
+    return;
+  }
+
+  this->DataExtent[0] = 0;
+  this->DataExtent[1] = width - 1;
+  this->DataExtent[2] = 0;
+  this->DataExtent[3] = height - 1;
+
+  this->SetNumberOfScalarComponents(4);
+  this->SetDataScalarTypeToUnsignedChar();
+
+  this->vtkImageReader::ExecuteInformation();
+}
+
+//------------------------------------------------------------------------------
+int vtkF3DWebPReader::CanReadFile(const char* fname)
+{
+  // get the magic number by reading in a file
+  vtksys::ifstream ifs(fname, vtksys::ifstream::in);
+
+  if (ifs.fail())
+  {
+    vtkErrorMacro(<< "Could not open file " << fname);
+    return 0;
+  }
+
+  // The file must begin with magic number RIFF
+  if ((ifs.get() != 'R') || (ifs.get() != 'I') || (ifs.get() != 'F') || (ifs.get() != 'F'))
+  {
+    ifs.close();
+    return 0;
+  }
+
+  // Skip 4 bytes (file size field) to get to position 8
+  ifs.ignore(4);
+
+  // Check for WEBP signature at bytes 8-11
+  if ((ifs.get() != 'W') || (ifs.get() != 'E') || (ifs.get() != 'B') || (ifs.get() != 'P'))
+  {
+    ifs.close();
+    return 0;
+  }
+
+  ifs.close();
+  return 1;
+}
+
+//------------------------------------------------------------------------------
+void vtkF3DWebPReader::ExecuteDataWithInformation(vtkDataObject* output, vtkInformation* outInfo)
+{
+  vtkImageData* data = this->AllocateOutputData(output, outInfo);
+
+  vtkUnsignedCharArray* scalars =
+    vtkUnsignedCharArray::SafeDownCast(data->GetPointData()->GetScalars());
+  if (!scalars)
+  {
+    vtkErrorMacro(<< "Could not find expected scalar array");
+    return;
+  }
+
+  scalars->SetName("Pixels");
+
+  uint8_t* pixels = WebPDecodeRGBA(
+    static_cast<uint8_t*>(this->BufferData.data()), this->BufferData.size(), nullptr, nullptr);
+  std::copy_n(pixels, scalars->GetSize(), scalars->GetPointer(0));
+
+  WebPFree(pixels);
+}
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DWebPReader.h f3d-3.4.1/vtkext/private/module/vtkF3DWebPReader.h
--- f3d-3.2.0+dfsg/vtkext/private/module/vtkF3DWebPReader.h	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/vtkext/private/module/vtkF3DWebPReader.h	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,51 @@
+/**
+ * @class   vtkF3DWebPReader
+ * @brief   Read a WebP image file
+ */
+#ifndef vtkF3DWebPReader_h
+#define vtkF3DWebPReader_h
+
+#include "vtkImageReader.h"
+
+class vtkF3DWebPReader : public vtkImageReader
+{
+public:
+  static vtkF3DWebPReader* New();
+  vtkTypeMacro(vtkF3DWebPReader, vtkImageReader);
+
+  /**
+   * Return 1 if the reader is compatible with the given file
+   */
+  int CanReadFile(const char* fname) override;
+
+  /**
+   * List of extensions supported by this reader
+   */
+  const char* GetFileExtensions() override
+  {
+    return ".webp";
+  }
+
+  /**
+   * Descriptive name of the reader
+   */
+  const char* GetDescriptiveName() override
+  {
+    return "WebP";
+  }
+
+protected:
+  vtkF3DWebPReader();
+  ~vtkF3DWebPReader() override;
+
+  void ExecuteInformation() override;
+  void ExecuteDataWithInformation(vtkDataObject* out, vtkInformation* outInfo) override;
+
+private:
+  vtkF3DWebPReader(const vtkF3DWebPReader&) = delete;
+  void operator=(const vtkF3DWebPReader&) = delete;
+
+  std::vector<unsigned char> BufferData;
+};
+
+#endif
diff -Nru f3d-3.2.0+dfsg/vtkext/private/module/vtk.module f3d-3.4.1/vtkext/private/module/vtk.module
--- f3d-3.2.0+dfsg/vtkext/private/module/vtk.module	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/private/module/vtk.module	2026-01-08 08:41:55.000000000 +0200
@@ -23,7 +23,9 @@
 OPTIONAL_DEPENDS
   VTK::opengl
   VTK::RenderingRayTracing
+  VTK::RenderingGridAxes
 TEST_DEPENDS
+  VTK::FiltersSources
   VTK::IOGeometry
   VTK::IOPLY
   VTK::IOXML
diff -Nru f3d-3.2.0+dfsg/vtkext/public/CMakeLists.txt f3d-3.4.1/vtkext/public/CMakeLists.txt
--- f3d-3.2.0+dfsg/vtkext/public/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -24,15 +24,14 @@
   set(export_name "f3d_vtkext")
   set(headers_component "plugin_sdk")
 else()
-  if(VTK_VERSION VERSION_GREATER_EQUAL 9.2.20220928)
-    # When building as a static lib or against a static VTK, the module is not installed at all
-    set(f3d_vtk_no_install "NO_INSTALL")
-  endif()
+  # When building as a static lib or against a static VTK, the module is not installed at all
+  set(f3d_vtk_no_install "NO_INSTALL")
   set(f3d_vtk_force_static "FORCE_STATIC")
 endif()
 
 vtk_module_build(
   MODULES ${modules}
+  ENABLE_WRAPPING OFF
   INSTALL_EXPORT ${export_name}
   INSTALL_HEADERS ON
   HEADERS_COMPONENT ${headers_component}
diff -Nru f3d-3.2.0+dfsg/vtkext/public/doxygen/conf.py f3d-3.4.1/vtkext/public/doxygen/conf.py
--- f3d-3.2.0+dfsg/vtkext/public/doxygen/conf.py	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/doxygen/conf.py	1970-01-01 02:00:00.000000000 +0200
@@ -1,80 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# This file only contains a selection of the most common options. For a full
-# list see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Path setup --------------------------------------------------------------
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
-
-
-# -- Project information -----------------------------------------------------
-
-project = "vtkext"
-copyright = "2025, Michael Migliore and Mathieu Westphal"
-author = "Michael Migliore and Mathieu Westphal"
-
-# The full version, including alpha/beta/rc tags
-release = "3.2.0"
-
-
-# -- General configuration ---------------------------------------------------
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = ["breathe", "exhale"]
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
-
-
-# -- Options for HTML output -------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = "sphinx_book_theme"
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
-
-# Setup the breathe extension
-breathe_projects = {"libf3d": "./_doxygen/xml"}
-breathe_default_project = "libf3d"
-
-# Setup the exhale extension
-exhale_args = {
-    # These arguments are required
-    "containmentFolder": "./api",
-    "rootFileName": "library_root.rst",
-    "doxygenStripFromPath": "..",
-    # Heavily encouraged optional argument (see docs)
-    "rootFileTitle": "Library API",
-    # Suggested optional arguments
-    "createTreeView": True,
-    # TIP: if using the sphinx-bootstrap-theme, you need
-    # "treeViewIsBootstrap": True,
-    "exhaleExecutesDoxygen": True,
-    "exhaleDoxygenStdin": "INPUT = ../module\nDISTRIBUTE_GROUP_DOC = YES\nEXCLUDE_SYMBOLS = std",
-}
-
-# Tell sphinx what the primary language being documented is.
-primary_domain = "cpp"
-
-# Tell sphinx what the pygments highlight language should be.
-highlight_language = "cpp"
diff -Nru f3d-3.2.0+dfsg/vtkext/public/doxygen/index.rst f3d-3.4.1/vtkext/public/doxygen/index.rst
--- f3d-3.2.0+dfsg/vtkext/public/doxygen/index.rst	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/doxygen/index.rst	1970-01-01 02:00:00.000000000 +0200
@@ -1,26 +0,0 @@
-.. vtkext documentation master file, created by
-   sphinx-quickstart on Sat Nov 19 12:29:28 2022.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-vtkext doxygen documentation
-============================
-
-This is the automatically generated doxygen documentation for the vtkext_,
-which is part of F3D_.
-
-.. _vtkext: https://f3d.app/doc/libf3d/PLUGINS.html
-.. _F3D: https://f3d.app
-
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Contents:
-
-   api/library_root
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`search`
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/CMakeLists.txt f3d-3.4.1/vtkext/public/module/CMakeLists.txt
--- f3d-3.2.0+dfsg/vtkext/public/module/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -1,9 +1,13 @@
-set(shader_files
-  glsl/vtkF3DBitonicSortGlobalDisperseCS.glsl
-  glsl/vtkF3DBitonicSortGlobalFlipCS.glsl
-  glsl/vtkF3DBitonicSortLocalDisperseCS.glsl
-  glsl/vtkF3DBitonicSortLocalSortCS.glsl
-  glsl/vtkF3DBitonicSortFunctions.glsl)
+set(shader_files "")
+
+if(NOT ANDROID AND NOT EMSCRIPTEN)
+  set(shader_files ${shader_files}
+    glsl/vtkF3DBitonicSortGlobalDisperseCS.glsl
+    glsl/vtkF3DBitonicSortGlobalFlipCS.glsl
+    glsl/vtkF3DBitonicSortLocalDisperseCS.glsl
+    glsl/vtkF3DBitonicSortLocalSortCS.glsl
+    glsl/vtkF3DBitonicSortFunctions.glsl)
+endif()
 
 foreach(file IN LISTS shader_files)
   vtk_encode_string(
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/F3DUtils.h f3d-3.4.1/vtkext/public/module/F3DUtils.h
--- f3d-3.2.0+dfsg/vtkext/public/module/F3DUtils.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/F3DUtils.h	2026-01-08 08:41:55.000000000 +0200
@@ -10,7 +10,9 @@
 
 #include "vtkextModule.h"
 
+/// @cond
 #include <string>
+/// @endcond
 
 namespace F3DUtils
 {
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/Testing/TestF3DBitonicSort.cxx f3d-3.4.1/vtkext/public/module/Testing/TestF3DBitonicSort.cxx
--- f3d-3.2.0+dfsg/vtkext/public/module/Testing/TestF3DBitonicSort.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/Testing/TestF3DBitonicSort.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -10,6 +10,7 @@
 #include "vtkF3DBitonicSortLocalSortCS.h"
 
 #include <algorithm>
+#include <iostream>
 #include <random>
 
 int TestF3DBitonicSort(int argc, char* argv[])
@@ -54,40 +55,40 @@
   // check invalid workgroup size
   if (sorter->Initialize(-1, VTK_FLOAT, VTK_FLOAT))
   {
-    std::cerr << "The invalid workgroup size is not failing" << std::endl;
+    std::cerr << "The invalid workgroup size is not failing\n";
     return EXIT_FAILURE;
   }
 
   // check invalid types
   if (sorter->Initialize(128, VTK_CHAR, VTK_FLOAT))
   {
-    std::cerr << "The invalid key type is not failing" << std::endl;
+    std::cerr << "The invalid key type is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (sorter->Initialize(128, VTK_FLOAT, VTK_CHAR))
   {
-    std::cerr << "The invalid key type is not failing" << std::endl;
+    std::cerr << "The invalid key type is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (sorter->Run(
         vtkOpenGLRenderWindow::SafeDownCast(renWin), nbElements, bufferKeys, bufferValues))
   {
-    std::cerr << "Uninitialized run is not failing" << std::endl;
+    std::cerr << "Uninitialized run is not failing\n";
     return EXIT_FAILURE;
   }
 
   if (!sorter->Initialize(128, VTK_DOUBLE, VTK_INT))
   {
-    std::cerr << "Valid Initialize call failed" << std::endl;
+    std::cerr << "Valid Initialize call failed\n";
     return EXIT_FAILURE;
   }
 
   if (!sorter->Run(
         vtkOpenGLRenderWindow::SafeDownCast(renWin), nbElements, bufferKeys, bufferValues))
   {
-    std::cerr << "Sorter Run call failed" << std::endl;
+    std::cerr << "Sorter Run call failed\n";
     return EXIT_FAILURE;
   }
 
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DBitonicSort.cxx f3d-3.4.1/vtkext/public/module/vtkF3DBitonicSort.cxx
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DBitonicSort.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DBitonicSort.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -28,7 +28,7 @@
 //----------------------------------------------------------------------------
 bool vtkF3DBitonicSort::Initialize(int workgroupSize, int keyType, int valueType)
 {
-  if (workgroupSize < 0)
+  if (workgroupSize <= 0)
   {
     vtkErrorMacro("Invalid workgroupSize");
     return false;
@@ -116,7 +116,7 @@
 bool vtkF3DBitonicSort::Run(vtkOpenGLRenderWindow* context, int nbPairs,
   vtkOpenGLBufferObject* keys, vtkOpenGLBufferObject* values)
 {
-  if (this->WorkgroupSize < 0)
+  if (this->WorkgroupSize <= 0)
   {
     vtkErrorMacro("Shaders are not initialized");
     return false;
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DBitonicSort.h f3d-3.4.1/vtkext/public/module/vtkF3DBitonicSort.h
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DBitonicSort.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DBitonicSort.h	2026-01-08 08:41:55.000000000 +0200
@@ -3,15 +3,17 @@
  * @brief   Compute shader used to sort key/value pairs
  *
  * This class is used to sort buffers based on the Bitonic Sort algorithm.
- * Inspired by https://poniesandlight.co.uk/reflect/bitonic_merge_sort/
- * The original code can be found there: https://github.com/tgfrerer/island
- * It's mostly rewritten but some parts are copied (MIT license, Tim Gfrerer)
+ * Inspired by https://poniesandlight.co.uk/reflect/bitonic_merge_sort/.
+ * The original code can be found there: https://github.com/tgfrerer/island.
+ * It's mostly rewritten but some parts are copied (MIT license, Tim Gfrerer).
  */
 #ifndef vtkF3DBitonicSort_h
 #define vtkF3DBitonicSort_h
 
+/// @cond
 #include <vtkNew.h>
 #include <vtkObject.h>
+/// @endcond
 
 #include "vtkextModule.h"
 
@@ -28,20 +30,23 @@
 
   /**
    * Initialize the compute shaders.
-   * workgroupSize is the number of threads running in a single GPU workgroup
-   * keyType and valueType are the VTK types of the key and value to sort respectively
+   * @param workgroupSize The number of threads running in a single GPU workgroup.
+   * @param keyType The VTK type of the key to sort.
+   * @param valueType The VTK type of the value to sort.
    * Only VTK_DOUBLE, VTK_FLOAT, VTK_INT and VTK_UNSIGNED_INT are supported
-   * Returns true if succeeded
+   * @return true if succeeded.
    */
   bool Initialize(int workgroupSize, int keyType, int valueType);
 
   /**
    * Run the compute shader and sort the buffers.
    * An OpenGL context must exists and given as input in the first argument
-   * nbPairs is the number of element in the buffer keys and values
-   * OpenGL buffers keys and values must be valid and containing data types specified when
-   * this class has been initialized
-   * Returns true if succeeded
+   * @param nbPairs The number of element in the buffer keys and values.
+   * @param keys OpenGL buffers keys. Must be valid and match data type specified during
+   * initialization.
+   * @param values OpenGL buffers values. Must be valid and match data type specified during
+   * initialization.
+   * @return true if succeeded.
    */
   bool Run(vtkOpenGLRenderWindow* context, int nbPairs, vtkOpenGLBufferObject* keys,
     vtkOpenGLBufferObject* values);
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DFaceVaryingPointDispatcher.h f3d-3.4.1/vtkext/public/module/vtkF3DFaceVaryingPointDispatcher.h
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DFaceVaryingPointDispatcher.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DFaceVaryingPointDispatcher.h	2026-01-08 08:41:55.000000000 +0200
@@ -1,21 +1,22 @@
 /**
  * @class vtkF3DFaceVaryingPointDispatcher
- * @brief dispatch face-varying attributes by duplicating points.
+ * @brief Dispatch face-varying attributes by duplicating points.
  *
  * This filter processes arrays on point data in case some of them are flagged
  * as face-varying in which case points must be duplicated before rendering
  *
  * Face-varying attributes are a special case between point and cell data where the number of tuples
  * in the attributes is equal to the cell connectivity array size.
- * For example, if we have two adjacent quads, we will have 6 points and 8 cell indices (4 per quad)
- * Face-varying attributes, even if located on point data will have 8 tuples, and not 6
- * It can be seen as attributes, but this filter will normalize it by outputting 8 points.
- *
+ * For example, if we have two adjacent quads, we will have 6 points and 8 cell indices (4 per
+ * quad). Face-varying attributes, even if located on point data will have 8 tuples, and not 6. It
+ * can be seen as attributes, but this filter will normalize it by outputting 8 points.
  */
 #ifndef vtkF3DFaceVaryingPointDispatcher_h
 #define vtkF3DFaceVaryingPointDispatcher_h
 
+/// @cond
 #include <vtkPolyDataAlgorithm.h>
+/// @endcond
 
 #include "vtkextModule.h"
 
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DGLTFImporter.h f3d-3.4.1/vtkext/public/module/vtkF3DGLTFImporter.h
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DGLTFImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DGLTFImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -10,8 +10,10 @@
 
 #include "vtkextModule.h"
 
+/// @cond
 #include <vtkGLTFImporter.h>
 #include <vtkVersion.h>
+/// @endcond
 
 class vtkInformationIntegerKey;
 
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DImporter.cxx f3d-3.4.1/vtkext/public/module/vtkF3DImporter.cxx
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DImporter.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DImporter.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -28,6 +28,16 @@
 
 #endif
 
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20251210)
+//----------------------------------------------------------------------------
+bool vtkF3DImporter::GetTemporalInformation(
+  vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps, vtkDoubleArray* timeSteps)
+{
+  return this->vtkImporter::GetTemporalInformation(
+    animationIndex, 0, nbTimeSteps, timeRange, timeSteps);
+}
+#endif
+
 //----------------------------------------------------------------------------
 void vtkF3DImporter::SetFailureStatus()
 {
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DImporter.h f3d-3.4.1/vtkext/public/module/vtkF3DImporter.h
--- f3d-3.2.0+dfsg/vtkext/public/module/vtkF3DImporter.h	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtkF3DImporter.h	2026-01-08 08:41:55.000000000 +0200
@@ -11,14 +11,22 @@
 
 #include "vtkextModule.h"
 
+/// @cond
 #include <vtkImporter.h>
 #include <vtkVersion.h>
 
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20250923)
+#include <vtkResourceStream.h>
+#include <vtkSmartPointer.h>
+#endif
+/// @endcond
+
 class vtkInformationIntegerKey;
 
 class VTKEXT_EXPORT vtkF3DImporter : public vtkImporter
 {
 public:
+  vtkTypeMacro(vtkF3DImporter, vtkImporter);
   /**
    * Information key used to flag actors.
    * Actors having this flag will be drawn on top.
@@ -50,11 +58,45 @@
   }
 #endif
 
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20251210)
+  using vtkImporter::GetTemporalInformation;
+  /**
+   * Forward call to pre-20251210 implementation for GetTemporalInformation
+   * with framerate at 0
+   */
+  virtual bool GetTemporalInformation(
+    vtkIdType animationIndex, double timeRange[2], int& nbTimeSteps, vtkDoubleArray* timeSteps);
+#endif
+
   /**
    * Call this method to set the status to failure if supported
    * by the VTK version in use
    */
   void SetFailureStatus();
+
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20250923)
+  ///@{
+  /**
+   * Specify file name of the file to read
+   */
+  vtkSetFilePathMacro(FileName);
+  vtkGetFilePathMacro(FileName);
+  ///@}
+
+  ///@{
+  /**
+   * Specify stream to read from
+   */
+  vtkSetSmartPointerMacro(Stream, vtkResourceStream);
+  vtkGetSmartPointerMacro(Stream, vtkResourceStream);
+  ///@}
+#endif
+
+private:
+#if VTK_VERSION_NUMBER < VTK_VERSION_CHECK(9, 5, 20250923)
+  char* FileName = nullptr;
+  vtkSmartPointer<vtkResourceStream> Stream;
+#endif
 };
 
 #endif
diff -Nru f3d-3.2.0+dfsg/vtkext/public/module/vtk.module f3d-3.4.1/vtkext/public/module/vtk.module
--- f3d-3.2.0+dfsg/vtkext/public/module/vtk.module	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/vtkext/public/module/vtk.module	2026-01-08 08:41:55.000000000 +0200
@@ -5,6 +5,7 @@
 DEPENDS
   VTK::CommonExecutionModel
   VTK::IOImport
+  VTK::IOCore
 PRIVATE_DEPENDS
   VTK::CommonCore
   VTK::RenderingOpenGL2
diff -Nru f3d-3.2.0+dfsg/webassembly/app.html f3d-3.4.1/webassembly/app.html
--- f3d-3.2.0+dfsg/webassembly/app.html	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/app.html	1970-01-01 02:00:00.000000000 +0200
@@ -1,489 +0,0 @@
-<!doctype html>
-<html class="theme-dark">
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <title>F3D Web</title>
-    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
-    <link
-      rel="stylesheet"
-      href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css"
-    />
-    <link
-      rel="stylesheet"
-      href="https://cdn.jsdelivr.net/npm/bulma-switch@2.0.4/dist/css/bulma-switch.min.css"
-    />
-    <link rel="manifest" href="manifest.json" />
-    <style>
-      #main {
-        min-height: 70vh;
-      }
-    </style>
-  </head>
-
-  <body>
-    <section class="section">
-      <div class="notification is-warning">
-        <button class="delete"></button>
-        This web application is experimental and currently not well tested. It's
-        deployed continuously and can break at anytime. Don't hesitate to reach
-        out on <a href="https://discord.f3d.app">Discord</a>
-        if it's currently not working or if you want to contribute.
-      </div>
-      <div class="columns">
-        <div class="column">
-          <h1 class="title">F3D Web</h1>
-          <p class="subtitle">A WebAssembly application using libf3d</p>
-        </div>
-        <div class="column">
-          <div class="field is-pulled-right">
-            <input
-              id="dark"
-              type="checkbox"
-              name="dark"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="dark">Dark theme</label>
-          </div>
-        </div>
-      </div>
-    </section>
-    <section class="section">
-      <div class="columns">
-        <aside class="menu">
-          <div class="file has-name">
-            <label class="file-label">
-              <input
-                class="file-input"
-                type="file"
-                id="file-selector"
-                accept=".gml,.gltf,.glb,.obj,.ply,.pts,.stl,.vtk,.vtp,.vtu,.vtkhdf,.3ds,.wrl,.vrml,.fbx,.off,.x,.dae,.ex2,.exo,.e,.stp,.step,.igs,.iges,.brep,.xbf,.drc,.mdl"
-              />
-              <span class="file-cta">
-                <span class="file-label">Open a file...</span>
-              </span>
-              <span class="file-name" id="file-name"></span>
-            </label>
-          </div>
-          <div class="is-flex is-flex-direction-row is-align-items-center">
-            <button id="y-up" class="button mx-1 is-small">+Y</button>
-            <button id="z-up" class="button mx-1 is-small is-active">+Z</button>
-            <label class="mx-1">Up</label>
-          </div>
-          <p class="menu-label">Widgets</p>
-          <div class="field">
-            <input
-              id="grid"
-              type="checkbox"
-              name="grid"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="grid">Grid</label>
-          </div>
-          <div class="field">
-            <input
-              id="axis"
-              type="checkbox"
-              name="axis"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="axis">Axis</label>
-          </div>
-          <p class="menu-label">Rendering</p>
-          <div class="field">
-            <input
-              id="fxaa"
-              type="checkbox"
-              name="fxaa"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="fxaa">Anti-aliasing</label>
-          </div>
-          <div class="field">
-            <input
-              id="tone"
-              type="checkbox"
-              name="tone"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="tone">Tone mapping</label>
-          </div>
-          <div class="field">
-            <input
-              id="ssao"
-              type="checkbox"
-              name="ssao"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="ssao">Ambient occlusion</label>
-          </div>
-          <p class="menu-label">Lighting</p>
-          <div class="field">
-            <input
-              id="ambient"
-              type="checkbox"
-              name="ambient"
-              class="switch is-rounded"
-              checked
-            />
-            <label for="ambient">Ambient light</label>
-          </div>
-        </aside>
-        <div class="column">
-          <div class="container" id="main">
-            <canvas id="canvas"></canvas>
-          </div>
-        </div>
-      </div>
-      <div
-        class="event-log"
-        style="
-          position: absolute;
-          z-index: 10;
-          bottom: 50px;
-          width: 100%;
-          display: none;
-        "
-      >
-        <p style="text-align: center">Loading 3D object...</p>
-        <progress
-          class="progress is-primary"
-          id="progressEl"
-          value="0"
-          max="100"
-          style="transition: all 2s; width: 500px; margin: auto"
-        >
-          30%
-        </progress>
-      </div>
-    </section>
-    <script type="text/javascript" src="f3d.js"></script>
-    <script type="text/javascript">
-      // remove warning when delete button is clicked
-      (document.querySelectorAll(".notification .delete") || []).forEach(
-        ($delete) => {
-          const $notification = $delete.parentNode;
-
-          $delete.addEventListener("click", () => {
-            $notification.parentNode.removeChild($notification);
-          });
-        },
-      );
-
-      const settings = {
-        canvas: document.getElementById("canvas"),
-        setupOptions: (options) => {
-          // background must be set to black for proper blending with transparent canvas
-          options.set_color("render.background.color", 0, 0, 0);
-
-          // setup coloring
-          options.toggle("model.scivis.enable");
-          options.set_string("model.scivis.array_name", "Colors");
-          options.set_integer("model.scivis.component", -2);
-          options.toggle("model.scivis.cells");
-
-          // make it look nice
-          options.toggle("render.effect.antialiasing.enable");
-          options.toggle("render.effect.tone_mapping");
-          options.toggle("render.effect.ambient_occlusion");
-          options.toggle("render.hdri.ambient");
-
-          // display widgets
-          options.toggle("ui.axis");
-          options.toggle("render.grid.enable");
-
-          // default to +Z
-          options.set_as_string("scene.up_direction", "+Z");
-        },
-      };
-
-      f3d(settings)
-        .then((Module) => {
-          // automatically load all supported file format readers
-          Module.Engine.autoloadPlugins();
-
-          Module.engineInstance = Module.Engine.create();
-
-          const openFile = (name) => {
-            document.getElementById("file-name").innerHTML = name;
-            const filePath = "/" + name;
-            const scene = Module.engineInstance.getScene();
-            if (scene.supports(filePath)) {
-              scene.clear();
-              scene.add(filePath);
-            } else {
-              console.error("File " + filePath + " cannot be opened");
-            }
-            Module.engineInstance.getWindow().resetCamera();
-            Module.engineInstance.getWindow().render();
-          };
-
-          // setup file open event
-          const progressEl = document.querySelector("#progressEl");
-          const fileSelector = document.querySelector("#file-selector");
-          fileSelector.addEventListener("change", (evt) => {
-            for (const file of evt.target.files) {
-              const reader = new FileReader();
-              reader.addEventListener("loadend", (e) => {
-                progressEl.textContent = "100%";
-                progressEl.value = 100;
-                Module.FS.writeFile(file.name, new Uint8Array(reader.result));
-                openFile(file.name);
-                progressEl.parentElement.style.display = "none";
-              });
-              reader.addEventListener("progress", (evt) => {
-                console.log(evt);
-                const progress = Math.floor((100 * evt.loaded) / evt.total);
-                progressEl.parentElement.style.display = "block";
-                progressEl.value = progress;
-                progressEl.textContent = `${progress}%`;
-              });
-              reader.readAsArrayBuffer(file);
-            }
-          });
-
-          Module.setupOptions(Module.engineInstance.getOptions());
-
-          // Storing dom el ids to f3d option mappings since also useful for url-param parsing
-          const idOptionMappings = [
-            ["grid", "render.grid.enable"],
-            ["axis", "ui.axis"],
-            ["fxaa", "render.effect.antialiasing.enable"],
-            ["tone", "render.effect.tone_mapping"],
-            ["ssao", "render.effect.ambient_occlusion"],
-            ["ambient", "render.hdri.ambient"],
-          ];
-
-          // toggle callback
-          const mapToggleIdToOption = (id, option) => {
-            document
-              .querySelector("#" + id)
-              .addEventListener("change", (evt) => {
-                Module.engineInstance.getOptions().toggle(option);
-                Module.engineInstance.getWindow().render();
-              });
-          };
-
-          // This assumes all toggles are 'on' before mapping their state to options
-          // Ok after f3d(settings) where settings = {..., setupOptions} which toggles some options
-          for (let [id, option] of idOptionMappings) {
-            mapToggleIdToOption(id, option);
-          }
-
-          switchDark = () => {
-            document.documentElement.classList.add("theme-dark");
-            document.documentElement.classList.remove("theme-light");
-            Module.engineInstance
-              .getOptions()
-              .set_color("render.grid.color", 0.25, 0.27, 0.33);
-            Module.engineInstance.getWindow().render();
-          };
-
-          switchLight = () => {
-            document.documentElement.classList.add("theme-light");
-            document.documentElement.classList.remove("theme-dark");
-            Module.engineInstance
-              .getOptions()
-              .set_color("render.grid.color", 0.67, 0.69, 0.75);
-            Module.engineInstance.getWindow().render();
-          };
-
-          // theme switch
-          document.querySelector("#dark").addEventListener("change", (evt) => {
-            if (evt.target.checked) switchDark();
-            else switchLight();
-          });
-
-          switchDark();
-
-          // up callback
-          document.querySelector("#z-up").addEventListener("click", (evt) => {
-            Module.engineInstance
-              .getOptions()
-              .set_as_string("scene.up_direction", "+Z");
-            document.getElementById("z-up").classList.add("is-active");
-            document.getElementById("y-up").classList.remove("is-active");
-            openFile(document.getElementById("file-name").innerHTML);
-          });
-
-          document.querySelector("#y-up").addEventListener("click", (evt) => {
-            Module.engineInstance
-              .getOptions()
-              .set_as_string("scene.up_direction", "+Y");
-            document.getElementById("y-up").classList.add("is-active");
-            document.getElementById("z-up").classList.remove("is-active");
-            openFile(document.getElementById("file-name").innerHTML);
-          });
-
-          // setup the window size based on the canvas size
-          const main = document.getElementById("main");
-          const scale = window.devicePixelRatio;
-          Module.engineInstance
-            .getWindow()
-            .setSize(scale * main.clientWidth, scale * main.clientHeight);
-
-          // do a first render and start the interactor
-          Module.engineInstance.getWindow().render();
-          Module.engineInstance.getInteractor().start();
-
-          // Parse model from url-param search query hash via model url and extension
-          function filename_for_model_url(
-            model_url,
-            extension_parsed,
-            contentDisposition,
-          ) {
-            // Build filename given extension urlparam or response header content-disposition
-            if (extension_parsed) {
-              return `model_urlparam.${extension_parsed}`;
-            } else if (contentDisposition) {
-              // If extension is not provided by user, try to get it auto from content-disposition header of url extension
-              return contentDisposition.split("filename=")[1].split(";")[0];
-            } else {
-              return model_url.split("/").pop();
-            }
-            throw new Error(
-              `Could not parse filename/extension from either urlparam extension, response header content-disposition, nor filename present in url`,
-            );
-          }
-
-          // Have fetch support onProgress event
-          async function fetchWithProgress(url, onProgress) {
-            const response = await fetch(url);
-            if (!response.ok) {
-              throw new Error(`HTTP error! status: ${response.status}`);
-            }
-            const contentLength = response.headers.get("Content-Length");
-            if (!contentLength) {
-              console.warn("Content-Length header is missing");
-            }
-            const total = contentLength ? parseInt(contentLength, 10) : 0;
-            let loaded = 0;
-            const reader = response.body.getReader();
-            const stream = new ReadableStream({
-              start(controller) {
-                function push() {
-                  reader
-                    .read()
-                    .then(({ done, value }) => {
-                      if (done) {
-                        controller.close();
-                        return;
-                      }
-                      loaded += value.length;
-                      if (onProgress) {
-                        onProgress(loaded, total);
-                      }
-                      controller.enqueue(value);
-                      push();
-                    })
-                    .catch((err) => {
-                      console.error("Stream reading error:", err);
-                      controller.error(err);
-                    });
-                }
-                push();
-              },
-            });
-            return new Response(stream);
-          }
-          function onFetchProgress(loaded, total) {
-            const progress = Math.floor((100 * loaded) / total);
-            progressEl.parentElement.style.display = "block";
-            progressEl.value = progress;
-            progressEl.textContent = `${progress}%`;
-            console.log(`${progress}%`, loaded, total);
-          }
-
-          // Parse search-query model url-param or load default model file
-          function load_from_url() {
-            // const params = new URLSearchParams(window.location.search);
-            // Replace first hash with question mark to have real search query parsing and avoid leading # in first parsed urlparam
-            const params = new URLSearchParams(
-              window.location.hash.replace(/^#/, "?"),
-            );
-            const model_url_passed = params.get("model");
-            const extension_parsed = params.get("extension");
-            if (model_url_passed) {
-              const model_url = decodeURI(model_url_passed);
-              fetchWithProgress(model_url, onFetchProgress)
-                // fetch(model_url)
-                .then((response) => {
-                  if (!response.ok) {
-                    throw new Error(`HTTP error, status = ${response.status}`);
-                  }
-                  const contentDisposition = response.headers.get(
-                    "content-disposition",
-                  );
-                  const filename = filename_for_model_url(
-                    model_url,
-                    extension_parsed,
-                    contentDisposition,
-                  );
-                  // Convert buffer to Uint8Array and openFile
-                  response.arrayBuffer().then((buffer) => {
-                    Module.FS.writeFile(filename, new Uint8Array(buffer));
-                    openFile(filename);
-                    progressEl.parentElement.style.display = "none";
-                  });
-                })
-                .catch(function (error) {
-                  console.log("error caught during fetch", error);
-                  alert(
-                    "Error occurred while fetching the model at provided url (passed as url-param). \n\nThis might be a CORS issue (you can avoid it by using a Allow-CORS plugin) because server does not allow cross-origin requests, or the url to the file is wrong. \n\nComplete error message: " +
-                      error.message,
-                  );
-                });
-            } else {
-              // load the file located in the virtual filesystem
-              openFile("f3d.vtp");
-            }
-
-            // Parse other options like #axis=false&grid=false&fxaa=false&tone=false&ssao=false&ambient=false
-            const options = Module.engineInstance.getOptions();
-            for (let [id, option] of idOptionMappings) {
-              // Set f3d option parameter values
-              const parsed_param_value = params.get(id);
-              if (parsed_param_value) {
-                // Let set_as_string handle value conversion and validation.
-                options.set_as_string(option, parsed_param_value.toLowerCase());
-                // However, get_as_string returns string rather than typed value
-                const new_val = options.get_as_string(option) == "true";
-                ui_switch = document.querySelector("#" + id);
-                if (ui_switch) ui_switch.checked = new_val;
-              }
-            }
-
-            // Parse up vector and update UI, up is either +Z or +Y
-            // + Plus sign means spaces in urls, so +Z has to be replaced by %2BZ
-            const parsed_up_value = params.get("up");
-            if (parsed_up_value) {
-              console.log("up", parsed_up_value);
-              options.set_string("scene.up_direction", parsed_up_value);
-              for (const el of ["z-up", "y-up"])
-                document.getElementById(el).classList.remove("is-active");
-              if (parsed_up_value == "+Y") {
-                document.getElementById("y-up").classList.add("is-active");
-              } else {
-                document.getElementById("z-up").classList.add("is-active");
-              }
-              openFile(document.getElementById("file-name").innerHTML);
-            }
-
-            // Needs re-render to update F3D options
-            Module.engineInstance.getWindow().render();
-          }
-          addEventListener("hashchange", (event) => {
-            load_from_url();
-          });
-          load_from_url();
-        })
-        .catch((error) => console.error("Internal exception: " + error));
-    </script>
-  </body>
-</html>
diff -Nru f3d-3.2.0+dfsg/webassembly/build.sh f3d-3.4.1/webassembly/build.sh
--- f3d-3.2.0+dfsg/webassembly/build.sh	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/build.sh	2026-01-08 08:41:55.000000000 +0200
@@ -2,7 +2,7 @@
 
 set -euo pipefail
 
-cmake -S /src -B /build \
+cmake -S /src -B /src/_wasm_build \
     -DBUILD_SHARED_LIBS=OFF \
     -DCMAKE_BUILD_TYPE=$1 \
     -DCMAKE_FIND_ROOT_PATH:PATH=/depends \
@@ -11,11 +11,10 @@
     -DF3D_PLUGIN_BUILD_DRACO=ON \
     -DF3D_PLUGIN_BUILD_HDF=OFF \
     -DF3D_PLUGIN_BUILD_OCCT=ON \
-    -DF3D_STRICT_BUILD=ON \
-    -DF3D_WASM_COPY_APP=ON
+    -DF3D_STRICT_BUILD=ON
 
-cmake --build /build
+cmake --build /src/_wasm_build
 
 # Copy generated js/wasm to dist
-mkdir -p /src/webassembly/dist
-mv /build/bin/* /src/webassembly/dist
+mkdir -p /src/dist
+mv /src/_wasm_build/bin/* /src/dist
diff -Nru f3d-3.2.0+dfsg/webassembly/CMakeLists.txt f3d-3.4.1/webassembly/CMakeLists.txt
--- f3d-3.2.0+dfsg/webassembly/CMakeLists.txt	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -5,57 +5,34 @@
   message(FATAL_ERROR "Cannot build emscripten bindings with VTK version below 9.3.20231007")
 endif()
 
-set(F3D_WASM_DATA_FILE "${F3D_SOURCE_DIR}/testing/data/f3d.vtp"
-  CACHE PATH "File to copy in the webassembly root virtual filesystem")
-
 add_executable(f3djs F3DEmscriptenBindings.cxx)
 
 set_target_properties(f3djs PROPERTIES
   OUTPUT_NAME f3d
 )
 
-target_link_libraries(f3djs PRIVATE libf3d)
+target_link_libraries(f3djs PRIVATE libf3d embind)
 
 target_link_options(f3djs PRIVATE
   "$<IF:$<CONFIG:Release>,-Oz,-O0>"
-  "--bind"
-  "--closure 1"
-  "SHELL:--preload-file ${F3D_WASM_DATA_FILE}@/"
+  "--emit-tsd=f3d.d.ts"
   "SHELL:-sEXPORT_NAME=f3d"
   "SHELL:-sALLOW_MEMORY_GROWTH=1"
   "SHELL:-sEMULATE_FUNCTION_POINTER_CASTS=0"
   "SHELL:-sMODULARIZE=1"
+  "SHELL:-sEXPORT_ES6=1"
+  "SHELL:-sENVIRONMENT=web"
   "SHELL:-sUSE_PTHREADS=0"
   "SHELL:-sSTACK_SIZE=1048576"
   "SHELL:-sWASM=1"
   "SHELL:-sFORCE_FILESYSTEM"
   "SHELL:-sEXPORTED_RUNTIME_METHODS=FS"
   "SHELL:-sWASM_BIGINT"
-  "SHELL:-sNO_DISABLE_EXCEPTION_CATCHING"
 )
 
-# Option to copy app.html file to index.html in the binary folder
-# In order to test the app:
-# - go to the binary folder where f3d.js, f3d.wasm and f3d.data are located
-# - run `python -m http.server 8000`
-# - open a browser to http://localhost:8000
-option(F3D_WASM_COPY_APP "Copy HTML app file to binary folder" ON)
-
-if(F3D_WASM_COPY_APP)
-  configure_file(
-    "${CMAKE_CURRENT_SOURCE_DIR}/app.html"
-    "${CMAKE_BINARY_DIR}/bin/index.html"
-    COPYONLY)
-  configure_file(
-    "${F3D_SOURCE_DIR}/resources/logo.ico"
-    "${CMAKE_BINARY_DIR}/bin/favicon.ico"
-    COPYONLY)
-  configure_file(
-    "${F3D_SOURCE_DIR}/resources/logo256.png"
-    "${CMAKE_BINARY_DIR}/bin/icon256.png"
-    COPYONLY)
-  configure_file(
-    "${CMAKE_CURRENT_SOURCE_DIR}/manifest.json"
-    "${CMAKE_BINARY_DIR}/bin/manifest.json"
-    COPYONLY)
-endif()
+# The tests rely on the presence of chromium
+# Since we rely on Docker, we know it's there so testing is always enabled
+# Moreover, we cannot use BUILD_TESTING since it enables testing in our VTK modules
+# and breaks the configuration (we should fix that properly at some point)
+enable_testing()
+add_subdirectory(testing)
diff -Nru f3d-3.2.0+dfsg/webassembly/F3DEmscriptenBindings.cxx f3d-3.4.1/webassembly/F3DEmscriptenBindings.cxx
--- f3d-3.2.0+dfsg/webassembly/F3DEmscriptenBindings.cxx	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/F3DEmscriptenBindings.cxx	2026-01-08 08:41:55.000000000 +0200
@@ -1,128 +1,462 @@
 #include <emscripten/bind.h>
 
+#include "camera.h"
 #include "engine.h"
 #include "interactor.h"
 #include "options.h"
 #include "scene.h"
 #include "window.h"
 
+// This is needed to avoid compilation issues because the destructors are protected
 namespace emscripten::internal
 {
 template<>
-void raw_destructor<f3d::scene>(f3d::scene* ptr)
+void raw_destructor(f3d::scene* ptr)
 {
 }
 
 template<>
-void raw_destructor<f3d::window>(f3d::window* ptr)
+void raw_destructor(f3d::window* ptr)
 {
 }
 
 template<>
-void raw_destructor<f3d::interactor>(f3d::interactor* ptr)
+void raw_destructor(f3d::interactor* ptr)
 {
 }
 
 template<>
-void raw_destructor<f3d::options>(f3d::options* ptr)
+void raw_destructor(f3d::camera* ptr)
 {
 }
 }
 
-f3d::options& toggle(f3d::options& o, const std::string& name)
+template<typename T>
+emscripten::val containerToJSArray(const T& container)
 {
-  return o.toggle(name);
-}
-f3d::options& set_as_string(f3d::options& o, const std::string& name, const std::string& value)
-{
-  return o.setAsString(name, value);
-}
-std::string get_as_string(f3d::options& o, const std::string& name)
-{
-  return o.getAsString(name);
-}
-f3d::options& set_string(f3d::options& o, const std::string& name, const std::string& value)
-{
-  return o.set(name, value);
-}
-f3d::options& set_integer(f3d::options& o, const std::string& name, int value)
-{
-  return o.set(name, value);
-}
-f3d::options& set_color(f3d::options& o, const std::string& name, double r, double g, double b)
-{
-  return o.set(name, std::vector{ r, g, b });
-}
-
-f3d::scene& getScenePtr(f3d::engine& e)
-{
-  return e.getScene();
-}
-f3d::scene& add(f3d::scene& l, const std::string& p)
-{
-  return l.add(p);
-}
-bool supports(f3d::scene& l, const std::string& p)
-{
-  return l.supports(p);
-}
-f3d::scene* clear(f3d::scene& l)
-{
-  return &l.clear();
-}
-
-f3d::window& resetCamera(f3d::window& win)
-{
-  win.getCamera().resetToBounds();
-  return win;
+  emscripten::val jsArray = emscripten::val::array();
+  for (const auto& elem : container)
+  {
+    jsArray.call<void>("push", elem);
+  }
+  return jsArray;
 }
 
-f3d::interactor& start(f3d::interactor& inter)
+template<typename U, typename V>
+emscripten::val pairToJSArray(const std::pair<U, V>& p)
 {
-  inter.start(1.0 / 30, nullptr);
-  return inter;
-}
-
-f3d::engine createEngine()
-{
-  return f3d::engine::create();
+  emscripten::val jsArray = emscripten::val::array();
+  jsArray.call<void>("push", p.first);
+  jsArray.call<void>("push", p.second);
+  return jsArray;
 }
 
 EMSCRIPTEN_BINDINGS(f3d)
 {
   // f3d::options
   emscripten::class_<f3d::options>("Options")
-    .function("toggle", &toggle, emscripten::return_value_policy::reference())
-    .function("set_as_string", &set_as_string, emscripten::return_value_policy::reference())
-    .function("get_as_string", &get_as_string, emscripten::return_value_policy::reference())
-    .function("set_string", &set_string, emscripten::return_value_policy::reference())
-    .function("set_integer", &set_integer, emscripten::return_value_policy::reference())
-    .function("set_color", &set_color, emscripten::return_value_policy::reference());
+    .constructor<>()
+    .function(
+      "get",
+      +[](f3d::options& options, const std::string& name) -> emscripten::val
+      {
+        try
+        {
+          f3d::option_variant_t value = options.get(name);
+          if (std::holds_alternative<bool>(value))
+          {
+            return emscripten::val(std::get<bool>(value));
+          }
+          if (std::holds_alternative<int>(value))
+          {
+            return emscripten::val(std::get<int>(value));
+          }
+          if (std::holds_alternative<double>(value))
+          {
+            return emscripten::val(std::get<double>(value));
+          }
+          if (std::holds_alternative<std::string>(value))
+          {
+            return emscripten::val(std::get<std::string>(value));
+          }
+          if (std::holds_alternative<std::vector<double>>(value))
+          {
+            return containerToJSArray(std::get<std::vector<double>>(value));
+          }
+          if (std::holds_alternative<std::vector<int>>(value))
+          {
+            return containerToJSArray(std::get<std::vector<int>>(value));
+          }
+        }
+        catch (f3d::exception)
+        {
+          return emscripten::val::undefined();
+        }
+
+        return emscripten::val::undefined();
+      })
+    .function(
+      "setAsString",
+      +[](f3d::options& o, const std::string& name, const std::string& value) -> f3d::options&
+      { return o.setAsString(name, value); }, emscripten::return_value_policy::reference())
+    .function(
+      "getAsString",
+      +[](f3d::options& o, const std::string& name) -> std::string { return o.getAsString(name); })
+    .function(
+      "toggle", +[](f3d::options& o, const std::string& name) -> f3d::options&
+      { return o.toggle(name); }, emscripten::return_value_policy::reference())
+    .function(
+      "isSame", +[](f3d::options& o, f3d::options& other, const std::string& name) -> bool
+      { return o.isSame(other, name); })
+    .function(
+      "hasValue",
+      +[](f3d::options& o, const std::string& name) -> bool { return o.hasValue(name); })
+    .function(
+      "copy", +[](f3d::options& o, f3d::options& other, const std::string& name) -> f3d::options&
+      { return o.copy(other, name); }, emscripten::return_value_policy::reference())
+    .class_function(
+      "getAllNames",
+      +[]() -> emscripten::val { return containerToJSArray(f3d::options::getAllNames()); })
+    .function(
+      "getNames",
+      +[](f3d::options& o) -> emscripten::val { return containerToJSArray(o.getNames()); })
+    .function(
+      "getClosestOption", +[](f3d::options& o, const std::string& name) -> emscripten::val
+      { return pairToJSArray(o.getClosestOption(name)); })
+    .function(
+      "isOptional",
+      +[](f3d::options& o, const std::string& name) -> bool { return o.isOptional(name); })
+    .function(
+      "reset", +[](f3d::options& o, const std::string& name) -> f3d::options&
+      { return o.reset(name); }, emscripten::return_value_policy::reference())
+    .function(
+      "removeValue", +[](f3d::options& o, const std::string& name) -> f3d::options&
+      { return o.removeValue(name); }, emscripten::return_value_policy::reference());
 
   // f3d::scene
+  // TODO:
+  // - add lights support
+  // - add f3d::mesh_t support
   emscripten::class_<f3d::scene>("Scene")
-    .function("supports", &supports, emscripten::return_value_policy::reference())
-    .function("add", &add, emscripten::return_value_policy::reference())
-    .function("clear", &clear, emscripten::return_value_policy::reference());
+    .function(
+      "supports",
+      +[](f3d::scene& scene, const std::string& path) -> bool { return scene.supports(path); })
+    .function(
+      "add",
+      +[](f3d::scene& scene, emscripten::val arg) -> f3d::scene&
+      {
+        if (arg.isArray())
+        {
+          return scene.add(emscripten::vecFromJSArray<std::string>(arg));
+        }
+        else
+        {
+          return scene.add(arg.as<std::string>());
+        }
+      },
+      emscripten::return_value_policy::reference())
+    .function("clear", &f3d::scene::clear, emscripten::return_value_policy::reference())
+    .function("loadAnimationTime", &f3d::scene::loadAnimationTime,
+      emscripten::return_value_policy::reference())
+    .function(
+      "animationTimeRange",
+      +[](f3d::scene& o) -> emscripten::val { return pairToJSArray(o.animationTimeRange()); })
+    .function("availableAnimations", &f3d::scene::availableAnimations);
+
+  // f3d::image
+  emscripten::enum_<f3d::image::SaveFormat>("ImageSaveFormat")
+    .value("PNG", f3d::image::SaveFormat::PNG)
+    .value("JPG", f3d::image::SaveFormat::JPG)
+    .value("TIF", f3d::image::SaveFormat::TIF)
+    .value("BMP", f3d::image::SaveFormat::BMP);
+
+  emscripten::enum_<f3d::image::ChannelType>("ImageChannelType")
+    .value("BYTE", f3d::image::ChannelType::BYTE)
+    .value("SHORT", f3d::image::ChannelType::SHORT)
+    .value("FLOAT", f3d::image::ChannelType::FLOAT);
+
+  emscripten::class_<f3d::image>("Image")
+    .constructor<>()
+    .constructor<const std::string&>()
+    .constructor<unsigned int, unsigned int, unsigned int, f3d::image::ChannelType>()
+    .function("equals", &f3d::image::operator==)
+    .function(
+      "getNormalizedPixel", +[](const f3d::image& img, int x, int y) -> emscripten::val
+      { return containerToJSArray(img.getNormalizedPixel({ x, y })); })
+    .class_function(
+      "getSupportedFormats",
+      +[]() -> emscripten::val { return containerToJSArray(f3d::image::getSupportedFormats()); })
+    .property("width", &f3d::image::getWidth)
+    .property("height", &f3d::image::getHeight)
+    .property("channelCount", &f3d::image::getChannelCount)
+    .property("channelType", &f3d::image::getChannelType)
+    .property("channelTypeSize", &f3d::image::getChannelTypeSize)
+    .function(
+      "setContent",
+      +[](f3d::image& img, emscripten::val jsbuf) -> f3d::image&
+      {
+        std::vector<uint8_t> data = emscripten::vecFromJSArray<uint8_t>(jsbuf);
+        size_t expected = static_cast<size_t>(img.getWidth()) * img.getHeight() *
+          img.getChannelCount() * img.getChannelTypeSize();
+        if (data.size() != expected)
+        {
+          throw std::runtime_error("Buffer size does not match image size");
+        }
+        img.setContent(data.data());
+        return img;
+      },
+      emscripten::allow_raw_pointers())
+    .function(
+      "getContent",
+      +[](const f3d::image& img) -> emscripten::val
+      {
+        size_t totalSize =
+          img.getWidth() * img.getHeight() * img.getChannelCount() * img.getChannelTypeSize();
+        return emscripten::val(
+          emscripten::typed_memory_view(totalSize, static_cast<const uint8_t*>(img.getContent())));
+      },
+      emscripten::allow_raw_pointers())
+    .function("compare", &f3d::image::compare)
+    .function(
+      "save",
+      +[](const f3d::image& img, const std::string& path,
+         f3d::image::SaveFormat format) -> const f3d::image& { return img.save(path, format); },
+      emscripten::allow_raw_pointers())
+    .function(
+      "saveBuffer",
+      +[](const f3d::image& img, f3d::image::SaveFormat format) -> emscripten::val
+      {
+        std::vector<uint8_t> buffer = img.saveBuffer(format);
+        return emscripten::val(emscripten::typed_memory_view(buffer.size(), buffer.data()));
+      })
+    .function("toTerminalText",
+      static_cast<std::string (f3d::image::*)() const>(&f3d::image::toTerminalText))
+    .function("setMetadata", &f3d::image::setMetadata, emscripten::allow_raw_pointers())
+    .function("getMetadata", &f3d::image::getMetadata)
+    .function(
+      "allMetadata", +[](const f3d::image& img) -> emscripten::val
+      { return containerToJSArray(img.allMetadata()); });
+
+  // f3d::camera
+  // TODO:
+  // - camera state
+  emscripten::class_<f3d::camera>("Camera")
+    .property(
+      "position", +[](const f3d::camera& cam) -> emscripten::val
+      { return containerToJSArray(cam.getPosition()); },
+      +[](f3d::camera& cam, emscripten::val jsArray) {
+        cam.setPosition({ jsArray[0].as<float>(), jsArray[1].as<float>(), jsArray[2].as<float>() });
+      })
+    .property(
+      "focalPoint", +[](const f3d::camera& cam) -> emscripten::val
+      { return containerToJSArray(cam.getFocalPoint()); },
+      +[](f3d::camera& cam, emscripten::val jsArray) {
+        cam.setFocalPoint(
+          { jsArray[0].as<float>(), jsArray[1].as<float>(), jsArray[2].as<float>() });
+      })
+    .property(
+      "viewUp", +[](const f3d::camera& cam) -> emscripten::val
+      { return containerToJSArray(cam.getViewUp()); },
+      +[](f3d::camera& cam, emscripten::val jsArray) {
+        cam.setViewUp({ jsArray[0].as<float>(), jsArray[1].as<float>(), jsArray[2].as<float>() });
+      })
+    .property("viewAngle",
+      static_cast<f3d::angle_deg_t (f3d::camera::*)() const>(&f3d::camera::getViewAngle),
+      &f3d::camera::setViewAngle)
+    .function("dolly", &f3d::camera::dolly, emscripten::return_value_policy::reference())
+    .function("pan", &f3d::camera::pan, emscripten::return_value_policy::reference())
+    .function("zoom", &f3d::camera::zoom, emscripten::return_value_policy::reference())
+    .function("roll", &f3d::camera::roll, emscripten::return_value_policy::reference())
+    .function("azimuth", &f3d::camera::azimuth, emscripten::return_value_policy::reference())
+    .function("yaw", &f3d::camera::yaw, emscripten::return_value_policy::reference())
+    .function("elevation", &f3d::camera::elevation, emscripten::return_value_policy::reference())
+    .function("pitch", &f3d::camera::pitch, emscripten::return_value_policy::reference())
+    .function("setCurrentAsDefault", &f3d::camera::setCurrentAsDefault,
+      emscripten::return_value_policy::reference())
+    .function(
+      "resetToDefault", &f3d::camera::resetToDefault, emscripten::return_value_policy::reference())
+    .function(
+      "resetToBounds", &f3d::camera::resetToBounds, emscripten::return_value_policy::reference());
 
   // f3d::window
+  // Not bound on purpose because these functions make no sense on the web:
+  // getType, isOffscreen, setPosition, setIcon, setWindowName
   emscripten::class_<f3d::window>("Window")
-    .function("setSize", &f3d::window::setSize, emscripten::return_value_policy::reference())
+    .function("getCamera", &f3d::window::getCamera, emscripten::return_value_policy::reference())
     .function("render", &f3d::window::render)
-    .function("resetCamera", &resetCamera, emscripten::return_value_policy::reference());
+    .function("renderToImage", &f3d::window::renderToImage)
+    .function("setSize", &f3d::window::setSize, emscripten::return_value_policy::reference())
+    .property("width", &f3d::window::getWidth)
+    .property("height", &f3d::window::getHeight)
+    .function(
+      "getWorldFromDisplay",
+      +[](const f3d::window& win, emscripten::val jsArray) -> emscripten::val
+      {
+        return containerToJSArray(win.getWorldFromDisplay(
+          { jsArray[0].as<float>(), jsArray[1].as<float>(), jsArray[2].as<float>() }));
+      })
+    .function(
+      "getDisplayFromWorld",
+      +[](const f3d::window& win, emscripten::val jsArray) -> emscripten::val
+      {
+        return containerToJSArray(win.getDisplayFromWorld(
+          { jsArray[0].as<float>(), jsArray[1].as<float>(), jsArray[2].as<float>() }));
+      });
 
   // f3d::interactor
+  emscripten::enum_<f3d::interactor::AnimationDirection>("InteractorAnimationDirection")
+    .value("FORWARD", f3d::interactor::AnimationDirection::FORWARD)
+    .value("BACKWARD", f3d::interactor::AnimationDirection::BACKWARD);
+
+  // Not bound on purpose because usually used for external interactors:
+  // trigger*
+  // TODO:
+  // - bindings
   emscripten::class_<f3d::interactor>("Interactor")
-    .function("start", &start, emscripten::return_value_policy::reference());
+    .function(
+      "initCommands", &f3d::interactor::initCommands, emscripten::return_value_policy::reference())
+    .function(
+      "addCommand",
+      +[](f3d::interactor& interactor, const std::string& action,
+         const emscripten::val& callback) -> f3d::interactor&
+      {
+        auto wrapCallback = [=](const std::vector<std::string>& args)
+        { callback(containerToJSArray(args)); };
+        return interactor.addCommand(action, wrapCallback);
+      },
+      emscripten::return_value_policy::reference())
+    .function("removeCommand", &f3d::interactor::removeCommand,
+      emscripten::return_value_policy::reference())
+    .function(
+      "getCommandActions", +[](const f3d::interactor& interactor) -> emscripten::val
+      { return containerToJSArray(interactor.getCommandActions()); })
+    .function(
+      "triggerCommand",
+      +[](f3d::interactor& interactor, const std::string& command, bool keepComments) -> bool
+      { return interactor.triggerCommand(command, keepComments); })
+    .function(
+      "toggleAnimation",
+      +[](f3d::interactor& interactor, emscripten::val direction) -> f3d::interactor&
+      {
+        return direction.isUndefined()
+          ? interactor.toggleAnimation()
+          : interactor.toggleAnimation(direction.as<f3d::interactor::AnimationDirection>());
+      },
+      emscripten::return_value_policy::reference())
+    .function(
+      "startAnimation",
+      +[](f3d::interactor& interactor, emscripten::val direction) -> f3d::interactor&
+      {
+        return direction.isUndefined()
+          ? interactor.startAnimation()
+          : interactor.startAnimation(direction.as<f3d::interactor::AnimationDirection>());
+      },
+      emscripten::return_value_policy::reference())
+    .function("stopAnimation", &f3d::interactor::stopAnimation,
+      emscripten::return_value_policy::reference())
+    .function("isPlayingAnimation", &f3d::interactor::isPlayingAnimation)
+    .function("getAnimationDirection", &f3d::interactor::getAnimationDirection)
+    .function("enableCameraMovement", &f3d::interactor::enableCameraMovement,
+      emscripten::return_value_policy::reference())
+    .function("disableCameraMovement", &f3d::interactor::disableCameraMovement,
+      emscripten::return_value_policy::reference())
+    .function(
+      "start", +[](f3d::interactor& interactor) -> f3d::interactor& { return interactor.start(); },
+      emscripten::return_value_policy::reference())
+    .function("stop", &f3d::interactor::stop, emscripten::return_value_policy::reference())
+    .function("requestRender", &f3d::interactor::requestRender,
+      emscripten::return_value_policy::reference())
+    .function(
+      "requestStop", &f3d::interactor::requestStop, emscripten::return_value_policy::reference());
 
   // f3d::engine
-  emscripten::class_<f3d::engine> engine("Engine");
-
-  engine.class_function("create", &createEngine, emscripten::return_value_policy::take_ownership())
-    .function("getScene", &f3d::engine::getScene, emscripten::return_value_policy::reference())
+  // Not bound on purpose because only one engine is supported:
+  // create*, getRenderingBackendList
+  emscripten::class_<f3d::engine::libInformation>("EngineLibInformation")
+    .property("version", &f3d::engine::libInformation::Version)
+    .property("versionFull", &f3d::engine::libInformation::VersionFull)
+    .property("buildDate", &f3d::engine::libInformation::BuildDate)
+    .property("buildSystem", &f3d::engine::libInformation::BuildSystem)
+    .property("compiler", &f3d::engine::libInformation::Compiler)
+    .property(
+      "modules",
+      +[](const f3d::engine::libInformation& libInfo) -> emscripten::val
+      {
+        emscripten::val jsArray = emscripten::val::array();
+        for (const auto& [module, enabled] : libInfo.Modules)
+        {
+          emscripten::val pair = emscripten::val::array();
+          pair.call<void>("push", module);
+          pair.call<void>("push", enabled);
+          jsArray.call<void>("push", pair);
+        }
+        return jsArray;
+      })
+    .property("vtkVersion", &f3d::engine::libInformation::VTKVersion)
+    .property(
+      "copyrights", +[](const f3d::engine::libInformation& libInfo) -> emscripten::val
+      { return containerToJSArray(libInfo.Copyrights); })
+    .property("license", &f3d::engine::libInformation::License);
+
+  emscripten::class_<f3d::engine::readerInformation>("EngineReaderInformation")
+    .property("name", &f3d::engine::readerInformation::Name)
+    .property("description", &f3d::engine::readerInformation::Description)
+    .property(
+      "extensions", +[](const f3d::engine::readerInformation& readerInfo) -> emscripten::val
+      { return containerToJSArray(readerInfo.Extensions); })
+    .property(
+      "mimeTypes", +[](const f3d::engine::readerInformation& readerInfo) -> emscripten::val
+      { return containerToJSArray(readerInfo.MimeTypes); })
+    .property("pluginName", &f3d::engine::readerInformation::PluginName)
+    .property("hasSceneReader", &f3d::engine::readerInformation::HasSceneReader)
+    .property("hasGeometryReader", &f3d::engine::readerInformation::HasGeometryReader);
+
+  emscripten::class_<f3d::engine>("Engine")
+    .class_function(
+      "create", +[]() { return f3d::engine::create(); },
+      emscripten::return_value_policy::take_ownership())
+    .function(
+      "setCachePath", +[](f3d::engine& engine, const std::string& path) -> f3d::engine&
+      { return engine.setCachePath(path); }, emscripten::return_value_policy::reference())
+    .function("setOptions",
+      static_cast<f3d::engine& (f3d::engine::*)(const f3d::options&)>(&f3d::engine::setOptions),
+      emscripten::return_value_policy::reference())
+    .function("getOptions", &f3d::engine::getOptions, emscripten::return_value_policy::reference())
     .function("getWindow", &f3d::engine::getWindow, emscripten::return_value_policy::reference())
+    .function("getScene", &f3d::engine::getScene, emscripten::return_value_policy::reference())
     .function(
       "getInteractor", &f3d::engine::getInteractor, emscripten::return_value_policy::reference())
-    .function("getOptions", &f3d::engine::getOptions, emscripten::return_value_policy::reference())
-    .class_function("autoloadPlugins", &f3d::engine::autoloadPlugins);
+    .class_function("autoloadPlugins", &f3d::engine::autoloadPlugins)
+    .class_function(
+      "getAllReaderOptionNames", +[]() -> emscripten::val
+      { return containerToJSArray(f3d::engine::getAllReaderOptionNames()); })
+    .class_function("setReaderOption", &f3d::engine::setReaderOption)
+    .class_function("getLibInfo", &f3d::engine::getLibInfo)
+    .class_function(
+      "getReadersInfo",
+      +[]() -> emscripten::val { return containerToJSArray(f3d::engine::getReadersInfo()); });
+
+  // f3d::log
+  emscripten::enum_<f3d::log::VerboseLevel>("LogVerboseLevel")
+    .value("DEBUG", f3d::log::VerboseLevel::DEBUG)
+    .value("INFO", f3d::log::VerboseLevel::INFO)
+    .value("WARN", f3d::log::VerboseLevel::WARN)
+    .value("ERROR", f3d::log::VerboseLevel::ERROR)
+    .value("QUIET", f3d::log::VerboseLevel::QUIET);
+
+  emscripten::class_<f3d::log>("Log")
+    .class_function("setVerboseLevel", f3d::log::setVerboseLevel)
+    .class_function("getVerboseLevel", f3d::log::getVerboseLevel)
+    .class_function("setUseColoring", f3d::log::setUseColoring)
+    .class_function(
+      "print",
+      +[](f3d::log::VerboseLevel level, const std::string& message)
+      { f3d::log::print(level, message); })
+    .class_function(
+      "forward",
+      +[](const emscripten::val& callback)
+      {
+        f3d::log::forward(
+          [=](f3d::log::VerboseLevel level, const std::string& txt) { callback(level, txt); });
+      });
 }
diff -Nru f3d-3.2.0+dfsg/webassembly/manifest.json f3d-3.4.1/webassembly/manifest.json
--- f3d-3.2.0+dfsg/webassembly/manifest.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/manifest.json	1970-01-01 02:00:00.000000000 +0200
@@ -1,15 +0,0 @@
-{
-  "short_name": "F3D",
-  "name": "F3D Web",
-  "icons": [
-    {
-      "src": "./icon256.png",
-      "sizes": "256x256",
-      "type": "image/png"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#788BFF",
-  "background_color": "#14161A"
-}
diff -Nru f3d-3.2.0+dfsg/webassembly/package.json f3d-3.4.1/webassembly/package.json
--- f3d-3.2.0+dfsg/webassembly/package.json	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/package.json	1970-01-01 02:00:00.000000000 +0200
@@ -1,34 +0,0 @@
-{
-  "name": "f3d",
-  "version": "3.2.0",
-  "description": "Fast and minimalist 3D viewer framework",
-  "main": "f3d.js",
-  "files": [
-    "main.js",
-    "dist/f3d.*"
-  ],
-  "scripts": {
-    "_docker": "docker run --rm -e CMAKE_BUILD_PARALLEL_LEVEL -v $(pwd)/..:/src ghcr.io/f3d-app/f3d-wasm:latest",
-    "clean": "npm run _docker -- rm -rf /src/webassembly/dist",
-    "build:rel": "npm run _docker -- /src/webassembly/build.sh Release",
-    "build:deb": "npm run _docker -- /src/webassembly/build.sh Debug",
-    "serve": "npx serve dist",
-    "prepare": "npm run clean && npm run build:rel"
-  },
-  "repository": "github:f3d-app/f3d",
-  "keywords": [
-    "3d",
-    "webassembly",
-    "viewer",
-    "animation",
-    "cad",
-    "gltf",
-    "vtk",
-    "stl"
-  ],
-  "author": "Michael Migliore and Mathieu Westphal",
-  "license": "BSD-2-Clause",
-  "bugs": "https://github.com/f3d-app/f3d/issues",
-  "funding": "https://github.com/sponsors/f3d-app",
-  "homepage": "https://f3d.app"
-}
diff -Nru f3d-3.2.0+dfsg/webassembly/README.md f3d-3.4.1/webassembly/README.md
--- f3d-3.2.0+dfsg/webassembly/README.md	2025-07-08 20:52:00.000000000 +0300
+++ f3d-3.4.1/webassembly/README.md	1970-01-01 02:00:00.000000000 +0200
@@ -1,13 +0,0 @@
-F3D is a fast and minimalist 3D viewer. Implemented in C++, it has been cross-compiled with emscripten to a WebAssembly module.
-
-While the webassembly module might be quite large (~14MB), it is self contained and requires no external dependency.
-
-The current web build parses some parameters provided as url-params, via a hash search-query:
-
-- `model` and `extension` let you pass a model to load via its url. Extension can be provided in case it is not obvious to detect it automatically. In case extension is provided, the loader will enforce it, otherwise, it can try to get file type from header if available, otherwise from url.
-- `axis, grid, fxaa, tone, ssao, ambient` can be set to false to disable toggle
-- `up` can take values +Y or +Z
-
-Some passed model urls might be stored on servers which do not set the Cross-Origin request header parameter. In that case, you can still load these with plugins like Allow-Cors that do exist for chrome, firefox etc.
-
-Example url which requires CORS Plugin: `https://f3d.app/web#up=+Y&axis=false&ssao=true&model=https://groups.csail.mit.edu/graphics/classes/6.837/F03/models/teapot.obj`. Also see this [url with all params](https://f3d.app/web#model=https://groups.csail.mit.edu/graphics/classes/6.837/F03/models/teapot.obj&up=%2BY&grid=false&fxaa=false&tone=false&ssao=false&ambient=false&axis=false)
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/CMakeLists.txt f3d-3.4.1/webassembly/testing/CMakeLists.txt
--- f3d-3.2.0+dfsg/webassembly/testing/CMakeLists.txt	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/CMakeLists.txt	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,48 @@
+set(f3djsTests_list
+  "test_camera"
+  "test_engine"
+  "test_image"
+  "test_interactor"
+  "test_options"
+  "test_render"
+  "test_scene"
+  "test_splats"
+)
+
+find_program(CHROMIUM_EXECUTABLE chromium)
+
+if (NOT CHROMIUM_EXECUTABLE)
+  message(FATAL_ERROR "chromium executable not found.")
+endif()
+
+# Add all the ADD_TEST for each test
+foreach(test_name ${f3djsTests_list})
+
+  set(TEST_JS_MODULE "${CMAKE_CURRENT_SOURCE_DIR}/${test_name}.js")
+  configure_file(
+    "${CMAKE_CURRENT_SOURCE_DIR}/test.html.in"
+    "${CMAKE_CURRENT_BINARY_DIR}/${test_name}.html"
+    @ONLY)
+
+  add_test(NAME f3djs::${test_name}
+    COMMAND ${CHROMIUM_EXECUTABLE}
+      --disable-application-cache
+      --disable-extensions
+      --disable-notifications
+      --disable-restore-session-state
+      --disable-gpu
+      --enable-unsafe-swiftshader
+      --new-window
+      --no-default-browser-check
+      --no-first-run
+      --no-sandbox
+      --headless=new
+      --enable-logging=stderr
+      --user-data-dir=${CMAKE_CURRENT_BINARY_DIR}/${test_name}_data_dir
+      --allow-file-access-from-files
+      file://${CMAKE_CURRENT_BINARY_DIR}/${test_name}.html
+  )
+
+  set_tests_properties(f3djs::${test_name} PROPERTIES
+    FAIL_REGULAR_EXPRESSION "F3D_ERROR")
+endforeach()
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_camera.js f3d-3.4.1/webassembly/testing/test_camera.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_camera.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_camera.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,59 @@
+import utils from "./utils.js";
+
+const settings = {
+  runBefore: (Module) => {
+    const options = Module.engineInstance.getOptions();
+
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "black");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+  },
+
+  runAfter: (Module) => {
+    const camera = Module.engineInstance.getWindow().getCamera();
+
+    utils.assert(
+      utils.numArrayEquals(camera.position, [0, -0.016, 7.059], 0.01),
+    );
+    utils.assert(
+      utils.numArrayEquals(camera.focalPoint, [0, -0.016, 0.022], 0.01),
+    );
+    utils.assert(utils.numArrayEquals(camera.viewUp, [0, 1, 0], 0.01));
+    utils.assert(Math.abs(camera.viewAngle - 26.444) < 0.01);
+
+    camera.position = [0, 5, 0];
+    camera.focalPoint = [0, 0, 0];
+    camera.viewUp = [0, 0, 1];
+    camera.viewAngle = 70;
+
+    utils.assert(utils.numArrayEquals(camera.position, [0, 5, 0], 0.01));
+    utils.assert(utils.numArrayEquals(camera.focalPoint, [0, 0, 0], 0.01));
+    utils.assert(utils.numArrayEquals(camera.viewUp, [0, 0, 1], 0.01));
+    utils.assert(Math.abs(camera.viewAngle - 70) < 0.01);
+
+    camera.setCurrentAsDefault();
+
+    camera.resetToBounds(0.9);
+
+    // just call every control function
+    camera
+      .dolly(1.5)
+      .pan(1, 2, -3)
+      .zoom(0.8)
+      .roll(10)
+      .azimuth(15)
+      .yaw(-20)
+      .elevation(25)
+      .pitch(-30);
+
+    camera.resetToDefault();
+  },
+};
+
+utils.runRenderTest(settings, {
+  data: "suzanne.ply",
+  baseline: "TestWasmCamera.png",
+});
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_engine.js f3d-3.4.1/webassembly/testing/test_engine.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_engine.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_engine.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,46 @@
+import utils from "./utils.js";
+
+const settings = {
+  run: (Module) => {
+    Module.Log.setVerboseLevel(Module.LogVerboseLevel.INFO, false);
+    Module.Engine.autoloadPlugins();
+
+    const readerOptions = Module.Engine.getAllReaderOptionNames();
+    utils.assert(
+      readerOptions.includes("QuakeMDL.skin_index"),
+      "Cannot find reader option",
+    );
+
+    const libInfo = Module.Engine.getLibInfo();
+    utils.assert(libInfo.compiler.includes("Clang"), "Cannot find compiler");
+    utils.assert(libInfo.license.includes("BSD"), "Cannot find license");
+    utils.assert(libInfo.modules.length > 0, "Cannot find modules");
+    utils.assert(libInfo.copyrights.length > 0, "Cannot find copyrights");
+
+    const readersInfo = Module.Engine.getReadersInfo();
+
+    utils.assert(readersInfo.length > 0, "Cannot find readers");
+
+    readersInfo.forEach((reader) => {
+      utils.assert(
+        reader.extensions.length > 0,
+        "Cannot find reader extensions for " + reader.name,
+      );
+      utils.assert(
+        reader.mimeTypes.length > 0,
+        "Cannot find reader mime types for " + reader.name,
+      );
+      utils.assert(
+        reader.hasSceneReader || reader.hasGeometryReader,
+        "Cannot find reader scene or geometry support for " + reader.name,
+      );
+    });
+
+    utils.assert(
+      readersInfo.map((reader) => reader.name).includes("GLTF"),
+      "Cannot find GLTF reader",
+    );
+  },
+};
+
+utils.runBasicTest(settings);
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test.html.in f3d-3.4.1/webassembly/testing/test.html.in
--- f3d-3.2.0+dfsg/webassembly/testing/test.html.in	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test.html.in	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>F3D Testing</title>
+    <style>
+      html, body {
+        width:  300px;
+        height: 300px;
+        margin: 0;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div id="main">
+      <canvas id="canvas"></canvas>
+    </div>
+    <script src="@TEST_JS_MODULE@" type="module"></script>
+  </body>
+</html>
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_image.js f3d-3.4.1/webassembly/testing/test_image.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_image.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_image.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,93 @@
+import utils from "./utils.js";
+
+const settings = {
+  run: async (Module) => {
+    utils.assert(
+      Module.Image.getSupportedFormats().includes(".png"),
+      "supported formats",
+    );
+
+    const img = new Module.Image(10, 10, 4, Module.ImageChannelType.BYTE);
+
+    utils.assert(img.channelTypeSize === 1, "channel type size");
+    utils.assert(img.channelCount === 4, "channel count");
+    utils.assert(
+      img.channelType === Module.ImageChannelType.BYTE,
+      "channel type",
+    );
+
+    // create a red/blue checkerboard pattern
+    let buffer = new Uint8Array(img.width * img.height * img.channelCount);
+    for (let i = 0; i < img.width * img.height; i++) {
+      let x = Math.floor(i % img.width);
+      let y = Math.floor(i / img.width);
+      let checker = (x + y) % 2 === 0;
+
+      buffer[i * 4 + 0] = checker ? 255 : 0;
+      buffer[i * 4 + 1] = 0;
+      buffer[i * 4 + 2] = checker ? 0 : 255;
+      buffer[i * 4 + 3] = 255;
+    }
+
+    img.setContent(buffer);
+
+    const content = img.getContent();
+    utils.assert(
+      content.length === img.width * img.height * img.channelCount,
+      "image content length",
+    );
+    utils.assert(
+      content[0] === 255 &&
+        content[1] === 0 &&
+        content[2] === 0 &&
+        content[3] === 255 &&
+        content[4] === 0 &&
+        content[5] === 0 &&
+        content[6] === 255 &&
+        content[7] === 255,
+      "image content of first two pixels",
+    );
+
+    const pngBuffer = img.saveBuffer(Module.ImageSaveFormat.PNG);
+    utils.assert(pngBuffer.length > 0, "png buffer length");
+    utils.assert(
+      pngBuffer[0] === 137 &&
+        pngBuffer[1] === 80 &&
+        pngBuffer[2] === 78 &&
+        pngBuffer[3] === 71,
+      "png buffer header",
+    );
+
+    utils.assert(img.toTerminalText().length === 224, "toTerminalText length");
+
+    console.log("\n" + img.toTerminalText() + "\n");
+
+    await utils.copyLocalFileToWasmFS(
+      Module,
+      "/src/testing/data/10x10_checker.png",
+      "checker.png",
+    );
+
+    const baselineImg = new Module.Image("checker.png");
+    utils.assert(img.equals(baselineImg), "image equals");
+
+    const normalizedPixel = img.getNormalizedPixel(0, 0);
+    utils.assert(normalizedPixel.length === 4, "normalized pixel length");
+    utils.assert(
+      utils.numArrayEquals(normalizedPixel, [1, 0, 0, 1]),
+      "normalized pixel",
+    );
+
+    img.setMetadata("Author", "John Doe");
+    utils.assert(
+      img.allMetadata().includes("Author"),
+      "allMetadata includes Author",
+    );
+    utils.assert(
+      img.getMetadata("Author") === "John Doe",
+      "getMetadata after setMetadata",
+    );
+  },
+};
+
+utils.runBasicTest(settings);
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_interactor.js f3d-3.4.1/webassembly/testing/test_interactor.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_interactor.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_interactor.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,123 @@
+import utils from "./utils.js";
+
+const settings = {
+  runBefore: (Module) => {
+    // does nothing but called for coverage
+    Module.engineInstance.getScene().clear();
+
+    const options = Module.engineInstance.getOptions();
+
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+  },
+
+  runAfter: (Module) => {
+    const interactor = Module.engineInstance.getInteractor();
+
+    // commands
+    interactor.initCommands();
+
+    let callbackCalled = false;
+
+    interactor.addCommand("foo", (args) => {
+      utils.assert(args[0] === "bar");
+      callbackCalled = true;
+    });
+
+    utils.assert(
+      interactor.getCommandActions().includes("foo"),
+      "command not added",
+    );
+
+    interactor.triggerCommand("foo bar baz", true);
+
+    utils.assert(callbackCalled, "command callback not called");
+
+    interactor.removeCommand("foo");
+    utils.assert(
+      !interactor.getCommandActions().includes("foo"),
+      "command not removed",
+    );
+
+    // animations
+    utils.assert(
+      !interactor.isPlayingAnimation(),
+      "animation should not be playing",
+    );
+
+    interactor.startAnimation();
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.FORWARD,
+      "animation should be playing",
+    );
+
+    interactor.stopAnimation();
+    utils.assert(
+      !interactor.isPlayingAnimation(),
+      "animation should not be playing",
+    );
+
+    interactor.startAnimation(Module.InteractorAnimationDirection.FORWARD);
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.FORWARD,
+      "animation should be playing forward",
+    );
+    interactor.stopAnimation();
+
+    interactor.startAnimation(Module.InteractorAnimationDirection.BACKWARD);
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.BACKWARD,
+      "animation should be playing backward",
+    );
+    interactor.stopAnimation();
+
+    interactor.toggleAnimation();
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.FORWARD,
+      "animation should be playing forward",
+    );
+    interactor.stopAnimation();
+
+    interactor.toggleAnimation(Module.InteractorAnimationDirection.FORWARD);
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.FORWARD,
+      "animation should be playing forward",
+    );
+    interactor.stopAnimation();
+
+    interactor.toggleAnimation(Module.InteractorAnimationDirection.BACKWARD);
+    utils.assert(
+      interactor.isPlayingAnimation() &&
+        interactor.getAnimationDirection() ==
+          Module.InteractorAnimationDirection.BACKWARD,
+      "animation should be playing backward",
+    );
+    interactor.stopAnimation();
+
+    // only for coverage, do not test the actual feature yet
+    interactor.disableCameraMovement();
+    interactor.enableCameraMovement();
+    interactor.requestRender();
+    interactor.requestStop();
+    interactor.stop();
+  },
+};
+
+utils.runRenderTest(settings, {
+  data: "f3d.glb",
+  baseline: "TestWasmInteraction.png",
+});
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_options.js f3d-3.4.1/webassembly/testing/test_options.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_options.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_options.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,122 @@
+import utils from "./utils.js";
+
+const settings = {
+  run: (Module) => {
+    Module.Log.setVerboseLevel(Module.LogVerboseLevel.INFO, false);
+
+    const options = new Module.Options();
+
+    // set a bunch of options of each type
+    options.setAsString("render.background.color", "#000000");
+    options.toggle("model.scivis.enable");
+    options.setAsString("model.scivis.array_name", "Colors");
+    options.setAsString("model.scivis.component", "-2");
+    options.setAsString("model.scivis.range", "0.7,1.4"); // not used for the baseline, only for coverage
+
+    utils.assert(options.getAsString("model.scivis.array_name") === "Colors");
+
+    // check that the getter works
+    utils.assert(
+      options.get("model.scivis.enable") === true,
+      "options getter for boolean failed",
+    );
+    utils.assert(
+      options.get("model.scivis.component") === -2,
+      "options getter for int failed",
+    );
+    utils.assert(
+      options.get("model.point_sprites.size") === 10.0,
+      "options getter for double failed",
+    );
+    utils.assert(
+      options.get("model.point_sprites.type") === "sphere",
+      "options getter for string failed",
+    );
+
+    // comparing JS arrays
+    utils.assert(
+      utils.numArrayEquals(options.get("model.scivis.range"), [0.7, 1.4]),
+      "options getter for vec<double> failed",
+    );
+
+    utils.assert(
+      utils.numArrayEquals(options.get("scene.animation.indices"), [0]),
+      "options getter for vec<int> failed",
+    );
+
+    // try not valued option
+    utils.assert(
+      options.get("model.color.opacity") === undefined,
+      "options getter for undefined",
+    );
+
+    // try not existing option
+    utils.assert(
+      options.get("foo") === undefined,
+      "options getter for not existing",
+    );
+
+    const options2 = new Module.Options();
+    options2.toggle("model.scivis.enable");
+
+    utils.assert(
+      options.isSame(options2, "model.scivis.enable"),
+      "options isSame",
+    );
+    utils.assert(
+      !options.isSame(options2, "render.background.color"),
+      "options !isSame",
+    );
+    options.copy(options2, "render.background.color");
+    utils.assert(
+      options.isSame(options2, "render.background.color"),
+      "options isSame after copy",
+    );
+
+    utils.assert(
+      options.hasValue("model.scivis.array_name"),
+      "options hasValue",
+    );
+    utils.assert(!options.hasValue("model.color.opacity"), "options !hasValue");
+
+    utils.assert(
+      Module.Options.getAllNames().includes("ui.drop_zone.enable"),
+      "options getAllNames",
+    );
+    utils.assert(
+      options.getNames().includes("ui.drop_zone.enable"),
+      "options getNames",
+    );
+    utils.assert(
+      !options.getNames().includes("model.color.rgb"),
+      "options getNames not set",
+    );
+
+    const closestOptions = options.getClosestOption("modele.colr.rgb");
+    utils.assert(
+      closestOptions[0] === "model.color.rgb",
+      "closest option name",
+    );
+    utils.assert(closestOptions[1] === 2, "closest option distance");
+
+    utils.assert(options.isOptional("model.color.rgb"), "options isOptional");
+    utils.assert(
+      !options.isOptional("model.point_sprites.size"),
+      "options !isOptional",
+    );
+
+    options.removeValue("model.scivis.array_name");
+    utils.assert(
+      !options.hasValue("model.scivis.array_name"),
+      "options !hasValue after removal",
+    );
+
+    options.reset("model.scivis.enable");
+    utils.assert(
+      options.get("model.scivis.enable") === false,
+      "options getter after reset",
+    );
+  },
+};
+
+utils.runBasicTest(settings);
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_render.js f3d-3.4.1/webassembly/testing/test_render.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_render.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_render.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,56 @@
+import utils from "./utils.js";
+
+const settings = {
+  runBefore: (Module) => {
+    const options = Module.engineInstance.getOptions();
+
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // setup coloring
+    options.toggle("model.scivis.enable");
+    options.setAsString("model.scivis.array_name", "Colors");
+    options.setAsString("model.scivis.component", "-2");
+    options.toggle("model.scivis.cells");
+
+    // make it look nice
+    options.toggle("render.effect.antialiasing.enable");
+    options.toggle("render.effect.tone_mapping");
+    options.toggle("render.effect.ambient_occlusion");
+    options.toggle("render.hdri.ambient");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+
+    // default to +Z
+    options.setAsString("scene.up_direction", "+Z");
+  },
+
+  runAfter: (Module) => {
+    // check that space conversion works
+    const window = Module.engineInstance.getWindow();
+
+    const ptWorld = window.getWorldFromDisplay([0, 0, 0]);
+
+    utils.assert(
+      !utils.numArrayEquals(ptWorld, [0, 0, 0], 0.001),
+      "point has no been transformed",
+    );
+
+    const ptDisplay = window.getDisplayFromWorld(ptWorld);
+
+    utils.assert(
+      utils.numArrayEquals(ptDisplay, [0, 0, 0], 0.001),
+      "point has no been restored to original value",
+    );
+
+    // just for coverage
+    Module.engineInstance.setCachePath("/tmp");
+  },
+};
+
+utils.runRenderTest(settings, {
+  data: "f3d.vtp",
+  baseline: "TestWasmOptions.png",
+});
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_scene.js f3d-3.4.1/webassembly/testing/test_scene.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_scene.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_scene.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,38 @@
+import utils from "./utils.js";
+
+const settings = {
+  runBefore: (Module) => {
+    // does nothing but called for coverage
+    Module.engineInstance.getScene().clear();
+
+    const options = Module.engineInstance.getOptions();
+
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // display widgets
+    options.toggle("ui.axis");
+    options.toggle("render.grid.enable");
+  },
+
+  runAfter: (Module) => {
+    const scene = Module.engineInstance.getScene();
+
+    utils.assert(
+      scene.availableAnimations() == 1,
+      "There should be a single animation",
+    );
+
+    const [start, end] = scene.animationTimeRange();
+
+    utils.assert(start === 0, "Start value should be 0");
+    utils.assert(end === 4, "End value should be 4");
+
+    scene.loadAnimationTime(1.5);
+  },
+};
+
+utils.runRenderTest(settings, {
+  data: "f3d.glb",
+  baseline: "TestWasmAnimation.png",
+});
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/test_splats.js f3d-3.4.1/webassembly/testing/test_splats.js
--- f3d-3.2.0+dfsg/webassembly/testing/test_splats.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/test_splats.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,37 @@
+import utils from "./utils.js";
+
+const settings = {
+  runBefore: (Module) => {
+    const options = Module.engineInstance.getOptions();
+
+    // background must be set to black for proper blending with transparent canvas
+    options.setAsString("render.background.color", "#000000");
+
+    // setup coloring
+    options.toggle("model.scivis.enable");
+    options.setAsString("model.scivis.component", "-2");
+
+    // splats
+    options.toggle("model.point_sprites.enable");
+    options.toggle("model.point_sprites.absolute_size");
+    options.setAsString("model.point_sprites.type", "gaussian");
+    options.toggle("render.effect.blending.enable");
+    options.setAsString("render.effect.blending.mode", "stochastic");
+
+    // display widgets
+    options.toggle("ui.axis");
+
+    // default to +Z
+    options.setAsString("scene.up_direction", "-Y");
+  },
+
+  runAfter: (Module) => {
+    const cam = Module.engineInstance.getWindow().getCamera();
+    cam.dolly(3.0);
+  },
+};
+
+utils.runRenderTest(settings, {
+  data: "small.splat",
+  baseline: "TestWasmSplats.png",
+});
diff -Nru f3d-3.2.0+dfsg/webassembly/testing/utils.js f3d-3.4.1/webassembly/testing/utils.js
--- f3d-3.2.0+dfsg/webassembly/testing/utils.js	1970-01-01 02:00:00.000000000 +0200
+++ f3d-3.4.1/webassembly/testing/utils.js	2026-01-08 08:41:55.000000000 +0200
@@ -0,0 +1,145 @@
+import f3d from "/src/dist/f3d.js";
+
+const utils = {
+  assert: (condition, description) => {
+    if (!condition) {
+      console.error("F3D_ERROR: " + description);
+    }
+  },
+
+  numArrayEquals: (arr1, arr2, epsilon = Number.EPSILON) => {
+    if (arr1.length !== arr2.length) {
+      return false;
+    }
+    for (let i = 0; i < arr1.length; i++) {
+      if (Math.abs(arr1[i] - arr2[i]) > epsilon) {
+        return false;
+      }
+    }
+    return true;
+  },
+
+  printImageBase64: (Module, img) => {
+    img.save("/img.png", Module.ImageSaveFormat.PNG);
+
+    const uint8ToBase64 = (uint8Array) => {
+      let binary = "";
+      for (let i = 0; i < uint8Array.length; i++) {
+        binary += String.fromCharCode(uint8Array[i]);
+      }
+      return btoa(binary);
+    };
+
+    const imgData = Module.FS.readFile("/img.png", {
+      encoding: "binary",
+    });
+    const base64 = uint8ToBase64(imgData);
+
+    console.log(
+      "New baseline png file:\n\n\ndata:image/png;base64," + base64 + "\n\n\n",
+    );
+  },
+
+  copyLocalFileToWasmFS: async (Module, localPath, wasmPath) => {
+    const data = await fetch(localPath).then((b) => b.arrayBuffer());
+    Module.FS.writeFile(wasmPath, new Uint8Array(data));
+  },
+
+  runBasicTest: (settings) => {
+    f3d(settings)
+      .then(async (Module) => {
+        await Module.run(Module);
+
+        window.close();
+      })
+      .catch((error) => {
+        console.error('F3D_ERROR: exception thrown "' + error + '"');
+        window.close();
+      });
+  },
+
+  runRenderTest: (settings, args) => {
+    settings.canvas = document.getElementById("canvas");
+
+    f3d(settings)
+      .then(async (Module) => {
+        // write in the wasm filesystem
+        await utils.copyLocalFileToWasmFS(
+          Module,
+          "/src/testing/data/" + args.data,
+          args.data,
+        );
+
+        await utils.copyLocalFileToWasmFS(
+          Module,
+          "/src/testing/baselines/" + args.baseline,
+          "baseline.png",
+        );
+
+        // automatically load all supported file format readers
+        Module.Engine.autoloadPlugins();
+
+        Module.Log.setVerboseLevel(Module.LogVerboseLevel.DEBUG, false);
+
+        Module.engineInstance = Module.Engine.create();
+
+        // setup the window size based on the canvas size
+        const scale = window.devicePixelRatio;
+        Module.engineInstance
+          .getWindow()
+          .setSize(
+            scale * Module.canvas.clientWidth,
+            scale * Module.canvas.clientHeight,
+          );
+
+        utils.assert(
+          Module.engineInstance.getWindow().width ===
+            scale * Module.canvas.clientWidth,
+          "Failed to get width",
+        );
+        utils.assert(
+          Module.engineInstance.getWindow().height ===
+            scale * Module.canvas.clientHeight,
+          "Failed to get height",
+        );
+
+        const scene = Module.engineInstance.getScene();
+
+        utils.assert(
+          scene.supports(args.data),
+          args.data + " is not supported",
+        );
+
+        Module.runBefore?.(Module);
+
+        scene.add(args.data);
+
+        Module.runAfter?.(Module);
+
+        // do a first render and start the interactor
+        Module.engineInstance.getWindow().render();
+        Module.engineInstance.getInteractor().start();
+
+        // compare images
+        const result = Module.engineInstance.getWindow().renderToImage(true);
+        const baseline = new Module.Image("/baseline.png");
+        const ssim = result.compare(baseline);
+
+        if (ssim <= 0.05) {
+          console.log("Passed with SSIM = " + ssim);
+        } else {
+          console.log("F3D_ERROR: Comparison failed with SSIM " + ssim);
+
+          utils.printImageBase64(Module, result);
+        }
+
+        window.close();
+      })
+      .catch((error) => {
+        console.error('F3D_ERROR: exception thrown "' + error.stack + '"');
+        window.close();
+      });
+  },
+};
+
+export default utils;
