Skip to content

Epic: Phase 1 — Scene Editing Power Tools #256

Description

@fernandotonon

Overview

Unlock the day-to-day editing workflows that indie devs need most. The scene tree and transform system exist but lack fundamental operations like duplicate, reparent, snap, and sub-mesh editing. Also includes pose baking for 3D printing and animation keyframe resampling.

Scope

1. Duplicate Objects (Ctrl+D)

  • Deep-clone selected scene nodes (mesh, material assignments, transforms)
  • Duplicated node gets a unique name suffix (_copy1, _copy2)
  • New node is auto-selected after duplication
  • Works with multi-selection
  • Undo support via DuplicateCommand

2. Drag-and-Drop Node Reparenting

  • Scene tree supports drag-and-drop to reparent nodes
  • Visual drop indicator (insert above/below/as child)
  • Reparenting preserves world-space transform (recalculates local transform)
  • Undo support via ReparentCommand
  • Prevent invalid reparenting (node into its own subtree)

3. Transform Snapping

  • Grid snapping for translation (configurable grid size: 0.1, 0.25, 0.5, 1.0, etc.)
  • Angle snapping for rotation (5°, 15°, 45°, 90° increments)
  • Scale snapping (0.1, 0.25, 0.5 increments)
  • Toggle via toolbar button or hold Ctrl during drag
  • Visual grid overlay updates to match snap settings
  • Snap settings persisted in QSettings

4. Pivot Point Editing

  • Pivot modes: Center, Bottom, Origin, Custom
  • Toolbar dropdown or keyboard shortcut to cycle modes
  • Custom pivot: click-to-place in viewport
  • Gizmo renders at the active pivot point
  • Rotation and scale operate around the pivot

5. Node Grouping / Folders

  • Create empty group node (Ctrl+G on selection)
  • Groups appear as folders in scene tree with expand/collapse
  • Transform a group → transforms all children
  • Ungroup (Ctrl+Shift+G) moves children to parent, deletes empty group
  • Groups can be nested

6. Sub-Mesh Selection & Transform

  • Click a sub-mesh in the scene tree or viewport to select it
  • Transform gizmo operates on the sub-mesh (modifies vertex positions in the mesh buffer)
  • Translate, rotate, scale sub-meshes independently
  • Visual highlight of the selected sub-mesh (wireframe overlay or color tint)
  • Changes are reflected in the Ogre mesh data (exportable)
  • Undo support

7. Export Current Pose (Bake Animation Frame)

Use case: 3D printing animated models. A user downloads a Mixamo animation, scrubs to a dynamic pose (mid-jump, sword swing), and exports a static mesh in that exact pose — no Blender required.

  • GUI: "Export Current Pose" button in the Animation panel (or File → Export Pose)
  • Scrub animation to desired frame → click Export → static mesh with deformed vertex positions
  • Reads software-skinned vertex positions from Ogre (Entity::_getBuffersMarkedForAnimation())
  • Exports as a new mesh with NO skeleton, NO animation — just the posed geometry
  • Supported formats: STL (3D printing), OBJ, glTF, FBX
  • CLI:
    # Export a single frame
    qtmesh pose model.fbx --animation "Walking" --time 0.5 -o posed.stl
    
    # Export a specific frame number (at 30fps)
    qtmesh pose model.fbx --animation "Jump" --frame 15 -o jump_pose.obj
    
    # Export N evenly-spaced poses (e.g. 4 poses = 0%, 33%, 66%, 100% of animation)
    qtmesh pose model.fbx --animation "Dance" --count 4 -o pose_%02d.stl
    
    # Export all frames with step (every 10th frame)
    qtmesh pose model.fbx --animation "Dance" --all-frames --step 10 -o frame_%03d.stl
    
    # Export all frames
    qtmesh pose model.fbx --animation "Dance" --all-frames --fps 30 -o frames/frame_%03d.stl
  • MCP tool: export_pose with args: entity, animation, time/frame, output_path, format

