How to create Undo/Redo operations in Qt3D? - qt

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()})
}
}
}
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.

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()
}
}
}
}
}

Related

QML need to create component with bool property all list elements were within limits

I was thinking I need a component similar to ListModel, but I need to extend it to expose a readonly bool property such as "all list elements were within minimum and maximum limit" so I can do logic outside the component the determine certain things. How should I go about doing this extending a boolean property based on model's contents?
I guess naive way is to just add the qml property and do javascript loop on QML side to check all model contents but that might not be so good performance
Have you considered DelegateModel? It allows you to create "views" on your ListModel so you can control what you want to be displayed via the filterOnGroup property.
It is rather difficult to comprehend, but, in the following example, I have a ListModel containing 5 cities. When you start changing the RangeSlider the 5 cities will be filtered based on the minimum/maximum population selected. This works by updating the boolean function filter on the DelegateModel to reflect the cities that are now visible.
property var filter: model => model.pop >= rangeSlider.first.value
&& model.pop <= rangeSlider.second.value
Here's the full code snippet:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
Page {
anchors.fill: parent
ColumnLayout {
anchors.fill: parent
Label { text: qsTr("States") }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: DelegateModel {
id: filterDelegateModel
property int updateIndex: 0
property var filter: model => model.pop >= rangeSlider.first.value
&& model.pop <= rangeSlider.second.value
onFilterChanged: Qt.callLater(update)
model: us_states
groups: [
DelegateModelGroup {
id: allItems
name: "all"
includeByDefault: true
onCountChanged: {
if (filterDelegateModel.updateIndex > allItems.count) filterDelegateModel.updateIndex = allItems.count;
if (filterDelegateModel.updateIndex < allItems.count) Qt.callLater(update, filterDelegateModel.updateIndex);
}
},
DelegateModelGroup {
id: visibleItems
name: "visible"
}]
filterOnGroup: "visible"
delegate: Frame {
id: frame
width: ListView.view.width - 20
background: Rectangle {
color: (frame.DelegateModel.visibleIndex & 1) ? "#f0f0f0" : "#e0e0e0"
border.color: "#c0c0c0"
}
RowLayout {
width: parent.width
Text {
text: (frame.DelegateModel.visibleIndex + 1)
color: "#808080"
}
Text {
Layout.fillWidth: true
text: model.state
}
Text {
text: qsTr("pop: %1 M").arg((pop / 1000000).toFixed(2))
}
}
}
function update(startIndex) {
startIndex = startIndex ?? 0;
if (startIndex < 0) startIndex = 0;
if (startIndex >= allItems.count) {
updateIndex = allItems.count;
return;
}
updateIndex = startIndex;
if (updateIndex === 0) {
allItems.setGroups(0, allItems.count, ["all"]);
}
for (let ts = Date.now(); updateIndex < allItems.count && Date.now() < ts + 50; updateIndex++) {
let visible = !filter || filter(allItems.get(filterDelegateModel.updateIndex).model);
if (!visible) continue;
allItems.setGroups(updateIndex, 1, ["all", "visible"]);
}
if (updateIndex < allItems.count) Qt.callLater(update, updateIndex);
}
Component.onCompleted: Qt.callLater(update)
}
}
Label { text: "Population Range" }
RangeSlider {
id: rangeSlider
Layout.fillWidth: true
from: 0
to: 100000000
first.value: 1
first.onMoved: Qt.callLater(filterDelegateModel.update)
second.value: 100000000
second.onMoved: Qt.callLater(filterDelegateModel.update)
stepSize: 1000000
}
Label { text: qsTr("Minimum %1 M").arg((rangeSlider.first.value / 1000000).toFixed(2)) }
Label { text: qsTr("Maximum %1 M").arg((rangeSlider.second.value / 1000000).toFixed(2)) }
}
ListModel {
id: us_states
ListElement { state:"California"; pop: 39350000 }
ListElement { state:"Texas"; pop: 28640000 }
ListElement { state:"New York"; pop: 8380000 }
ListElement { state:"Nevada"; pop: 3030000 }
ListElement { state:"Las Vegas"; pop: 644000 }
}
}
You can Try it Online!
I have refactored the above into a FilterDelegateModel reusable component. Feel free to check it out:
https://github.com/stephenquan/qt5-qml-toolkit
https://github.com/stephenquan/qt5-qml-toolkit/wiki/FilterDelegateModel

Qt3D Different Material for different RenderPasses

I have a QML Scene3D with 2 viewports, left and right half of window. The scene contains two cameras (one per viewport) and a torus mesh. I want to use a viewport-specific material for the same torus, i.e. different QMaterials (QEffects, QShaderPrograms, ...) when rendering in the left and right viewport respectively.
// main.qml
Entity {
id: rootEntity
components: [
MyFrameGraph {
camLeft: cameraLeft
camRight: cameraRight
},
InputSettings { }
]
Camera {
id: cameraLeft
projectionType: CameraLens.PerspectiveProjection
position: Qt.vector3d( 0.0, 0.0, -200.0 )
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
}
Camera {
id: cameraRight
projectionType: CameraLens.PerspectiveProjection
position: Qt.vector3d( 0.0, 200.0, 0.0 )
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
}
TorusMesh {
id: torusMesh
radius: 20
minorRadius: 5
}
Transform {
id:torusTransform
}
Material{
id: myTorusMaterial
effect: MyEffect{}
}
Entity {
id: torus
components: [torusMesh,torusTransform,myTorusMaterial]
}
}
My FrameGraph contains two branches for the two viewports, whereas the leaf nodes are RenderPassFilters.
//MyFrameGraph.qml
RenderSettings {
id: root
property Camera camLeft
property Camera camRight
activeFrameGraph: RenderSurfaceSelector {
id: surfaceSelector
Viewport {
id: mainViewport
normalizedRect: Qt.rect(0.0, 0.0, 1.0, 1.0)
ClearBuffers {
buffers: ClearBuffers.ColorDepthBuffer
clearColor: "black"
}
Viewport {
normalizedRect: Qt.rect(0.0, 0.0, 0.5, 1)
CameraSelector {
camera: root.camLeft
RenderStateSet {
renderStates: [
DepthTest {depthFunction: DepthTest.Less}
]
RenderPassFilter {
matchAny: [FilterKey { name: "pass"; value: "myMaterialLeft" }]
}
}
}
}
Viewport {
normalizedRect: Qt.rect(0.5, 0.0, 0.5, 1)
CameraSelector {
camera: root.camRight
RenderStateSet {
renderStates: [
DepthTest {depthFunction: DepthTest.Less}
]
RenderPassFilter {
matchAny: [FilterKey { name: "pass"; value: "myMaterialRight" }]
}
}
}
}
}
}
}
In my custom Material/Effects I use FilterKeys in the RenderPass object.
//MyEffect.qml
Effect {
id: root
property color maincolor: Qt.rgba(0.0, 1.0, 0.0, 1.0)
ShaderProgram {
id: myShaderLeft
vertexShaderCode: loadSource("qrc:/shader/myShaderLeft.vert")
fragmentShaderCode: loadSource("qrc:/shader/myShaderLeft.frag")
}
ShaderProgram {
id: myShaderRight
vertexShaderCode: loadSource("qrc:/shader/myShaderRight.vert")
fragmentShaderCode: loadSource("qrc:/shader/myShaderRight.frag")
}
parameters: [
Parameter {
name: "maincolor"
value: Qt.vector3d(root.maincolor.r, root.maincolor.g, root.maincolor.b)
}
]
techniques: [
Technique {
filterKeys: [FilterKey { name: "renderingStyle"; value: "forward" }]
graphicsApiFilter {
api: GraphicsApiFilter.OpenGL
profile: GraphicsApiFilter.CoreProfile
majorVersion: 3
minorVersion: 1
}
RenderPass {
id:renderPassLeft
filterKeys: [ FilterKey { name: "pass"; value: "myMaterialLeft" } ]
shaderProgram: myShaderLeft
renderStates: [CullFace { mode: CullFace.NoCulling }]
}
RenderPass {
id: renderPassRight
filterKeys: [ FilterKey { name: "pass"; value: "myMaterialRight" } ]
shaderProgram: myShaderRight
renderStates: [CullFace { mode: CullFace.NoCulling }]
}
renderPasses:[renderPassLeft,renderPassRight]
}
]
}
This works as expected, the object is rendered with myShaderLeft in the left viewport and with myShaderRight in the right viewport.
Now I want to use an default Qt-material (QMetalRoughMaterial) when rendering in the left viewport, and my custom material/effect when rendering in the right viewport. How can I achieve that?
An idea was to access the ShaderProgram of the MetalRoughMaterial through metalRougMaterial.effect.techniques[0].renderPasses[0].shaderProgram and use it one of the RenderPasses of my custom Material/Effect. However, metalRougMaterial.effect.techniques returns an undefined object.
MetalRoughMaterial {
id: metalRoughMaterial
baseColor: "red"
}
From the source code, it looks like effect is not a property that you can access on the material.
I'm not sure if this will work but I guess you could subclass the material in C++ and make the effect accessible. This way, you could modify it in QML, once you've registered it as a new QML type and used this QML type instead of the normal material one.
Another option that I can think of would be to create the object twice, once of the right viewport and once for the left. Then create two QLayers and add one to each and two respective QLayerFilters for each framegraph branch, to which you also add one layer each.
I gues computationally this comes down to the same load but you'd have to think about how setting position etc. works (I think you can add the same QTransform to both, but I'm not sure).
Last but not least you could probably recreate the whole material in QML completely (with your filter key) but that's probably a lot of work.

