Skip to content

feat: auto-suggest wearable category from model geometry at import#3430

Merged
juanmahidalgo merged 3 commits into
masterfrom
feat/wearable-category-auto-suggest
Jun 30, 2026
Merged

feat: auto-suggest wearable category from model geometry at import#3430
juanmahidalgo merged 3 commits into
masterfrom
feat/wearable-category-auto-suggest

Conversation

@juanmahidalgo

Copy link
Copy Markdown
Contributor

Why

Analysis of 4,340 real wearable submissions shows wrong-category is a top curation-feedback reason (11.5% of collections). Curators routinely ask creators to fix the category (e.g. "Making this an upper body means it replaces the torso. Please change the category to Top head."). Today the category is a manual dropdown with no guidance, so a wrong pick means a full curation round-trip and re-upload.

This change auto-suggests the most likely wearable category from the model geometry at import time and pre-selects it (still fully overridable), removing a whole class of "wrong category → re-upload" loops.

Changes

  • Add a pure helper suggestWearableCategory(Three, scene) in src/lib/glbValidation/suggestWearableCategory.ts. It walks the GLTF scene's skinned meshes, tallies skin weight per avatar region using the skeleton bone names (Avatar_Head/Avatar_Neck → head, Avatar_Spine*/arms → spine, Avatar_*UpLeg/Avatar_*Leg → legs, Avatar_*Foot/Avatar_*ToeBase → feet, hands → hands), and returns a WearableCategory only when one region owns ≥60% of the classified weight. Returns null when ambiguous (e.g. full-body skins) so the creator picks manually.
    • Region → category mapping is intentionally conservative: head → hat (default head category, not over-fit to helmet/top_head), spine → upper_body, legs → lower_body, feet → feet, hands → hands_wear.
  • Thread the suggestion through loadAndValidateModelprocessModelAcceptedFileProps → modal state (mirrors the existing validationIssues flow). No GLTF re-load — reuses the one already loaded for validation.
  • Pre-select the suggested category in the reducer's SET_ACCEPTED_PROPS only when no category was already declared (bare .glb with no wearable.json), and only when it's part of the body-shape-filtered selectable list. Explicit categories (zip wearable.json, change-item-file, representation flows) are never overridden.
  • Show a subtle "Suggested from your model: …" hint under the category dropdown in CommonFields.
  • Add create_single_item_modal.suggested_category to en / es / zh.
  • Focused unit tests for the helper (head/torso/legs/feet/hands → expected category; ambiguous / no skin data / unknown bones → null).

Submission behavior is unchanged beyond the default selection + hint.

Test plan

  • suggestWearableCategory unit tests pass (9/9)
  • lint and build pass
  • Import a bare .glb skinned to the head → category defaults to hat, hint shown
  • Import a .glb skinned to legs → defaults to lower_body
  • Import a full-body skin → no suggestion, dropdown stays empty
  • Import a zip with wearable.json declaring a category → declared category wins, no override

@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
builder Ready Ready Preview, Comment Jun 29, 2026 3:30pm

Request Review

@coveralls

coveralls commented Jun 25, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28383377116

