Search code examples
qtqmlqt3d

How to create Undo/Redo operations in Qt3D?


I created some entities using qt3d in QML. For example, this code shows a Scene3D element that declares RootEntity which is another QML element that contains the scene graph:

Scene3D
{
    id : scene3d
    anchors.fill: parent
    focus: true
    aspects: ["render", "logic", "input"]
    hoverEnabled: true
    cameraAspectRatioMode: Scene3D.AutomaticAspectRatio


    antialiasing: true

    RootEntity
    {
        id:root
    }

}

RootEntity.qml:

Entity {
id:root

property double x : 0.0


Camera {
    id: mainCamera
    projectionType: CameraLens.PerspectiveProjection
    fieldOfView: 45
    aspectRatio: 16/9
    nearPlane : 0.1
    farPlane : 1000.0
    position: Qt.vector3d(0.0, 4.49373, -3.78577)
    upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
    viewCenter: Qt.vector3d(0.0, 0.5, 0.0)

}

OrbitCameraController
{
    id: mainCameraController
    camera: mainCamera
}

components: [
    RenderSettings {

        Viewport {
            normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
            RenderSurfaceSelector {
                CameraSelector {
                    id: cameraSelector
                    camera: mainCamera
                    FrustumCulling {
                        ClearBuffers {
                            buffers: ClearBuffers.AllBuffers
                            clearColor: "#444449"
                            NoDraw {}
                        }
                        LayerFilter {
                            filterMode: LayerFilter.DiscardAnyMatchingLayers
                            layers: [topLayer]
                        }
                        LayerFilter {
                            filterMode: LayerFilter.AcceptAnyMatchingLayers
                            layers: [topLayer]
                            ClearBuffers {
                                buffers: ClearBuffers.DepthBuffer
                            }
                        }
                    }
                }
            }
        }      
    },
    InputSettings {}
]

Layer {
    id: topLayer
    recursive: true
}

ListModel {
    id: entityModel
    ListElement { x:0;y:0;z:0 }
}

NodeInstantiator
{
    id:instance

    model: entityModel

    delegate: Entity {
        id: sphereEntity
        components: [
            SphereMesh
            {
                id:sphereMesh
                radius: 0.3
            },

            PhongMaterial
            {
                id: materialSphere
                ambient:"red"
            },

            Transform {
                id: transform
                translation:Qt.vector3d(x, y, z)
            }
        ]
    }
}

MouseDevice
{
    id: mouseDev
}

MouseHandler
{
    id: mouseHandler
    sourceDevice: mouseDev

    onPressed:
    {
        x++;
        entityModel.append({"x":x,"y":0.0,"z": Math.random()})
    }
}
}

Output screenshot

When the mouse is clicked in my Scene3D, one sphere is displayed.

I don't know how to delete a specific Entity or create undo/redo effect by hitting Ctrl+Z and Ctrl+Shift+Z in Qt3d. Thanks.


