Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cf9ffb0
feat(#519): Anim Slice B1 — VertexAnimationManager + VAT_POSE playback
fernandotonon Jul 4, 2026
755e43d
fix(#519): add VertexAnimationManager.cpp to the test-common library
fernandotonon Jul 4, 2026
19fa1fd
feat(#519): Anim Slice B2 — Alembic (.abc) vertex-cache reader
fernandotonon Jul 4, 2026
f8997f6
ci(#519): don't enable Alembic on the coverage lane
fernandotonon Jul 4, 2026
f9b9096
fix(#519): stop playback before morph/vertex delete; fix morph header…
fernandotonon Jul 4, 2026
b89b751
feat(anim #519 B3): Alembic --info CLI + import_alembic/play_vertex_a…
fernandotonon Jul 4, 2026
d10aa95
feat(anim #519 B3): frame-cap honesty + readInfo/truncation tests
fernandotonon Jul 4, 2026
b927eee
docs(#519 B3): document Alembic --info CLI + import_alembic/play_vert…
fernandotonon Jul 4, 2026
28ed49d
feat(#519 B3): add Alembic (.abc) filter to the import file dialog
fernandotonon Jul 4, 2026
0f6dc8c
fix(#519): drop stale per-frame poses when rebuilding a vertex clip
fernandotonon Jul 4, 2026
acf5aa0
fix(#519): address B3 review — runOgreOp guard, readInfo empty-sample…
fernandotonon Jul 4, 2026
0ae3cd2
feat(#519): Vertex Morph Animation group in Edit Mode + per-format im…
fernandotonon Jul 5, 2026
dae6833
feat(#519): morph reorder + Add-popup focus/theme; hide targets from …
fernandotonon Jul 5, 2026
7e4c89f
feat(#519): base-preserving morph sculpt session + live render during…
fernandotonon Jul 5, 2026
f6a1f28
feat(#519): morph weight keyframing over time + glTF morph-weights ex…
fernandotonon Jul 5, 2026
b44d4bd
docs(#519): document morph authoring UX + weight keyframing + glTF ex…
fernandotonon Jul 5, 2026
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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ qtmesh info model.fbx --json # show mesh info (JSON)
qtmesh convert model.fbx -o model.gltf2 # convert between formats
qtmesh fix model.fbx -o fixed.fbx # re-import/export with standard optimizations
qtmesh fix model.fbx --all # apply all extra fixes (remove degenerates, merge materials)
qtmesh anim cache.abc --info # Alembic vertex-cache metadata (frames/verts/fps/duration; needs -DENABLE_ALEMBIC)
qtmesh anim cache.abc --info --json # same, as JSON
qtmesh anim model.fbx --list # list animations
qtmesh anim model.fbx --list --json # list animations (JSON)
qtmesh anim model.fbx --rename "Take 001" "Idle" -o out.fbx # rename an animation
Expand Down Expand Up @@ -208,6 +210,15 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
- **Subdivide**: 1-to-4 triangle split. Adjacent non-selected faces are retriangulated against the new midpoints to avoid T-junctions (1/2/3 split-edge cases). Wired to a toolbar button (⊞) — face mode subdivides selected tris, edge mode subdivides every triangle incident to a selected edge.
- **Fill**: vertex mode fan-triangulates the selected verts (3 → triangle, 4 → quad, N → N-2 tris); edge mode detects a closed boundary loop via degree-2 walk and caps it. Toolbar button (◆) and `F` shortcut. Cross-submesh inputs and duplicates of existing triangles are rejected.

### Animation systems beyond skeletal (epic #517)

The animation pipeline started skeleton-only; the #517 epic broadens it. Slices A/C/D shipped: **MorphAnimationManager** (`src/MorphAnimationManager.{h,cpp}`, morph targets / blend shapes — Ogre `Pose` + `VAT_POSE` tracks, `qtmesh morph` CLI, MCP, undo via `commands/MorphCommands`), **NodeAnimationManager** (non-skinned node TRS), **PoseLibrary** (named poses). All are `QML_SINGLETON`s registered in `mainwindow.cpp`.

- **Morph authoring UX (Blender-parity, #519):** morph targets are authored/edited/reordered in the Edit-Mode **"Vertex Morph Animation"** Inspector group (NOT Animation Mode — Animation Mode's list shows only real clips, morph animations are filtered out of `PropertiesPanelController::animationData()` by name). Authoring is a **non-destructive sculpt session**: `EditModeController::beginMorphSculpt()` snapshots base positions; `+ Add` captures the current edit as a target (delta vs base); `endMorphSculpt()` / exiting Edit Mode **restores the base** (the target lives on as a `Pose` + weight). `EditableMesh::commitToEntity` refreshes the pose buffer (`AnimationStateSet::_notifyDirty()` + `entity->_updateAnimation()`) for vertex-animated entities so edits render live while the frame loop is paused. Reorder via `MorphAnimationManager::moveMorphTarget`/`moveMorphTargetToIndex` (undoable `ReorderMorphTargetsCommand` — VAT_POSE keyframes reference poses by index, so it rebuilds all targets in the new order). **Weight keyframing over time** (Slice 2): `setMorphWeightKeyframe(name, time, weight)` writes to one shared clip `MorphAnimationManager::kWeightClipName` ("MorphAnim") — a VAT_POSE track per target's pose, each keyframe referencing the pose at influence == weight; the per-target "◈ Key" button records the weight at the timeline playhead (`AnimationControlController.sliderValue`), diamonds show in the dope sheet (`allMorphRows()` prefers the MorphAnim track's per-pose keyframe times). **glTF export:** `buildAiScene` emits blend-shape targets (`aiMesh::mAnimMeshes` via `attachMorphTargetsToAiMesh`) AND a morph-weights animation (`aiMeshMorphAnim` from the MorphAnim clip). FBX blend-shape weight export is a follow-up.

- **VertexAnimationManager** (`src/VertexAnimationManager.{h,cpp}`, Slice B #519): full-mesh per-vertex animation (cloth / sims / fluid bakes / Alembic caches — every vertex moves, no skeleton). Reuses Ogre's `VAT_POSE` path so the existing timeline/dope-sheet/loop play it with no new playback code. `FrameSet`/`FrameData` are the source-agnostic decoded-cache types; `buildClipFromFrames(mesh, name, frames)` reads submesh-0 bind positions and builds one `Ogre::Pose` per frame (delta vs bind) + one `VAT_POSE` track keyed per frame time. `sampleHeuristic(frameCount)` (< 32 → poses, else stream — the issue's rule) is static + unit-tested. Sentry `scene.anim.vertex_anim`.
- **AlembicImporter** (`src/AlembicImporter.{h,cpp}`, Slice B2): Alembic (.abc) reader behind `-DENABLE_ALEMBIC` (default OFF; `cmake/Alembic.cmake` FetchContents Imath 3.1.12 + Alembic 1.8.8, both BSD-3, fully static, all optional components off — Imath install stays ON so Alembic's unconditional `install(EXPORT)` finds it in an export set). `readFrameSet` decodes the first `IPolyMesh` into a `FrameSet` (pure data — rejects variable-topology caches, fan-triangulates n-gon faces); `importToScene` builds the base mesh + `VAT_POSE` clip + entity. `.abc` routes through `MeshImporterExporter::importer` (guarded — a non-Alembic build logs a clear "rebuild with -DENABLE_ALEMBIC" and skips). The reader is `#ifdef ENABLE_ALEMBIC`-guarded so the default build is unaffected; a round-trip test writes+reads a synthetic `.abc` (only compiled/run in the Alembic-on coverage CI lane). **B3 (shipped):** `readInfo(path)` reads cache metadata (frames/verts/tris/fps/duration/storage) from the schema header + first sample without decoding all frames → **`qtmesh anim <file>.abc --info [--json]`** (`CLIPipeline::cmdAnim`); MCP **`import_alembic`** (heavy — decodes into the live scene, reports node/entities/vertexClips) + **`play_vertex_animation`** (delegates to `toolPlayAnimation` since a vertex clip is an ordinary `AnimationState`). Frame cap: `readFrameSet(maxFrames)` and `importToScene` cap the decode at 512 frames and set `ReadResult::truncated` / log a warning when it bites (no silent cap) — VAT_POSE holds every frame resident, so true per-frame vertex-buffer streaming remains future work.

### Undo/Redo System

- **UndoManager** (`src/UndoManager.h/cpp`): Singleton wrapping `QUndoStack`. Push commands, undo/redo via `Ctrl+Z`/`Ctrl+Shift+Z`.
Expand Down
19 changes: 19 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,25 @@ if(ENABLE_ONNX)
message(STATUS "AI PBR map synthesis enabled with ONNX Runtime")
endif()
##############################################################
# Alembic — mesh / vertex animation import (#519, Anim Slice B)
##############################################################
# Vendored (BSD) reader for baked per-vertex caches (cloth / sims / fluids /
# Houdini + Blender exports). Default OFF for now: the Alembic + Imath build is
# non-trivial across the three target platforms, so Slice B lands in sub-slices
# — B1 (VertexAnimationManager + Ogre VAT_POSE playback + tests) needs NO
# Alembic and builds/tests everywhere; B2 flips this ON for desktop and wires
# cmake/Alembic.cmake (FetchContent). The C++ #ifdef ENABLE_ALEMBIC guards the
# reader so a build without it still compiles and skips .abc with a clear message.
option(ENABLE_ALEMBIC "Enable Alembic (.abc) vertex-animation import" OFF)

if(ENABLE_ALEMBIC)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Alembic.cmake)
Comment on lines +288 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not expose an Alembic option that includes a missing file

When someone configures with -DENABLE_ALEMBIC=ON, this branch executes include(.../cmake/Alembic.cmake), but that file is not present in the repo (checked rg --files cmake). CMake will fail during configure before any guards can apply, so desktop/package builds that opt into the new feature cannot configure; either add the module in this commit or keep the option inert until B2.

Useful? React with 👍 / 👎.

add_definitions(-DENABLE_ALEMBIC)
message(STATUS "Alembic vertex-animation import enabled")
else()
message(STATUS "Alembic vertex-animation import disabled (VAT_POSE playback still available)")
endif()
##############################################################
# PS1 runtime geometry extraction (experimental)
##############################################################
option(ENABLE_PS1_RIP "Enable experimental PS1 runtime geometry extraction" OFF)
Expand Down
92 changes: 92 additions & 0 deletions cmake/Alembic.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Alembic (.abc) vertex-animation import — Anim epic Slice B (#519), sub-slice B2.
#
# Vendors Imath 3 + Alembic (both BSD-3-Clause) via FetchContent, statically,
# with every optional component OFF (no HDF5, Python, tests, binaries, install).
# Exposes an imported target `qtmesh_alembic` that the app links; the C++ side
# is #ifdef ENABLE_ALEMBIC-guarded so a build without this still compiles.
#
# Why build from source rather than find_package: Alembic + Imath system
# packages are absent or version-skewed across our three targets (macOS via
# brew, Ubuntu CI, Windows MinGW). FetchContent gives one reproducible version
# everywhere, matching how the project already vendors ONNX Runtime / libsodium
# / tinyexr.
#
# Dependency chain: Alembic FIND_PACKAGE(Imath) → we build Imath as a
# subproject first, point Imath_DIR at its generated build-tree config so
# Alembic's find_package(Imath CONFIG) resolves to the target we just built.

if(TARGET qtmesh_alembic)
return()
endif()

include(FetchContent)

set(QTMESH_IMATH_TAG "v3.1.12" CACHE STRING "Imath git tag")
set(QTMESH_ALEMBIC_TAG "1.8.8" CACHE STRING "Alembic git tag")

# ---- Imath ---------------------------------------------------------------
# Static, no tests/python. IMATH_INSTALL stays ON: Alembic's own
# INSTALL(EXPORT AlembicTargets) is unconditional and references Imath, so
# Imath MUST be in an export set too or CMake's generate step fails
# ("target Alembic requires target Imath that is not in any export set").
# We never actually run `make install` from the app build, so an ON install
# rule is harmless — it just keeps the export sets consistent.
set(IMATH_INSTALL ON CACHE BOOL "" FORCE)
set(IMATH_INSTALL_PKG_CONFIG OFF CACHE BOOL "" FORCE)
set(PYTHON OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
qtmesh_imath
GIT_REPOSITORY https://github.com/AcademySoftwareFoundation/Imath.git
GIT_TAG ${QTMESH_IMATH_TAG}
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(qtmesh_imath)

# Alembic's find_package(Imath) resolves against a CONFIG. Imath-as-subproject
# writes its package config under the build dir; point find_package there.
if(NOT DEFINED Imath_DIR)
set(Imath_DIR "${qtmesh_imath_BINARY_DIR}/config" CACHE PATH "" FORCE)
endif()
Comment on lines +48 to +52

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

🌐 Web query:

Where does Imath 3.1.12 CMakeLists.txt write its build-tree package config (Imath_DIR) when building as a FetchContent subproject?

💡 Result:

When using Imath 3.1.12 as a FetchContent subproject, the build-tree configuration files (including ImathConfig.cmake) are generated within the Imath build directory, specifically inside the directory defined by ${CMAKE_CURRENT_BINARY_DIR} [1]. In the Imath build system, the configuration files are managed in config/CMakeLists.txt [1]. The ImathConfig.cmake and associated version files are generated into the root of the Imath build directory (within that subproject's binary directory) using CMake's configure_package_config_file command [1]. Because FetchContent adds the project directly into the main build tree, you generally do not need to manually set Imath_DIR to locate the package [2][3]. Instead, you can simply call find_package(Imath CONFIG) in your downstream CMake code [3]. CMake will typically find the Imath targets exported within the current build session, allowing you to link against them using the Imath::Imath namespace [1][3]. If for any reason manual path resolution is required, it would point to the binary directory where the Imath subproject was configured [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo root: '; pwd
echo '--- files mentioning qtmesh_imath_BINARY_DIR / Imath_DIR / ImathConfig.cmake / configure_package_config_file ---'
rg -n --hidden --glob '!**/.git/**' 'qtmesh_imath_BINARY_DIR|Imath_DIR|ImathConfig\.cmake|configure_package_config_file|write_basic_package_version_file' .

echo '--- candidate files ---'
git ls-files | rg '(^|/)(Alembic\.cmake|CMakeLists\.txt|Imath|config/CMakeLists\.txt|config\.cmake)$'

Repository: fernandotonon/QtMeshEditor

Length of output: 984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the specific files that can establish the package-config output path.
for f in cmake/Alembic.cmake config/CMakeLists.txt; do
  if [ -f "$f" ]; then
    echo "=== $f ==="
    wc -l "$f"
    echo "--- outline ---"
    ast-grep outline "$f" --view expanded || true
    echo "--- excerpt ---"
    cat -n "$f" | sed -n '1,220p'
    echo
  fi
done

Repository: fernandotonon/QtMeshEditor

Length of output: 5125


🌐 Web query:

Imath 3.1.12 config/CMakeLists.txt configure_package_config_file ImathConfig.cmake CMAKE_CURRENT_BINARY_DIR config subdirectory

💡 Result:

In Imath 3.1.12, the config/CMakeLists.txt file uses the CMake command configure_package_config_file to generate the ImathConfig.cmake file [1]. This command is part of the CMakePackageConfigHelpers module, which is essential for creating relocatable CMake package configuration files [2][3][4]. The usage in Imath 3.1.12 follows this standard pattern [1]: include(CMakePackageConfigHelpers) configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/ImathConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/ImathConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Imath) Key technical details regarding this configuration include: 1. Purpose of configure_package_config_file: Unlike the standard configure_file, this command is specifically designed for generating Config.cmake files [3][4]. It enables the created file to be relocatable by calculating path variables (like install directories) relative to the installation prefix, rather than using absolute build-time paths [3][4][5]. 2. CMAKE_CURRENT_BINARY_DIR: This variable is used as the output destination for the generated ImathConfig.cmake file [1]. Generating it here ensures that the file is created within the build tree, from where it can subsequently be installed to the final destination using an install command [1][5]. 3. INSTALL_DESTINATION: This mandatory argument specifies where the package configuration file will eventually be installed [3][5]. The command uses this path to correctly calculate relative paths within the generated configuration file, ensuring that when a user subsequently finds the package via find_package, the paths resolve correctly regardless of where the package was installed on their system [3][4]. 4. Workflow: After generating the file in the CMAKE_CURRENT_BINARY_DIR, the Imath build system uses the install command to copy the generated ImathConfig.cmake and associated version file (generated via write_basic_package_version_file) to the appropriate system directory (e.g., ${CMAKE_INSTALL_LIBDIR}/cmake/Imath) [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrowly inspect any local copy of Imath sources if present.
fd -a 'config/CMakeLists.txt' .
fd -a 'ImathConfig.cmake.in' .
fd -a 'ImathConfigVersion.cmake.in' .

Repository: fernandotonon/QtMeshEditor

Length of output: 164


🌐 Web query:

site:github.com/AcademySoftwareFoundation/Imath v3.1.12 config/CMakeLists.txt ImathConfig.cmake configure_package_config_file

💡 Result:

In Imath v3.1.12, the configuration of the package's CMake files is managed within the config/CMakeLists.txt file [1][2]. The project utilizes the standard CMake command configure_package_config_file to generate the ImathConfig.cmake file from an input template, ImathConfig.cmake.in [1]. Specifically, the following process is used in config/CMakeLists.txt: 1. The CMakePackageConfigHelpers module is included to provide the necessary helper functions [1]. 2. configure_package_config_file is called to generate the final ImathConfig.cmake in the build directory, with the INSTALL_DESTINATION set to the appropriate subdirectory within the installation library path (typically lib/cmake/Imath) [1]. 3. A corresponding version file is generated using write_basic_package_version_file to ensure compatibility checks during find_package(Imath CONFIG) calls [1]. 4. The generated ImathConfig.cmake and ImathConfigVersion.cmake files are subsequently installed to the target directory alongside the exported project targets [1]. This setup allows downstream projects to reliably locate Imath using find_package(Imath CONFIG) [3][4]. Notably, version 3.1.12 specifically addressed issues related to improper versioning in these configuration files that were present in the previous v3.1.11 release [2][5].

Citations:


🌐 Web query:

site:github.com/AcademySoftwareFoundation/Imath v3.1.12 "configure_package_config_file" "CMAKE_CURRENT_BINARY_DIR"

💡 Result:

The function configure_package_config_file is a command provided by the CMakePackageConfigHelpers module in CMake. It is used to generate a package configuration file (typically named Config.cmake) for a project, which enables other projects to find and use your library via the find_package command [1]. In practice, CMAKE_CURRENT_BINARY_DIR is frequently used as the location to generate this configuration file before it is installed [1][2]. By generating the file in the binary directory, you ensure that the project can reference the generated configuration file (and associated version files) during both the build process and installation phase [1][2]. A typical usage pattern, as seen in projects like Imath, involves these steps: 1. Include the helper module: include(CMakePackageConfigHelpers) 2. Call the function: configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/MyPackageConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/MyPackageConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyPackage) This command configures the input template file (.cmake.in) by replacing variables with their current values and outputs the result to the specified file in CMAKE_CURRENT_BINARY_DIR, while setting up path variables to be relocatable based on the INSTALL_DESTINATION [1].

Citations:


Point Imath_DIR at the Imath binary dir. Imath 3.1.12 writes ImathConfig.cmake to ${qtmesh_imath_BINARY_DIR}, not ${qtmesh_imath_BINARY_DIR}/config, so the current path makes Alembic’s find_package(Imath CONFIG) miss the package.

🤖 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/Alembic.cmake` around lines 48 - 52, Update the Imath package hint in
the Alembic setup so `find_package(Imath CONFIG)` can actually locate
`ImathConfig.cmake`; the current `Imath_DIR` default in the `if(NOT DEFINED
Imath_DIR)` block points to the wrong subdirectory. Set `Imath_DIR` to the Imath
binary directory produced by the subproject (`qtmesh_imath_BINARY_DIR`) rather
than the `config` subfolder, keeping the existing cache/force behavior in the
`cmake/Alembic.cmake` logic.


# ---- Alembic -------------------------------------------------------------
# Everything optional OFF: no HDF5 backend (we only read the modern Ogawa
# backend), no tests/binaries/python/prman/maya/arnold, static lib.
set(USE_HDF5 OFF CACHE BOOL "" FORCE)
set(USE_TESTS OFF CACHE BOOL "" FORCE)
set(USE_BINARIES OFF CACHE BOOL "" FORCE)
set(USE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(USE_PYALEMBIC OFF CACHE BOOL "" FORCE)
set(USE_ARNOLD OFF CACHE BOOL "" FORCE)
set(USE_PRMAN OFF CACHE BOOL "" FORCE)
set(USE_MAYA OFF CACHE BOOL "" FORCE)
set(ALEMBIC_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(ALEMBIC_ILMBASE_LINK_STATIC ON CACHE BOOL "" FORCE)
set(ALEMBIC_LIB_INSTALL_DIR "lib" CACHE STRING "" FORCE)

FetchContent_Declare(
qtmesh_alembic
GIT_REPOSITORY https://github.com/alembic/alembic.git
GIT_TAG ${QTMESH_ALEMBIC_TAG}
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(qtmesh_alembic)

# Alembic's core library target is `Alembic` (with an `Alembic::Alembic` alias
# in recent versions). Wrap whichever exists behind our stable name so the app
# links `qtmesh_alembic` regardless.
if(TARGET Alembic::Alembic)
add_library(qtmesh_alembic INTERFACE)
target_link_libraries(qtmesh_alembic INTERFACE Alembic::Alembic Imath::Imath)
elseif(TARGET Alembic)
add_library(qtmesh_alembic INTERFACE)
target_link_libraries(qtmesh_alembic INTERFACE Alembic Imath::Imath)
# The plain `Alembic` target's public include dirs cover its own headers;
# Imath::Imath carries the Imath headers Alembic's public API exposes.
else()
message(FATAL_ERROR "Alembic.cmake: neither Alembic nor Alembic::Alembic target was created")
endif()

message(STATUS "Alembic ${QTMESH_ALEMBIC_TAG} + Imath ${QTMESH_IMATH_TAG} vendored (static)")
Loading