refactor: Implement Rectangle tool with snapping and dimensions

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2026-02-16 20:45:39 -07:00
parent 2b455f57d4
commit 246372b847
9 changed files with 267 additions and 1 deletions

View File

@@ -2,6 +2,7 @@
#include "Document.h"
#include "SketchFeature.h"
#include "SketchLine.h"
#include "SketchRectangle.h"
#include "MainWindow.h"
#include <QInputDialog>
@@ -140,6 +141,13 @@ void ApplicationController::addLine(const gp_Pnt& start, const gp_Pnt& end)
}
}
void ApplicationController::addRectangle(const gp_Pnt& corner1, const gp_Pnt& corner2)
{
if (m_activeSketch) {
m_activeSketch->addObject(new SketchRectangle(corner1, corner2));
}
}
void ApplicationController::endSketch()
{
m_activeSketch = nullptr;

View File

@@ -32,6 +32,7 @@ public:
public slots:
void setActiveTool(ToolType tool);
void addLine(const gp_Pnt& start, const gp_Pnt& end);
void addRectangle(const gp_Pnt& corner1, const gp_Pnt& corner2);
void newDocument();
bool openDocument();
bool saveDocument();

View File

@@ -1,6 +1,7 @@
#include "SketchFeature.h"
#include "SketchObject.h"
#include "SketchLine.h"
#include "SketchRectangle.h"
#include <QJsonArray>
@@ -66,6 +67,10 @@ void SketchFeature::read(const QJsonObject& json)
auto line = new SketchLine();
line->read(objectJson);
m_objects.append(line);
} else if (type == "Rectangle") {
auto rect = new SketchRectangle();
rect->read(objectJson);
m_objects.append(rect);
}
}
}

View File