Solution

  • One approach is to maintain a global list of Qt.vector3d elements and use it to record the position of the spheres that are removed with the "Undo" operation:

    • When the user hits CTRL+Z, create a new Qt.vector3d object to store the position of the last sphere rendered (that is, the one that was last appended to entityModel) and add that position to the global list of 3d vectors;
    • Then, to remove a sphere from the screen, call entityModel.remove() with the index of the sphere that needs to be erased;

    The "Redo" operation simply does the opposite:

    • When the user hits CTRL+Y, the last element of the global list of 3d vectors holds the location of the lastest sphere removed: append this position to entityModel so the sphere can be rendered again;
    • Then, remember to erase this position from the global list so the next Undo operation can render a different sphere;

    RootEntity.qml:

    import QtQuick 2.0
    
    import QtQml.Models 2.15
    
    import Qt3D.Core 2.12
    import Qt3D.Render 2.12
    import Qt3D.Extras 2.12
    import Qt3D.Input 2.12
    
    Entity {
        id: root
    
        // global list of Qt.vector3d elements that store the location of the spheres that are removed
        property variant removedSpheres : []
    
        // x-coordinate of the next sphere that will be added
        property double x : 0.0
    
        Camera {
            id: mainCamera
            projectionType: CameraLens.PerspectiveProjection
            fieldOfView: 45
            aspectRatio: 16/9
            nearPlane : 0.1
            farPlane : 1000.0
            position: Qt.vector3d(0.0, 4.49373, -3.78577)
            upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
            viewCenter: Qt.vector3d(0.0, 0.5, 0.0)
        }
    
        OrbitCameraController {
            id: mainCameraController
            camera: mainCamera
        }
    
        components: [
            RenderSettings {
    
                Viewport {
                    normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
                    RenderSurfaceSelector {
                        CameraSelector {
                            id: cameraSelector
                            camera: mainCamera
                            FrustumCulling {
                                ClearBuffers {
                                    buffers: ClearBuffers.AllBuffers
                                    clearColor: "#444449"
                                    NoDraw {}
                                }
                                LayerFilter {
                                    filterMode: LayerFilter.DiscardAnyMatchingLayers
                                    layers: [topLayer]
                                }
                                LayerFilter {
                                    filterMode: LayerFilter.AcceptAnyMatchingLayers
                                    layers: [topLayer]
                                    ClearBuffers {
                                        buffers: ClearBuffers.DepthBuffer
                                    }
                                }
                            }
                        }
                    }
                }
            },
            InputSettings {}
        ]
    
        Layer {
            id: topLayer
            recursive: true
        }
    
        ListModel {
            id: entityModel
    
            ListElement { x: 0; y: 0; z: 0 }
        }
    
        NodeInstantiator {
            id: instance
    
            model: entityModel
    
            delegate: Entity {
                id: sphereEntity
    
                components: [
                    SphereMesh { id:sphereMesh; radius: 0.3 },
    
                    PhongMaterial { id: materialSphere; ambient:"red" },
    
                    Transform { id: transform; translation:Qt.vector3d(x, y, z) }
                ]
            }
        }
    
        MouseDevice {
            id: mouseDev
        }
    
        MouseHandler {
            id: mouseHandler
            sourceDevice: mouseDev
    
            onPressed:
            {
                if (mouse.button === Qt.LeftButton)
                {
                    console.log("LeftButton: new sphere")
    
                    // add new sphere
                    entityModel.append( {"x" : ++root.x, "y" : 0.0, "z" : Math.random()} )
                }
    
                if (mouse.button === Qt.MiddleButton)
                {
                    console.log("MiddleButton: clear spheres")
    
                    // removes all spheres (can't be undone)
                    root.x = 0;
                    entityModel.clear();
                    removedSpheres.length = 0;
                }
            }
        }
    
        KeyboardDevice {
            id: keyboardDev
        }
    
        KeyboardHandler {
            id: keyboardHandler
            sourceDevice: keyboardDev
            focus: true
    
            onPressed: {
                // handle CTRL+Z: undo
                if (event.key === Qt.Key_Z && (event.modifiers & Qt.ControlModifier))
                {
                    console.log("CTRL+Z")
    
                    // remove the last sphere added to the screen
                    let lastIdx = entityModel.count - 1;
                    if (lastIdx >= 0)
                    {
                        // save sphere position before removal
                        removedSpheres.push(Qt.vector3d(entityModel.get(lastIdx).x, entityModel.get(lastIdx).y, entityModel.get(lastIdx).z));
    
                        // remove sphere from the model
                        entityModel.remove(lastIdx);
                    }
                }
    
                // handle CTRL+Y: redo
                if (event.key === Qt.Key_Y && (event.modifiers & Qt.ControlModifier))
                {
                    console.log("CTRL+Y")
    
                    // add the last sphere removed back into the model
                    if (removedSpheres.length > 0)
                    {
                        // add the sphere
                        let lastIdx = removedSpheres.length - 1;
                        entityModel.append( {"x" : removedSpheres[lastIdx].x, "y" : removedSpheres[lastIdx].y, "z" : removedSpheres[lastIdx].z} )
    
                        // erase the last item added to removedSpheres
                        removedSpheres.pop()
                    }
                }
            }
        }
    
    }