feat: Implement feature graph and JSON document save/load

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2026-02-09 17:16:32 -07:00
parent bc851585f7
commit d2d49726d6
9 changed files with 340 additions and 0 deletions

View File

@@ -16,6 +16,9 @@ add_executable(OpenCAD
src/ViewportWidget.cpp src/ViewportWidget.cpp
src/ViewCube.cpp src/ViewCube.cpp
src/SketchGrid.cpp src/SketchGrid.cpp
src/Document.cpp
src/Feature.cpp
src/SketchFeature.cpp
resources.qrc resources.qrc
) )

90
src/Document.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "Document.h"
#include "Feature.h"
#include "SketchFeature.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
Document::Document(QObject *parent) : QObject(parent)
{
}
Document::~Document()
{
clear();
}
void Document::addFeature(Feature* feature)
{
m_features.append(feature);
}
void Document::clear()
{
qDeleteAll(m_features);
m_features.clear();
}
bool Document::save(const QString& path) const
{
QJsonArray featuresArray;
for (const Feature* feature : m_features) {
QJsonObject featureObject;
feature->write(featureObject);
featuresArray.append(featureObject);
}
QJsonObject rootObject;
rootObject["features"] = featuresArray;
QJsonDocument doc(rootObject);
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
file.write(doc.toJson());
return true;
}
bool Document::load(const QString& path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return false;
}
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull() || !doc.isObject()) {
return false;
}
clear();
QJsonObject rootObject = doc.object();
if (rootObject.contains("features") && rootObject["features"].isArray()) {
QJsonArray featuresArray = rootObject["features"].toArray();
for (const QJsonValue& value : featuresArray) {
QJsonObject obj = value.toObject();
if (obj.contains("type") && obj["type"].isString()) {
QString type = obj["type"].toString();
Feature* feature = nullptr;
if (type == "Sketch") {
feature = new SketchFeature("");
}
if (feature) {
feature->read(obj);
addFeature(feature);
}
}
}
}
return true;
}

26
src/Document.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef DOCUMENT_H
#define DOCUMENT_H
#include <QObject>
#include <QList>
class Feature;
class Document : public QObject
{
Q_OBJECT
public:
explicit Document(QObject *parent = nullptr);
~Document();
void addFeature(Feature* feature);
void clear();
bool save(const QString& path) const;
bool load(const QString& path);
private:
QList<Feature*> m_features;
};
#endif // DOCUMENT_H

28
src/Feature.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "Feature.h"
Feature::Feature(const QString& name) : m_name(name)
{
}
QString Feature::name() const
{
return m_name;
}
void Feature::setName(const QString& name)
{
m_name = name;
}
void Feature::read(const QJsonObject& json)
{
if (json.contains("name") && json["name"].isString()) {
m_name = json["name"].toString();
}
}
void Feature::write(QJsonObject& json) const
{
json["type"] = type();
json["name"] = m_name;
}

25
src/Feature.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef FEATURE_H
#define FEATURE_H
#include <QString>
#include <QJsonObject>
class Feature
{
public:
Feature(const QString& name);
virtual ~Feature() = default;
QString name() const;
void setName(const QString& name);
virtual QString type() const = 0;
virtual void read(const QJsonObject &json);
virtual void write(QJsonObject &json) const;
private:
QString m_name;
};
#endif // FEATURE_H

View File

@@ -1,6 +1,12 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "ViewportWidget.h" #include "ViewportWidget.h"
#include "Document.h"
#include "SketchFeature.h"
#include <QMenuBar>
#include <QMenu>
#include <QFileDialog>
#include <QMessageBox>
#include <QToolBar> #include <QToolBar>
#include <QTabWidget> #include <QTabWidget>
#include <QWidget> #include <QWidget>
@@ -16,6 +22,20 @@ MainWindow::MainWindow(QWidget *parent)
setWindowTitle("OpenCAD"); setWindowTitle("OpenCAD");
resize(1280, 720); resize(1280, 720);
QMenu *fileMenu = menuBar()->addMenu("&File");
QAction *newAction = fileMenu->addAction("&New");
connect(newAction, &QAction::triggered, this, &MainWindow::newFile);
QAction *openAction = fileMenu->addAction("&Open...");
connect(openAction, &QAction::triggered, this, &MainWindow::open);
QAction *saveAction = fileMenu->addAction("&Save");
connect(saveAction, &QAction::triggered, this, &MainWindow::save);
QAction *saveAsAction = fileMenu->addAction("Save &As...");
connect(saveAsAction, &QAction::triggered, this, &MainWindow::saveAs);
QToolBar* mainToolBar = addToolBar("Main Toolbar"); QToolBar* mainToolBar = addToolBar("Main Toolbar");
mainToolBar->setMovable(false); mainToolBar->setMovable(false);
mainToolBar->setFloatable(false); mainToolBar->setFloatable(false);
@@ -58,6 +78,9 @@ MainWindow::MainWindow(QWidget *parent)
m_viewport = new ViewportWidget; m_viewport = new ViewportWidget;
setCentralWidget(m_viewport); setCentralWidget(m_viewport);
m_document = new Document(this);
setCurrentFile(QString());
} }
void MainWindow::createSketch() void MainWindow::createSketch()
@@ -69,12 +92,75 @@ void MainWindow::createSketch()
QString item = QInputDialog::getItem(this, "Select Sketch Plane", QString item = QInputDialog::getItem(this, "Select Sketch Plane",
"Plane:", items, 0, false, &ok); "Plane:", items, 0, false, &ok);
if (ok && !item.isEmpty()) { if (ok && !item.isEmpty()) {
auto feature = new SketchFeature("Sketch");
if (item == "XY-Plane") { if (item == "XY-Plane") {
m_viewport->startSketch(ViewportWidget::SketchPlane::XY); m_viewport->startSketch(ViewportWidget::SketchPlane::XY);
feature->setPlane(SketchFeature::SketchPlane::XY);
} else if (item == "XZ-Plane") { } else if (item == "XZ-Plane") {
m_viewport->startSketch(ViewportWidget::SketchPlane::XZ); m_viewport->startSketch(ViewportWidget::SketchPlane::XZ);
feature->setPlane(SketchFeature::SketchPlane::XZ);
} else if (item == "YZ-Plane") { } else if (item == "YZ-Plane") {
m_viewport->startSketch(ViewportWidget::SketchPlane::YZ); m_viewport->startSketch(ViewportWidget::SketchPlane::YZ);
feature->setPlane(SketchFeature::SketchPlane::YZ);
} }
m_document->addFeature(feature);
} }
} }
void MainWindow::newFile()
{
m_document->clear();
setCurrentFile(QString());
}
void MainWindow::open()
{
const QString fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty()) {
if (!m_document->load(fileName)) {
QMessageBox::warning(this, tr("OpenCAD"),
tr("Cannot read file %1").arg(QDir::toNativeSeparators(fileName)));
return;
}
setCurrentFile(fileName);
}
}
bool MainWindow::save()
{
if (m_currentFile.isEmpty()) {
return saveAs();
} else {
if (!m_document->save(m_currentFile)) {
QMessageBox::warning(this, tr("OpenCAD"),
tr("Cannot write file %1").arg(QDir::toNativeSeparators(m_currentFile)));
return false;
}
return true;
}
}
bool MainWindow::saveAs()
{
QFileDialog dialog(this);
dialog.setWindowModality(Qt::WindowModal);
dialog.setAcceptMode(QFileDialog::AcceptSave);
if (dialog.exec() != QDialog::Accepted)
return false;
const QString fileName = dialog.selectedFiles().first();
setCurrentFile(fileName);
return save();
}
void MainWindow::setCurrentFile(const QString &fileName)
{
m_currentFile = fileName;
setWindowFilePath(m_currentFile);
QString shownName = m_currentFile;
if (m_currentFile.isEmpty())
shownName = "untitled.json";
setWindowTitle(tr("%1[*] - %2").arg(QFileInfo(shownName).fileName(), tr("OpenCAD")));
}

View File

@@ -4,6 +4,7 @@
#include <QMainWindow> #include <QMainWindow>
class ViewportWidget; class ViewportWidget;
class Document;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@@ -13,10 +14,18 @@ public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
private slots: private slots:
void newFile();
void open();
bool save();
bool saveAs();
void createSketch(); void createSketch();
private: private:
void setCurrentFile(const QString &fileName);
ViewportWidget *m_viewport; ViewportWidget *m_viewport;
Document *m_document;
QString m_currentFile;
}; };
#endif // MAINWINDOW_H #endif // MAINWINDOW_H

44
src/SketchFeature.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "SketchFeature.h"
SketchFeature::SketchFeature(const QString& name)
: Feature(name)
{
}
QString SketchFeature::type() const
{
return "Sketch";
}
void SketchFeature::setPlane(SketchPlane plane)
{
m_plane = plane;
}
SketchFeature::SketchPlane SketchFeature::plane() const
{
return m_plane;
}
void SketchFeature::read(const QJsonObject& json)
{
Feature::read(json);
if (json.contains("plane") && json["plane"].isString()) {
QString planeStr = json["plane"].toString();
if (planeStr == "XY") m_plane = SketchPlane::XY;
else if (planeStr == "XZ") m_plane = SketchPlane::XZ;
else if (planeStr == "YZ") m_plane = SketchPlane::YZ;
}
}
void SketchFeature::write(QJsonObject& json) const
{
Feature::write(json);
QString planeStr;
switch (m_plane) {
case SketchPlane::XY: planeStr = "XY"; break;
case SketchPlane::XZ: planeStr = "XZ"; break;
case SketchPlane::YZ: planeStr = "YZ"; break;
}
json["plane"] = planeStr;
}

29
src/SketchFeature.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef SKETCHFEATURE_H
#define SKETCHFEATURE_H
#include "Feature.h"
class SketchFeature : public Feature
{
public:
enum class SketchPlane {
XY,
XZ,
YZ
};
SketchFeature(const QString& name);
QString type() const override;
void setPlane(SketchPlane plane);
SketchPlane plane() const;
void read(const QJsonObject &json) override;
void write(QJsonObject &json) const override;
private:
SketchPlane m_plane;
};
#endif // SKETCHFEATURE_H