Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ jobs:
-DCMAKE_CXX_FLAGS="-g" -DCMAKE_C_FLAGS="-g" \
-DENABLE_STABLE_DIFFUSION=ON \
-DENABLE_ONNX=ON \
-DQTMESH_ONNX_GPU=OFF \
-DENABLE_MOCAP=ON \
-DENABLE_AUTO_UPDATER=OFF \
-DASSIMP_DIR=/usr/local/lib/cmake/assimp-${{ env.ASSIMP_DIR_VERSION }} \
Expand Down Expand Up @@ -916,6 +917,14 @@ jobs:
./pack-deb/usr/lib/qtmesheditor/plugins/multimedia/ 2>/dev/null || true
fi

# MJPEG webcams deliver Format_Jpeg frames; decoding them via
# QImage::fromData("JPEG") requires the qjpeg imageformat plugin.
if [ -d "$QT_DIR/plugins/imageformats" ]; then
mkdir -p ./pack-deb/usr/lib/qtmesheditor/plugins/imageformats
cp -R "$QT_DIR/plugins/imageformats/"*.so \
./pack-deb/usr/lib/qtmesheditor/plugins/imageformats/ 2>/dev/null || true
fi

# Qt FFmpeg stub shims — libffmpegmediaplugin.so depends on these; when
# they are absent the FFmpeg backend fails to load and
# QMediaDevices::videoInputs() returns empty ("no camera available").
Expand Down Expand Up @@ -1124,6 +1133,7 @@ jobs:
-DENABLE_SENTRY=OFF \
-DENABLE_PS1_RIP=ON \
-DENABLE_ONNX=ON \
-DQTMESH_ONNX_GPU=OFF \
-DENABLE_MOCAP=ON

- name: Run build-wrapper
Expand Down Expand Up @@ -1944,6 +1954,7 @@ jobs:
-DCMAKE_CXX_FLAGS="-g" -DCMAKE_C_FLAGS="-g" \
-DENABLE_STABLE_DIFFUSION=ON \
-DENABLE_ONNX=ON \
-DQTMESH_ONNX_GPU=OFF \
-DENABLE_MOCAP=ON \
-DCMAKE_OSX_ARCHITECTURES="$(uname -m)" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ option(ENABLE_ONNX "Enable AI PBR map synthesis via ONNX Runtime" OFF)
if(ENABLE_ONNX)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/OnnxRuntime.cmake)
add_definitions(-DENABLE_ONNX)
if(QTMESH_ONNX_GPU)
add_definitions(-DQTMESH_ONNX_GPU_BUILD)
message(STATUS "ONNX Runtime GPU package enabled (CUDA EP on Linux x64 when cuDNN 9 is installed)")
endif()
message(STATUS "AI PBR map synthesis enabled with ONNX Runtime")
endif()
##############################################################
Expand Down
64 changes: 46 additions & 18 deletions cmake/OnnxRuntime.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
# macOS uses the universal2 archive (covers arm64 + x86_64) so there is no
# per-arch selection trap (the lesson from libsodium being built x86_64). CoreML
# execution provider ships inside that archive; the CPU EP is always present.
#
# Linux x64 GPU: pass -DQTMESH_ONNX_GPU=ON (auto-defaults ON when nvidia-smi is
# found outside CI) to fetch onnxruntime-linux-x64-gpu-*.tgz. The CUDA provider
# .so must ship next to the binary AND cuDNN 9 + CUDA 12 must be on the system.
# Windows MinGW is intentionally NOT wired here — the official Windows archive is
# MSVC-built and won't link under MinGW; that path degrades gracefully (the
# feature reports "rebuild with -DENABLE_ONNX"). See the #404 follow-up.
# MSVC-built and won't link under MinGW.
Comment on lines +11 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 \
  'QTMESH_ONNX_GPU|_qtmesh_onnx_gpu_default|CMAKE_SYSTEM_PROCESSOR|_ort_archive|QTMESH_ONNX_GPU_BUILD' \
  cmake/OnnxRuntime.cmake CMakeLists.txt

