Skip to content

Epic: SceneOps — Scene tree & submesh hierarchy operations #623

Description

@fernandotonon

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:

  • 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".
    • removeSubMesh(mesh, subIndex) — workaround for Ogre 14.5's missing removeSubMesh: 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).
  • 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_INVOKABLE setSubMeshVisible(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.

Acceptance Criteria (epic-level)

  • 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.
  • User can drag an entity (mesh) to a different scene node; transforms preserved in "keep world" or "keep local" mode (matching Skel: Slice B — Bone hierarchy editing (reparent / detach / attach / split / connect) #556's convention).
  • Every operation is undoable via UndoManager.
  • Save → close → load round-trips all hierarchy changes through the project file and glTF.
  • CLI: 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.
  • 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

Out of scope

  • 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.

Notes for implementers

  • Ogre 14.5 has no removeSubMesh. Slice E (SceneOps: Slice E — Remove submesh (rebuild-the-mesh workaround) #628) rebuilds the Ogre::Mesh with one fewer submesh, then re-creates the affected Ogre::Entity (or rebinds via Entity::_setMesh if 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.
  • 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions