Skip to content

feat(skinning): Skinning v2 — geodesic voxel binding default + weight post-passes (#819 Slices A+B) - #829

Merged
fernandotonon merged 3 commits into
masterfrom
feat/skinning-v2-gvb-819
Jul 10, 2026
Merged

feat(skinning): Skinning v2 — geodesic voxel binding default + weight post-passes (#819 Slices A+B)#829
fernandotonon merged 3 commits into
masterfrom
feat/skinning-v2-gvb-819

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Implements Slices A and B of #819, with full GUI/CLI/MCP surface parity and the Slice-C enum plumbing.

What's in

Slice A — Geodesic Voxel Binding (src/GeodesicVoxelBind.{h,cpp}, new default)
The method Maya ships as "Geodesic Voxel" bind (Dionne & de Lasa, SCA 2013), implemented natively with zero new dependencies:

  • Surface voxelization via Akenine-Möller triangle/box SAT at voxelResolution (default 64, max 256)
  • Exterior flood-fill classification — closes holes at voxel resolution, so non-watertight / self-intersecting / multi-component meshes work
  • 3D-DDA bone-segment rasterization; bones outside the solid snap to the nearest solid voxel within 4.5 voxels (world-distance-checked) or get no seeds and are reported
  • One multi-source Dijkstra over interior+surface voxels (26-connectivity), each voxel keeping its best K=8 (bone, distance) pairs — one pass total, not one per bone
  • Per-vertex (1/d)^falloff weighting reusing the existing SkinWeightsOptions semantics unchanged

Cross-limb bleed is impossible by construction: distances travel through the volume, so a hand near a thigh can never pick up leg weights.

Slice B — Weight post-pass pipeline (src/SkinWeightsPost.{h,cpp}, applies to every algorithm)

  • Laplacian relaxation over the vertex adjacency graph (default 3 iterations, --smooth-iterations, 0 = off). Merge-mode (--merge) manual weights act as Dirichlet constraints — they shape the blend at the boundary but are never modified
  • Prune < 0.01 + top-K + renormalize (partition of unity guaranteed; largest weight always survives)
  • Bleed report metric: fraction of committed weights not geodesically local (0 for GVB by construction)

Whole-mesh single-pass compute: computeAndApply now gathers every vertex-data owner (shared + per-submesh) into one combined vertex/index set and computes once — matching the paper (voxelize the whole character). This was load-bearing: per-submesh grids stranded bones outside accessory AABBs (verified on a 39-submesh character where per-owner grids reported all 119 bones seedless).

Algorithm enum + fallbacks

  • Algorithm { InverseDistance, GeodesicVoxel (default), UniRigML }
  • Volume-less input (planes, cloth, billboards) → automatic InverseDistance fallback, never fails
  • Vertices in bone-less floating islands → inverse-distance fill so they still move with the rig
  • UniRigML → falls back to GeodesicVoxel with a clear reason until the Slice-C skin.onnx export is hosted (same pattern as UniRig AI: RigNet auto-rigging (ONNX, ML) #408)
  • Report gains algorithmUsed, fallbackReason, bleedFraction, bonesWithoutSeeds

Surfaces

  • CLI: qtmesh skin --algo geodesic-voxel|inverse-distance|unirig --voxel-res N --smooth-iterations N
  • MCP: compute_skin_weights gains algo, voxel_resolution, smooth_iterations
  • GUI: Skin Weights dialog gains an Algorithm dropdown + voxel-resolution and smoothing knobs; result line shows the algorithm that actually ran + any fallback
  • Sentry breadcrumbs ai.assist.skin.<algo>; qtmesh rig --skin / MCP auto_rig {skin:true} chain through the new default
  • QTMESH_GVB_DEBUG=1 dumps grid/AABB/seed diagnostics to stderr

Tests

Pure-data suites (no Ogre/GL): GeodesicVoxelBind_test.cpp, SkinWeightsPost_test.cpp + updated SkinWeights_test.cpp:

  • Two parallel limbs, 4-voxel gap → zero weight crossover, with a contrast assert that inverse-distance bleeds (0.20) on the same fixture
  • U-shape bend fixture → weights follow the bend, not the chord (GVB 0 vs ID 0.012 through the chord)
  • Cracked (non-watertight) box → identical weights to the closed box (hole closing)
  • Plane → clean fallback with reason; far-outside bone → no seeds, reported, zero weight
  • Post-passes: partition of unity, locked-vertex constraints bit-identical, prune bounds

Verified end-to-end

bandit.fbx (90,573 verts, 140k tris, 119 bones, 39 submeshes): geodesic-voxel end-to-end in 0.64 s at --voxel-res 128 (target was ≤2 s at 64³/100k), bleed fraction 0.005, valid glb round-trip. Thin-rod asset falls back to inverse-distance with a clear reason.

Out of scope (per issue slicing)

  • Slice C (UniRig skin-head ONNX export/hosting) — enum + fallback plumbing ships now, the offline export script + parity test is a follow-up
  • Slice D (DQS display toggle), Slice E (acceptance suite / Mixamo benchmark protocol)

Closes nothing yet — tracks #819 (Slices A+B).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Made Geodesic Voxel the default skin-weight algorithm (volume-aware), with selectable Inverse Distance and UniRig.
    • Added voxel resolution and smoothing iterations controls across the UI, CLI, and compute tools.
    • Enhanced compute output/status to report the algorithm used and any fallback reason.
  • Bug Fixes
    • Reduced cross-limb weight bleeding, including improved behavior on flat/degenerate meshes via automatic fallback and vertex backfilling.
  • Documentation
    • Updated CLI help and skinning guidance to reflect the new defaults and parameters.
  • Tests
    • Added unit coverage for Geodesic Voxel binding and Slice-B post-processing behavior.

…819 Slices A+B)

Skinning v2: GeodesicVoxel (Dionne & de Lasa, SCA 2013 — Maya's
production bind) becomes the default skin-weights algorithm on every
surface. Distances travel through the mesh's interior voxels, so
cross-limb bleed is impossible by construction; voxel-scale hole
closing makes non-watertight/self-intersecting/multi-component
meshes work.

- GeodesicVoxelBind (Ogre-free): Akenine-Möller tri/box voxelization,
  exterior flood-fill interior classification, 3D-DDA bone seeding
  with world-distance-checked snap-to-solid, one multi-source
  Dijkstra keeping best K=8 (bone,distance) pairs per voxel.
- SkinWeightsPost (Ogre-free): Laplacian weight relaxation over the
  vertex adjacency (merge-mode manual weights = Dirichlet
  constraints), prune + renormalize, geodesic bleed metric.
- Whole-mesh single-pass compute in computeAndApply: all vertex-data
  owners voxelized together so accessories bind through the body and
  bones seed one shared field (per-submesh grids stranded bones).
- Algorithm enum {InverseDistance, GeodesicVoxel, UniRigML}; UniRigML
  falls back to GeodesicVoxel until the Slice-C ONNX export is
  hosted; degenerate (volume-less) input falls back to
  InverseDistance automatically.
- Surfaces: CLI `qtmesh skin --algo/--voxel-res/--smooth-iterations`,
  MCP compute_skin_weights algo/voxel_resolution/smooth_iterations,
  GUI dialog algorithm dropdown + knobs. Report gains algorithmUsed/
  fallbackReason/bleedFraction/bonesWithoutSeeds. Sentry
  ai.assist.skin.<algo>.
- Tests: parallel-limb zero-crossover (with inverse-distance contrast
  assert), bend-not-chord U-fixture, cracked non-watertight box ≈
  closed, plane fallback, far-bone no-seed, post-pass invariants.

Verified: bandit.fbx (90k verts, 119 bones, 39 submeshes) skins via
geodesic-voxel in 0.64s at --voxel-res 128, bleed 0.005, valid glb
round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96f298bf-cb4f-4c73-af75-dcc15e378446

📥 Commits

Reviewing files that changed from the base of the PR and between b1da54b and 5062d33.

📒 Files selected for processing (10)
  • qml/PropertiesPanel.qml
  • qml/SkinWeightsDialog.qml
  • src/GeodesicVoxelBind.cpp
  • src/GeodesicVoxelBind_test.cpp
  • src/MCPServer.cpp
  • src/SkinWeights.cpp
  • src/SkinWeightsController.cpp
  • src/SkinWeightsPost.cpp
  • src/SkinWeightsPost_test.cpp
  • tests/CMakeLists.txt
✅ Files skipped from review due to trivial changes (1)
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (8)
  • qml/PropertiesPanel.qml
  • src/SkinWeightsController.cpp
  • src/SkinWeightsPost_test.cpp
  • qml/SkinWeightsDialog.qml
  • src/GeodesicVoxelBind.cpp
  • src/MCPServer.cpp
  • src/SkinWeightsPost.cpp
  • src/SkinWeights.cpp

📝 Walkthrough

Walkthrough

The change introduces geodesic voxel skinning as the default, with inverse-distance fallback and UniRig compatibility. It adds voxel binding, smoothing and pruning, algorithm-aware diagnostics, CLI/MCP/QML configuration, undo propagation, updated documentation, and comprehensive tests.

Changes

Skinning v2

Layer / File(s) Summary
Geodesic voxel binding and validation
src/GeodesicVoxelBind.*, src/GeodesicVoxelBind_test.cpp, src/CMakeLists.txt
Adds mesh voxelization, solid-volume detection, bone seeding, geodesic propagation, normalized influences, diagnostics, and coverage tests.
Weight post-processing utilities
src/SkinWeightsPost.*, src/SkinWeightsPost_test.cpp
Adds adjacency construction, locked Laplacian smoothing, pruning, renormalization, bleed measurement, and unit tests.
Algorithm-aware SkinWeights pipeline
src/SkinWeights.*, src/SkinWeights_test.cpp
Makes geodesic voxel the default, adds inverse-distance and UniRigML dispatch, fallback reporting, unified mesh processing, post-processing, and expanded reports.
CLI, MCP, controller, and undo integration
src/CLIPipeline.cpp, src/MCPServer.cpp, src/SkinWeightsController.*, src/commands/ComputeSkinWeightsCommand.*
Propagates algorithm, voxel resolution, and smoothing options through computation entry points and undo commands.
Skinning UI and documentation
qml/SkinWeightsDialog.qml, qml/PropertiesPanel.qml, CLAUDE.md
Adds algorithm controls and richer completion status while documenting geodesic voxel as the default and inverse-distance as fallback.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SkinWeightsDialog
  participant SkinWeightsController
  participant ComputeSkinWeightsCommand
  participant SkinWeights
  participant GeodesicVoxelBind
  User->>SkinWeightsDialog: Select algorithm and tuning values
  SkinWeightsDialog->>SkinWeightsController: computeWeightsForSelected(...)
  SkinWeightsController->>ComputeSkinWeightsCommand: Create command with algorithm
  ComputeSkinWeightsCommand->>SkinWeights: computeAndApply(entity, options, algorithm)
  SkinWeights->>GeodesicVoxelBind: Compute geodesic weights
  GeodesicVoxelBind-->>SkinWeights: Return weights and diagnostics
  SkinWeights-->>ComputeSkinWeightsCommand: Return report
  ComputeSkinWeightsCommand-->>SkinWeightsDialog: Show algorithm and fallback status
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new default geodesic-voxel skinning pipeline and post-passes.
Description check ✅ Passed The description covers the required summary and technical details, including tests and out-of-scope items, though it doesn't use the template headings verbatim.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skinning-v2-gvb-819

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SkinWeightsController.cpp (1)

111-129: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clamp smoothIterations here and reject unsupported algorithms. voxelResolution is already clamped downstream, but smoothIterations still reaches the Laplacian post-pass unchecked and can make this call run far longer than intended. algorithmFromString() also silently falls back to geodesic-voxel on typos, so a bad caller can get the wrong result instead of an error.

🤖 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 `@src/SkinWeightsController.cpp` around lines 111 - 129, Clamp smoothIterations
to the supported range before assigning it to SkinWeightsOptions, ensuring the
value passed through the Laplacian post-pass cannot exceed intended limits.
Validate algorithm before calling algorithmFromString in the controller method:
reject unknown or unsupported strings with an error and return, rather than
allowing the fallback to geodesic-voxel; only add the breadcrumb and continue
once validation succeeds.
🧹 Nitpick comments (3)
qml/SkinWeightsDialog.qml (1)

284-302: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Smoothing-iterations UI cap (20) is narrower than the backend-supported range (50).

MCP validates smooth_iterations up to 50, but this field caps at 20, so GUI users can't reach the full range CLI/MCP callers can.

♻️ Align GUI cap with backend range
             InspectorNumberField {
                 Layout.preferredWidth: 80
                 value: dialog.smoothIterations
                 minValue: 0
-                maxValue: 20
+                maxValue: 50
                 isInt: true
                 onNewValue: dialog.smoothIterations = Math.round(v)
             }
🤖 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/SkinWeightsDialog.qml` around lines 284 - 302, Align the
smoothing-iterations UI limit with the backend-supported range by changing the
maxValue in the InspectorNumberField for dialog.smoothIterations from 20 to 50.
Keep the existing integer handling and default behavior unchanged.
qml/PropertiesPanel.qml (1)

2255-2264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale tooltip nearby still describes the old default algorithm.

The description text here was updated to describe Geodesic Voxel as the default, but the button's ToolTip.text just a few lines below (Line 2307) still says "via inverse-distance to bone segments," contradicting the text just updated here.

📝 Suggested tooltip update
                     ToolTip.text: SkinWeightsController.hasSkinnedSelection
-                        ? "Compute per-vertex bone weights via inverse-distance to bone segments. Mesh must have a skeleton."
+                        ? "Compute per-vertex bone weights via geodesic voxel bind (volume-aware). Mesh must have a skeleton."
                         : "Select a skinned mesh (with a skeleton) first."
🤖 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/PropertiesPanel.qml` around lines 2255 - 2264, Update the button’s
ToolTip.text near the auto-generate weights control to remove the outdated “via
inverse-distance to bone segments” description and accurately describe Geodesic
Voxel binding as volume-aware with no cross-limb bleed, matching the Text
content in the surrounding PropertiesPanel UI.
src/MCPServer.cpp (1)

7968-7973: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add enum to the algo schema property for stricter MCP client-side validation.

decimate_mesh/generate_lods declare their algo choices via an "enum" JSON-schema array; this new algo property only documents the allowed values in free text. Aligning it with the same pattern improves client-side validation/tooling.

♻️ Proposed fix
         props["algo"] = QJsonObject{{"type", "string"},
+            {"enum", QJsonArray{"geodesic-voxel", "inverse-distance", "unirig"}},
             {"description",
              "Weighting algorithm: 'geodesic-voxel' (default — Maya-style volume-aware "
              "bind, no cross-limb bleed; falls back to inverse-distance on volume-less "
              "meshes), 'inverse-distance' (legacy straight-line heuristic), or 'unirig' "
              "(ML skinning head — currently falls back to geodesic-voxel)."}};
🤖 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 `@src/MCPServer.cpp` around lines 7968 - 7973, Add an "enum" array to the
"algo" property schema in the relevant MCP tool definition, listing
"geodesic-voxel", "inverse-distance", and "unirig" to match the documented
choices and the schema pattern used by decimate_mesh/generate_lods; retain the
existing description.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/GeodesicVoxelBind.cpp`:
- Around line 184-235: Update ddaSegment to clip the segment [a, b] against the
grid’s padded AABB before converting endpoints to cells or running DDA; handle
fully disjoint and degenerate segments appropriately, then run the existing
traversal using the clipped endpoints and recomputed direction/endpoint cells.
Add a regression test where one endpoint is far outside the grid and the other
is distant on the opposite/asymmetric side, verifying the intersected cells are
returned.
- Around line 253-285: Validate all vertex coordinates in vertexPositions and
bone coordinate data in bones for finiteness before AABB/grid arithmetic, using
std::isfinite and returning an appropriate res.error for invalid input. Apply
the same validation to the later bone-processing path around the referenced
logic, ensuring no non-finite values reach ceil, floor, integer conversion, or
allocation.

In `@src/SkinWeights.cpp`:
- Around line 470-473: Check and store the boolean return value from
SkinWeights::computeWeights at this call site, and return or handle failure
before setting report.applied or reporting success; follow the existing
failure-handling pattern around lines 198–199 so failed computation cannot
produce an applied report with empty weights.

In `@src/SkinWeightsPost.cpp`:
- Around line 45-55: Renormalize the retained weights in toVertexWeights after
sorting and truncating r to maxK, dividing each remaining weight by their sum
before populating vw; preserve safe handling for an empty or zero-sum row. Add a
test covering laplacianSmooth propagation to more than eight bones and verify
the resulting vertex weights sum to one.

---

Outside diff comments:
In `@src/SkinWeightsController.cpp`:
- Around line 111-129: Clamp smoothIterations to the supported range before
assigning it to SkinWeightsOptions, ensuring the value passed through the
Laplacian post-pass cannot exceed intended limits. Validate algorithm before
calling algorithmFromString in the controller method: reject unknown or
unsupported strings with an error and return, rather than allowing the fallback
to geodesic-voxel; only add the breadcrumb and continue once validation
succeeds.

---

Nitpick comments:
In `@qml/PropertiesPanel.qml`:
- Around line 2255-2264: Update the button’s ToolTip.text near the auto-generate
weights control to remove the outdated “via inverse-distance to bone segments”
description and accurately describe Geodesic Voxel binding as volume-aware with
no cross-limb bleed, matching the Text content in the surrounding
PropertiesPanel UI.

In `@qml/SkinWeightsDialog.qml`:
- Around line 284-302: Align the smoothing-iterations UI limit with the
backend-supported range by changing the maxValue in the InspectorNumberField for
dialog.smoothIterations from 20 to 50. Keep the existing integer handling and
default behavior unchanged.

In `@src/MCPServer.cpp`:
- Around line 7968-7973: Add an "enum" array to the "algo" property schema in
the relevant MCP tool definition, listing "geodesic-voxel", "inverse-distance",
and "unirig" to match the documented choices and the schema pattern used by
decimate_mesh/generate_lods; retain the existing description.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d79cd35d-b3b4-4171-a907-34e607d43c32

📥 Commits

Reviewing files that changed from the base of the PR and between c30bacb and b1da54b.

📒 Files selected for processing (19)
  • CLAUDE.md
  • qml/PropertiesPanel.qml
  • qml/SkinWeightsDialog.qml
  • src/CLIPipeline.cpp
  • src/CMakeLists.txt
  • src/GeodesicVoxelBind.cpp
  • src/GeodesicVoxelBind.h
  • src/GeodesicVoxelBind_test.cpp
  • src/MCPServer.cpp
  • src/SkinWeights.cpp
  • src/SkinWeights.h
  • src/SkinWeightsController.cpp
  • src/SkinWeightsController.h
  • src/SkinWeightsPost.cpp
  • src/SkinWeightsPost.h
  • src/SkinWeightsPost_test.cpp
  • src/SkinWeights_test.cpp
  • src/commands/ComputeSkinWeightsCommand.cpp
  • src/commands/ComputeSkinWeightsCommand.h

Comment thread src/GeodesicVoxelBind.cpp Outdated
Comment thread src/GeodesicVoxelBind.cpp
Comment thread src/SkinWeights.cpp Outdated
Comment thread src/SkinWeightsPost.cpp
- tests/CMakeLists.txt: add GeodesicVoxelBind.cpp + SkinWeightsPost.cpp
  to qtmesh_test_common (Linux per-suite test build failed to link)
- GeodesicVoxelBind: slab-clip bone segments to the grid AABB before
  the DDA walk — a bone with endpoints far outside the grid could
  exhaust the step budget and lose its seeds despite intersecting the
  mesh (regression test: ±1000-unit endpoints through the box)
- GeodesicVoxelBind: reject non-finite vertex AABBs (int conversion of
  NaN/Inf is UB) and skip non-finite bones as seedless (tested)
- SkinWeights::applyToEntity: check the computeWeights return value
  instead of reporting success with zero assignments
- SkinWeightsPost: renormalize after truncating a >8-bone smoothed
  row (hub-vertex fan test: 11 bones → 8, sum stays 1)
- SkinWeightsController: validate the algorithm string (reject typos
  instead of silently defaulting) and clamp voxelResolution /
  smoothIterations to the CLI/MCP ranges
- Nitpicks: GUI smoothing cap 20→50 to match backend, stale
  "inverse-distance" tooltip in PropertiesPanel, JSON-schema enum on
  the MCP algo param

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fernandotonon

Copy link
Copy Markdown
Owner Author

Addressed all review feedback in 5062d33:

Actionable:

  • DDA grid clipping (GeodesicVoxelBind.cpp): bone segments are now slab-clipped to the padded grid AABB before the DDA walk, so distant endpoints can't exhaust the step budget — added the asymmetric distant-endpoint regression test (±1000-unit endpoints through the box → seeds, full weight)
  • Non-finite input (GeodesicVoxelBind.cpp): NaN/Inf vertex AABBs are rejected before grid arithmetic (→ inverse-distance fallback); non-finite bones are skipped and reported in bonesWithoutSeeds. Both tested.
  • computeWeights return check (SkinWeights.cpp): failure now returns an error report instead of applied=true with zero assignments
  • Renormalize after truncation (SkinWeightsPost.cpp): toVertexWeights renormalizes when cutting a >8-bone row; added the 10-spoke hub-vertex fan test (11 bones → 8, sum stays 1)
  • Controller validation (SkinWeightsController.cpp): unknown algorithm strings are rejected with an error (no silent default); voxelResolution/smoothIterations clamped to the CLI/MCP ranges

Nitpicks: GUI smoothing cap raised 20→50, stale "inverse-distance" tooltip in PropertiesPanel updated, MCP algo property now declares a JSON-schema enum.

Also fixed the unit-tests-linux failure: the new GeodesicVoxelBind.cpp/SkinWeightsPost.cpp sources were missing from tests/CMakeLists.txt's qtmesh_test_common list (link error).

🤖 Generated with Claude Code

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant