Рад снова всех приветствовать! Без лишних слов переходим к продолжению деятельности 🤐 Сегодня тоже план вполне понятен, то есть размышлять над последовательностью ближайших шагов не придется. Во-первых, во вчерашней части было одно упущение, которое стоит сразу устранить.
Заключается оно в том, что, как вы помните, у нас каждый PaintObject
имеет свой компонент QObjectPicker
. Также аналогичные возможности реализованы для базовой плоскости - baseCuboidPicker
для baseCuboidEntity
. Логика работы приложения не предусматривает их одновременного срабатывания, поскольку либо идет добавление новых объектов, либо идет взаимодействие с существующими. Инструменты физически разные. Помимо того, что совместная работа PaintObject
'овских picker
и baseCuboidPicker
попросту не нужна, она будет еще и вредна при совпадении некоторых условий. Так что тут даже думать не о чем, надо просто эту ситуацию обработать.
Причем частично это уже сделано, при выборе активного инструмента через соответствующую панель мы проверяем, нужен ли нам в текущем режиме baseCuboidPicker
:
onSelectedToolChanged: { if ((toolPanel.selectedTool === PaintEntity.ToolType.Select) || (toolPanel.selectedTool === PaintEntity.ToolType.Delete)) { baseCuboidPicker.paintEnabled = false; } else { baseCuboidPicker.paintEnabled = true; } paintEntity.activeTool = toolPanel.selectedTool; }
По такой же схеме поступим и с PaintObject
. Добавим метод:
void PaintObject::setPickerEnabled(bool state) { picker->setEnabled(state); }
Осталось определить ситуацию, при который произойдет заветный вызов данной функции. По логике все просто - данные действия должны происходить при изменении выбранного инструмента, в зависимости от этого выбора. На практике все не сильно сложнее. В PaintEntity
мы объявили свойство activeTool
так:
Q_PROPERTY(ToolType activeTool READ getActiveTool WRITE setActiveTool);
Что это нам дает? А то, что при использовании activeTool
в QML произойдет вызов функции getActiveTool
для получения актуальной информации о значении свойства. И по образу и подобию работает и обратный механизм. Когда в QML мы меняем значение:
paintEntity.activeTool = toolPanel.selectedTool;
То происходит вызов setActiveTool
из PaintEntity
. И клоню я к тому, что даже не потребуется дополнительно отслеживать изменения выбранного инструмента:
void PaintEntity::setActiveTool(ToolType tool) { activeTool = tool; bool pickerState = false; if ((activeTool == PaintEntity::ToolType::Select) || (activeTool == PaintEntity::ToolType::Delete)) { pickerState = true; } for (int i = 0; i < paintObjects.size(); i++) { paintObjects.at(i)->setPickerEnabled(pickerState); } }
Поскольку по умолчанию у нас режим кубоида, так что сразу же автоматом в конструкторе меняем:
picker->setEnabled(true);
на:
picker->setEnabled(false);
Теперь все работает вроде бы четко. НО! Ключевое слово - вроде бы. От текущего механизма анимации выбранного объекта так и веет потенциальными проблемами. И эти проблемы были обнаружены:
Если предыдущая анимация не успела завершиться (а 250 мс - довольно приличный срок), и при этом происходит новый клик мышью, то наступает полный крах в виде неконтролируемого изменения размеров объекта. Навскидку приходят на ум три варианта разрешения сложившейся ситуации:
- Заблокировать новую анимацию, если старая не закончена. Это легко сделать анализируя свойство state у
transformAnimationGroup
. При этом выбор объекта будет осуществлен, но без обратной связи в виде анимации. - Запретить повторный выбор объекта на корню, пока не закончится анимация. Тоже реализуется очень просто - через тот же сигнал
QPropertyAnimation::finished()
иsetPickerEnabled()
. - Третий вариант - с нуля полностью пересмотреть концепцию и переделать механизм анимирования выбранного объекта.
Можно, конечно, вообще выкинуть анимацию, но это я даже не выношу на рассмотрение. Объективно говоря, первые два варианта настолько отвратительны в своей неидеальности, что и их я привел чисто для примера ) Здесь только третий вариант, других быть не может.
Практическая суть заключается в том, что все действия анимационного характера, соответствующие члены и методы мы перенесем в класс PaintObject
. То есть каждый 3D-объект будет иметь свои QPropertyAnimation
, поэтому выбор другого элемента не будет конфликтовать с незавершившейся в какой-то степени анимации предыдущего объекта. Из PaintEntity
, соответственно, все убирается. В принципе, концептуально мне данный вариант также нравится больше.
PaintObject
/ PaintEntity
⬇️
paintentity.cpp:
#include <QDebug> #include "paintentity.h" #include "objects/cuboid.h" PaintEntity::PaintEntity(QNode *parent) : Qt3DCore::QEntity(parent), activeTool(ToolType::Cuboid) { } void PaintEntity::paintObjectPicked(quint32 pickedId) { if (activeTool == PaintEntity::ToolType::Select) { paintObjects.at(pickedId)->startSelectAnimation(); activePaintObject = paintObjects.at(pickedId).data(); } else { if (activeTool == PaintEntity::ToolType::Delete) { paintObjects.at(pickedId)->startDeleteAnimation(); } } } void PaintEntity::removePaintObject(quint32 objectId) { if (activePaintObject != nullptr) { if (objectId == activePaintObject->getId()) { activePaintObject = nullptr; } } paintObjects.remove(objectId); updatePaintObjectIds(objectId); } void PaintEntity::updatePaintObjectIds(int deletedIndex) { for (int i = deletedIndex; i < paintObjects.size(); i++) { paintObjects.at(i)->setId(i); } } void PaintEntity::onBasePlaneMousePressed(const QVector3D &coordinate) { switch (activeTool) { case PaintEntity::ToolType::Cuboid: paintObjects.append(QSharedPointer<PaintObject>(new Cuboid(this, paintObjects.size(), coordinate))); break; default: // This branch should never be executed break; } int currentIndex = paintObjects.size() - 1; QObject::connect(paintObjects.at(currentIndex).data(), &PaintObject::picked, this, &PaintEntity::paintObjectPicked); QObject::connect(paintObjects.at(currentIndex).data(), &PaintObject::readyForDeletion, this, &PaintEntity::removePaintObject); } void PaintEntity::onBasePlaneMouseMoved(const QVector3D &coordinate) { paintObjects.at(paintObjects.size() - 1)->onMouseMoved(coordinate); } PaintEntity::ToolType PaintEntity::getActiveTool() { return activeTool; } void PaintEntity::setActiveTool(ToolType tool) { activeTool = tool; bool pickerState = false; if ((activeTool == PaintEntity::ToolType::Select) || (activeTool == PaintEntity::ToolType::Delete)) { pickerState = true; } for (int i = 0; i < paintObjects.size(); i++) { paintObjects.at(i)->setPickerEnabled(pickerState); } }
paintentity.h:
#ifndef PAINTENTITY_H #define PAINTENTITY_H #include <Qt3DCore> #include <Qt3DExtras> #include "objects/paintobject.h" class PaintEntity : public Qt3DCore::QEntity { Q_OBJECT public: enum class ToolType { Select = 0, Delete, Cuboid }; Q_ENUM(ToolType) PaintEntity(QNode *parent = nullptr); Q_PROPERTY(ToolType activeTool READ getActiveTool WRITE setActiveTool); Q_INVOKABLE void onBasePlaneMousePressed(const QVector3D &coordinate); Q_INVOKABLE void onBasePlaneMouseMoved(const QVector3D &coordinate); ToolType getActiveTool(); void setActiveTool(ToolType tool); private slots: void paintObjectPicked(quint32 pickedId); void removePaintObject(quint32 objectId); signals: private: QVector< QSharedPointer<PaintObject> > paintObjects; ToolType activeTool; PaintObject *activePaintObject; void updatePaintObjectIds(int deletedIndex); }; #endif // PAINTENTITY_H
paintobject.cpp:
#include <QDebug> #include "paintobject.h" PaintObject::PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId) : type(PaintObject::Type::None), id(objectId) { createAnimation(); entity = QSharedPointer<Qt3DCore::QEntity>(new Qt3DCore::QEntity(parentEntity)); picker = new Qt3DRender::QObjectPicker(); picker->setEnabled(false); picker->setHoverEnabled(true); entity->addComponent(picker); QObject::connect(picker, &Qt3DRender::QObjectPicker::clicked, this, &PaintObject::onClicked); } void PaintObject::createAnimation() { transformAnimationGroup = new QSequentialAnimationGroup(this); increaseAnimation = new QPropertyAnimation(this); increaseAnimation->setDuration(125); decreaseAnimation = new QPropertyAnimation(this); decreaseAnimation->setDuration(125); deleteAnimation = new QPropertyAnimation(this); deleteAnimation->setDuration(50); deleteAnimation->setEndValue(QVector3D(0, 0, 0)); QObject::connect(deleteAnimation, &QPropertyAnimation::finished, this, &PaintObject::deleteAnimationFinished); transformAnimationGroup->addAnimation(increaseAnimation); transformAnimationGroup->addAnimation(decreaseAnimation); } void PaintObject::startSelectAnimation() { QVector3D currentScale3D = getTransform()->scale3D(); if (transformAnimationGroup->state() == QAbstractAnimation::Stopped) { increaseAnimation->setTargetObject(getTransform()); increaseAnimation->setPropertyName("scale3D"); increaseAnimation->setStartValue(currentScale3D); increaseAnimation->setEndValue(currentScale3D * 1.15); decreaseAnimation->setTargetObject(getTransform()); decreaseAnimation->setPropertyName("scale3D"); decreaseAnimation->setStartValue(currentScale3D * 1.15); decreaseAnimation->setEndValue(currentScale3D); transformAnimationGroup->start(); } } void PaintObject::startDeleteAnimation() { setPickerEnabled(false); QVector3D currentScale3D = getTransform()->scale3D(); deleteAnimation->setTargetObject(getTransform()); deleteAnimation->setPropertyName("scale3D"); deleteAnimation->setStartValue(currentScale3D); deleteAnimation->start(); } void PaintObject::deleteAnimationFinished() { emit readyForDeletion(id); } Qt3DCore::QTransform* PaintObject::getTransform() { Qt3DCore::QTransform *transform = entity->componentsOfType<Qt3DCore::QTransform>().at(0); return transform; } void PaintObject::onClicked(Qt3DRender::QPickEvent *event) { if (event->button() == Qt3DRender::QPickEvent::LeftButton) { emit picked(id); } } void PaintObject::addMaterial() { Qt3DExtras::QDiffuseSpecularMaterial *defaultMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); defaultMaterial->setAmbient(QColor(Qt::black)); defaultMaterial->setSpecular(QColor(Qt::white)); defaultMaterial->setShininess(4); entity->addComponent(defaultMaterial); } quint32 PaintObject::getId() { return id; } void PaintObject::setId(quint32 objectId) { id = objectId; } PaintObject::Type PaintObject::getType() { return type; } void PaintObject::setType(Type objectType) { type = objectType; } void PaintObject::setPickerEnabled(bool state) { picker->setEnabled(state); }
paintobject.h:
#ifndef PAINTOBJECT_H #define PAINTOBJECT_H #include <QObject> #include <Qt3DCore> #include <Qt3DExtras> #include <QSharedPointer> #include "constants.h" class PaintObject : public QObject { Q_OBJECT public: enum class Type { None = 0, Cuboid, Sphere, Cone, Cylinder, Torus, ExtrudedText, Curve }; Q_ENUM(Type) PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId); Q_PROPERTY(Type type READ getType WRITE setType); Type getType(); void setType(Type objectType); quint32 getId(); void setId(quint32 objectId); void setPickerEnabled(bool state); void startSelectAnimation(); void startDeleteAnimation(); virtual void onMouseMoved(const QVector3D &coordinate) = 0; virtual void addMaterial(); virtual Qt3DCore::QTransform* getTransform(); signals: void picked(quint32 pickedId); void readyForDeletion(quint32 objectId); private slots: virtual void onClicked(Qt3DRender::QPickEvent *event); void deleteAnimationFinished(); protected: Type type; QSharedPointer<Qt3DCore::QEntity> entity; QVector3D initialCoordinate; quint32 id; private: Qt3DRender::QObjectPicker *picker; QSequentialAnimationGroup *transformAnimationGroup; QPropertyAnimation *increaseAnimation; QPropertyAnimation *decreaseAnimation; QPropertyAnimation *deleteAnimation; void createAnimation(); }; #endif // PAINTOBJECT_H
На этом, наконец-то, заканчиваем с тем, что тянулось с предыдущего этапа 👍 А на сегодня задача будет заключаться в реализации панели перемещения и вращения 3D-объектов:
Вижу я эту панель примерно так:
Шесть кнопок для разных типов трансформации:
- перемещение по оси x
- перемещение по y
- перемещение по z
- вращение вокруг оси x
- вращение вокруг оси y
- вращение вокруг оси z
И в зависимости от текущего режима будет меняться графический элемент интерфейса. Дизайн кнопок по-прежнему откладываем на более поздние этапы разработки, а вот элементам управления немного внимания все же уделим. Такой результат получился на практике:
Полный код панели:
import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQml.Models 2.15 import QtQuick.Layouts 1.15 import QtQuick.Shapes 1.15 Rectangle { id: rootRectangle color: "transparent" height: buttonsGrid.height + controlsArea.height + column.spacing property var angle: Qt.vector3d(0.0, 0.0, 0.0) property var initialPosition: Qt.vector3d(0.0, 0.0, 0.0) property var position: Qt.vector3d(0.0, 0.0, 0.0) property var activeMode: TransformPanel.Mode.XMove property var positionLimits: Qt.vector3d(0.0, 0.0, 0.0) enum Mode { XMove, YMove, ZMove, XRotate, YRotate, ZRotate } property var buttonsModel: [ {name: qsTr("x_move"), mode: TransformPanel.Mode.XMove}, {name: qsTr("y_move"), mode: TransformPanel.Mode.YMove}, {name: qsTr("z_move"), mode: TransformPanel.Mode.ZMove}, {name: qsTr("x_rotate"), mode: TransformPanel.Mode.XRotate}, {name: qsTr("y_rotate"), mode: TransformPanel.Mode.YRotate}, {name: qsTr("z_rotate"), mode: TransformPanel.Mode.ZRotate}, ] onInitialPositionChanged: { position = initialPosition; } function reset() { for (var i = 0; i < buttonsGroup.buttons.length; i++) { if (buttonsGroup.buttons[i].transformMode === TransformPanel.Mode.XMove) { if (buttonsGroup.buttons[i].checked === false) { buttonsGroup.buttons[i].toggle(); } break; } } buttonsGroup.updateControls(TransformPanel.Mode.XMove); } function setMoveControlInitial(initial, range) { moveLine.currentX = controlsArea.width / 2 + initial * moveControl.lineLength / range; if (moveLine.currentX > moveControl.lineEnd) { moveLine.currentX = moveControl.lineEnd; } if (moveLine.currentX < moveControl.lineStart) { moveLine.currentX = moveControl.lineStart; } } ButtonGroup { id: buttonsGroup exclusive: true function updateControls(mode) { activeMode = mode; if ((activeMode === TransformPanel.Mode.XRotate) || (activeMode === TransformPanel.Mode.YRotate) || (activeMode === TransformPanel.Mode.ZRotate)) { rotationArc.sweepAngle = 0; mouseArea.previousAngle = 0; angle = Qt.vector3d(0.0, 0.0, 0.0); if (activeMode === TransformPanel.Mode.XRotate) { rotationControl.transformAngle = 60; rotationControl.transformAxis = Qt.vector3d(0.0, 1.0, 0.0) } if (activeMode === TransformPanel.Mode.YRotate) { rotationControl.transformAngle = 0; rotationControl.transformAxis = Qt.vector3d(0.0, 0.0, 0.0) } if (activeMode === TransformPanel.Mode.ZRotate) { rotationControl.transformAngle = 60; rotationControl.transformAxis = Qt.vector3d(1.0, 0.0, 0.0) } moveControl.visible = false; rotationControl.visible = true; } else { if (activeMode === TransformPanel.Mode.XMove) { moveControl.transformAngle = 45; moveControl.transformAxis = Qt.vector3d(1.0, 0.0, 0.0) setMoveControlInitial(initialPosition.x, positionLimits.x); } if (activeMode === TransformPanel.Mode.YMove) { moveControl.transformAngle = 0; moveControl.transformAxis = Qt.vector3d(0.0, 0.0, 0.0) setMoveControlInitial(initialPosition.y, positionLimits.y); } if (activeMode === TransformPanel.Mode.ZMove) { moveControl.transformAngle = -90; moveControl.transformAxis = Qt.vector3d(0.0, 0.0, 1.0) setMoveControlInitial(initialPosition.z, positionLimits.z); } moveControl.visible = true; rotationControl.visible = false; } } onClicked: { updateControls(checkedButton.transformMode); } } Column { id: column anchors.fill: parent Grid { id: buttonsGrid columns: 3 spacing: 2 anchors.horizontalCenter: parent.horizontalCenter Repeater { id: buttonsRepeater model: buttonsModel Button { checkable: true width: (rootRectangle.width - (buttonsGrid.columns - 1) * buttonsGrid.spacing) / buttonsGrid.columns height: width text: modelData['name'] property var transformMode: modelData['mode'] property var normalColor: "#a0a080" property var hoveredColor: "#a0a0a0" property var checkedColor: "#505050" ButtonGroup.group: buttonsGroup checked: transformMode === TransformPanel.Mode.XMove background: Rectangle { anchors.fill: parent color: checked ? checkedColor : hovered ? hoveredColor : normalColor } } } } Rectangle { id: controlsArea color: "transparent" width: parent.width height: width Shape { id: rotationControl anchors.fill: parent visible: false property real transformAngle: 0 property var transformAxis: Qt.vector3d(0.0, 0.0, 0.0) transform: Rotation { origin.x: controlsArea.width / 2; origin.y: controlsArea.height / 2; angle: rotationControl.transformAngle; axis: rotationControl.transformAxis } ShapePath { id: rotationOutline strokeColor: "white" strokeWidth: 1.5 capStyle: ShapePath.FlatCap property var radius: 0.8 * rotationControl.width / 2 fillGradient: LinearGradient { x1: rotationControl.width / 2 - rotationOutline.radius y1: rotationControl.height / 2 x2: rotationControl.width / 2 + rotationOutline.radius y2: rotationControl.height / 2 GradientStop { position: 0.0; color: "#218e59" } GradientStop { position: 0.5; color: "#75bd4c" } GradientStop { position: 1.0; color: "#dbe62f" } } PathAngleArc { centerX: rotationControl.width / 2; centerY: rotationControl.height / 2 radiusX: rotationOutline.radius - rotationArc.size; radiusY: radiusX startAngle: 0 sweepAngle: 360 } PathAngleArc { centerX: rotationControl.width / 2; centerY: rotationControl.height / 2 radiusX: rotationOutline.radius; radiusY: radiusX startAngle: 0 sweepAngle: 360 } } ShapePath { fillColor: "transparent" strokeColor: "white" strokeWidth: 1.5 capStyle: ShapePath.FlatCap PathAngleArc { property var offset: 5 centerX: rotationControl.width / 2; centerY: rotationControl.height / 2 radiusX: rotationOutline.radius + offset; radiusY: radiusX startAngle: 0 sweepAngle: 360 } } ShapePath { fillColor: "transparent" strokeColor: "#606065" strokeWidth: rotationArc.size - rotationOutline.strokeWidth capStyle: ShapePath.FlatCap PathAngleArc { id: rotationArc centerX: rotationControl.width / 2; centerY: rotationControl.height / 2 radiusX: rotationOutline.radius - size / 2; radiusY: radiusX startAngle: 0 sweepAngle: 360 property var size: 40 } } MouseArea { id: mouseArea anchors.fill: parent preventStealing: true property var previousAngle: Qt.vector3d(0.0, 0.0, 0.0) onPositionChanged: { var mouseCoordinate = Qt.vector2d(mouseX - width / 2, mouseY - height / 2); var objectAngle = Math.atan2(mouseCoordinate.y, mouseCoordinate.x) * 180 / Math.PI; var uiAngle = objectAngle; if (uiAngle < 0) { uiAngle += 360; } rotationArc.sweepAngle = uiAngle; objectAngle *= (-1); if (activeMode === TransformPanel.Mode.XRotate) { angle = Qt.vector3d(objectAngle - previousAngle, 0.0, 0.0); } if (activeMode === TransformPanel.Mode.YRotate) { angle = Qt.vector3d(0.0, objectAngle - previousAngle, 0.0); } if (activeMode === TransformPanel.Mode.ZRotate) { angle = Qt.vector3d(0.0, 0.0, objectAngle - previousAngle); } previousAngle = objectAngle; } } } Shape { id: moveControl anchors.fill: parent visible: true property real transformAngle: 0 property var transformAxis: Qt.vector3d(0.0, 0.0, 0.0) property var lineStart: controlsArea.width / 8 property var lineEnd: controlsArea.width * 7 / 8 property var lineLength: lineEnd - lineStart transform: Rotation { origin.x: controlsArea.width / 2; origin.y: controlsArea.height / 2; angle: moveControl.transformAngle; axis: moveControl.transformAxis } ShapePath { id: moveOutline fillColor: "#606065" strokeColor: "white" strokeWidth: 1.5 property var offset: 5 startX: moveControl.lineStart - offset startY: moveControl.height / 2 - moveLine.size / 2 - offset PathLine { x: moveControl.lineEnd + moveOutline.offset y: moveControl.height / 2 - moveLine.size / 2 - moveOutline.offset } PathLine { x: moveControl.lineEnd + moveOutline.offset y: moveControl.height / 2 + moveLine.size / 2 + moveOutline.offset } PathLine { x: moveControl.lineStart - moveOutline.offset y: moveControl.height / 2 + moveLine.size / 2 + moveOutline.offset } PathLine { x: moveControl.lineStart - moveOutline.offset y: moveControl.height / 2 - moveLine.size / 2 - moveOutline.offset } } ShapePath { id: moveLine strokeColor: "white" strokeWidth: 1.5 property var currentX: 0 property var size: 50 fillGradient: LinearGradient { x1: moveControl.lineStart y1: moveControl.height / 2 x2: moveControl.lineEnd y2: moveControl.height / 2 GradientStop { position: 0.0; color: "#218e59" } GradientStop { position: 0.5; color: "#75bd4c" } GradientStop { position: 1.0; color: "#dbe62f" } } startX: moveControl.lineStart startY: moveControl.height / 2 - size / 2 PathLine { x: moveLine.currentX y: moveControl.height / 2 - moveLine.size / 2 } PathLine { x: moveLine.currentX y: moveControl.height / 2 + moveLine.size / 2 } PathLine { x: moveControl.lineStart y: moveControl.height / 2 + moveLine.size / 2 } PathLine { x: moveControl.lineStart y: moveControl.height / 2 - moveLine.size / 2 } } MouseArea { anchors.fill: parent preventStealing: true onPositionChanged: { var mouseCoordinate = mouseX; if (mouseCoordinate > moveControl.lineEnd) { mouseCoordinate = moveControl.lineEnd; } if (mouseCoordinate < moveControl.lineStart) { mouseCoordinate = moveControl.lineStart; } mouseCoordinate -= moveControl.lineStart; moveLine.currentX = moveControl.lineStart + mouseCoordinate ; var coef = (moveLine.currentX - moveControl.width / 2) / moveControl.lineLength; if (activeMode === TransformPanel.Mode.XMove) { position = Qt.vector3d(coef * positionLimits.x, position.y, position.z); } if (activeMode === TransformPanel.Mode.YMove) { position = Qt.vector3d(position.x, coef * positionLimits.y, position.z); } if (activeMode === TransformPanel.Mode.ZMove) { position = Qt.vector3d(position.x, position.y, coef * positionLimits.z); } } } } } } }
В main.qml данную панель добавляем сразу под панелью инструментов за тем лишь отличием от предыдущей, что по умолчанию ее не отображаем:
TransformPanel { id: transformPanel anchors.left: parent.left anchors.right: parent.right anchors.top: toolPanel.bottom anchors.topMargin: 10 visible: false }
Идем дальше. Поведение TransformPanel
определяется рядом ее свойств. В частности, positionLimits
- определяет допустимые пределы для перемещения 3D-объектов. Их мы зададим равными размерам базовой плоскости baseCuboid
- по осям x и y. По оси z я сделаю ограничение равным 20, то есть фигура сможет быть перемещена в пределах координат [-10; 10] по z.
Далее идет initialPosition
. Свойство используется для задания текущего положения объекта. При выборе какого-либо из существующих на сцене 3D-объектов он уже находится в некоторой точке пространства, относительно которой его и нужно перемещать. Чтобы элементы управления соответствовали этим значениям при изменении активного объекта необходимо задать значение initialPosition
. Чуть ниже это осуществим на практике.
Чтобы отслеживать действия пользователя есть два свойства - angle
и position
, отвечающие за угол поворота и перемещение соответственно. За тот угол и за то перемещение, на которые необходимо повернуть/переместить фигуру. Для отлавливания изменений используются onAngleChanged
и onPositionChanged
, и по итогу получаем:
TransformPanel { id: transformPanel anchors.left: parent.left anchors.right: parent.right anchors.top: toolPanel.bottom anchors.topMargin: 10 visible: false positionLimits: Qt.vector3d(baseCuboid.xExtent, baseCuboid.yExtent, 20) onAngleChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.rotate(angle); } } onPositionChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.move(position); } } }
И на данном этапе в приоритете - добавление отсутствующих методов move()
/rotate()
к PaintObject
и обработка изменения активного объекта, за которым должно следовать изменение созданной панели. Начнем со второго. Сведения о выбранном объекте перекинем в QML из PaintEntity
, добавив свойство:
Q_PROPERTY(PaintObject *activePaintObject READ getPaintObject NOTIFY activePaintObjectChanged);
После NOTIFY
указано имя сигнала, который мы должны выдать в случае необходимости уведомления QML об изменениях activePaintObject
. Данная необходимость присутствует всегда при соответствующих изменениях, поэтому emit activePaintObjectChanged()
отправляется во все точки, где происходит смена активного объекта. На данный момент это два случая - непосредственно выбор объекта и удаление объекта, в том случае, если удаляемый был активным. Не частями накидывать, а полный код и проект размещу ближе к концу статьи.
В PaintObject
же внесем еще одно свойство, а именно position, которое и используем для задания начального положения TransformPanel
. Поскольку это свойство мы будем только считывать из QML, то объявление выглядит так:
Q_PROPERTY(QVector3D position READ getPosition);
Все, завершаем в main.qml:
PaintEntity { id: paintEntity onActivePaintObjectChanged: { transformPanel.visible = false; if (activePaintObject !== null) { transformPanel.initialPosition = activePaintObject.position; transformPanel.reset(); transformPanel.visible = true; } } }
Опять же текст предназначен, в первую очередь, для того, чтобы упростить навигацию и использование исходников в случае необходимости. Поэтому первичен именно полный код, и поэтому же я по возможности стараюсь не забивать статью непрерывными врезками с кодом )
А тем временем для получения работоспособной панели перемещения остались методы move()
и rotate()
:
Q_INVOKABLE virtual void rotate(const QVector3D &angle); Q_INVOKABLE virtual void move(const QVector3D &position);
void PaintObject::move(const QVector3D &position) { Qt3DCore::QTransform *transform = getTransform(); transform->setTranslation(position); } void PaintObject::rotate(const QVector3D &angle) { Qt3DCore::QTransform *transform = getTransform(); transform->setRotationX(transform->rotationX() + angle.x()); transform->setRotationY(transform->rotationY() + angle.y()); transform->setRotationZ(transform->rotationZ() + angle.z()); }
Полученный результат полностью удовлетворителен, так что 4-ю часть считаем завершенной!
#include <QDebug> #include "paintentity.h" #include "objects/cuboid.h" PaintEntity::PaintEntity(QNode *parent) : Qt3DCore::QEntity(parent), activeTool(ToolType::Cuboid), activePaintObject(nullptr) { } void PaintEntity::paintObjectPicked(quint32 pickedId) { if (activeTool == PaintEntity::ToolType::Select) { paintObjects.at(pickedId)->startSelectAnimation(); activePaintObject = paintObjects.at(pickedId).data(); emit activePaintObjectChanged(); } else { if (activeTool == PaintEntity::ToolType::Delete) { paintObjects.at(pickedId)->startDeleteAnimation(); } } } void PaintEntity::removePaintObject(quint32 objectId) { if (activePaintObject != nullptr) { if (objectId == activePaintObject->getId()) { activePaintObject = nullptr; emit activePaintObjectChanged(); } } paintObjects.remove(objectId); updatePaintObjectIds(objectId); } void PaintEntity::updatePaintObjectIds(int deletedIndex) { for (int i = deletedIndex; i < paintObjects.size(); i++) { paintObjects.at(i)->setId(i); } } void PaintEntity::onBasePlaneMousePressed(const QVector3D &coordinate) { switch (activeTool) { case PaintEntity::ToolType::Cuboid: paintObjects.append(QSharedPointer<PaintObject>(new Cuboid(this, paintObjects.size(), coordinate))); break; default: // This branch should never be executed break; } int currentIndex = paintObjects.size() - 1; QObject::connect(paintObjects.at(currentIndex).data(), &PaintObject::picked, this, &PaintEntity::paintObjectPicked); QObject::connect(paintObjects.at(currentIndex).data(), &PaintObject::readyForDeletion, this, &PaintEntity::removePaintObject); } void PaintEntity::onBasePlaneMouseMoved(const QVector3D &coordinate) { paintObjects.at(paintObjects.size() - 1)->onMouseMoved(coordinate); } PaintEntity::ToolType PaintEntity::getActiveTool() { return activeTool; } void PaintEntity::setActiveTool(ToolType tool) { activeTool = tool; bool pickerState = false; if ((activeTool == PaintEntity::ToolType::Select) || (activeTool == PaintEntity::ToolType::Delete)) { pickerState = true; } for (int i = 0; i < paintObjects.size(); i++) { paintObjects.at(i)->setPickerEnabled(pickerState); } } PaintObject* PaintEntity::getPaintObject() { return activePaintObject; }
#include <Qt3DCore> #include <Qt3DExtras> #include "objects/paintobject.h" class PaintEntity : public Qt3DCore::QEntity { Q_OBJECT public: enum class ToolType { Select = 0, Delete, Cuboid }; Q_ENUM(ToolType) PaintEntity(QNode *parent = nullptr); Q_PROPERTY(ToolType activeTool READ getActiveTool WRITE setActiveTool); Q_PROPERTY(PaintObject *activePaintObject READ getPaintObject NOTIFY activePaintObjectChanged); Q_INVOKABLE void onBasePlaneMousePressed(const QVector3D &coordinate); Q_INVOKABLE void onBasePlaneMouseMoved(const QVector3D &coordinate); PaintObject* getPaintObject(); ToolType getActiveTool(); void setActiveTool(ToolType tool); private slots: void paintObjectPicked(quint32 pickedId); void removePaintObject(quint32 objectId); signals: void activePaintObjectChanged(); private: QVector< QSharedPointer<PaintObject> > paintObjects; ToolType activeTool; PaintObject *activePaintObject; void updatePaintObjectIds(int deletedIndex); }; #endif // PAINTENTITY_H
PaintObject::PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId) : type(PaintObject::Type::None), id(objectId) { createAnimation(); entity = QSharedPointer<Qt3DCore::QEntity>(new Qt3DCore::QEntity(parentEntity)); picker = new Qt3DRender::QObjectPicker(); picker->setEnabled(false); picker->setHoverEnabled(true); entity->addComponent(picker); QObject::connect(picker, &Qt3DRender::QObjectPicker::clicked, this, &PaintObject::onClicked); } void PaintObject::createAnimation() { transformAnimationGroup = new QSequentialAnimationGroup(this); increaseAnimation = new QPropertyAnimation(this); increaseAnimation->setDuration(125); decreaseAnimation = new QPropertyAnimation(this); decreaseAnimation->setDuration(125); deleteAnimation = new QPropertyAnimation(this); deleteAnimation->setDuration(50); deleteAnimation->setEndValue(QVector3D(0, 0, 0)); QObject::connect(deleteAnimation, &QPropertyAnimation::finished, this, &PaintObject::deleteAnimationFinished); transformAnimationGroup->addAnimation(increaseAnimation); transformAnimationGroup->addAnimation(decreaseAnimation); } void PaintObject::startSelectAnimation() { QVector3D currentScale3D = getTransform()->scale3D(); if (transformAnimationGroup->state() == QAbstractAnimation::Stopped) { increaseAnimation->setTargetObject(getTransform()); increaseAnimation->setPropertyName("scale3D"); increaseAnimation->setStartValue(currentScale3D); increaseAnimation->setEndValue(currentScale3D * 1.15); decreaseAnimation->setTargetObject(getTransform()); decreaseAnimation->setPropertyName("scale3D"); decreaseAnimation->setStartValue(currentScale3D * 1.15); decreaseAnimation->setEndValue(currentScale3D); transformAnimationGroup->start(); } } void PaintObject::startDeleteAnimation() { setPickerEnabled(false); QVector3D currentScale3D = getTransform()->scale3D(); deleteAnimation->setTargetObject(getTransform()); deleteAnimation->setPropertyName("scale3D"); deleteAnimation->setStartValue(currentScale3D); deleteAnimation->start(); } void PaintObject::deleteAnimationFinished() { emit readyForDeletion(id); } Qt3DCore::QTransform* PaintObject::getTransform() { Qt3DCore::QTransform *transform = entity->componentsOfType<Qt3DCore::QTransform>().at(0); return transform; } void PaintObject::onClicked(Qt3DRender::QPickEvent *event) { if (event->button() == Qt3DRender::QPickEvent::LeftButton) { emit picked(id); } } void PaintObject::addMaterial() { Qt3DExtras::QDiffuseSpecularMaterial *defaultMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); defaultMaterial->setAmbient(QColor(Qt::black)); defaultMaterial->setSpecular(QColor(Qt::white)); defaultMaterial->setShininess(4); entity->addComponent(defaultMaterial); } quint32 PaintObject::getId() { return id; } void PaintObject::setId(quint32 objectId) { id = objectId; } PaintObject::Type PaintObject::getType() { return type; } void PaintObject::setType(Type objectType) { type = objectType; } void PaintObject::setPickerEnabled(bool state) { picker->setEnabled(state); } QVector3D PaintObject::getPosition() { Qt3DCore::QTransform *transform = getTransform(); return transform->translation(); } void PaintObject::move(const QVector3D &position) { Qt3DCore::QTransform *transform = getTransform(); transform->setTranslation(position); } void PaintObject::rotate(const QVector3D &angle) { Qt3DCore::QTransform *transform = getTransform(); transform->setRotationX(transform->rotationX() + angle.x()); transform->setRotationY(transform->rotationY() + angle.y()); transform->setRotationZ(transform->rotationZ() + angle.z()); }
#ifndef PAINTOBJECT_H #define PAINTOBJECT_H #include <QObject> #include <Qt3DCore> #include <Qt3DExtras> #include <QSharedPointer> #include "constants.h" class PaintObject : public QObject { Q_OBJECT public: enum class Type { None = 0, Cuboid, Sphere, Cone, Cylinder, Torus, ExtrudedText, Curve }; Q_ENUM(Type) PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId); Q_PROPERTY(Type type READ getType WRITE setType); Q_PROPERTY(QVector3D position READ getPosition); Type getType(); void setType(Type objectType); quint32 getId(); void setId(quint32 objectId); QVector3D getPosition(); void setPickerEnabled(bool state); void startSelectAnimation(); void startDeleteAnimation(); Q_INVOKABLE virtual void rotate(const QVector3D &angle); Q_INVOKABLE virtual void move(const QVector3D &position); virtual void onMouseMoved(const QVector3D &coordinate) = 0; virtual void addMaterial(); virtual Qt3DCore::QTransform* getTransform(); signals: void picked(quint32 pickedId); void readyForDeletion(quint32 objectId); private slots: virtual void onClicked(Qt3DRender::QPickEvent *event); void deleteAnimationFinished(); protected: Type type; QSharedPointer<Qt3DCore::QEntity> entity; QVector3D initialCoordinate; quint32 id; private: Qt3DRender::QObjectPicker *picker; QSequentialAnimationGroup *transformAnimationGroup; QPropertyAnimation *increaseAnimation; QPropertyAnimation *decreaseAnimation; QPropertyAnimation *deleteAnimation; void createAnimation(); }; #endif // PAINTOBJECT_H
import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import Qt3D.Core 2.15 import QtQuick3D 1.15 import QtQuick.Scene3D 2.15 import Qt3D.Render 2.15 import Qt3D.Input 2.15 import Qt3D.Extras 2.15 import QtQuick3D.Materials 1.15 import microtechnics.paintEntity 1.0 ApplicationWindow { id: window width: Screen.width * 3 / 4 height: Screen.height * 3 / 4 visible: true title: qsTr("mtPaint3D") color: "#505050" onClosing: { paintEntity.destroy(); } GridLayout { id: mainLayout rows: 12 columns: 6 flow: GridLayout.TopToBottom anchors.fill: parent component GridRectangle : Rectangle { Layout.fillHeight: true Layout.fillWidth: true Layout.preferredWidth: Layout.columnSpan Layout.preferredHeight: Layout.rowSpan color: "#808080" border.color: Qt.darker(color) border.width: 1 } GridRectangle { id: objectBlock Layout.row: 0 Layout.column: 0 Layout.rowSpan: 12 Layout.columnSpan: 1 ToolPanel { id: toolPanel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top onSelectedToolChanged: { if ((toolPanel.selectedTool === PaintEntity.ToolType.Select) || (toolPanel.selectedTool === PaintEntity.ToolType.Delete)) { baseCuboidPicker.paintEnabled = false; } else { baseCuboidPicker.paintEnabled = true; } paintEntity.activeTool = toolPanel.selectedTool; } } TransformPanel { id: transformPanel anchors.left: parent.left anchors.right: parent.right anchors.top: toolPanel.bottom anchors.topMargin: 10 visible: false positionLimits: Qt.vector3d(baseCuboid.xExtent, baseCuboid.yExtent, 20) onAngleChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.rotate(angle); } } onPositionChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.move(position); } } } } GridRectangle { id: viewBlock Layout.row: 0 Layout.column: 1 Layout.rowSpan: 1 Layout.columnSpan: 5 Row { id: viewButtonsRow spacing: 2 anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter Button { id: topViewButton text: qsTr("Top View") onClicked: ParallelAnimation { PropertyAnimation { target: camera; property: "position"; to: Qt.vector3d(0.0, 0.0, 35.0); duration: 675; } PropertyAnimation { target: camera; property: "upVector"; to: Qt.vector3d(0.0, 1.0, 0.0); duration: 675; } } } Button { id: frontViewButton text: qsTr("Front View") onClicked: ParallelAnimation { PropertyAnimation { target: camera; property: "position"; to: Qt.vector3d(0.0, -25.0, 25.0); duration: 675; } PropertyAnimation { target: camera; property: "upVector"; to: Qt.vector3d(0.0, 0.0, 1.0); duration: 675; } } } } } GridRectangle { id: paintBlock Layout.row: 1 Layout.column: 1 Layout.rowSpan: 11 Layout.columnSpan: 5 Scene3D { id: scene3D anchors.fill: parent focus: true aspects: ["input", "logic"] Entity { id: rootEntity Entity { id: lightEntity components: [ PointLight { color: "#ffffa7" intensity: 0.5 enabled: true }, Transform { translation: Qt.vector3d(10, 0, 10) } ] } Camera { id: camera projectionType: CameraLens.PerspectiveProjection fieldOfView: 45 nearPlane : 0.1 farPlane : 100 aspectRatio: 1 position: Qt.vector3d(0.0, -25.0, 25.0) upVector: Qt.vector3d(0.0, 0.0, 1.0) viewCenter: Qt.vector3d(0.0, 0.0, 0.0) } RenderSettings { id: renderSettings activeFrameGraph: ForwardRenderer { camera: camera clearColor: "transparent" } pickingSettings.pickMethod: PickingSettings.TrianglePicking } InputSettings { id: inputSettings } Entity { id: baseCuboidEntity CuboidMesh { id: baseCuboid xExtent: 35 yExtent: 25 zExtent: basePlaneZExtent } Transform { id: baseCuboidTransform } DiffuseSpecularMaterial { id: baseCuboidMaterial ambient: "#ffffff" shininess: 100 } ObjectPicker { id: baseCuboidPicker hoverEnabled: true dragEnabled: true enabled: true property bool isAddingPaintObject: false property bool paintEnabled: true onPressed: { if (paintEnabled === true) { if (pick.buttons === Qt.LeftButton) { isAddingPaintObject = true; paintEntity.onBasePlaneMousePressed(pick.worldIntersection); } } } onMoved: { if (paintEnabled === true) { if (isAddingPaintObject === true) { paintEntity.onBasePlaneMouseMoved(pick.worldIntersection); } } } onReleased: { isAddingPaintObject = false; } } components: [ baseCuboid, baseCuboidMaterial, baseCuboidTransform, baseCuboidPicker ] } PaintEntity { id: paintEntity onActivePaintObjectChanged: { transformPanel.visible = false; if (activePaintObject !== null) { transformPanel.initialPosition = activePaintObject.position; transformPanel.reset(); transformPanel.visible = true; } } } } } } } }
Исходный код и проект: mtPaint3D_day_4