@@ -7,7 +7,8 @@ class SketchObject
{
public:
enum class ObjectType {
Line
Line,
Rectangle
};
SketchObject() = default;

61
src/SketchRectangle.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "SketchRectangle.h"
#include <QJsonArray>
namespace
{
void pointToJson(const gp_Pnt& p, QJsonArray& arr)
{
arr.append(p.X());
arr.append(p.Y());
arr.append(p.Z());
}
gp_Pnt jsonToPoint(const QJsonArray& arr)
{
return gp_Pnt(arr[0].toDouble(), arr[1].toDouble(), arr[2].toDouble());
}
}
SketchRectangle::SketchRectangle()
{
}
SketchRectangle::SketchRectangle(const gp_Pnt& corner1, const gp_Pnt& corner2)
: m_corner1(corner1), m_corner2(corner2)
{
}
SketchObject::ObjectType SketchRectangle::type() const
{
return ObjectType::Rectangle;
}
void SketchRectangle::read(const QJsonObject& json)
{
if (json.contains("corner1") && json["corner1"].isArray()) {
m_corner1 = jsonToPoint(json["corner1"].toArray());
}
if (json.contains("corner2") && json["corner2"].isArray()) {
m_corner2 = jsonToPoint(json["corner2"].toArray());
}
}
void SketchRectangle::write(QJsonObject& json) const
{
json["type"] = "Rectangle";
QJsonArray c1, c2;
pointToJson(m_corner1, c1);
pointToJson(m_corner2, c2);
json["corner1"] = c1;
json["corner2"] = c2;
}
const gp_Pnt& SketchRectangle::corner1() const
{
return m_corner1;
}
const gp_Pnt& SketchRectangle::corner2() const
{
return m_corner2;
}

26
src/SketchRectangle.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef SKETCHRECTANGLE_H
#define SKETCHRECTANGLE_H
#include "SketchObject.h"
#include <gp_Pnt.hxx>
class SketchRectangle : public SketchObject
{
public:
SketchRectangle();
SketchRectangle(const gp_Pnt& corner1, const gp_Pnt& corner2);
ObjectType type() const override;
void read(const QJsonObject& json) override;
void write(QJsonObject& json) const override;
const gp_Pnt& corner1() const;
const gp_Pnt& corner2() const;
private:
gp_Pnt m_corner1;
gp_Pnt m_corner2;
};
#endif // SKETCHRECTANGLE_H

View File

@@ -6,6 +6,7 @@
#include "FeatureBrowser.h"
#include "SketchFeature.h"
#include "SketchLine.h"
#include "SketchRectangle.h"
#include "SketchObject.h"
#include "ApplicationController.h"
#include <QMouseEvent>
@@ -196,6 +197,47 @@ void ViewportWidget::paintGL()
glDisable(GL_BLEND);
}
if (m_isDefiningRectangle && m_activeTool == static_cast<int>(ApplicationController::ToolType::Rectangle)) {
vertices.clear();
QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane);
QVector3D startPos(m_firstRectanglePoint.X(), m_firstRectanglePoint.Y(), m_firstRectanglePoint.Z());
if (m_isSnappingOrigin) {
worldPos.setX(0); worldPos.setY(0); worldPos.setZ(0);
} else if (m_isSnappingVertex) {
worldPos.setX(m_snapVertex.X()); worldPos.setY(m_snapVertex.Y()); worldPos.setZ(m_snapVertex.Z());
}
QVector3D p1 = startPos;
QVector3D p2, p3, p4;
p3 = worldPos;
if (m_currentPlane == SketchPlane::XY) {
p2.setX(p3.x()); p2.setY(p1.y()); p2.setZ(p1.z());
p4.setX(p1.x()); p4.setY(p1.y()); p4.setZ(p3.z());
} else if (m_currentPlane == SketchPlane::XZ) {
p2.setX(p3.x()); p2.setY(p1.y()); p2.setZ(p1.z());
p4.setX(p1.x()); p4.setY(p3.y()); p4.setZ(p1.z());
} else if (m_currentPlane == SketchPlane::YZ) {
p2.setX(p1.x()); p2.setY(p3.y()); p2.setZ(p1.z());
p4.setX(p1.x()); p4.setY(p1.y()); p4.setZ(p3.z());
}
vertices << p1.x() << p1.y() << p1.z();
vertices << p2.x() << p2.y() << p2.z();
vertices << p2.x() << p2.y() << p2.z();
vertices << p3.x() << p3.y() << p3.z();
vertices << p3.x() << p3.y() << p3.z();
vertices << p4.x() << p4.y() << p4.z();
vertices << p4.x() << p4.y() << p4.z();
vertices << p1.x() << p1.y() << p1.z();
m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 1.0f));
m_vbo.bind();
m_vbo.allocate(vertices.constData(), vertices.size() * sizeof(GLfloat));
glDrawArrays(GL_LINES, 0, 8);
}
if (m_isDefiningLine && m_activeTool == static_cast<int>(ApplicationController::ToolType::Line)) {
vertices.clear();
QVector3D worldPos;
@@ -544,6 +586,65 @@ void ViewportWidget::paintGL()
}
m_featureBrowser->paint(painter, width(), height());
if (m_isDefiningRectangle && m_activeTool == static_cast<int>(ApplicationController::ToolType::Rectangle)) {
QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane);
if (m_isSnappingOrigin) {
worldPos.setX(0); worldPos.setY(0); worldPos.setZ(0);
} else if (m_isSnappingVertex) {
worldPos.setX(m_snapVertex.X()); worldPos.setY(m_snapVertex.Y()); worldPos.setZ(m_snapVertex.Z());
}
QVector3D p1_3d(m_firstRectanglePoint.X(), m_firstRectanglePoint.Y(), m_firstRectanglePoint.Z());
QVector3D p3_3d = worldPos;
QVector3D p2_3d, p4_3d;
double w, h;
if (m_currentPlane == SketchPlane::XY) {
p2_3d.setX(p3_3d.x()); p2_3d.setY(p1_3d.y()); p2_3d.setZ(p1_3d.z());
p4_3d.setX(p1_3d.x()); p4_3d.setY(p1_3d.y()); p4_3d.setZ(p3_3d.z());
w = qAbs(p3_3d.x() - p1_3d.x());
h = qAbs(p3_3d.z() - p1_3d.z());
} else if (m_currentPlane == SketchPlane::XZ) {
p2_3d.setX(p3_3d.x()); p2_3d.setY(p1_3d.y()); p2_3d.setZ(p1_3d.z());
p4_3d.setX(p1_3d.x()); p4_3d.setY(p3_3d.y()); p4_3d.setZ(p1_3d.z());
w = qAbs(p3_3d.x() - p1_3d.x());
h = qAbs(p3_3d.y() - p1_3d.y());
} else { // YZ
p2_3d.setX(p1_3d.x()); p2_3d.setY(p3_3d.y()); p2_3d.setZ(p1_3d.z());
p4_3d.setX(p1_3d.x()); p4_3d.setY(p1_3d.y()); p4_3d.setZ(p3_3d.z());
w = qAbs(p3_3d.y() - p1_3d.y());
h = qAbs(p3_3d.z() - p1_3d.z());
}
painter.setRenderHint(QPainter::Antialiasing);
QFontMetrics fm(painter.font());
// Width dimension
QVector3D widthTextPos3D = (p1_3d + p2_3d) / 2.0f;
QVector3D screenPosW = project(widthTextPos3D, model, projection, rect());
if (screenPosW.z() < 1.0f) {
QString widthText = QString::number(w, 'f', 2);
QRect textRect = fm.boundingRect(widthText + "__");
textRect.moveCenter(screenPosW.toPoint() + QPoint(0, (p3_3d.z() > p1_3d.z() || p3_3d.y() > p1_3d.y()) ? -15 : 15));
painter.fillRect(textRect.adjusted(-4, -2, 4, 2), QColor(50, 50, 50));
painter.setPen(Qt::white);
painter.drawText(textRect, Qt::AlignCenter, widthText);
}
// Height dimension
QVector3D heightTextPos3D = (p2_3d + p3_3d) / 2.0f;
QVector3D screenPosH = project(heightTextPos3D, model, projection, rect());
if (screenPosH.z() < 1.0f) {
QString heightText = QString::number(h, 'f', 2);
QRect textRect = fm.boundingRect(heightText + "__");
textRect.moveCenter(screenPosH.toPoint() + QPoint((p3_3d.x() > p1_3d.x()) ? 15 : -15, 0));
painter.fillRect(textRect.adjusted(-4, -2, 4, 2), QColor(50, 50, 50));
painter.setPen(Qt::white);
painter.drawText(textRect, Qt::AlignCenter, heightText);
}
}
if (m_isDefiningLine && m_activeTool == static_cast<int>(ApplicationController::ToolType::Line)) {
QVector3D worldPos;
QVector3D startPos(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z());
@@ -802,6 +903,28 @@ void ViewportWidget::mousePressEvent(QMouseEvent *event)
return;
}
if (m_currentPlane != SketchPlane::NONE && m_activeTool == static_cast<int>(ApplicationController::ToolType::Rectangle)) {
gp_Pnt p;
if (m_isSnappingOrigin) {
p.SetCoord(0, 0, 0);
} else if (m_isSnappingVertex) {
p = m_snapVertex;
} else {
QVector3D worldPos = unproject(event->pos(), m_currentPlane);
p.SetCoord(worldPos.x(), worldPos.y(), worldPos.z());
}
if (!m_isDefiningRectangle) {
m_firstRectanglePoint = p;
m_isDefiningRectangle = true;
} else {
emit rectangleAdded(m_firstRectanglePoint, p);
m_isDefiningRectangle = false;
}
update();
return;
}
if (m_currentPlane != SketchPlane::NONE && m_activeTool == static_cast<int>(ApplicationController::ToolType::Line)) {
gp_Pnt p;
QString dimInput = property("dimensionInput").toString();
@@ -1202,6 +1325,12 @@ void ViewportWidget::keyPressEvent(QKeyEvent *event)
update();
return;
}
if (m_isDefiningRectangle) {
m_isDefiningRectangle = false;
emit toolDeactivated();
update();
return;
}
if (m_isSelectingPlane) {
m_isSelectingPlane = false;
m_highlightedPlane = SketchPlane::NONE;
@@ -1394,6 +1523,7 @@ void ViewportWidget::onActiveToolChanged(int tool)
{
m_activeTool = tool;
m_isDefiningLine = false;
m_isDefiningRectangle = false;
if (tool == static_cast<int>(ApplicationController::ToolType::None)) {
unsetCursor();
@@ -1499,6 +1629,36 @@ void ViewportWidget::drawSketch(const SketchFeature* sketch)
vertexCounts[start]++;
vertexCounts[end]++;
} else if (obj->type() == SketchObject::ObjectType::Rectangle) {
auto rect = static_cast<const SketchRectangle*>(obj);
const auto& p1 = rect->corner1();
const auto& p3 = rect->corner2();
gp_Pnt p2, p4;
if (sketch->plane() == SketchFeature::SketchPlane::XY) {
p2.SetCoord(p3.X(), p1.Y(), p1.Z());
p4.SetCoord(p1.X(), p1.Y(), p3.Z());
} else if (sketch->plane() == SketchFeature::SketchPlane::XZ) {
p2.SetCoord(p3.X(), p1.Y(), p1.Z());
p4.SetCoord(p1.X(), p3.Y(), p1.Z());
} else if (sketch->plane() == SketchFeature::SketchPlane::YZ) {
p2.SetCoord(p1.X(), p3.Y(), p1.Z());
p4.SetCoord(p1.X(), p1.Y(), p3.Z());
}
lineVertices << p1.X() << p1.Y() << p1.Z();
lineVertices << p2.X() << p2.Y() << p2.Z();
lineVertices << p2.X() << p2.Y() << p2.Z();
lineVertices << p3.X() << p3.Y() << p3.Z();
lineVertices << p3.X() << p3.Y() << p3.Z();
lineVertices << p4.X() << p4.Y() << p4.Z();
lineVertices << p4.X() << p4.Y() << p4.Z();
lineVertices << p1.X() << p1.Y() << p1.Z();
vertexCounts[p1] += 2;
vertexCounts[p2] += 2;
vertexCounts[p3] += 2;
vertexCounts[p4] += 2;
}
}

View File

@@ -46,6 +46,7 @@ public slots:
signals:
void lineAdded(const gp_Pnt& start, const gp_Pnt& end);
void rectangleAdded(const gp_Pnt& corner1, const gp_Pnt& corner2);
void planeSelected(SketchPlane plane);
void toolDeactivated();
@@ -91,6 +92,8 @@ private:
int m_activeTool = 0;
bool m_isDefiningLine = false;
gp_Pnt m_firstLinePoint;
bool m_isDefiningRectangle = false;
gp_Pnt m_firstRectanglePoint;
QPoint m_currentMousePos;
bool m_isSnappingOrigin = false;
bool m_isSnappingVertex = false;