Repository: fernandotonon/QtMeshEditor

Length of output: 6882


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf =='cmake/OnnxRuntime.cmake relevant sections=='\n
sed -n '20,70p' cmake/OnnxRuntime.cmake
printf '\n==cmake/OnnxRuntime.cmake cache/status sections=='\n
sed -n '95,125p' cmake/OnnxRuntime.cmake
printf '\n==CMakeLists.txt ONNX consume section=='\n
sed -n '298,314p' CMakeLists.txt

printf '\n==Behavioral probe for CMake-like conditional/option/processor branches=='\n
python3 - <<'PY'
import os
cases = [
    ("Linux-aarch64-unspecified", "Linux", "aarch64", False),
    ("Linux-arm/unspecified", "Linux", "arm", True),
    ("Linux-arm64-nvidia", "Linux", "arm64", True),
    ("macOS-unspecified", "Darwin", "arm64", True),
    ("macOS-unspecified-off", "Darwin", "x86_64", False),
    ("Windows-unspecified", "Windows", "AMD64", True),
    ("Windows-GPU", "Windows", "AMD64", True),
]
for name, system, processor, nvidia_smiship in cases:
    qtmesh = True # user/default-on
    if nvidia_smiship:
        qtmesh = True
    if processor == "":
        qtmesh = False
    if nvidia_smiship and system != "Linux":
        qtmesh = False
    if qtmesh:
        selected = "GPU"
        used = "CUDA"
    else:
        selected = "CPU"
        used = "CPU"
    status = "GPU" if qtmesh else "CPU"
    build_def = "QTMESH_ONNX_GPU_BUILD" if qtmesh else ""
    print(f"{name}: QTMESH_ONNX_GPU={qtmesh!s:>5}, selected={selected:>3}, status_message={status:>3}, add_definitions={build_def}")
PY

Repository: fernandotonon/QtMeshEditor

Length of output: 5539


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '==cmake/OnnxRuntime.cmake relevant sections=='\n
sed -n '20,70p' cmake/OnnxRuntime.cmake
printf '\n==cmake/OnnxRuntime.cmake cache/status sections=='\n
sed -n '95,125p' cmake/OnnxRuntime.cmake
printf '\n==CMakeLists.txt ONNX consume section=='\n
sed -n '298,314p' CMakeLists.txt

printf '\n==Behavioral probe for supported-/archive selection branch combinations=='\n
python3 - <<'PY'
cases = [
    {"name": "Linux aarch64 nvidia-smi", "system": "Linux", "os_family": "UnixApple", "processor": "aarch64", "def_option_if_nvidia": True},
    {"name": "Linux ARM nvidia-smi", "system": "Linux", "os_family": "UnixApple", "processor": "arm", "def_option_if_nvidia": True},
    {"name": "macOS option forced ON", "system": "Darwin", "os_family": "UnixApple", "processor": "arm64", "def_option_if_nvidia": False},
    {"name": "Windows option forced ON", "system": "Windows", "os_family": "Win", "processor": "AMD64", "def_option_if_nvidia": False},
]
for case in cases:
    qtmesh_onnx_gpu = case["def_option_if_nvidia"]
    if case["system"] == "Linux" and not case["os_family"] == "UnixApple" and case["os_family"] == "UnixApple" and case["processor"] in ("aarch64", "arm", "arm64"):
        archive = "onnxruntime-linux-aarch64-..tgz"
    elif case["system"] == "Linux" and qtmesh_onnx_gpu:
        archive = "onnxruntime-linux-x64-gpu-..tgz"
    elif case["system"] == "Linux":
        archive = "onnxruntime-linux-x64-..tgz"
    elif case["system"] == "Darwin":
        archive = "onnxruntime-osx-universal2-..tgz"
    elif case["system"] == "Windows" and qtmesh_onnx_gpu:
        archive = "Windows warns OFF, CPU archive"
    else:
        archive = "CPU archive"
    status = "GPU" if qtmesh_onnx_gpu else "CPU"
    print(f"{case['name']}: QTMESH_ONNX_GPU={qtmesh_onnx_gpu!s:>5}, archive={archive!r}, status_line={status}")
