Top.Mail.Ru

Qt и QML. mtPaint3D. Создаем утилиту для 3D-рисования. День 4.

Рад снова всех приветствовать! Без лишних слов переходим к продолжению деятельности 🤐 Сегодня тоже план вполне понятен, то есть размышлять над последовательностью ближайших шагов не придется. Во-первых, во вчерашней части было одно упущение, которое стоит сразу устранить.

Заключается оно в том, что, как вы помните, у нас каждый 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-объектов:

mtPaint3D верстка

Вижу я эту панель примерно так:

Панель перемещения 3D-объекта

Шесть кнопок для разных типов трансформации:

  • перемещение по оси x
  • перемещение по y
  • перемещение по z
  • вращение вокруг оси x
  • вращение вокруг оси y
  • вращение вокруг оси z

И в зависимости от текущего режима будет меняться графический элемент интерфейса. Дизайн кнопок по-прежнему откладываем на более поздние этапы разработки, а вот элементам управления немного внимания все же уделим. Такой результат получился на практике:

QML move, rotate objects

Полный код панели:

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

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x