Top.Mail.Ru

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

Продолжаем планомерно продвигаться к достижению необходимого результата. Вчера мы закончили на том, что осуществили отрисовку 3D-объекта в виде кубоида, так что логически верным будет добавить к приложению панель выбора инструментов. Сейчас у нас всегда рисуются кубоиды по событиям мыши, независимо ни от каких внешних факторов.

Здесь я решил поместить небольшую врезку-спойлер с финальным результатом. Пока здесь будет пусто (update: результат добавлен), но по окончанию проекта я размещу результаты работы. И в некоторых из следующих статей "марафона" продублирую аналогичным образом. Итак:

mtPaint3D

Данная панель по нашему приблизительному плану должна находиться здесь:

Утилита для 3D-рисования на C++.

В целом, по-прежнему это место кажется наиболее предпочтительным, так что так и делаем )

Добавим файл ToolPanel.qml, но предварительно добавим в PaintEntity enum со всеми вариантами инструментов. На данный момент это будут:

  • выбор объекта
  • удаление объекта
  • создание кубоида

Пока ограничимся этими тремя, соответственно, сегодня нужно будет реализовать логику выбора и удаления добавленных на сцену фигур:

class PaintEntity : public Qt3DCore::QEntity
{
    Q_OBJECT
public:
    enum class ToolType {
        Select = 0,
        Delete,
        Cuboid
    };
    Q_ENUM(ToolType)
// ...............

По традиции полный код и ссылки на проекты всегда находятся в конце статьи.

Возвращаемся к ToolPanel.qml и добавляем группу кнопок:

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.15
import microtechnics.paintEntity 1.0



Rectangle {
    id: rootRectangle
    color: "transparent"
    height: buttonsGrid.height

    property int selectedTool: PaintEntity.ToolType.Cuboid

    property var buttonsModel: [
        {name: qsTr("select"), type: PaintEntity.ToolType.Select},
        {name: qsTr("delete"), type: PaintEntity.ToolType.Delete},
        {name: qsTr("cuboid"), type: PaintEntity.ToolType.Cuboid}
    ]

    ButtonGroup {
        id: buttonGroup
        exclusive: true
    }

    Grid {
        id: buttonsGrid
        columns: 3
        spacing: 2
        anchors.horizontalCenter: parent.horizontalCenter

        Repeater {
            id: buttonsRepeater
            model: buttonsModel

            Button {
                checkable: true
                text: modelData['name']
                width: (rootRectangle.width - (buttonsGrid.columns - 1) * buttonsGrid.spacing) / buttonsGrid.columns
                height: width

                property var type: modelData['type']
                property var normalColor: "#a0a080"
                property var hoveredColor: "#a0a0a0"
                property var checkedColor: "#505050"

                ButtonGroup.group: buttonGroup

                checked: type === PaintEntity.ToolType.Cuboid

                background: Rectangle {
                    anchors.fill: parent
                    color: checked ? checkedColor : hovered ? hoveredColor : normalColor
                }

                onCheckedChanged: {
                    if (checked === true) {
                        selectedTool = type;
                    }
                }
            }
        }
    }
}

Количество кнопок и некоторые их свойства полностью определяются моделью buttonsModel. Если, а точнее, когда понадобятся еще кнопки, изменения будут заключаться только в добавлении элементов к buttonsModel, что мы и проделаем в дальнейшем по мере развития проекта.

При выборе инструмента рисования есть один очевидный нюанс, который заключается в том, что одновременно может быть выбран только один, соответственно, и кнопка нажата может быть только одна. В связи с чем для ButtonGroup выставляем:

exclusive: true

Далее кнопки помещаются в Grid, а само их создание происходит при помощи Repeater, которому мы указываем на модель buttonsModel. И для использования свойств, перечисленных в buttonsModel, применяется синтаксис вида:

text: modelData['name']

По итогу мы получим три кнопки, у которых текст и свойство type будут в точности соответствовать тому, что указано в buttonsModel. По причинам, описанным в первой части, продолжаем успешно забивать на разработку дизайна, установим лишь разные цвета для разных состояний:

  • Цвет кнопки, с которой ничего не происходит - property var normalColor: "#808080"
  • Button hovered - property var hoveredColor: "#a0a0a0"
  • Button checked - property var checkedColor: "#505050"

Эти цвета следуют прямиком в свойство color элемента Rectangle:

background: Rectangle {
	anchors.fill: parent
	color: checked ? checkedColor : hovered ? hoveredColor : normalColor
}

Не буду дальше на этом останавливаться, в случае возникновения вопросов, последние крайне приветствуются и в комментариях, и на форуме.

QML элемент для панели инструментов создан, вставляем его в положенное место в main.qml:

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

Контрольный запуск:

QML Tool Panel.

Все в порядке ) Опять возвращаемся к тому, что QML-часть приложения у нас отвечает строго за интерфейс, поэтому на выборе инструмента полномочия ToolPanel заканчиваются. Необходимо только передать данные пользовательского выбора в PaintEntity, для чего добавляем в класс соответствующее свойство и стандартные getter/setter для него:

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:

signals:

private:
    QVector< QSharedPointer<PaintObject> > paintObjects;
    ToolType activeTool;

};

И реализация в .cpp:

PaintEntity::ToolType PaintEntity::getActiveTool()
{
    return activeTool;
}



void PaintEntity::setActiveTool(ToolType tool)
{
    activeTool = tool;
}

И, кстати, что для кнопок, что для activeTool, по умолчанию я задал режим кубоида.

Поскольку свойство activeTool мы объявили с Q_PROPERTY, то из main.qml спокойно можем иметь к нему доступ, что и осуществляем в toolPanel:

onSelectedToolChanged: {
	paintEntity.activeTool = toolPanel.selectedTool;
}

Данный обработчик будет вызван при изменении свойства selectedTool в ToolPanel, это у нас происходит при нажатии какой-либо из кнопок панели. Никаких дополнительных объявлений не требуется - для добавляемых свойств QML автоматически генерирует событие вида onНазваниеСвойстваChanged.

На данном этапе можно примерно прикинуть, как у нас будет осуществляться выбор одной из имеющихся на сцене фигур при выборе инструмента Select Tool. И для этого я планирую использовать QObjectPicker, но не тот, который у нас прикреплен к baseCuboidEntity, а свой собственный для каждого из PaintObject. В принципе, пока на этом можно не зацикливаться, важен лишь только следующий вывод - при активации инструментов Delete и Select baseCuboidPicker нам не нужен. И onBasePlaneMousePressed() с onBasePlaneMouseMoved() вызываться не должны.

Поэтому банальнейшим образом добавляем к вышеупомянутому baseCuboidPicker еще одно свойство:

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

И, соответственно, при выборе в ToolPanel:

onSelectedToolChanged: {
	if ((toolPanel.selectedTool === PaintEntity.ToolType.Select) ||
		(toolPanel.selectedTool === PaintEntity.ToolType.Delete)) {
		baseCuboidPicker.paintEnabled = false;
	} else {
		baseCuboidPicker.paintEnabled = true;
	}
	
	paintEntity.activeTool = toolPanel.selectedTool;
}

Финишируем модификацией paintentity.cpp:

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;
    }
}

Это нужно для создания других фигур, опять же в недалеком будущем. На Select и Delete не повлияет, так как априори в данный метод мы не должны попадать при выборе этих инструментов.

С панелью инструментов на этом заканчиваем на сегодня, никаких катастрофически сложных вещей мы там не осуществили, поэтому и результат закономерно работоспособен:

Paint 3D в Qt.

И сразу переходим к оставшимся двум задачам на эту статью:

  • функционал выбора фигуры
  • функционал удаления фигуры

Как я уже мельком упомянул в обоих случаях будем использовать QObjectPicker. Добавим соответствующий член к классу PaintObject. Идея тут проста - при нажатии на конкретную фигуру будет использован конкретный же QObjectPicker, и из обработчика мы сможем прокинуть в PaintEntity данные о выбранном таким образом объекте 👍

Привожу только изменения и дополнения. Первое, в PaintObject - сигнал и слот. Сигнал - для передачи в PaintEntity, слот - для фиксирования события от QObjectPicker:

signals:
    void picked(quint32 pickedId);

private slots:
    virtual void onClicked(Qt3DRender::QPickEvent *event);

Второе, там же:

private:
    Qt3DRender::QObjectPicker *picker;

Третье:

PaintObject::PaintObject(Qt3DCore::QEntity *parentEntity, quint32 objectId) :
    type(PaintObject::Type::None),
    id(objectId)
{
    entity = QSharedPointer<Qt3DCore::QEntity>(new Qt3DCore::QEntity(parentEntity));
    picker = new Qt3DRender::QObjectPicker();
    picker->setEnabled(true);
    picker->setHoverEnabled(true);  

    entity->addComponent(picker);

    QObject::connect(picker, &Qt3DRender::QObjectPicker::clicked, this, &PaintObject::onClicked);
}

Четвертое - слот:

void PaintObject::onClicked(Qt3DRender::QPickEvent *event)
{
    if (event->button() == Qt3DRender::QPickEvent::LeftButton)
    {
        emit picked(id);
    }
}

Таким образом, PaintObject будет пробрасывать свой id для идентификации объекта. На то он и идентификатор. В PaintEntity объявляем слот и его реализацию:

private slots:
    void paintObjectPicked(quint32 pickedId);
void PaintEntity::paintObjectPicked(quint32 pickedId)
{
    qDebug() << "Object picked" << pickedId;
}

Остается законнектить сигнал и слот. Данное действие производим после создания 3D-объекта:

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

Результат не заставляет себя ждать:

QML ObjectPicker.

Неплохо бы дать пользователю обратную связь при выборе того или иного объекта. Для этого я решил добавить анимированное изменение размеров объекта, подвергнутого нажатию ) Не буду подробно описывать, ключевые строки в PaintEntity (полный код в конце статьи):

void PaintEntity::createAnimation()
{
    transformAnimationGroup = new QSequentialAnimationGroup(this);

    increaseAnimation = new QPropertyAnimation(this);
    increaseAnimation->setDuration(125);

    decreaseAnimation = new QPropertyAnimation(this);
    decreaseAnimation->setDuration(125);

    transformAnimationGroup->addAnimation(increaseAnimation);
    transformAnimationGroup->addAnimation(decreaseAnimation);
}
void PaintEntity::paintObjectPicked(quint32 pickedId)
{
    if (activeTool == PaintEntity::ToolType::Select)
    {
        activePaintObject = paintObjects.at(pickedId).data();

        QVector3D currentScale3D = activePaintObject->getTransform()->scale3D();

        increaseAnimation->setTargetObject(activePaintObject->getTransform());
        increaseAnimation->setPropertyName("scale3D");
        increaseAnimation->setStartValue(currentScale3D);
        increaseAnimation->setEndValue(currentScale3D * 1.15);

        decreaseAnimation->setTargetObject(activePaintObject->getTransform());
        decreaseAnimation->setPropertyName("scale3D");
        decreaseAnimation->setStartValue(currentScale3D * 1.15);
        decreaseAnimation->setEndValue(currentScale3D);

        transformAnimationGroup->start();
    }
}

increaseAnimation производит увеличение геометрических размеров объекта в 1.15 раз за 125 мс, decreaseAnimation - аналогичный возврат к исходным размерам. transformAnimationGroup отвечает за последовательное выполнение добавленных к ней QPropertyAnimation.

Между делом я добавил к PaintEntity указатель на текущий активный (выбранный) элемент:

PaintObject *activePaintObject;

И метод getTransform() к PaintObject для получения компонента Qt3DCore::QTransform:

virtual Qt3DCore::QTransform* getTransform();
Qt3DCore::QTransform* PaintObject::getTransform()
{
    Qt3DCore::QTransform *transform = entity->componentsOfType<Qt3DCore::QTransform>().at(0);
    return transform;
}

И снова сразу делаем его виртуальным, поскольку есть четкое осознание того, что для кривых Безье многие методы будут переопределены. Я по мере возможности, конечно, накидал необходимый код, но очевидно, что полная картина может быть только от полных исходников. Которые будут наличествовать в конце статьи 👇

Собираем, запускаем, созерцаем:

При наличии обратной связи о предпринятом действии у пользователя уже совсем другие ощущения при работе. Остается нечто похожее сделать и для удаления объекта. Также используем QPropertyAnimation, но ключевое отличие будет заключаться в том, что удалить объект мы не можем, пока он используется для анимации. К счастью, QPropertyAnimation имеет сигнал finished, который мы и используем:

QObject::connect(deleteAnimation, &QPropertyAnimation::finished,
	         this, &PaintEntity::deleteAnimationFinished);

Итоговый процесс:

void PaintEntity::paintObjectPicked(quint32 pickedId)
{
    if (activeTool == PaintEntity::ToolType::Select)
    {
        activePaintObject = paintObjects.at(pickedId).data();

        QVector3D currentScale3D = activePaintObject->getTransform()->scale3D();

        increaseAnimation->setTargetObject(activePaintObject->getTransform());
        increaseAnimation->setPropertyName("scale3D");
        increaseAnimation->setStartValue(currentScale3D);
        increaseAnimation->setEndValue(currentScale3D * 1.15);

        decreaseAnimation->setTargetObject(activePaintObject->getTransform());
        decreaseAnimation->setPropertyName("scale3D");
        decreaseAnimation->setStartValue(currentScale3D * 1.15);
        decreaseAnimation->setEndValue(currentScale3D);

        transformAnimationGroup->start();
    }
    else
    {
        if (activeTool == PaintEntity::ToolType::Delete)
        {
            objectToDeleteId = pickedId;

            QVector3D currentScale3D = paintObjects.at(objectToDeleteId)->getTransform()->scale3D();

            deleteAnimation->setTargetObject(paintObjects.at(objectToDeleteId)->getTransform());
            deleteAnimation->setPropertyName("scale3D");
            deleteAnimation->setStartValue(currentScale3D);

            deleteAnimation->start();
        }
    }
}



void PaintEntity::deleteAnimationFinished()
{
    removePaintObject(objectToDeleteId);
}



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

Так выглядит процедура удаления объекта:

Несколько нюансов, достойных отдельного упоминания:

  • как уже обсудили - удаление производим по окончанию анимации
  • проверяем случай, если удаляемый объект является активным
  • поскольку id всех элементов численно равен их индексу в paintObjects, то после удаления какого-либо из них, следует переопределить id оставшихся

Все-таки пока не особо получается опускать мелкие детали, поэтому получилось относительно объемно, так что нам не остается ничего иного, как на сегодня завершить конструктивную деятельность, до скорого!

Исходный код и проект: mtPaint3D_day_3

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