Предпоследний, восьмой день, из отведенных девяти на разработку аналога Paint3D. В целом, времени с запасом, можно не спешить 😉 Среди добавленных вчера фигур не хватает одной из наиболее интересных, а именно кривой Безье. Вот восьмой день и надо ознаменовать решением этого вопроса!
Мне в данной утилите требуется на данный момент кривая 2-го порядка, то есть квадратичная кривая Безье, построенная соответственно по 3-м опорным точкам:
Положение любой из опорных точек в пространстве может быть изменено, в результате кривая должна быть перестроена в соответствии с новыми вводными (координатами точек). Акцент я сделаю как и во всей серии статей на конкретной реализации, если есть дополнительный интерес или вопросы, пишите в комментарии, буду рад ответить. Может будет просматриваться смысл в создании отдельной статьи на эту тему с более глубоким рассмотрением теоретических аспектов 🤔
Так вот, на практике мы будем находить координаты N точек кривой. Это будет первичными данными, на основе которых в дальнейшем получим 3D-объект на сцене. Но не забегаем вперед. Итак, по сути входные данные представляют из себя следующее:
- координаты опорных точек -
basePointsCoordinates
:
QVector<QVector3D> basePointsCoordinates;
- количество промежуточных точек кривой, которые мы будем рассчитывать -
curvePointsNum
:
constexpr quint32 curvePointsNum = 10;
- количество опорных точек -
pointsNum
:
constexpr quint32 pointsNum = 3;
Выходными данными этой локальной задачи будут координаты промежуточных точек - curvePointsCoordinates
:
QVector<QVector3D> curvePointsCoordinates;
Графически смысл такой. Для примера я тут привел 10 точек, чтобы было проще нарисовать ) При этом крайние точки совпадают с опорными.
Резюмируем - финальную задачу по отрисовке кривых Безье мы разбили на несколько локальных, первая из которых заключается в определении curvePointsCoordinates
из перечисленных выше входных данных 👍
Переходим к осуществлению... "Базовый" алгоритм можно реализовать так:
void Curve::computeCurvePointsCoordinates() { qreal tStep = static_cast<qreal>(1) / (curvePointsNum - 1); qreal t = 0; for(quint32 j = 0; j < curvePointsNum; j++) { QVector3D newPoint(0, 0, 0); for (quint32 i = 0; i < pointsNum; i++) { newPoint.setX(computeCoordinate(newPoint.x(), basePointsCoordinates.at(i).x(), i, t)); newPoint.setY(computeCoordinate(newPoint.y(), basePointsCoordinates.at(i).y(), i, t)); newPoint.setZ(computeCoordinate(newPoint.z(), basePointsCoordinates.at(i).z(), i, t)); } curvePointsCoordinates[j] = newPoint; t += tStep; }
Где также используются вспомогательные функции, по мере появления:
qreal Curve::computeCoordinate(qreal currentCoordinate, qreal baseCoordinate, quint32 baseIndex, qreal t) { currentCoordinate += computeBinomial((pointsNum - 1), baseIndex) * qPow(t, baseIndex) * qPow((1 - t), (pointsNum - 1 - baseIndex)) * baseCoordinate; return currentCoordinate; } quint32 Curve::computeFactorial(quint32 arg) { if (arg <= 1) { return 1; } else { arg = arg * computeFactorial(arg - 1); } return arg; } qreal Curve::computeBinomial(qreal arg1, qreal arg2) { qreal res; res = computeFactorial(arg1) / (computeFactorial(arg2) * computeFactorial(arg1 - arg2)); return res; }
На условном входе: basePointsCoordinates
, curvePointsNum
и pointsNum
.
На выходе: curvePointsCoordinates
.
Но данный алгоритм крайне печален с точки зрения производительности, причем это именно тот случай, когда можно упростить без потери качества. И получаем второй вариант, который и оставим в качестве основного:
void Curve::computeCurvePointsCoordinates() { qreal tStep = static_cast<qreal>(1) / (curvePointsNum - 1); qreal t = 0; for(quint32 j = 0; j < curvePointsNum; j++) { QVector3D newPoint(0, 0, 0); newPoint = (qPow(1 - t, 2) * basePointsCoordinates.at(0) + 2 * (1 - t) * t * basePointsCoordinates.at(1) + qPow(t, 2) * basePointsCoordinates.at(2)); curvePointsCoordinates[j] = newPoint; t += tStep; } }
Все, точки у нас есть, осталось произвести их графическое отображение. Зная возможности QML, которые нам предоставляются для базовых 3D-объектов, можем прикинуть, какие есть варианты для отображения кривой на сцене. И основных варианта два:
- представить кривую как набор близко расположенных друг к другу точек
- представить кривую как набор цилиндров небольшого диаметра
На изображениях я специально параметры "промежуточных" фигур сделал такими, чтобы была наглядно видна структура. В реальности мы сделаем так, чтобы кривая выглядела как единый объект. И остановимся на втором варианте, так как он намного более оптимален и удобен.
Соответственно, кривая будет составлена из (curvePointsNum - 1) цилиндров:
Логика прежняя, просто теперь один 3D-объект состоит из ряда Entity
, механизм от этого не меняется. Таким образом, в конструкторе объекта Curve
создаем все необходимые нам сущности:
Curve::Curve(Qt3DCore::QEntity *parentEntity, quint32 objectId, const QVector3D &coordinate) : PaintObject(parentEntity, objectId), activePointIndex(0) { type = PaintObject::Type::Curve; Qt3DExtras::QDiffuseSpecularMaterial *defaultMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); defaultMaterial->setSpecular(QColor(Qt::white)); defaultMaterial->setShininess(15); Qt3DExtras::QDiffuseSpecularMaterial *pointsMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); pointsMaterial->setSpecular(QColor(Qt::white)); pointsMaterial->setShininess(15); pointsMaterial->setAmbient(QColor("#218e59")); for (quint32 i = 0; i < curvePointsNum; i++) { curvePointsCoordinates.append(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); } for (quint32 i = 0; i < pointsNum; i++) { basePointsCoordinates.append(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); pointEntities.append(new Qt3DCore::QEntity(entity.data())); Qt3DExtras::QSphereMesh *mesh = new Qt3DExtras::QSphereMesh(); Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); mesh->setRadius(pointSphereRadius); transform->setTranslation(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); pointEntities.at(i)->addComponent(mesh); pointEntities.at(i)->addComponent(transform); pointEntities.at(i)->addComponent(pointsMaterial); } for (quint32 i = 0; i < segmentsNum; i++) { segmentEntities.append(new Qt3DCore::QEntity(entity.data())); Qt3DExtras::QCylinderMesh *mesh = new Qt3DExtras::QCylinderMesh(); Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); mesh->setRadius(segmentCylinderRadius); mesh->setRings(100); mesh->setSlices(100); mesh->setLength(0.75); transform->setTranslation(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); segmentEntities.at(i)->addComponent(mesh); segmentEntities.at(i)->addComponent(transform); segmentEntities.at(i)->addComponent(defaultMaterial); } initialCoordinate = coordinate; }
И при перемещении указателя:
void Curve::onMouseMoved(const QVector3D &coordinate) { basePointsCoordinates[endPointIndex] = coordinate; basePointsCoordinates[endPointIndex].setZ(basePointsCoordinates.at(endPointIndex).z() + pointSphereRadius); basePointsCoordinates[middlePointIndex] = (coordinate + initialCoordinate) / 2; basePointsCoordinates[middlePointIndex].setZ(basePointsCoordinates.at(middlePointIndex).z() + pointSphereRadius); computeCurvePointsCoordinates(); updateEntities(); }
Построение кривой сводится к двум этапам:
- математический этап, рассмотренный в начале статьи, заключается в расчете координат промежуточных точек кривой -
computeCurvePointsCoordinates()
- графический этап представляет из себя обновление отображаемых примитивов, из которых складывается 3D-кривая -
updateEntities()
void Curve::updateEntities() { for (int i = 0; i < pointEntities.size(); i++) { Qt3DCore::QTransform *currentTransform = getEntityTransform(pointEntities.at(i)); currentTransform->setTranslation(QVector3D(basePointsCoordinates.at(i).x(), basePointsCoordinates.at(i).y(), basePointsCoordinates.at(i).z())); } for (int i = 0; i < segmentEntities.size(); i++) { QVector3D neighborPointsVector = curvePointsCoordinates.at(i + 1) - curvePointsCoordinates.at(i); Qt3DCore::QTransform *currentTransform = getEntityTransform(segmentEntities.at(i)); QVector3D middlePoint = (curvePointsCoordinates.at(i) + curvePointsCoordinates.at(i + 1)) / 2; currentTransform->setTranslation(middlePoint); currentTransform->setRotation(QQuaternion::rotationTo(QVector3D(0, 1, 0), neighborPointsVector)); Qt3DExtras::QCylinderMesh *currentMesh = getSegmentEntityMesh(segmentEntities.at(i)); currentMesh->setLength(neighborPointsVector.length()); } }
Полный код и ссылка на проект будут в конце статьи. А текущий результат выглядит так:
Соответственно, для кривых Безье, как и ожидалось, методы PaintObject
'а будут переопределены. В частности, move()
свое базовое предназначение имеет в полном перемещении объекта, здесь же перемещаться будет лишь одна из опорных точек. Вслед за этим будет осуществляться "двухэтапная" перестройка всей кривой:
void Curve::move(const QVector3D &position) { basePointsCoordinates[activePointIndex] = position; computeCurvePointsCoordinates(); updateEntities(); }
Аналогично, при изменении цвета будет изменяться цвет промежуточных цилиндров, опорные точки я сделал фиксированными (с точки зрения их цвета). Никаких нужных параметров кривой для изменения я пока не придумал, поэтому панель CommonObjectPanel
смысла для кривых Безье в себе не несет. Функционала, который дает произвольное перемещение любой из трех точек в пространстве, мне достаточно, он, в общем-то, и был необходим изначально. Отрывки кода для вышеперечисленного, пожалуй, вставлять не буду, все равно удобнее посмотреть полностью 👇
Да, еще один момент. По клику на кривую и при срабатывании Qt3DRender::QObjectPicker
в случае кривой определяем ближайшую из трех опорных точек и помечаем ее как активную. Соответственно, благодаря этому она становится доступной для перемещения и всего остального.
Так же как мы делали для новых 3D-объектов в предыдущей части производим добавление кнопки на панель инструментов и сопутствующие изменения. И получаем:
P. S. Для кривой стали неактуальными кнопки для вращения объекта, поэтому к TransformPanel
я добавил свойство rotationEnabled
, которое позволяет не отображать эти режимы. Таким образом, для кривой мы имеем там только три кнопки для перемещения по осям.
На этом заканчиваем 8-й день! ) Все проходит четко по плану, остался один день, и осталось доработать дизайн, изначально и отложенный на финальные этапы 👌
И традиционно - код и ссылка:
#include "curve.h" constexpr quint32 pointsNum = 3; constexpr quint32 curvePointsNum = 40; constexpr quint32 segmentsNum = curvePointsNum - 1; constexpr qreal pointSphereRadius = 0.16; constexpr qreal segmentCylinderRadius = 0.05; constexpr quint32 middlePointIndex = 1; constexpr quint32 endPointIndex = 2; Curve::Curve(Qt3DCore::QEntity *parentEntity, quint32 objectId, const QVector3D &coordinate) : PaintObject(parentEntity, objectId), activePointIndex(0) { type = PaintObject::Type::Curve; Qt3DExtras::QDiffuseSpecularMaterial *defaultMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); defaultMaterial->setSpecular(QColor(Qt::white)); defaultMaterial->setShininess(15); Qt3DExtras::QDiffuseSpecularMaterial *pointsMaterial = new Qt3DExtras::QDiffuseSpecularMaterial(); pointsMaterial->setSpecular(QColor(Qt::white)); pointsMaterial->setShininess(15); pointsMaterial->setAmbient(QColor("#218e59")); for (quint32 i = 0; i < curvePointsNum; i++) { curvePointsCoordinates.append(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); } for (quint32 i = 0; i < pointsNum; i++) { basePointsCoordinates.append(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); pointEntities.append(new Qt3DCore::QEntity(entity.data())); Qt3DExtras::QSphereMesh *mesh = new Qt3DExtras::QSphereMesh(); Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); mesh->setRadius(pointSphereRadius); transform->setTranslation(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); pointEntities.at(i)->addComponent(mesh); pointEntities.at(i)->addComponent(transform); pointEntities.at(i)->addComponent(pointsMaterial); } for (quint32 i = 0; i < segmentsNum; i++) { segmentEntities.append(new Qt3DCore::QEntity(entity.data())); Qt3DExtras::QCylinderMesh *mesh = new Qt3DExtras::QCylinderMesh(); Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(); mesh->setRadius(segmentCylinderRadius); mesh->setRings(100); mesh->setSlices(100); mesh->setLength(0.75); transform->setTranslation(QVector3D(coordinate.x(), coordinate.y(), constants::basePlaneZExtent / 2 + pointSphereRadius)); segmentEntities.at(i)->addComponent(mesh); segmentEntities.at(i)->addComponent(transform); segmentEntities.at(i)->addComponent(defaultMaterial); } initialCoordinate = coordinate; } void Curve::addMaterial() { } void Curve::onMouseMoved(const QVector3D &coordinate) { basePointsCoordinates[endPointIndex] = coordinate; basePointsCoordinates[endPointIndex].setZ(basePointsCoordinates.at(endPointIndex).z() + pointSphereRadius); basePointsCoordinates[middlePointIndex] = (coordinate + initialCoordinate) / 2; basePointsCoordinates[middlePointIndex].setZ(basePointsCoordinates.at(middlePointIndex).z() + pointSphereRadius); computeCurvePointsCoordinates(); updateEntities(); } QVector<qreal> Curve::getParameters() { QVector<qreal> dummy; return dummy; } void Curve::update(const QVector<qreal> ¶meters) { Q_UNUSED(parameters); } Qt3DCore::QTransform* Curve::getEntityTransform(Qt3DCore::QEntity *targetEntity) { return targetEntity->componentsOfType<Qt3DCore::QTransform>().at(0); } Qt3DExtras::QDiffuseSpecularMaterial* Curve::getEntityMaterial(Qt3DCore::QEntity *targetEntity) { return targetEntity->componentsOfType<Qt3DExtras::QDiffuseSpecularMaterial>().at(0); } Qt3DExtras::QCylinderMesh* Curve::getSegmentEntityMesh(Qt3DCore::QEntity *targetEntity) { return targetEntity->componentsOfType<Qt3DExtras::QCylinderMesh>().at(0); } Qt3DCore::QTransform* Curve::getTransform() { Qt3DCore::QTransform *currentTransform = getEntityTransform(pointEntities.at(activePointIndex)); return currentTransform; } QString Curve::getColor() { QString color; Qt3DExtras::QDiffuseSpecularMaterial *currentMaterial = getEntityMaterial(segmentEntities.at(0)); color = currentMaterial->ambient().name(); return color; } void Curve::setColor(const QString &color) { for (int i = 0; i < segmentEntities.size(); i++) { Qt3DExtras::QDiffuseSpecularMaterial *currentMaterial = getEntityMaterial(segmentEntities.at(i)); currentMaterial->setAmbient(QColor(color)); } } void Curve::updateEntities() { for (int i = 0; i < pointEntities.size(); i++) { Qt3DCore::QTransform *currentTransform = getEntityTransform(pointEntities.at(i)); currentTransform->setTranslation(QVector3D(basePointsCoordinates.at(i).x(), basePointsCoordinates.at(i).y(), basePointsCoordinates.at(i).z())); } for (int i = 0; i < segmentEntities.size(); i++) { QVector3D neighborPointsVector = curvePointsCoordinates.at(i + 1) - curvePointsCoordinates.at(i); Qt3DCore::QTransform *currentTransform = getEntityTransform(segmentEntities.at(i)); QVector3D middlePoint = (curvePointsCoordinates.at(i) + curvePointsCoordinates.at(i + 1)) / 2; currentTransform->setTranslation(middlePoint); currentTransform->setRotation(QQuaternion::rotationTo(QVector3D(0, 1, 0), neighborPointsVector)); Qt3DExtras::QCylinderMesh *currentMesh = getSegmentEntityMesh(segmentEntities.at(i)); currentMesh->setLength(neighborPointsVector.length()); } } void Curve::onClicked(Qt3DRender::QPickEvent *event) { int minIndex = 0; qreal minDistance = 0; for (int i = 0; i < pointEntities.size(); i++) { Qt3DCore::QTransform *currentTransform = getEntityTransform(pointEntities.at(i)); QVector3D point = currentTransform->translation(); qreal dist = point.distanceToPoint(event->worldIntersection()); if (i == 0) { minDistance = dist; } else { if (dist < minDistance) { minIndex = i; minDistance = dist; } } } activePointIndex = minIndex; emit picked(id); } void Curve::move(const QVector3D &position) { basePointsCoordinates[activePointIndex] = position; computeCurvePointsCoordinates(); updateEntities(); } quint32 Curve::computeFactorial(quint32 arg) { if (arg <= 1) { return 1; } else { arg = arg * computeFactorial(arg - 1); } return arg; } qreal Curve::computeBinomial(qreal arg1, qreal arg2) { qreal res; res = computeFactorial(arg1) / (computeFactorial(arg2) * computeFactorial(arg1 - arg2)); return res; } qreal Curve::computeCoordinate(qreal currentCoordinate, qreal baseCoordinate, quint32 baseIndex, qreal t) { currentCoordinate += computeBinomial((pointsNum - 1), baseIndex) * qPow(t, baseIndex) * qPow((1 - t), (pointsNum - 1 - baseIndex)) * baseCoordinate; return currentCoordinate; } void Curve::computeCurvePointsCoordinates() { qreal tStep = static_cast<qreal>(1) / (curvePointsNum - 1); qreal t = 0; for(quint32 j = 0; j < curvePointsNum; j++) { QVector3D newPoint(0, 0, 0); // for (quint32 i = 0; i < pointsNum; i++) // { // newPoint.setX(computeCoordinate(newPoint.x(), basePointsCoordinates.at(i).x(), i, t)); // newPoint.setY(computeCoordinate(newPoint.y(), basePointsCoordinates.at(i).y(), i, t)); // newPoint.setZ(computeCoordinate(newPoint.z(), basePointsCoordinates.at(i).z(), i, t)); // } newPoint = (qPow(1 - t, 2) * basePointsCoordinates.at(0) + 2 * (1 - t) * t * basePointsCoordinates.at(1) + qPow(t, 2) * basePointsCoordinates.at(2)); curvePointsCoordinates[j] = newPoint; t += tStep; } }
#ifndef CURVE_H #define CURVE_H #include <QObject> #include "paintobject.h" class Curve : public PaintObject { Q_OBJECT public: Curve(Qt3DCore::QEntity *parentEntity, quint32 objectId, const QVector3D &coordinate = QVector3D(0, 0, 0)); void onMouseMoved(const QVector3D &coordinate); void update(const QVector<qreal> ¶meters); QVector<qreal> getParameters(); Qt3DCore::QTransform* getTransform(); void move(const QVector3D &position); void setColor(const QString &color); QString getColor(); private slots: void onClicked(Qt3DRender::QPickEvent *event); private: void computeCurvePointsCoordinates(); qreal computeBinomial(qreal arg1, qreal arg2); quint32 computeFactorial(quint32 arg); qreal computeCoordinate(qreal currentCoordinate, qreal baseCoordinate, quint32 baseIndex, qreal t); void updateEntities(); void addMaterial(); Qt3DCore::QTransform* getEntityTransform(Qt3DCore::QEntity *targetEntity); Qt3DExtras::QDiffuseSpecularMaterial* getEntityMaterial(Qt3DCore::QEntity *targetEntity); Qt3DExtras::QCylinderMesh* getSegmentEntityMesh(Qt3DCore::QEntity *targetEntity); QVector<QVector3D> curvePointsCoordinates; QVector<QVector3D> basePointsCoordinates; QVector<Qt3DCore::QEntity*> pointEntities; QVector<Qt3DCore::QEntity*> segmentEntities; quint32 activePointIndex; }; #endif // CURVE_H
#include <QDebug> #include "paintentity.h" #include "objects/cuboid.h" #include "objects/curve.h" #include "objects/cone.h" #include "objects/sphere.h" #include "objects/cylinder.h" #include "objects/torus.h" #include "objects/extrudedtext.h" PaintEntity::PaintEntity(QNode *parent) : Qt3DCore::QEntity(parent), activeTool(ToolType::Cuboid), activePaintObject(nullptr), activeColor("#b22222") { } 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; case PaintEntity::ToolType::Sphere: paintObjects.append(QSharedPointer<PaintObject>(new Sphere(this, paintObjects.size(), coordinate))); break; case PaintEntity::ToolType::Cone: paintObjects.append(QSharedPointer<PaintObject>(new Cone(this, paintObjects.size(), coordinate))); break; case PaintEntity::ToolType::Cylinder: paintObjects.append(QSharedPointer<PaintObject>(new Cylinder(this, paintObjects.size(), coordinate))); break; case PaintEntity::ToolType::Torus: paintObjects.append(QSharedPointer<PaintObject>(new Torus(this, paintObjects.size(), coordinate))); break; case PaintEntity::ToolType::ExtrudedText: paintObjects.append(QSharedPointer<PaintObject>(new ExtrudedText(this, paintObjects.size(), coordinate))); break; case PaintEntity::ToolType::Curve: paintObjects.append(QSharedPointer<PaintObject>(new Curve(this, paintObjects.size(), coordinate))); break; default: // This branch should never be executed break; } int currentIndex = paintObjects.size() - 1; paintObjects.at(currentIndex)->setColor(activeColor); 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); } void PaintEntity::setActiveColor(const QString &color) { activeColor = color; if (activeTool == PaintEntity::ToolType::Select) { if (activePaintObject != nullptr) { activePaintObject->setColor(activeColor); } } } 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; }
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 "../qml/objects" import microtechnics.paintEntity 1.0 import microtechnics.paintObject 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 ScrollView { clip: true anchors.fill: parent contentHeight: toolPanel.height + colorPanel.height + colorPanel.anchors.topMargin + transformPanel.height + transformPanel.anchors.topMargin + objectsPanels.childrenRect.height + extrudedTextPanel.anchors.topMargin 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; } } ColorPanel { id: colorPanel anchors.left: parent.left anchors.right: parent.right anchors.top: toolPanel.bottom anchors.topMargin: 10 visible: true onSelectedColorChanged: { paintEntity.setActiveColor(selectedColor); } } TransformPanel { id: transformPanel anchors.left: parent.left anchors.right: parent.right anchors.top: colorPanel.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); } } } Item { id: objectsPanels anchors.left: parent.left anchors.right: parent.right anchors.top: transformPanel.bottom CommonObjectPanel { id: commonObjectPanel anchors.left: parent.left anchors.right: parent.right visible: false onParametersChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.update(parameters); } } } ExtrudedTextPanel { id: extrudedTextPanel anchors.left: parent.left anchors.right: parent.right anchors.top: commonObjectPanel.bottom anchors.topMargin: 20 visible: false onTextInputChanged: { if (paintEntity.activePaintObject !== null) { paintEntity.activePaintObject.updateText(textInput); } } } } } } 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) } PaintCameraController { camera: camera lookSpeed: 35 } 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: { var hoverCoordinate = Qt.vector3d(pick.worldIntersection.x, pick.worldIntersection.y, basePlaneHoverTransform.translation.z); basePlaneHoverTransform.translation = hoverCoordinate; if (paintEnabled === true) { if (isAddingPaintObject === true) { paintEntity.onBasePlaneMouseMoved(pick.worldIntersection); } } } onReleased: { isAddingPaintObject = false; } } components: [ baseCuboid, baseCuboidMaterial, baseCuboidTransform, baseCuboidPicker ] } Entity { id: basePlaneHoverEntity enabled: true property var zCoordinate: 0.01 PlaneMesh { id: basePlaneHoverMesh width: 0.3 height: 0.3 } Transform { id: basePlaneHoverTransform translation: Qt.vector3d(0, 0, baseCuboid.zExtent + basePlaneHoverEntity.zCoordinate) rotationX: 90 } GoochMaterial { id: basePlaneHoverMaterial alpha: 1 cool: "#0000b0" } components: [ basePlaneHoverMesh, basePlaneHoverMaterial, basePlaneHoverTransform ] } PaintEntity { id: paintEntity function hideObjectControls() { for (var i = 0; i < objectsPanels.visibleChildren.length; i++) { objectsPanels.visibleChildren[i].visible = false; } transformPanel.visible = false; extrudedTextPanel.visible = false; commonObjectPanel.visible = false; } onActivePaintObjectChanged: { hideObjectControls(); if (activePaintObject !== null) { colorPanel.setCurrentItem(activePaintObject.color); if (activePaintObject.type === PaintObject.Curve) { transformPanel.rotationEnabled = false; } else { transformPanel.rotationEnabled = true; } transformPanel.initialPosition = activePaintObject.position; transformPanel.reset(); transformPanel.visible = true; switch(activePaintObject.type) { case PaintObject.Type.Cuboid: commonObjectPanel.controlsModel = [ {text: qsTr("X extent"), sliderMin: 0.1, sliderMax: baseCuboid.xExtent, sliderCurrent: activePaintObject.parameters[0]}, {text: qsTr("Y extent"), sliderMin: 0.1, sliderMax: baseCuboid.yExtent, sliderCurrent: activePaintObject.parameters[1]}, {text: qsTr("Z extent"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[2]} ]; break; case PaintObject.Type.Cone: commonObjectPanel.controlsModel = [ {text: qsTr("Length"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[0]}, {text: qsTr("Bottom radius"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[1]}, {text: qsTr("Top radius"), sliderMin: 0.1, sliderMax: 8, sliderCurrent: activePaintObject.parameters[2]} ]; break; case PaintObject.Type.Sphere: commonObjectPanel.controlsModel = [ {text: qsTr("Radius"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[0]} ]; break; case PaintObject.Type.Cylinder: commonObjectPanel.controlsModel = [ {text: qsTr("Length"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[0]}, {text: qsTr("Radius"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[1]} ]; break; case PaintObject.Type.Torus: commonObjectPanel.controlsModel = [ {text: qsTr("Minor radius"), sliderMin: 0.1, sliderMax: 1, sliderCurrent: activePaintObject.parameters[0]}, {text: qsTr("Radius"), sliderMin: 0.1, sliderMax: 10, sliderCurrent: activePaintObject.parameters[1]} ]; break; case PaintObject.Type.ExtrudedText: commonObjectPanel.controlsModel = [ {text: qsTr("Scale"), sliderMin: 0.1, sliderMax: 5, sliderCurrent: activePaintObject.parameters[0]} ]; extrudedTextPanel.initialText = activePaintObject.text; extrudedTextPanel.visible = true; break; default: break; } if (activePaintObject.type !== PaintObject.Type.Curve) { commonObjectPanel.visible = true; } } } } } } } } }
Исходный код и проект: mtPaint3D_day_8