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
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)
_copy1,_copy2)DuplicateCommand2. Drag-and-Drop Node Reparenting
ReparentCommand3. Transform Snapping
4. Pivot Point Editing
5. Node Grouping / Folders
6. Sub-Mesh Selection & Transform
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.
Entity::_getBuffersMarkedForAnimation())export_posewith args:entity,animation,time/frame,output_path,formatTechnical notes:
entity->addSoftwareAnimationRequest(false)to get CPU-side deformed positions, then read fromentity->getMesh()->getSubMesh(i)->vertexDataor the shared vertex data after_updateAnimation()Ogre::Mesh, save viaMeshImporterExporteranimState->setTimePosition(frame / fps)→entity->_updateAnimation()→ export--count N: compute time positions asi * duration / (N - 1)for i in 0..N-1--step S: export frames at indices 0, S, 2S, 3S, ... up to total frame count8. 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.
--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)--animation "Name") or all animations in the fileTechnical notes:
NodeAnimationTrack::getKeyFrame(i)provides access to individual keyframesNodeAnimationTrack::getInterpolatedKeyFrame(time, kf)evaluates the curve at any time — use this for resamplingAnimationwith fewer keyframes, copy interpolated values, replace the original9. Undo Coverage Expansion
DuplicateCommand— stores cloned node reference for undo (destroy) / redo (recreate)ReparentCommand— stores old parent + local transform for undoMaterialAssignCommand— stores old material name per sub-entityTechnical Notes
Ogre::HardwareVertexBufferdirectly — need to lock, transform vertices, unlock, and recalculate boundsOgre::SceneNode::removeChild()+addChild(), recalculate local transform to preserve world positionTransformOperator::mouseMoveEvent(), round to nearest grid incrementUndoManager::getSingleton()->push()Acceptance Criteria
qtmesh pose --count 4exports 4 evenly-spaced posesqtmesh anim --resample 30reduces keyframes with interpolationqtmesh anim --decimate-step 5keeps every 5th keyframeqtmesh poseexports single frames and batch framesexport_poseworks from AI chat ("export this pose as STL")duplicate_entity,reparent_node,set_snap_settings,export_pose