How to add a light to a 3d scene in Qml?

I am developing an application with a cube with 6 different textures in QML.
I made the cube pattern with Blender and attached the texture to the model I created and saved.
But I have a small problem, the textures load correctly but I have shadows that appear and darken the different sides of the cube.
Does anyone have an idea how to remove these shadows?
I put below the code of my application and a screenshot of what I get as a result.
main.qml
import QtQuick 2.12
import QtQuick.Scene3D 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
Scene3D
{
id : scene3d
anchors.fill: parent
focus: false
aspects: ["render", "logic", "input"]
hoverEnabled: true
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio
RootEntity
{
id:root
}
}
}
SOrbitCamera.qml
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
Entity{
id: root
property Camera camera;
property real dt: 0.001
property real linearSpeed: 1
property real lookSpeed: 500
MouseDevice {
id: mouseDevice
sensitivity: 0.007
}
MouseHandler {
id: mh
property vector3d upVect: Qt.vector3d(0, 1, 0)
property point lastPos;
property real pan;
property real tilt;
sourceDevice: mouseDevice
onPanChanged: {
if (camera.position.y < 0.5 && camera.position.y > -3.) {
root.camera.panAboutViewCenter(-pan, upVect);
}
if (camera.position.y > 0.5 && camera.position.y < 3.) {
root.camera.panAboutViewCenter(pan, upVect);
}
if (camera.position.y < 6. && camera.position.y > 3.) {
root.camera.panAboutViewCenter(-pan, upVect);
}
if (camera.position.y > -6. && camera.position.y < -3.) {
root.camera.panAboutViewCenter(pan, upVect);
}
}
onTiltChanged: root.camera.tiltAboutViewCenter(tilt);
onPressed: {
lastPos = Qt.point(mouse.x, mouse.y);
}
onPositionChanged: {
if (mouse.buttons === 1){ // Left button for rotation
pan = -(mouse.x - lastPos.x) * dt * lookSpeed;
tilt = (mouse.y - lastPos.y) * dt * lookSpeed;
console.log(camera.position)
}
lastPos = Qt.point(mouse.x, mouse.y)
}
}
}
RootEntity.qml
import QtQuick 2.0
import QtQuick3D 1.15
import Qt3D.Core 2.12
import Qt3D.Render 2.12
import Qt3D.Extras 2.12
import Qt3D.Input 2.12
Entity {
id: root
//create camera
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: cubeTransform.translation
}
//use my class instead of OrbitCameraController
SOrbitCameraController {
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: "black"
NoDraw {}
}
LayerFilter {
filterMode: LayerFilter.DiscardAnyMatchingLayers
layers: [topLayer]
}
LayerFilter {
filterMode: LayerFilter.AcceptAnyMatchingLayers
layers: [topLayer]
ClearBuffers {
buffers: ClearBuffers.DepthBuffer
}
}
}
}
}
}
},
InputSettings {}
,
ScreenRayCaster
{
id:screenRayCaster
onHitsChanged:
{
drawLineMesh(hits)
}
}
]
Layer {
id: topLayer
recursive: true
}
Entity {
id: cubeEntity
Texture2D {
id:texture
TextureImage {
source: "qrc:/../Downloads/texture.png"
}
}
Mesh {
id: cubeMesh
source: "qrc:/../Downloads/cube.obj"
}
Transform {
id: cubeTransform
translation: Qt.vector3d(0, 0, 0)
}
NormalDiffuseMapMaterial{
id: material
normal: texture
diffuse: texture
specular: texture
}
components: [cubeMesh, cubeTransform, material]
}
}
I add the following code in RootEntity.qml
import QtQuick 2.0
import QtQuick3D 1.15
import Qt3D.Core 2.12
import Qt3D.Render 2.12
import Qt3D.Extras 2.12
import Qt3D.Input 2.12
import QtQuick 2.15
import QtQuick.Window 2.12
Entity {
id: root
Entity {
id: lightEntity
components: [
DirectionalLight{
intensity: 0.5
enabled: true
},
Transform{
translation: Qt.vector3d(0,50,0)
}
]
}
//create camera
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: cubeTransform.translation
}
SOrbitCameraController {
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: "black"
NoDraw {}
}
LayerFilter {
filterMode: LayerFilter.DiscardAnyMatchingLayers
layers: [topLayer]
}
LayerFilter {
filterMode: LayerFilter.AcceptAnyMatchingLayers
layers: [topLayer]
ClearBuffers {
buffers: ClearBuffers.DepthBuffer
}
}
}
}
}
}
},
InputSettings {}
]
Layer {
id: topLayer
recursive: true
}
/*
CuboidMesh{
id: cubeMesh
xExtent: 1
yExtent: 1
zExtent: 1
}*/
Entity {
id: cubeEntity
Texture2D {
id:texture
TextureImage {
source: "qrc:/../Downloads/texture.png"
}
}
Mesh {
id: cubeMesh
source: "qrc:/../Downloads/cube.obj"
}
Transform {
id: cubeTransform
translation: Qt.vector3d(0, 0, 0)
}
NormalDiffuseMapMaterial{
id: material
normal: texture
diffuse: texture
}
components: [cubeMesh, cubeTransform, material]
}
}
I have the following result :
You need to define some light source for your scene.
These are some example for Directional and Point lights:
DirectionalLight {
id: directionalLight
x: -0
y: 828.821
color: "#e0eef0"
shadowMapQuality: Light.ShadowMapQualityMedium
shadowFactor: 58
castsShadow: true
eulerRotation.z: 43.68403
eulerRotation.y: -57.35873
eulerRotation.x: -35.78494
z: 1415.04834
brightness: 80
}
PointLight {
id: lightPoint
x: -7.983
y: 737.434
shadowFactor: 4
castsShadow: true
brightness: 122
quadraticFade: 0.19392
z: 108.31305
}
You can move your lights or rotate the lights direction with changing the x, y and defining rotation values.