PY

Repository: fernandotonon/QtMeshEditor

Length of output: 5198


Clear the GPU flag when the selected archive is CPU-only.

Linux ARM can pick onnxruntime-linux-aarch64-*.tgz while QTMESH_ONNX_GPU=ON remains cached, reports GPU, and defines QTMESH_ONNX_GPU_BUILD. Force QTMESH_ONNX_GPU OFF before exporting/caching the selected CPU-only archive for unsupported platforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/OnnxRuntime.cmake` around lines 11 - 16, When selecting a CPU-only ONNX
runtime archive for unsupported GPU platforms like Linux ARM, explicitly set
QTMESH_ONNX_GPU to OFF before the archive selection is cached or exported. This
ensures the flag does not remain from a previous configuration state and
prevents incorrect GPU definitions like QTMESH_ONNX_GPU_BUILD from being set for
CPU-only builds.


if(TARGET qtmesh_onnx)
return()
Expand All @@ -20,23 +23,49 @@ set(QTMESH_ONNX_VERSION "1.20.1" CACHE STRING "ONNX Runtime release version")
set(QTMESH_ONNX_BASE_URL
"https://github.com/microsoft/onnxruntime/releases/download/v${QTMESH_ONNX_VERSION}")

# Default GPU package on local Linux x64 builds when an NVIDIA GPU is present.
set(_qtmesh_onnx_gpu_default OFF)
if(UNIX AND NOT APPLE AND NOT DEFINED ENV{CI})
find_program(_QTMESH_NVIDIA_SMI nvidia-smi)
if(_QTMESH_NVIDIA_SMI)
set(_qtmesh_onnx_gpu_default ON)
endif()
endif()
option(QTMESH_ONNX_GPU
"Download GPU ONNX Runtime (CUDA on Linux x64; adds ~700 MB provider libs)"
${_qtmesh_onnx_gpu_default})

# Select the archive + its SHA256 for this platform.
if(APPLE)
set(_ort_archive "onnxruntime-osx-universal2-${QTMESH_ONNX_VERSION}.tgz")
set(_ort_sha256 "da4349e01a7e997f5034563183c7183d069caadc1d95f499b560961787813efd")
set(_ort_libname "libonnxruntime.${QTMESH_ONNX_VERSION}.dylib")
set(_ort_fetch_name "qtmesh_onnxruntime")
elseif(UNIX)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
set(_ort_archive "onnxruntime-linux-aarch64-${QTMESH_ONNX_VERSION}.tgz")
set(_ort_sha256 "ae4fedbdc8c18d688c01306b4b50c63de3445cdf2dbd720e01a2fa3810b8106a")
set(_ort_fetch_name "qtmesh_onnxruntime")
else()
set(_ort_archive "onnxruntime-linux-x64-${QTMESH_ONNX_VERSION}.tgz")
set(_ort_sha256 "67db4dc1561f1e3fd42e619575c82c601ef89849afc7ea85a003abbac1a1a105")
if(QTMESH_ONNX_GPU)
set(_ort_archive "onnxruntime-linux-x64-gpu-${QTMESH_ONNX_VERSION}.tgz")
set(_ort_sha256 "6bfb87c6ebe55367a94509b8ef062239e188dccf8d5caac8d6909b2344893bf0")
set(_ort_fetch_name "qtmesh_onnxruntime_gpu")
else()
set(_ort_archive "onnxruntime-linux-x64-${QTMESH_ONNX_VERSION}.tgz")
set(_ort_sha256 "67db4dc1561f1e3fd42e619575c82c601ef89849afc7ea85a003abbac1a1a105")
set(_ort_fetch_name "qtmesh_onnxruntime")
endif()
endif()
set(_ort_libname "libonnxruntime.so.${QTMESH_ONNX_VERSION}")
elseif(WIN32 AND NOT MINGW)
if(QTMESH_ONNX_GPU)
message(WARNING "QTMESH_ONNX_GPU: Windows GPU package not wired in CMake yet; using CPU ONNX Runtime")
set(QTMESH_ONNX_GPU OFF CACHE BOOL "" FORCE)
endif()
set(_ort_archive "onnxruntime-win-x64-${QTMESH_ONNX_VERSION}.zip")
set(_ort_sha256 "78d447051e48bd2e1e778bba378bec4ece11191c9e538cf7b2c4a4565e8f5581")
set(_ort_fetch_name "qtmesh_onnxruntime")
set(_ort_libname "onnxruntime.dll")
else()
message(FATAL_ERROR "ENABLE_ONNX: unsupported platform — no ONNX Runtime archive mapping. "
Expand All @@ -45,19 +74,16 @@ endif()

include(FetchContent)
FetchContent_Declare(
qtmesh_onnxruntime
${_ort_fetch_name}
URL "${QTMESH_ONNX_BASE_URL}/${_ort_archive}"
URL_HASH SHA256=${_ort_sha256}
)
FetchContent_MakeAvailable(qtmesh_onnxruntime)
FetchContent_MakeAvailable(${_ort_fetch_name})

# The archive extracts to a single top-level dir with include/ and lib/.
set(QTMESH_ONNX_ROOT "${qtmesh_onnxruntime_SOURCE_DIR}")
# FetchContent sets <name>_SOURCE_DIR
set(QTMESH_ONNX_ROOT "${${_ort_fetch_name}_SOURCE_DIR}")
set(QTMESH_ONNX_INCLUDE_DIR "${QTMESH_ONNX_ROOT}/include")

# Resolve the actual shared-lib path. Prebuilt layouts vary slightly across
# platforms (versioned symlinks on *nix, lib/*.dll on Windows), so glob for it
# rather than hardcoding a single name.
file(GLOB _ort_libs
"${QTMESH_ONNX_ROOT}/lib/${_ort_libname}"
"${QTMESH_ONNX_ROOT}/lib/libonnxruntime*.dylib"
Expand All @@ -70,25 +96,27 @@ endif()
list(GET _ort_libs 0 QTMESH_ONNX_RUNTIME_LIB)
set(QTMESH_ONNX_RUNTIME_LIB "${QTMESH_ONNX_RUNTIME_LIB}"
CACHE FILEPATH "Path to the ONNX Runtime shared library to ship next to the binary" FORCE)
# The lib dir holds the versioned shared lib PLUS its SONAME symlinks
# (libonnxruntime.so.1 / libonnxruntime.so) that the loader actually requests
# at runtime — copying only the resolved file leaves the binary unable to find
# libonnxruntime.so.1. Expose the dir so the POST_BUILD copies the whole set.
get_filename_component(QTMESH_ONNX_LIB_DIR "${QTMESH_ONNX_RUNTIME_LIB}" DIRECTORY)
set(QTMESH_ONNX_LIB_DIR "${QTMESH_ONNX_LIB_DIR}"
CACHE PATH "Directory of the ONNX Runtime shared library + its SONAME symlinks" FORCE)
CACHE PATH "Directory of the ONNX Runtime shared library + provider libs" FORCE)

set(QTMESH_ONNX_GPU "${QTMESH_ONNX_GPU}" CACHE BOOL
"Using GPU ONNX Runtime package (CUDA/DirectML EPs)" FORCE)

add_library(qtmesh_onnx SHARED IMPORTED GLOBAL)
set_target_properties(qtmesh_onnx PROPERTIES
IMPORTED_LOCATION "${QTMESH_ONNX_RUNTIME_LIB}"
INTERFACE_INCLUDE_DIRECTORIES "${QTMESH_ONNX_INCLUDE_DIR}")
if(WIN32)
# On Windows the import library is needed for linking.
file(GLOB _ort_implib "${QTMESH_ONNX_ROOT}/lib/onnxruntime.lib")
if(_ort_implib)
list(GET _ort_implib 0 _ort_implib0)
set_target_properties(qtmesh_onnx PROPERTIES IMPORTED_IMPLIB "${_ort_implib0}")
endif()
endif()

message(STATUS "ONNX Runtime ${QTMESH_ONNX_VERSION}: ${QTMESH_ONNX_RUNTIME_LIB}")
if(QTMESH_ONNX_GPU)
message(STATUS "ONNX Runtime ${QTMESH_ONNX_VERSION} (GPU): ${QTMESH_ONNX_RUNTIME_LIB}")
else()
message(STATUS "ONNX Runtime ${QTMESH_ONNX_VERSION} (CPU): ${QTMESH_ONNX_RUNTIME_LIB}")
endif()
36 changes: 27 additions & 9 deletions docs/MOCAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ take as a clip (status line shows the result; Ctrl+Z discards it).

- **Head pose** needs a bone that resolves as the canonical Head
(`Head`, `mixamorig:Head`, …). Static meshes get node-TRS keyframes
instead. The take's first confident frame calibrates neutral ("look at
the camera at the start"); in live mode the `Neutral` button re-bases it.
instead. The first confident preview frame is the neutral reference
(look at the camera, relaxed face); the **Neutral** button re-bases head
and body.

- **Body capture** needs a **humanoid skeleton** resolving at least half of
the 22 canonical roles (hips/spine/neck/head, both arms, both legs —
Mixamo and most generic naming conventions resolve). Unrigged meshes: run
standard humanoid bone names resolve). Unrigged meshes: run
`qtmesh rig --skeleton humanoid --skin` first. The root stays locked to
the standing pose (v1 accepts some foot slide).
the standing pose (v1 accepts some foot slide). Body limbs calibrate on
the first visible frame too — start preview with arms in a natural rest
pose (similar to the character's idle) so raised/movement reads correctly.

## Backends

Expand Down Expand Up @@ -116,17 +119,32 @@ The Snap is strictly confined, so webcam access needs two things:
panel suggests the `snap connect` command when no cameras appear or
permission is denied.

If Preview stays on **“Starting camera…”** for more than a few seconds, the
camera opened but Qt never decoded frames. On Linux builds this was usually
(a) missing `libqjpeg` imageformat plugin for MJPEG webcams, or (b) a missing
FFmpeg stub — both are bundled from 3.25.4 onward. Close other apps using
the webcam, retry Preview, or use **Load Video…** as a workaround.

On desktop Linux outside Snap, allow camera access via your desktop portal /
privacy settings when prompted.
privacy settings when prompted. Qt's FFmpeg backend probes VA-API during camera
enumeration; on some NVIDIA + X11 setups that probe can crash unless hardware
decode is disabled — the app sets `QT_FFMPEG_DECODING_HW_DEVICE_TYPES=,` at
startup (software decode only; fine for live webcam preview).

## Known limitations (v1)

- Single person per frame; the highest-scoring detection wins.
- Head pose is camera-relative — walking around the camera reads as head
rotation. Keep the camera static.
- Body root is locked (no root motion); some foot slide is expected.
- Live mode drives face, head, and (humanoid rig) body; the SAM 3D Body
quality backend is offline-only (CLI/MCP), body-live uses pose-ik.
rotation. Keep the camera static. Up/down (pitch) and left/right (yaw) are
corrected for typical humanoid rigs and mirrored webcam previews (body uses
landmark directions separately; no mirror-L/R toggle).
- Body retargeting uses MediaPipe landmark directions (same geometry as the
PoseIK debug overlay) to aim skeleton bones — no mirror-L/R toggle.
- Body root is locked (no root motion); some foot slide is expected. Live
pose-ik uses anatomical bone names (no CMU L/R swap) and CMU-aligned solver
output; recorded body clips use the same path.
- Live mode drives face, head, and (humanoid rig) body; when Face + Body are
both enabled, head rotation always comes from the face graph (not PoseIK).
- Live camera needs a notarized build on macOS (see above); the CLI/MCP
video paths work regardless.
- Video decode is playback-driven (a 60 s video takes 60 s to capture).
Expand Down
38 changes: 37 additions & 1 deletion qml/AISettingsDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,42 @@ Dialog {
}
}

GroupBox {
Layout.fillWidth: true
title: "ONNX Models"
visible: OnnxRuntimeSettings.onnxAvailable

ColumnLayout {
anchors.fill: parent
spacing: 8

CheckBox {
id: onnxGpuCheckBox
text: "Prefer GPU for ONNX models (when available)"
checked: OnnxRuntimeSettings.preferGpu
onCheckedChanged: OnnxRuntimeSettings.preferGpu = checked

contentItem: Text {
text: onnxGpuCheckBox.text
color: textColor
leftPadding: onnxGpuCheckBox.indicator.width + onnxGpuCheckBox.spacing
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}

Text {
Layout.fillWidth: true
text: OnnxRuntimeSettings.gpuProviderNote
font.pointSize: 9
color: OnnxRuntimeSettings.gpuProviderReady ? "#2e7d32"
: (OnnxRuntimeSettings.preferGpu ? "#e65100" : Qt.darker(textColor, 1.3))
wrapMode: Text.WordWrap
}
}
}

GroupBox {
Layout.fillWidth: true
title: "Models Directory"
Expand Down Expand Up @@ -675,7 +711,7 @@ Dialog {

Text {
Layout.fillWidth: true
text: "Note: Settings changes will take effect when loading a new model."
text: "Note: LLM settings take effect when loading a new model. ONNX GPU preference applies to newly created ONNX sessions."
font.pointSize: 9
font.italic: true
color: Qt.darker(textColor, 1.5)
Expand Down
22 changes: 21 additions & 1 deletion qml/CollapsibleSection.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Column {
property bool sectionVisible: true
default property alias content: contentLoader.sourceComponent

signal contentReady()

visible: sectionVisible
width: parent ? parent.width : 200

Expand Down Expand Up @@ -58,7 +60,25 @@ Column {
Loader {
id: contentLoader
width: parent.width
active: root.expanded
// Defer activation to the next event-loop turn. Synchronous Loader
// startup while a parent component is still finalizing (e.g. expanding
// a section during a binding cascade) can SIGSEGV — see PropertiesPanel
// Component.onCompleted comment.
active: loadActive
visible: root.expanded
property bool loadActive: false
onLoaded: root.contentReady()
}

onExpandedChanged: {
if (root.expanded)
Qt.callLater(function() { contentLoader.loadActive = true })
else
contentLoader.loadActive = false
}

Component.onCompleted: {
if (root.expanded)
Qt.callLater(function() { contentLoader.loadActive = true })
}
Comment on lines +73 to 83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the deferred activation against a state change during the pending callback.

Qt.callLater(function() {...}) queues a new closure on each call. It does not dedupe against an earlier pending call from a different closure instance.

If root.expanded toggles from true to false before the deferred callback runs, the callback still sets contentLoader.loadActive = true unconditionally. The section stays visually collapsed (visible is bound to root.expanded), but the Loader instantiates its content anyway and fires onLoadedcontentReady(). Downstream, qml/PropertiesPanel.qml connects onContentReady to call MocapController.refreshDevices(), so this can trigger a device refresh while the section is collapsed.

Recheck root.expanded inside the deferred callback before activating the Loader.

🐛 Proposed fix to recheck expanded state in the deferred callback
     onExpandedChanged: {
         if (root.expanded)
-            Qt.callLater(function() { contentLoader.loadActive = true })
+            Qt.callLater(function() { if (root.expanded) contentLoader.loadActive = true })
         else
             contentLoader.loadActive = false
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/CollapsibleSection.qml` around lines 73 - 83, Guard both deferred
callbacks in CollapsibleSection’s onExpandedChanged and Component.onCompleted
handlers by rechecking root.expanded inside the Qt.callLater closure before
setting contentLoader.loadActive = true; keep immediate deactivation unchanged.

}
Loading
Loading