You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Turn QtMeshEditor's scene tree from a viewer into an editor for hierarchy and submesh structure. Today the user can see the Node → Entity → SubEntity tree, select items, and reparent scene nodes (SceneTreeModel::reparentNode), but they can't:
Move a submesh from one mesh to another mesh.
Move (or extract) a mesh into a different scene node.
Join multiple submeshes into one.
Split a submesh into multiple parts (by material, by selection, by connected component).
Toggle individual submesh visibility (the painter / animator workflow the user explicitly asked about — "hide the head submesh while painting the body").
Once those are in place, the scene tree becomes a real authoring surface: a user importing a humanoid as a single fused mesh can split it into head / torso / arms / legs submeshes for separate material work, hide the head while painting the body, and re-join when finished. Importing an asset as multiple submeshes per body part? Merge them into one submesh for engines that prefer fewer draw calls.
GUI + CLI + MCP parity, undo through UndoManager, Sentry breadcrumbs (scene.tree.* / mesh.submesh.*), and the slice-PR cadence used by every recent epic.
Why
Imported assets rarely match the user's submesh preferences. Mixamo characters fuse body parts; Sketchfab assets often arrive as one giant submesh; Houdini exports may have one submesh per piece. Today the only fix is round-tripping through Blender.
Submesh visibility is essential for non-trivial paint/edit work. Painting the torso of a character with the head in the way is awful. Animating a finger when the rest of the hand obscures it is awful. Today: no hide-toggle on submeshes; the user lives with the obstruction or hand-rewrites the material to be transparent.
Manager::groupNodes / ungroupNode proved the pattern at the scene-node level. This epic extends the same vocabulary down to the submesh level and gives the existing scene-node ops a proper editing surface.
Foundations exist: SceneTreeModel::reparentNode + canReparent already work for nodes, MeshImporterExporter already knows how to build a SubMesh from raw geometry (MeshImporterExporter.cpp:1142), and EditableMesh::subMeshCount() exists. The missing piece is the mutation surface and its UI.
Architecture
A new singleton, SubMeshOps (src/SubMeshOps.{h,cpp}):
Submesh-level operations — pure static helpers that operate on Ogre::Mesh + EditableMesh:
moveSubMesh(sourceMesh, subIndex, targetMesh) — move one submesh from mesh A to mesh B. Reconciles vertex declarations (re-encodes if needed), copies bone assignments (rejected if target's skeleton is incompatible — surfaced as a confirmation prompt in the GUI), updates Mesh::SubMeshNameMap.
joinSubMeshes(mesh, indices) — merge N submeshes into one. Concatenates index buffers, unifies vertex buffers (de-duplicating by epsilon), picks one material (with conflict UI), preserves bone assignments.
splitSubMeshByMaterial(mesh, subIndex) — split a submesh that has multiple material-tagged faces into one submesh per material (no-op if all faces share a material).
splitSubMeshBySelection(mesh, subIndex, faceIndices) — extract the selected faces into a new submesh; pairs with EditModeController's face-selection.
splitSubMeshByConnectedComponent(mesh, subIndex) — flood-fill connected components, one submesh per island. Useful for "body parts that happen to be modelled as separate islands but baked into one submesh".
Mesh-level reparent under a different scene node — extends SceneTreeModel::reparentNode to handle entities, not just nodes.
Submesh visibility wraps SubEntity::setVisible(bool) — purely per-instance state, not a mesh-level edit (no save-to-disk side effects).
Every mutation goes through new commands in src/commands/SubMeshCommands.{h,cpp}:
MoveSubMeshCommand, JoinSubMeshesCommand, SplitSubMeshCommand, RemoveSubMeshCommand, MoveEntityCommand. Each preserves enough state to round-trip the operation through UndoManager.
SceneTreeModel is extended to:
Recognize submeshes as drag-and-drop sources (already addresses them via materialName setter — extend to permit reparent within the tree).
Validate drops via a new canMoveSubMesh(sourceMaterial, targetMesh).
Expose Q_INVOKABLEsetSubMeshVisible(row, parentIndex, bool), joinSubMeshes(indices), etc.
The QML scene tree (qml/SceneTreeNode.qml) gets:
An eye icon per submesh row for the visibility toggle.
Right-click context menu with Move / Join / Split / Remove + the existing material picker.
Multi-select aware: Join applies to the selected submeshes; Split applies to one submesh at a time.
Child Issues
Slices A–B are the user's explicit asks (visibility + move). C–E cover the join/split/remove operations. F handles entity-level reparent + the existing node-group/ungroup UX surfacing. G is CLI/MCP/docs.
Each submesh row in the scene tree has an eye icon; clicking toggles SubEntity::setVisible and the geometry hides/shows in the viewport.
User can drag a submesh from one mesh's tree entry onto another mesh's entry; the submesh moves with material, bone assignments, and vertex data preserved.
User can select multiple submeshes and Join → one merged submesh remains with concatenated geometry.
User can Split a submesh three ways: by material (auto), by face selection (manual), by connected component (auto).
User can Remove a submesh from a mesh; the mesh is rebuilt without the deleted submesh and the entity continues to render correctly.
MCP tools mirror the CLI surface plus the visibility toggle.
Sentry breadcrumbs scene.tree.* and mesh.submesh.* per action.
No regression in Manager::groupNodes/ungroupNode or SceneTreeModel::reparentNode.
CLAUDE.md "Scene Tree & SubMesh Operations" section under Architecture.
Dependencies & related issues
Manager::groupNodes / ungroupNode (src/Manager.cpp:397/467) — already shipped for scene nodes. This epic does the analogous work at the submesh level and tightens the entity-reparent surface.
Epic: UV — UV Map editing #458 UV Editor — editing UVs is much easier when the user can isolate submeshes for view; the new visibility toggle feeds the UV panel's "show only selected submesh" mode.
Mesh-level booleans (union / intersection / subtract between meshes). Different epic; CSG is its own engineering line.
Submesh-level booleans. Same reason.
LOD-aware submesh ops (cascading changes through all LOD chains). Note the constraint, document the limitation; future stretch.
Animation track migration on entity move. Animations follow the mesh; moving the entity to a different scene node doesn't try to reassign clips to a different rig. (Cross-rig retargeting is Anim: Slice F — Animation retargeting between skeletons #523.)
Mesh-instance sharing (one mesh used by many entities, edited in one place). Out of scope.
Network-based or multi-user scene editing. Not in scope.
Vertex declaration reconciliation is the biggest hidden cost in moveSubMesh (Slice B / SceneOps: Slice B — Move submesh between meshes #625) and joinSubMeshes (Slice C / SceneOps: Slice C — Join (merge) selected submeshes into one #626). Submeshes from different meshes can have different vertex formats (some have tangents, some don't; some have UV1, some don't). The merge strategy: re-encode the smaller-declaration submesh into the larger declaration, zero-fill missing channels.
Bone assignment preservation across moveSubMesh: if the source and target meshes share the same skeleton (by name), reuse handles; otherwise reject the move with a clear error and a "convert to static mesh" fallback option.
Visibility (Slice A / SceneOps: Slice A — SubMesh visibility toggle #624) is per-SubEntity, not per-SubMesh. Two entities sharing the same mesh have independent visibility. Persist visibility in the project file but not in the mesh on export — visibility is an in-scene authoring affordance.
Material handling in Join: if the joined submeshes use different materials, present a one-time choice (pick winning material, or invoke SubMeshOps::splitByMaterial later to re-separate).
Every slice ships as one PR with screenshots, per the project convention; tests headless-CI safe.
Overview
Turn QtMeshEditor's scene tree from a viewer into an editor for hierarchy and submesh structure. Today the user can see the Node → Entity → SubEntity tree, select items, and reparent scene nodes (
SceneTreeModel::reparentNode), but they can't:Once those are in place, the scene tree becomes a real authoring surface: a user importing a humanoid as a single fused mesh can split it into
head / torso / arms / legssubmeshes for separate material work, hide the head while painting the body, and re-join when finished. Importing an asset as multiple submeshes per body part? Merge them into one submesh for engines that prefer fewer draw calls.GUI + CLI + MCP parity, undo through
UndoManager, Sentry breadcrumbs (scene.tree.*/mesh.submesh.*), and the slice-PR cadence used by every recent epic.Why
Manager::groupNodes/ungroupNodeproved the pattern at the scene-node level. This epic extends the same vocabulary down to the submesh level and gives the existing scene-node ops a proper editing surface.SceneTreeModel::reparentNode+canReparentalready work for nodes,MeshImporterExporteralready knows how to build aSubMeshfrom raw geometry (MeshImporterExporter.cpp:1142), andEditableMesh::subMeshCount()exists. The missing piece is the mutation surface and its UI.Architecture
A new singleton,
SubMeshOps(src/SubMeshOps.{h,cpp}):Ogre::Mesh+EditableMesh:moveSubMesh(sourceMesh, subIndex, targetMesh)— move one submesh from mesh A to mesh B. Reconciles vertex declarations (re-encodes if needed), copies bone assignments (rejected if target's skeleton is incompatible — surfaced as a confirmation prompt in the GUI), updatesMesh::SubMeshNameMap.joinSubMeshes(mesh, indices)— merge N submeshes into one. Concatenates index buffers, unifies vertex buffers (de-duplicating by epsilon), picks one material (with conflict UI), preserves bone assignments.splitSubMeshByMaterial(mesh, subIndex)— split a submesh that has multiple material-tagged faces into one submesh per material (no-op if all faces share a material).splitSubMeshBySelection(mesh, subIndex, faceIndices)— extract the selected faces into a new submesh; pairs with EditModeController's face-selection.splitSubMeshByConnectedComponent(mesh, subIndex)— flood-fill connected components, one submesh per island. Useful for "body parts that happen to be modelled as separate islands but baked into one submesh".removeSubMesh(mesh, subIndex)— workaround for Ogre 14.5's missingremoveSubMesh: rebuild the mesh with the deleted submesh excluded (same handle-rebuild gotcha as bone removal in Skel: Slice A — Bone CRUD (create / remove / rename / duplicate) #555).SceneTreeModel::reparentNodeto handle entities, not just nodes.SubEntity::setVisible(bool)— purely per-instance state, not a mesh-level edit (no save-to-disk side effects).Every mutation goes through new commands in
src/commands/SubMeshCommands.{h,cpp}:MoveSubMeshCommand,JoinSubMeshesCommand,SplitSubMeshCommand,RemoveSubMeshCommand,MoveEntityCommand. Each preserves enough state to round-trip the operation throughUndoManager.SceneTreeModelis extended to:materialNamesetter — extend to permit reparent within the tree).canMoveSubMesh(sourceMaterial, targetMesh).Q_INVOKABLEsetSubMeshVisible(row, parentIndex, bool),joinSubMeshes(indices), etc.The QML scene tree (
qml/SceneTreeNode.qml) gets:Child Issues
Slices A–B are the user's explicit asks (visibility + move). C–E cover the join/split/remove operations. F handles entity-level reparent + the existing node-group/ungroup UX surfacing. G is CLI/MCP/docs.
MeshRebuilderscaffold reused by Skel: Slice A — Bone CRUD (create / remove / rename / duplicate) #555)Acceptance Criteria (epic-level)
SubEntity::setVisibleand the geometry hides/shows in the viewport.UndoManager.qtmesh submesh model.fbx --move <subIndex> --to <targetMesh.fbx>,qtmesh submesh model.fbx --join 0,1,2 -o out.fbx,qtmesh submesh model.fbx --split <subIndex> --by material|selection|connected -o out.fbx,qtmesh submesh model.fbx --remove <subIndex> -o out.fbx.scene.tree.*andmesh.submesh.*per action.Manager::groupNodes/ungroupNodeorSceneTreeModel::reparentNode.CLAUDE.md"Scene Tree & SubMesh Operations" section under Architecture.Dependencies & related issues
Manager::groupNodes/ungroupNode(src/Manager.cpp:397/467) — already shipped for scene nodes. This epic does the analogous work at the submesh level and tightens the entity-reparent surface.SceneTreeModel::reparentNode(src/SceneTreeModel.h:74) — existing node-reparent API; Slice F (SceneOps: Slice F — Move entity to different scene node + improved node-reparent UX #629) extends it for entity-to-node moves.MeshRebuilderscaffold; Skel: Slice A — Bone CRUD (create / remove / rename / duplicate) #555 consumes it for bone removal.Out of scope
Notes for implementers
removeSubMesh. Slice E (SceneOps: Slice E — Remove submesh (rebuild-the-mesh workaround) #628) rebuilds theOgre::Meshwith one fewer submesh, then re-creates the affectedOgre::Entity(or rebinds viaEntity::_setMeshif available). Coordinate with Slice A of the Skeletal Rigging epic (Skel: Slice A — Bone CRUD (create / remove / rename / duplicate) #555) which faces the same gotcha for bone removal — share the rebuild scaffold.moveSubMesh(Slice B / SceneOps: Slice B — Move submesh between meshes #625) andjoinSubMeshes(Slice C / SceneOps: Slice C — Join (merge) selected submeshes into one #626). Submeshes from different meshes can have different vertex formats (some have tangents, some don't; some have UV1, some don't). The merge strategy: re-encode the smaller-declaration submesh into the larger declaration, zero-fill missing channels.moveSubMesh: if the source and target meshes share the same skeleton (by name), reuse handles; otherwise reject the move with a clear error and a "convert to static mesh" fallback option.SubMeshOps::splitByMateriallater to re-separate).