How to rotate a 3d cube in qml?

I am developing an application in Qt Creator with QtQuick. The purpose of this application is to rotate a cube in 3 dimensions with the help of a finger.
I managed to develop the creation of actions in relation to the finger movements but I am completely stuck on the rotation of the cube in relation to the fingers.
I have tried to create a direction vector with respect to the current and previous points as well as the normal vector. I have applied the Euler rotation in x and y on the cube but at a certain point, the cube does not rotate as expected.
Here is my qml code for the main page of the application :
main.qml
import QtQuick 2.15
import QtQuick.Window 2.14
import QtQuick3D 1.15
import Qt3D.Input 2.0
import QtQuick.Controls 2.1
import Qt3D.Extras 2.15
Window {
visible: true
width: 640
height: 480
title: qsTr("MouseArea Demo")
MouseArea{
property int previousX: -1
property int previousY: -1
anchors.fill : parent
onPressed: {
previousX = mouseX;
previousY = mouseY;
}
onPositionChanged: {
let direction = Qt.vector2d(mouseX - previousX, mouseY - previousY).normalized();
let normal = Qt.vector2d(direction.y, direction.x).normalized();
console.log("direction : " + direction);
console.log("normal : " + normal);
cube.eulerRotation.x += normal.x * 3;
cube.eulerRotation.y += normal.y * 3;
previousX = mouseX;
previousY = mouseY
}
onReleased: {
previousX = -1;
previousY = -1;
}
}
View3D {
id: view
anchors.fill: parent
camera: camera
renderMode: View3D.Overlay
PerspectiveCamera {
id: camera
position: Qt.vector3d(0, 200, 300)
eulerRotation.x: -30
}
DirectionalLight {
eulerRotation.x: -30
}
Model {
id: cube
visible: true
position: Qt.vector3d(0, 0, 0)
source: "#Cube"
materials: [ DefaultMaterial {
diffuseMap: Texture {
id: texture
source: "../build-colorpicker2d-Desktop_Qt_5_15_2_GCC_64bit-Debug/res.png"
}
}
]
}
}
}
here is a video about what I talk to you 2 days ago about the y rotation problem.
Kind regards.
First of all, you should add this in your .pro file :
QT += qml quick 3dcore 3dinput 3dquick 3dlogic 3dquickextras 3dextras
I have 3 class in qml :main.qml , RootEntity.qml and SOrbitCameraController.qml
I have one Scene3D and inside this, I can put all my Entities. I Create a separate class and call it RootEntity.
The more important point of this way is orbitController that I use in my class. this makes that you can rotate cube by mouse and for doing this you need Entity. because of this, I use Scene3D and Entity instead of view3d and models.
In main.qml:
import QtQuick 2.12
import QtQuick.Scene3D 2.12
import QtQuick.Window 2.12
import "."
Window {
visible: true
width: 640
height: 480
Scene3D
{
id : scene3d
anchors.fill: parent
focus: true
aspects: ["render", "logic", "input"]
hoverEnabled: true
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio
antialiasing: true
RootEntity
{
id:root
}
}
}
in RootEntity.qml:
import QtQuick 2.0
import Qt3D.Core 2.12
import Qt3D.Render 2.12
import Qt3D.Extras 2.12
import Qt3D.Input 2.12
import "."
Entity {
id: root
//create camera
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)
}
//use my class instead of OrbitCameraController
SOrbitCameraController {
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 {}
,
ScreenRayCaster
{
id:screenRayCaster
onHitsChanged:
{
drawLineMesh(hits)
}
}
]
Layer {
id: topLayer
recursive: true
}
Entity {
id: cubeEntity
components: [
CuboidMesh
{
xExtent: 1
yExtent: 1
zExtent: 1
}
,
Transform {
id: t
translation: Qt.vector3d(0, 0, 0)
}
,
PhongMaterial
{
ambient: "red"
}
]
}
}
in SOrbitCameraController.qml :
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
Entity{
id: root
property Camera camera;
property real dt: 0.001
property real linearSpeed: 1
property real lookSpeed: 500
property real zoomLimit: 0.16
MouseDevice {
id: mouseDevice
sensitivity: 0.001 // Make it more smooth
}
MouseHandler {
id: mh
readonly property vector3d upVect: Qt.vector3d(0, 1, 0)
property point lastPos;
property real pan;
property real tilt;
sourceDevice: mouseDevice
onPanChanged: root.camera.panAboutViewCenter(pan, upVect);
onTiltChanged: root.camera.tiltAboutViewCenter(tilt);
onPressed: {
lastPos = Qt.point(mouse.x, mouse.y);
}
onPositionChanged: {
// You can change the button as you like for rotation or translation
if (mouse.buttons === 1){ // Left button for rotation
pan = -(mouse.x - lastPos.x) * dt * lookSpeed;
tilt = (mouse.y - lastPos.y) * dt * lookSpeed;
} else if (mouse.buttons === 2) { // Right button for translate
var rx = -(mouse.x - lastPos.x) * dt * linearSpeed;
var ry = (mouse.y - lastPos.y) * dt * linearSpeed;
camera.translate(Qt.vector3d(rx, ry, 0))
} else if (mouse.buttons === 3) { // Left & Right button for zoom
ry = (mouse.y - lastPos.y) * dt * linearSpeed
zoom(ry)
}
lastPos = Qt.point(mouse.x, mouse.y)
}
onWheel: {
zoom(wheel.angleDelta.y * dt * linearSpeed)
}
function zoom(ry) {
if (ry > 0 && zoomDistance(camera.position, camera.viewCenter) < zoomLimit) {
return
}
camera.translate(Qt.vector3d(0, 0, ry), Camera.DontTranslateViewCenter)
}
function zoomDistance(posFirst, posSecond) {
return posSecond.minus(posFirst).length()
}
}
}
and at the end my main.cpp :
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
{
return -1;
}
return app.exec();
}
the out put
The Node has a rotate() method. The trick is to do the rotation in Node.SceneSpace. This will ensure that the rotations compound correctly in any axis you choose in that the rotations will be applied on top of the latest orientation. I use a MouseArea to capture 2d movements in either X or Y. In 3d we interpret this as rotations in Y-axis or X-axis respectively. Here's a working example:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick3D
Page {
background: Rectangle { color: "#848895" }
Node {
id: standAloneScene
DirectionalLight { ambientColor: Qt.rgba(1.0, 1.0, 1.0, 1.0) }
Node {
id: node
Model {
id: model
source: "#Cube"
materials: [
DefaultMaterial { diffuseColor: Qt.rgba(0.053, 0.130, 0.219, 0.75) }
]
}
}
OrthographicCamera {
id: cameraOrthographicFront
y: 500; z: 1000
lookAtNode: node
}
}
View3D {
anchors.fill: parent
importScene: standAloneScene
camera: cameraOrthographicFront
}
MouseArea {
anchors.fill:parent
property real pressedX
property real pressedY
onMouseXChanged: Qt.callLater(update)
onMouseYChanged: Qt.callLater(update)
onPressed: {
[pressedX,pressedY] = [mouseX,mouseY];
}
function update() {
let [dx,dy] = [mouseX - pressedX,mouseY - pressedY];
[pressedX,pressedY] = [mouseX,mouseY];
node.rotate(dx, Qt.vector3d(0, 1, 0), Node.SceneSpace);
node.rotate(dy, Qt.vector3d(1, 0, 0), Node.SceneSpace);
}
}
}
You can Try it Online!