Technical notes:

  • Ogre software skinning: call entity->addSoftwareAnimationRequest(false) to get CPU-side deformed positions, then read from entity->getMesh()->getSubMesh(i)->vertexData or the shared vertex data after _updateAnimation()
  • For each submesh: lock the position buffer, copy transformed vertices into a new Ogre::Mesh, save via MeshImporterExporter
  • STL export: triangulate + write binary STL (80-byte header + triangle normals + vertices)
  • Batch frames: iterate animState->setTimePosition(frame / fps)entity->_updateAnimation() → export
  • --count N: compute time positions as i * duration / (N - 1) for i in 0..N-1
  • --step S: export frames at indices 0, S, 2S, 3S, ... up to total frame count

8. Animation Keyframe Resampling

Use case: Mixamo animations often have 1 keyframe per frame (200+ keyframes for a 6-second clip at 30fps). Game engines don't need that density — resampling to 30-60 keyframes with interpolation cuts file size dramatically while preserving visual quality.

  • CLI:
    # Resample to exactly N keyframes (evenly spaced, re-interpolated)
    qtmesh anim model.fbx --resample 30 -o optimized.fbx
    
    # Keep every Nth keyframe, discard the rest
    qtmesh anim model.fbx --decimate-step 5 -o lighter.fbx
    
    # Adaptive decimation: remove keyframes below an error threshold
    qtmesh anim model.fbx --decimate-threshold 0.01 -o clean.fbx
  • GUI: Slider in Animation panel: "Keyframe density" with preview of keyframe count reduction
  • How it works:
    • --resample N: evaluate the original animation at N evenly-spaced times using Ogre's interpolation, write new keyframes at those times
    • --decimate-step S: iterate keyframes, keep every Sth, remove the rest
    • --decimate-threshold T: iteratively remove the keyframe with the smallest interpolation error until all remaining keyframes have error > T (greedy approach)
  • Works per-animation (specify with --animation "Name") or all animations in the file
  • Preserves bone hierarchy, just reduces keyframe count per track

Technical notes:

  • Ogre NodeAnimationTrack::getKeyFrame(i) provides access to individual keyframes
  • NodeAnimationTrack::getInterpolatedKeyFrame(time, kf) evaluates the curve at any time — use this for resampling
  • Create a new Animation with fewer keyframes, copy interpolated values, replace the original
  • Error metric for adaptive decimation: remove keyframe K, evaluate at K's time using neighbors, measure position + rotation delta

9. Undo Coverage Expansion

  • DuplicateCommand — stores cloned node reference for undo (destroy) / redo (recreate)
  • ReparentCommand — stores old parent + local transform for undo
  • MaterialAssignCommand — stores old material name per sub-entity
  • Undo history panel (QML) showing command names with click-to-jump

Technical Notes

  • Sub-mesh transform modifies Ogre::HardwareVertexBuffer directly — need to lock, transform vertices, unlock, and recalculate bounds
  • Reparenting: use Ogre::SceneNode::removeChild() + addChild(), recalculate local transform to preserve world position
  • Snapping: intercept in TransformOperator::mouseMoveEvent(), round to nearest grid increment
  • All new commands should integrate with UndoManager::getSingleton()->push()

Acceptance Criteria

  • Ctrl+D duplicates selected objects with unique names
  • Drag-and-drop reparenting in scene tree preserves world transform
  • Grid/angle snapping works during gizmo drag with visual feedback
  • Sub-mesh can be selected and transformed independently
  • Export Current Pose produces a static mesh matching the viewport pose
  • qtmesh pose --count 4 exports 4 evenly-spaced poses
  • qtmesh anim --resample 30 reduces keyframes with interpolation
  • qtmesh anim --decimate-step 5 keeps every 5th keyframe
  • CLI qtmesh pose exports single frames and batch frames
  • MCP tool export_pose works from AI chat ("export this pose as STL")
  • All operations are undoable
  • MCP tools added: duplicate_entity, reparent_node, set_snap_settings, export_pose

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions