Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
407c28d
feat(paint): texture paint MVP + bake vertex-colors to texture
fernandotonon May 14, 2026
c9c4b1b
feat(paint): standalone EditableMesh, slot picker, brush tools, live …
fernandotonon May 14, 2026
9636f69
test(paint): add controller + flood-fill tolerance coverage
fernandotonon May 14, 2026
8ecf45a
perf(paint): debounce preview refresh + show all paintable slots
fernandotonon May 14, 2026
bc365a5
fix(paint): auto-create texture session on selection change
fernandotonon May 14, 2026
e60300b
docs(paint): update Paint Tools quickstart for the new workflow
fernandotonon May 14, 2026
8ecf0e2
fix(paint): close session in controller destructor
fernandotonon May 14, 2026
99fd42b
feat(paint): resolution picker for new textures and bake output
fernandotonon May 14, 2026
d699beb
feat(paint): UV-island wireframe overlay on the 2D preview
fernandotonon May 14, 2026
cc62ee2
fix(paint): preserve resolution across slot switches
fernandotonon May 14, 2026
ea97cc4
fix(paint): clear UV overlay on session close
fernandotonon May 14, 2026
2f5539b
fix(paint): brush ring uses local-unit radius, not mesh-scaled
fernandotonon May 14, 2026
ab09588
fix(paint): kill TexturePaintController before Manager teardown
fernandotonon May 14, 2026
75b7638
fix(paint): restore original TUS textures on session close + scoped r…
fernandotonon May 14, 2026
560b5d0
fix(paint): non-destructive paint enable + 3 strategies for texture r…
fernandotonon May 14, 2026
9ff6c50
fix(paint): brush works in both Material+Edit modes; deferred rebind
fernandotonon May 14, 2026
771629d
fix(paint): build EditableMesh eagerly for hover, defer GPU rebind
fernandotonon May 14, 2026
c59a18e
fix(paint): mouse tracking + crosshair cursor for texture paint
fernandotonon May 14, 2026
21c4bb0
fix(paint): brush button state reset + 4-strategy texture readback + …
fernandotonon May 14, 2026
945fe32
fix(paint): blit strokes directly to original texture when possible
fernandotonon May 14, 2026
1fac4e8
feat(paint): vertex paint moves to Material Mode; target picker; sube…
fernandotonon May 14, 2026
087d030
feat(paint): tri-state target switch (Off/Vertex/Texture); default Ve…
fernandotonon May 14, 2026
1d32b11
fix(paint): re-resolve original texture each flush + safer shutdown
fernandotonon May 14, 2026
5a89aea
fix(paint): skip compressed formats + defer first-flush rebind
fernandotonon May 14, 2026
68af5c2
fix(paint): rebind eagerly on session create; disable in-place blit
fernandotonon May 14, 2026
fbb8680
fix(paint): bind TexturePtr directly + restore in-place blit fast path
fernandotonon May 14, 2026
651bc9f
fix(paint): upload full buffer per stroke (sub-rect blit unreliable o…
fernandotonon May 14, 2026
5a80405
perf(paint): debounce GPU upload + fix stuck-stroke on release outsid…
fernandotonon May 14, 2026
1f6c6c8
perf(paint): skip cross-surface ring updates during stroke; drop redu…
fernandotonon May 14, 2026
d61bc8e
perf(paint): thumbnail-scale the preview PNG before encoding
fernandotonon May 14, 2026
7c3a83d
fix(paint): stroke stops mid-drag in panel — drop pressed/buttons guard
fernandotonon May 14, 2026
b7d107c
fix(paint): preventStealing on paint MouseArea — Inspector ScrollView…
fernandotonon May 14, 2026
1b21102
fix(paint): defer first-flush rebind off the mouse-event stack
fernandotonon May 14, 2026
b973bf5
revert(paint): re-enable brush ring during stroke for both surfaces
fernandotonon May 14, 2026
cc4ea44
fix(paint): pre-create session on enable, refresh preview on load, pe…
fernandotonon May 14, 2026
16545cc
fix(paint): push painted PNG bytes into EmbeddedTextureCache for FBX …
fernandotonon May 14, 2026
796b601
test(paint): expose findMeshPointForUV publicly so unit test can call it
fernandotonon May 14, 2026
c982fc5
review(paint): address CodeRabbit findings + relax brush-center toler…
fernandotonon May 15, 2026
a0d01bc
fix(paint): recover Codex P1+P2 + CI test count guard
fernandotonon May 15, 2026
1ea0c75
fix(paint): ensurePaintableTexture left m_paintMesh null after closeS…
fernandotonon May 15, 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
47 changes: 47 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,12 @@ jobs:

echo "Found $test_name at $test_path"
local exit_code=0
local expected_tests
if [ -n "$filter" ]; then
expected_tests=$(count_listed_tests "$test_path" "$filter")
else
expected_tests=$(count_listed_tests "$test_path")
fi
if [ -n "$filter" ]; then
$test_path --gtest_output=xml:$output_file --gtest_filter="$filter"
else
Expand All @@ -1056,6 +1062,13 @@ jobs:
return $exit_code
fi

local actual_tests
actual_tests=$(count_testcases_in_xml "$output_file")
if [ "$actual_tests" -ne "$expected_tests" ]; then
echo "ERROR: $test_name ran $actual_tests/$expected_tests discovered test(s)."
return 1
fi

local skipped_count
skipped_count=$(count_skipped_in_xml "$output_file")
if [ "$skipped_count" -gt 0 ]; then
Expand Down Expand Up @@ -1098,6 +1111,25 @@ jobs:
grep -E '<testcase[^>]*(result="skipped"|status="notrun"|result="suppressed")' "$xml_file" | wc -l
}

count_testcases_in_xml() {
local xml_file=$1
if [ ! -f "$xml_file" ]; then
echo "0"
return 0
fi
grep '<testcase ' "$xml_file" | wc -l
}

count_listed_tests() {
local test_path=$1
local filter=${2:-}
if [ -n "$filter" ]; then
$test_path --gtest_filter="$filter" --gtest_list_tests 2>/dev/null | grep -E '^ [^[:space:]]' | wc -l
else
$test_path --gtest_list_tests 2>/dev/null | grep -E '^ [^[:space:]]' | wc -l
fi
}

# === Run UnitTests: each test suite in its own process ===
# This prevents a crash in one suite (e.g. Ogre GL init segfault)
# from killing all subsequent suites and losing their coverage data.
Expand All @@ -1108,16 +1140,26 @@ jobs:
echo "=== Running UnitTests suites in isolation ==="
# Get list of test suites
SUITES=$($UNIT_TEST_PATH --gtest_list_tests 2>/dev/null | grep -E '^\w' | sed 's/\.$//' | sort -u)
EXPECTED_UNIT_TESTS=$(count_listed_tests "$UNIT_TEST_PATH")
ACTUAL_UNIT_TESTS=0

for suite in $SUITES; do
TOTAL_SUITES=$((TOTAL_SUITES + 1))
echo "--- Running suite: $suite ---"
# Run without sudo so signal exit codes (e.g. 139 for SIGSEGV) propagate correctly.
# sudo can mask crash exit codes, reporting 0 for segfaulted children.
suite_xml="test-results-${suite}.xml"
expected_suite_tests=$(count_listed_tests "$UNIT_TEST_PATH" "${suite}.*")
$UNIT_TEST_PATH --gtest_filter="${suite}.*" --gtest_output=xml:${suite_xml} 2>&1
exit_code=$?
if [ $exit_code -eq 0 ]; then
actual_suite_tests=$(count_testcases_in_xml "$suite_xml")
ACTUAL_UNIT_TESTS=$((ACTUAL_UNIT_TESTS + actual_suite_tests))
if [ "$actual_suite_tests" -ne "$expected_suite_tests" ]; then
echo "ERROR: Suite $suite ran $actual_suite_tests/$expected_suite_tests discovered test(s)."
FAILED_SUITES=$((FAILED_SUITES + 1))
continue
fi
skipped_count=$(count_skipped_in_xml "$suite_xml")
if [ "$skipped_count" -gt 0 ]; then
echo "ERROR: Suite $suite produced $skipped_count skipped test(s)."
Expand All @@ -1141,7 +1183,12 @@ jobs:
fi
done

if [ "$ACTUAL_UNIT_TESTS" -ne "$EXPECTED_UNIT_TESTS" ]; then
echo "ERROR: UnitTests executed $ACTUAL_UNIT_TESTS/$EXPECTED_UNIT_TESTS discovered test(s)."
FAILED_SUITES=$((FAILED_SUITES + 1))
fi
echo "=== UnitTests summary: $PASSED_SUITES/$TOTAL_SUITES suites passed, $FAILED_SUITES failed, $CRASHED_SUITES crashed, $SKIPPED_TESTS skipped tests ==="
echo "=== UnitTests cases: $ACTUAL_UNIT_TESTS/$EXPECTED_UNIT_TESTS executed ==="
else
echo "ERROR: UnitTests not found!"
FAILED_SUITES=$((FAILED_SUITES + 1))
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,72 @@ qtmesh pose model.fbx --animation "Dance" --count 4 -o pose_%02d.stl

# LOD generation
qtmesh lod model.fbx --auto

# Bake vertex colors → texture (with UV-seam dilation)
qtmesh bake-vertex-colors model.fbx -o color_map.png --resolution 1024 --dilation 4
```

---

### 🎨 Paint Tools

ZBrush-style polypaint and BaseColor texture painting, with a 2D preview
panel, multiple brush tools (paint, erase, fill, color picker, smudge),
texture-slot picker per submesh, and a one-click bake that turns vertex
colors into a UV-space texture.

**Quick start (texture paint, GUI):**

1. Switch to **Material Mode** (mode bar at the top).
2. Select a mesh entity. The Inspector's **Texture Paint** section
shows a slot picker (every paintable TUS across submeshes) and a 256×256
live preview of the active texture.
3. Click the **paint brush** button in the toolbar to enable painting. The
first click on the mesh auto-creates a paint session against the active
slot (or you can pre-create with *Create / Attach Texture*).
4. Pick a tool from the row at the top of the panel: ✏ Paint, ⌫ Erase, ⧉ Fill,
⊰ Pick (eyedropper), ∿ Smudge. Brush color/radius/strength/falloff comes
from the shared Paint Brush section above the toolbar's brush popup.
5. Left-click-drag on **either** the 3D mesh or the 2D preview panel. Both
drive the same paint buffer; the 2D preview updates in real time and a
brush ring shows where the cursor maps on the 3D surface (and vice versa).
6. *Save…* / *Load…* round-trips the buffer to disk as PNG (or any format
QImage can write).

**Vertex paint (GUI):**

Same flow, but vertex paint operates in **Edit Mode** instead. Press **Tab**
on a selected mesh to enter Edit Mode, then tick *Vertex Color Preview* in
the Edit Mode Tools section to see vertex colors live.

**Bake Vertex Colors → Texture** rasterizes the active mesh's vertex colors
into a UV-space PNG via barycentric interpolation, then dilates the result
outward by N pixels to mask UV-seam bleed at MIP-map time.

**Headless (CLI):**

```bash
qtmesh bake-vertex-colors model.fbx -o color_map.png --resolution 1024 --dilation 4
```

**Export.** Vertex colors are preserved on export to formats that support
them (glTF preferred — verified round-trip).

**Notes / limitations.**

- Brush size is in local mesh units (shared with vertex paint). For texture
paint we divide by mesh bounding-box extent and clamp to a UV-friendly
range, so 0.25 produces a sensible stamp on both a unit cube and a
100-unit character.
- Painting auto-rebinds every TUS named `albedo` or `diffuse_map` on the
active material — imported PBR materials alias the diffuse texture under
both, and rebinding only one would leave the other pointing at the
original.
- Bake uses fan triangulation of n-gon faces; concave faces should be
pre-triangulated.

---

### ✨ Merge Mixamo Animations in Seconds

Download animations from [Mixamo](https://www.mixamo.com), drop them into QtMeshEditor, and merge into a single file — export as glTF, FBX, Collada, OBJ, or Ogre Mesh.
Expand Down Expand Up @@ -198,6 +260,7 @@ Split View|Skeleton Animation Controls
- **Animation resampling** — reduce keyframe density for game engines
- **Pose export** — bake animation frames as static meshes (3D printing)
- **LOD generation** — automatic level-of-detail mesh reduction
- **Paint tools** — vertex paint, texture paint (BaseColor), bake vertex colors to texture with seam dilation
- **Material editor** — visual editing with AI-assisted generation
- **Skeleton inspection** — bone weights, debug overlays, animation preview
- **Scene management** — duplicate (Ctrl+D), group (Ctrl+G), snap, pivot modes
Expand Down
Loading
Loading