Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/actions/qtmesh/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: 'Run qtmesh CLI for 3D mesh operations (info, convert, fix, anim, s

inputs:
command:
description: 'Subcommand: info, fix, convert, anim, validate, lod, pose, turntable, scan, material, optimize, …'
description: 'Subcommand: info, fix, convert, anim, validate, lod, pose, turntable, isometric, scan, material, optimize, …'
required: true
input-file:
description: 'Directory or file to scan (relative to workspace). Defaults to .'
Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ qtmesh pose model.fbx --animation "Walk" --time 0.5 -o posed.stl # export singl
qtmesh pose model.fbx --animation "Dance" --count 4 -o pose_%02d.stl # export N evenly spaced frames
qtmesh turntable model.fbx -o turntable.png # PNG sprite sheet (12 frames default)
qtmesh turntable model.fbx -o frame_%02d.png --frames 24 --axis y --camera-height 25
qtmesh isometric model.fbx -o iso.png # 8-direction static sprite grid (rows=directions)
qtmesh isometric model.fbx --resolution 256 -o iso.png # square 256px cells
qtmesh isometric model.fbx --animation "Walk" --frames 8 -o iso.png # 8×8 animated atlas
qtmesh isometric model.fbx -o iso.png --padding 1.5 # zoom out (auto-fit × 1.5)
qtmesh isometric model.fbx -o iso.png --camera-distance 5 # fixed orbit distance
qtmesh validate model.fbx # validate mesh (exit 1 if errors found)
qtmesh validate model.fbx --json # validation results as JSON
qtmesh lod model.fbx --info # show LOD levels
Expand Down Expand Up @@ -97,7 +102,7 @@ qtmesh uv model.fbx --unwrap -o unwrapped.glb # xatlas auto-UV unwrap (#400). N
qtmesh uv model.fbx --unwrap --channel 1 --resolution 2048 -o lightmap.glb # write into UV1 (lightmap workflow)
```

CLI mode is activated by: (1) invoking via the `qtmesh` symlink, (2) passing `--cli`, or (3) using a recognized subcommand (`info`, `fix`, `convert`, `anim`, `validate`, `lod`, `pose`, `turntable`, `scan`, `material`, `pack-textures`, `normal-from-height`, `atlas`, `atlas-apply`, `memory`, `analyze`, `vertex-cache`, `decimate`, `optimize`, `uv`, `retopo`, `skin`) as the first argument. Use `--verbose` to see Ogre/engine debug output. Use `--no-telemetry` to permanently opt out of anonymous usage data collection.
CLI mode is activated by: (1) invoking via the `qtmesh` symlink, (2) passing `--cli`, or (3) using a recognized subcommand (`info`, `fix`, `convert`, `anim`, `validate`, `lod`, `pose`, `turntable`, `isometric`, `scan`, `material`, `pack-textures`, `normal-from-height`, `atlas`, `atlas-apply`, `memory`, `analyze`, `vertex-cache`, `decimate`, `optimize`, `uv`, `retopo`, `skin`) as the first argument. Use `--verbose` to see Ogre/engine debug output. Use `--no-telemetry` to permanently opt out of anonymous usage data collection.

If Xcode SDK is updated, clear CMake cache (`rm build_local/CMakeCache.txt`) and reconfigure.

Expand Down Expand Up @@ -200,7 +205,7 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
### CLI Pipeline

- **CLIPipeline** (`src/CLIPipeline.h/cpp`): Headless command-line interface for mesh operations. All static methods — entry point is `CLIPipeline::run(argc, argv)`.
- Subcommands: `info`, `fix`, `convert`, `anim` (list/rename/merge), `validate`, `lod`, `pose`, `turntable`, `scan`, `material`, `pack-textures`, `normal-from-height`, `memory`, `analyze`, `vertex-cache`, `decimate`, `atlas`, `atlas-apply`, `optimize`.
- Subcommands: `info`, `fix`, `convert`, `anim` (list/rename/merge), `validate`, `lod`, `pose`, `turntable`, `isometric`, `scan`, `material`, `pack-textures`, `normal-from-height`, `memory`, `analyze`, `vertex-cache`, `decimate`, `atlas`, `atlas-apply`, `optimize`.
- Activated via `qtmesh` symlink (created at build time), `--cli` flag, or recognized subcommand as first arg.
- Redirects stdout to stderr (Ogre/Qt noise) and writes CLI output to the original stdout fd. Uses `_exit()` to avoid Ogre static destructor crashes on macOS.
- **AnimationMerger** (`src/AnimationMerger.h/cpp`): Public `renameAnimation()` static method used by both CLI and GUI for animation renaming.
Expand Down Expand Up @@ -244,6 +249,7 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
- **QuadRetopo** (`src/QuadRetopo.h/cpp`, issue #401): triangle-pairing quad-dominant retopology. The issue proposed wrapping Instant Meshes (Wenzel Jakob), but Instant Meshes ships as a research GUI app with no clean C++ library API and has been dormant since 2016. QuadriFlow (the production-grade alternative used by Blender 3.0+) requires Boost + Eigen + LEMON — heavy deps the project doesn't currently use. This first slice ships a native triangle-pairing backend with **zero new dependencies**: walks every interior edge whose two adjacent faces are triangles and scores the merge by (1) coplanarity (dot product of triangle normals; default `maxAngleDeg=25°`), (2) quad shape (deviation of interior angles from 90°; default `shapeToleranceDeg=65°`), (3) aspect ratio (longest/shortest edge; default `maxAspectRatio=6.0`). Pairs are taken greedily best-first; each triangle claimed at most once. Quads are emitted with opposing-corner winding `(opposing0, sharedA, opposing1, sharedB)`. Output goes through `EditableSubMesh::faces` → `triangulateFaces` (fan retri for GPU) → `writeNgonFacesToMesh` (n-gon binding for exporters / Edit Mode). **No new vertices** are introduced, so UVs and skin weights survive unchanged. Backends are pluggable via the `Algorithm` enum (only `TrianglePair` implemented; future `QuadriFlow` / `InstantMeshes` slot in here). Surfaced via `qtmesh retopo --target-faces N --max-angle DEG -o out`, MCP `retopologize`, and the **Material Mode → Mode Tools → "Quad Retopology…" button** (`qml/QuadRetopoDialog.qml`, driven by `QuadRetopoController` singleton). Sentry breadcrumb category `ai.assist.retopo`. Verified on Rumba Dancing.fbx: 10,220 tris → 6,032 faces (4188 quads + 1844 tris), 82% quad dominance. Hard lower bound on face count is ~50% of input (every triangle paired); strict gates typically land 60-70%.
- **UvUnwrap** (`src/UvUnwrap.h/cpp`, issue #400): xatlas-backed automatic UV unwrap. xatlas is the MIT library Blender and Godot use under the hood — single-translation-unit `xatlas.cpp` vendored via FetchContent and wrapped in an inline `add_library(xatlas STATIC …)` target (no upstream CMake config). Pipeline: extract (positions, indices) per submesh → `xatlas::AddMesh` → `xatlas::Generate` → for each output mesh, rebuild a single-binding VertexData copying every source attribute from `xref` (input vertex id) and overwriting the target UV channel with `xatlas::Vertex::uv / atlas.{width,height}`. Skinned-mesh bone assignments survive the seam splits because we rebuild `SubMesh::BoneAssignmentList` against the new vertex IDs via xref; for shared-vertex meshes the source assignments come from `Mesh::getBoneAssignments()`, not `SubMesh::getBoneAssignments()`. Surfaced via `qtmesh uv --unwrap`/`--info`, MCP `auto_uv_unwrap`, and the **Material Mode → Mode Tools → "Auto UV Unwrap…" button** (`qml/UvUnwrapDialog.qml`, driven by `UvUnwrapController` singleton). Sentry breadcrumb category `ai.assist.uv_unwrap`. The unwrap also erases `qtme.faces.<i>` n-gon bindings (they reference source vertex IDs and become stale). **GUI-safe entry point** (`unwrapEntityToFile`): live skinned meshes cannot survive in-place vertex-data mutation because the active `Ogre::SkeletonInstance` caches the hardware blend buffer and picks up stale state on the first frame after the swap. The GUI path snapshots `vertexData` / `indexData` / `mBoneAssignments` / `blendIndexToBoneIndexMap` for every submesh + the mesh's shared maps, calls `unwrapEntityKeepingOriginals` (which deliberately leaks its own allocations rather than freeing the originals), exports the unwrapped result, then restores the snapshot pointer-for-pointer (deleting only the unwrap's leaked allocations) and pastes the index maps back directly — `_compileBoneAssignments` is NOT called on restore because it would re-pack BLEND_INDICES/WEIGHTS bytes against the live buffer and shatter the on-screen mesh. CLI path uses the destructive `unwrapEntity` since the process exits before rendering.
- **ExportOptimizer** (`src/ExportOptimizer.h/cpp`, issue #399): Pipeline that runs `meshopt_optimizeVertexCache` → `meshopt_optimizeOverdraw` (threshold 1.05) → `meshopt_optimizeVertexFetchRemap` on every submesh of an entity. Surfaced through the **Inspector validation flow** — the "Optimize Geometry (cache + overdraw + fetch)" button in `PropertiesPanel.qml` runs it via `MeshValidator::optimizeVertexCache`. NOT hooked into `MeshImporterExporter::exporter` by default (an earlier draft did this and crashed on macOS during a normal export — silent buffer mutation during export is dangerous; explicit user invocation via the validation button is safer). Vertex-fetch is skipped when the submesh uses `useSharedVertices` since remapping shared verts would scramble other submeshes' indices. `qtmesh info --json` includes `submeshAcmr[]` per submesh so downstream tooling can decide whether to recommend re-optimization. Sentry breadcrumb category `ai.assist.optimize_export`.
- **Isometric sprite export** (`src/ModelIsometricRenderer.h/cpp`, epic #724): headless RTT renderer for 8-direction (configurable) isometric sprite atlases. Reuses the turntable's offscreen capture pattern (RTSS materials, stable orbit framing from rest bounds, single camera re-placed per direction). Outer loop = compass directions (row 0 = front/+Z, clockwise from above); inner loop = evenly spaced animation frames via `AnimationState::setTimePosition` + `_updateAnimation` before readback. Grid layout: rows = directions, columns = frames. Options include `--resolution`, `--camera-distance`, and `--padding` (auto-fit multiplier). Surfaced via `qtmesh isometric`, MCP `generate_isometric_sprites`. Sentry breadcrumb categories `file.export` / `ai.tool_call`.
- **FBX LOD export gotcha**: `FBXExporter` prefers the cached `qtme.faces.<i>` n-gon binding (set up by quad-migration #326) over `SubMesh::indexData`. The CLI `lod` per-LOD export path in `CLIPipeline::cmdLod` temporarily erases those bindings (and restores them after) so the swapped-in LOD indices actually reach the wire. If you add another LOD-export entry point, mirror that erase/restore pair.

## Development Guidelines
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.24.0)
cmake_policy(SET CMP0005 NEW)
cmake_policy(SET CMP0048 NEW) # manages project version

project(QtMeshEditor VERSION 3.6.0 LANGUAGES C CXX)
project(QtMeshEditor VERSION 3.7.0 LANGUAGES C CXX)
message(STATUS "Building QtMeshEditor version ${PROJECT_VERSION}")

set(QTMESHEDITOR_VERSION_STRING "\"${PROJECT_VERSION}\"")
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Available on the [GitHub Actions Marketplace](https://github.com/marketplace/act
**Versioning**

- **Always follow the latest GitHub release** — use the Marketplace floating tag `fernandotonon/QtMeshEditor@v1` (same pattern as the [Marketplace example](https://github.com/marketplace/actions/qtmesheditor)). The composite action defaults to `image-tag: latest`, so the Docker CLI tracks the newest published `ghcr.io/fernandotonon/qtmesh` image.
- **Reproducible builds** — pin the action and the container to the same semver as this repository’s `project(QtMeshEditor VERSION …)` in `CMakeLists.txt` (currently **3.5.3**). After bumping the version in CMake, run `./scripts/sync-doc-versions-from-cmake.sh` to refresh the pinned refs in `README.md` and the docs site fallback; CI enforces the match with `./scripts/sync-doc-versions-from-cmake.sh --check`.
- **Reproducible builds** — pin the action and the container to the same semver as this repository’s `project(QtMeshEditor VERSION …)` in `CMakeLists.txt` (currently **3.7.0**). After bumping the version in CMake, run `./scripts/sync-doc-versions-from-cmake.sh` to refresh the pinned refs in `README.md` and the docs site fallback; CI enforces the match with `./scripts/sync-doc-versions-from-cmake.sh --check`.

Pinned workflow template (action + `ghcr.io` image aligned):

Expand All @@ -53,10 +53,10 @@ jobs:
- uses: actions/checkout@v4

- name: Run QtMesh scan
uses: fernandotonon/QtMeshEditor@3.6.0
uses: fernandotonon/QtMeshEditor@3.7.0
with:
command: scan
image-tag: "3.6.0"
image-tag: "3.7.0"
env:
QTMESH_CLOUD_TOKEN: ${{ secrets.QTMESH_CLOUD_TOKEN }}
```
Expand All @@ -81,37 +81,37 @@ Release tags are listed on the [releases page](https://github.com/fernandotonon/

```yaml
# Validate a specific mesh
- uses: fernandotonon/QtMeshEditor@3.6.0
- uses: fernandotonon/QtMeshEditor@3.7.0
with:
command: validate
input-file: ./models/character.fbx
image-tag: "3.6.0"
image-tag: "3.7.0"

# Convert FBX → glTF
- uses: fernandotonon/QtMeshEditor@3.6.0
- uses: fernandotonon/QtMeshEditor@3.7.0
with:
command: convert
input-file: ./models/character.fbx
output-file: ./output/character.gltf2
image-tag: "3.6.0"
image-tag: "3.7.0"

# Resample Mixamo animations (200+ keyframes → 30)
- uses: fernandotonon/QtMeshEditor@3.6.0
- uses: fernandotonon/QtMeshEditor@3.7.0
with:
command: anim
input-file: ./animations/dance.fbx
output-file: ./output/dance_optimized.fbx
options: --resample 30
image-tag: "3.6.0"
image-tag: "3.7.0"

# Get mesh info as JSON
- uses: fernandotonon/QtMeshEditor@3.6.0
- uses: fernandotonon/QtMeshEditor@3.7.0
id: info
with:
command: info
input-file: ./models/character.fbx
options: --json
image-tag: "3.6.0"
image-tag: "3.7.0"

# Docker (alternative — :latest tracks newest image; pin :3.4.0 to match semver action ref)
docker run --rm -v $(pwd):/workspace ghcr.io/fernandotonon/qtmesh:latest scan ./assets --fail-on error
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ branding:

inputs:
command:
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose, turntable'
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose, turntable, isometric'
required: true
input-file:
description: 'Directory or file to scan (relative to workspace). Defaults to . (workspace root).'
Expand Down
3 changes: 3 additions & 0 deletions scripts/sync-doc-versions-from-cmake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ apply_perl_replace() {
QTMESH_DOC_VERSION="${VERSION}" perl -i -pe \
'BEGIN { $v = $ENV{QTMESH_DOC_VERSION}; } s/(?<!`)(image-tag:\s*")(\d+\.\d+\.\d+)(")(?!`)/$1 . $v . $3/ge' \
"$f"
QTMESH_DOC_VERSION="${VERSION}" perl -i -pe \
'BEGIN { $v = $ENV{QTMESH_DOC_VERSION}; } s/(currently \*\*)\d+\.\d+\.\d+(\*\*)/$1 . $v . $2/ge' \
"$f"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if [[ "${CHECK}" -eq 1 ]]; then
Expand Down
2 changes: 1 addition & 1 deletion src/AppLaunchHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ bool isCliSubcommand(const QString& arg)
static const QStringList kSubcommands = {
QStringLiteral("info"), QStringLiteral("fix"), QStringLiteral("convert"),
QStringLiteral("anim"), QStringLiteral("validate"), QStringLiteral("lod"),
QStringLiteral("pose"), QStringLiteral("turntable"), QStringLiteral("scan"),
QStringLiteral("pose"), QStringLiteral("turntable"), QStringLiteral("isometric"), QStringLiteral("scan"),
QStringLiteral("material"), QStringLiteral("pack-textures"),
QStringLiteral("normal-from-height"), QStringLiteral("memory"),
QStringLiteral("analyze"), QStringLiteral("vertex-cache"),
Expand Down
Loading
Loading