QML-Move item with arrow keys

I'm using Qt version 5.6.0 from windows 8.1. I draw a shape that has 4 sides and the ability to drag its apexes.I want to move apexes of shape with arrow keys.I use this code but it does not work.
Point.qml:
Item {
id: root
signal dragged()
property alias color :point.color
Rectangle {
id:point
anchors.centerIn: parent
width: 20
height: 20
opacity: 0.2
MouseArea {
anchors.fill: parent
drag.target: root
onPositionChanged: {
if(drag.active) {
dragged()
}
}
onClicked: point.focus=true;
}
// Keys.onPressed: {
// console.log("move");
// if (event.key === Qt.Key_Left) {
// console.log("move left");
// event.accepted = true;
// point.x-=1;
// }
// }
// Keys.onRightPressed: {
// console.log("move");
// point.x+=1;
// }
// Keys.onLeftPressed: {
// console.log("move");
// point.x-=1;
// }
Keys.onPressed: {
switch(event.key) {
case Qt.Key_Left: point.x-=1;
break;
case Qt.Key_Right: point.x+=1;
break;
case Qt.Key_Up: point.y-=1;
break;
case Qt.Key_Down: point.y+=1;
break;
}
}
focus: true;
}}
main.qml:
Point {
id: pointA
x: 50
y: 50
}
Point {
id: pointB
x: 250
y: 50
}
Point {
id: pointC
x: 250
y: 250
}
Point {
id: pointD
x: 50
y: 250
}
Item {
anchors.fill: parent
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
var ctx = canvas.getContext('2d');
ctx.moveTo(pointA.x, pointA.y);
ctx.lineTo(pointB.x, pointB.y);
ctx.lineTo(pointC.x, pointC.y);
ctx.lineTo(pointD.x, pointD.y);
ctx.lineTo(pointA.x, pointA.y);
ctx.stroke();
}
Component.onCompleted: {
pointA.dragged.connect(repaint)
pointB.dragged.connect(repaint)
pointC.dragged.connect(repaint)
pointD.dragged.connect(repaint)
}
function repaint() {
var ctx = getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
requestPaint()
}
}
}
Update:
main.qml:
PhotoPreview {
id : photoPreview
anchors.fill : parent
//focus:true //visible
visible: capture
}
photopreview.qml:
Item {
id:mywin
property real defaultSize:mywin.width
property var currentFrame: undefined
property real surfaceViewportRatio: 1.5
focus: true
ScrollView {
anchors.fill: parent
flickableItem.interactive: true
frameVisible: true
highlightOnFocus: true
Flickable {
id: flick
anchors.fill: parent
contentWidth: parent.width
contentHeight: parent.height
property alias source :image.source
signal closed
Rectangle {
id: photoFrame
width: parent.width
height: parent.height
color:"transparent"
scale:defaultSize / parent.width
Behavior on scale { NumberAnimation { duration: 200 } }
Behavior on x { NumberAnimation { duration: 200 } }
Behavior on y { NumberAnimation { duration: 200 } }
smooth: true
antialiasing: true
Image {
id:image
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
}
PinchArea {
anchors.fill: parent
pinch.target: photoFrame
pinch.minimumRotation: -360
pinch.maximumRotation: 360
pinch.minimumScale: 0.1
pinch.maximumScale: 10
pinch.dragAxis: Pinch.XAndYAxis
property real zRestore: 0
onSmartZoom: {
if (pinch.scale > 0) {
photoFrame.rotation = 0;
photoFrame.scale = Math.min(mywin.width, mywin.height) / Math.max(image.sourceSize.width, image.sourceSize.height) * 0.85
photoFrame.x = flick.contentX + (flick.width - photoFrame.width) / 2
photoFrame.y = flick.contentY + (flick.height - photoFrame.height) / 2
zRestore = photoFrame.z
photoFrame.z = ++mywin.highestZ;
} else {
photoFrame.rotation = pinch.previousAngle
photoFrame.scale = pinch.previousScale
photoFrame.x = pinch.previousCenter.x - photoFrame.width / 2
photoFrame.y = pinch.previousCenter.y - photoFrame.height / 2
photoFrame.z = zRestore
--mywin.highestZ
}
}
MouseArea {
id: dragArea
hoverEnabled: true
anchors.fill: parent
drag.target: photoFrame
scrollGestureEnabled: false // 2-finger-flick gesture should pass through to the Flickable
onPressed: {
photoFrame.z = ++mywin.highestZ;
}
onWheel: {
if (wheel.modifiers & Qt.ControlModifier) {
photoFrame.rotation += wheel.angleDelta.y / 120 * 5;
if (Math.abs(photoFrame.rotation) < 4)
photoFrame.rotation = 0;
} else {
photoFrame.rotation += wheel.angleDelta.x / 120;
if (Math.abs(photoFrame.rotation) < 0.6)
photoFrame.rotation = 0;
var scaleBefore = photoFrame.scale;
photoFrame.scale += photoFrame.scale * wheel.angleDelta.y / 120 / 10;
}
}
}
}
Point {
id: pointA
x: image.width/4
y: image.height/4
color: "blue"
}
Point {
id: pointB
x: image.width/2
y: image.height/2
color: "blue"
}
Point {
id: pointD
x: image.width/4
y: image.height/2
color: "red"
}
Point {
id: pointC
x: image.width/2
y: image.height/4
color: "red"
}
Item {
anchors.fill: parent
Canvas {
id: canvas
anchors.fill: parent
onPaint: {
//...
}
Component.onCompleted: {
//...
}
function repaint() {
//..
}
}
}
}
}
}}
You are using ScrollView that inherit FocusScope, which needs to have focus:true to forward it.
You instead are setting focus to PhotoPreview, which is plain Item that are not supposed to be focused.
So you need to simple remove focus:visible and set it to ScrollView.
The better fix is to remove that redundant Item, and leave ScrollView as PhotoPreview root item, with all its properties and signals.
In Point.qml the key event handler tries to change the x, y of point, however point is a rectangle that anchors.centerIn: parent. The key event handler should change root x, y instead:
//in Point.qml
case Qt.Key_Left:
root.x-=1;
break;
Point now changes it's position when keyboard event is triggered. Next, Point needs to notify the Canvas in main.qml to repaint. Point can emit dragged signal after changing it's x, y:
//in Point.qml
Keys.onPressed: {
switch(event.key) {
case Qt.Key_Left:
root.x-=1;
dragged();
break;
case Qt.Key_Right:
root.x+=1;
dragged();
break;
case Qt.Key_Up:
root.y-=1;
dragged();
break;
case Qt.Key_Down:
root.y+=1;
dragged();
break;
}
}

Resources