Coverage increased (+0.2%) to 52.184%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 6 uncovered changes across 1 file (51 of 57 lines covered, 89.47%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
src/lib/getModelData.ts 7 1 14.29%
Total (2 files) 57 51 89.47%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
src/lib/getModelData.ts 1 7.94%

Coverage Stats

Coverage Status
Relevant Lines: 12668
Covered Lines: 7228
Line Coverage: 57.06%
Relevant Branches: 5529
Covered Branches: 2268
Branch Coverage: 41.02%
Branches in Coverage %: Yes
Coverage Strength: 34.07 hits per line

💛 - Coveralls

@juanmahidalgo juanmahidalgo marked this pull request as ready for review June 25, 2026 18:39

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔄 Changes Requested — One defensive fix needed before merge

Overview

Auto-suggests the most likely wearable category from model geometry by tallying skin weight per avatar region. Clean heuristic with a 60% dominance threshold, conservative region→category mapping, and proper guards for ambiguous/multi-region geometry. i18n added for en/es/zh.

P1 — Should fix before merge

[P1] Missing try/catch around suggestWearableCategory in loadAndValidateModel
src/lib/getModelData.ts, ~line 369 — suggestWearableCategory is a non-critical enhancement, but if it throws (e.g., unexpected geometry attribute format on a malformed skeleton), it will propagate up and fail the entire model import. Since the suggestion is purely a UX convenience, it should never break the critical path:

try {
  suggestedCategory = suggestWearableCategory(Three, gltf.scene)
} catch {
  suggestedCategory = null
}

P2 — Minor improvements

[P2] forearm bones classified as spine (upper_body) instead of hands
src/lib/glbValidation/suggestWearableCategory.ts, classifyBone() — After normalization, Avatar_LeftForeArmforearm. The hands patterns (hand, finger, thumb) don't match, but spine patterns catch it via "forearm".includes("arm"). For most wearables (shirts, jackets) this is arguably correct since the forearm is part of the upper body. But for bracers/gauntlets, it would dilute the hands signal. Consider adding 'forearm' explicitly to spine patterns with a comment explaining the design choice, or to hands if bracers are the more common case.

[P2] Suggested category hint stays visible after user manually changes category
src/components/Modals/CreateSingleItemModal/CommonFields.tsx — The "Suggested from your model: …" hint renders whenever suggestedCategory is set, even after the user picks a different category. Consider hiding it when category !== suggestedCategory.

[P2] No test for Avatar_LeftForeArm / Avatar_RightForeArm classification
src/lib/glbValidation/suggestWearableCategory.spec.ts — Adding a test with forearm bones would document the current behavior and catch regressions if the classification changes.

[P2] No test for multiple SkinnedMesh nodes
The accumulation logic sums weight across all meshes, but all tests use a single mesh.

[P2] Redundant await import('three') in loadAndValidateModel
src/lib/getModelData.ts, line ~353 — loadGltf already imports Three.js internally. The module cache makes this free, but a comment explaining why it's needed (for suggestWearableCategory) would help readability.

CI Status

All checks passing (audit ✅, test ✅, Vercel ✅).

Verdict

The heuristic is well-designed and the implementation follows existing patterns cleanly. The try/catch fix is a one-liner that prevents a non-critical feature from breaking the import flow.


Reviewed by Jarvis 🤖 · Requested by Juan Manuel Hidalgo via Slack

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Approved — All findings addressed

Re-reviewed after changes. Every flagged issue has been fixed:

P1 resolved:

  • try/catch around suggestWearableCategory (src/lib/getModelData.ts) — ✅ Now wrapped; any throw falls back to null without breaking the import flow.

P2s resolved:

  • forearm classification — ✅ 'forearm' added explicitly to the spine patterns in LANDMARK_BONES, with a comment explaining the design choice (forearm = upper body for sleeved garments). New test confirms Avatar_LeftForeArm/Avatar_RightForeArmupper_body.
  • Stale suggestion hint — ✅ Hint now only renders when category === suggestedCategory; disappears as soon as the user picks a different category.
  • Multi-mesh test — ✅ Added test with two SkinnedMesh nodes whose accumulated spine weight crosses the threshold.
  • Redundant import('three') comment — ✅ Comment explains the import is cached and needed for suggestWearableCategory.

CI: all checks passing (audit ✅, test ✅, Vercel ✅).


Reviewed by Jarvis 🤖 · Requested by Juan Manuel Hidalgo via Slack

…ory-auto-suggest

# Conflicts:
#	src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.reducer.ts
#	src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.types.ts
@juanmahidalgo juanmahidalgo merged commit 0b7288d into master Jun 30, 2026
7 checks passed
@juanmahidalgo juanmahidalgo deleted the feat/wearable-category-auto-suggest branch June 30, 2026 07:57
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.

3 participants