#include "document.h" #include "designerScene.h" #include "graphicsItem/graphicsBaseItem.h" #include "graphicsItem/graphicsRectItem.h" #include "graphicsItem/graphicsPolygonItem.h" #include "graphicsItem/graphicsItemGroup.h" #include "graphicsItem/itemControlHandle.h" #include "global.h" #include #include #include #include #include #include Document::Document(QObject *parent) : QObject(parent) , m_pScene(nullptr) , m_pUndoStack(nullptr) , m_bModified(false) , m_nSavedIndex(0) { setupNewScene(); m_pUndoStack = new QUndoStack(this); connect(m_pUndoStack, &QUndoStack::indexChanged, this, [this](int idx) { setModified(idx != m_nSavedIndex); }); } Document::~Document() { } void Document::setupNewScene() { if (m_pScene) { // Clear items only — do NOT delete the scene. The view in DrawingPanel // still references it, and deleting would leave a dangling pointer. m_pScene->clear(); } else { m_pScene = new DesignerScene(this); } } DesignerScene *Document::scene() const { return m_pScene; } QUndoStack *Document::undoStack() const { return m_pUndoStack; } bool Document::isModified() const { return m_bModified; } QString Document::filePath() const { return m_sFilePath; } void Document::setModified(bool modified) { if (m_bModified != modified) { m_bModified = modified; emit modifiedChanged(m_bModified); } } void Document::execute(QUndoCommand *cmd) { m_pUndoStack->push(cmd); } void Document::markDirty() { setModified(true); } void Document::clear() { m_nSavedIndex = 0; m_pUndoStack->clear(); setupNewScene(); m_sFilePath.clear(); setModified(false); emit filePathChanged(m_sFilePath); } // -- Pen style string conversion ------------------------------------------ static QString penStyleToString(Qt::PenStyle style) { switch (style) { case Qt::NoPen: return "none"; case Qt::SolidLine: return "solid"; case Qt::DashLine: return "dash"; case Qt::DotLine: return "dot"; case Qt::DashDotLine: return "dashDot"; case Qt::DashDotDotLine: return "dashDotDot"; default: return "solid"; } } static Qt::PenStyle penStyleFromString(const QString &str) { static const QHash map = { {"none", Qt::NoPen}, {"solid", Qt::SolidLine}, {"dash", Qt::DashLine}, {"dot", Qt::DotLine}, {"dashDot", Qt::DashDotLine}, {"dashDotDot", Qt::DashDotDotLine} }; return map.value(str, Qt::SolidLine); } // -- Write helpers --------------------------------------------------------- static QJsonObject serializePen(const QPen &pen) { QJsonObject obj; obj["color"] = pen.color().name(QColor::HexArgb); obj["width"] = pen.widthF(); obj["style"] = penStyleToString(pen.style()); return obj; } static QJsonObject serializeBrush(const QBrush &brush) { QJsonObject obj; obj["color"] = brush.color().name(QColor::HexArgb); return obj; } static QJsonObject serializeCommon(QGraphicsItem *item, const QString &type) { QJsonObject obj; obj["type"] = type; obj["pos"] = QJsonObject{{"x", item->pos().x()}, {"y", item->pos().y()}}; obj["rotation"] = item->rotation(); obj["zValue"] = item->zValue(); return obj; } // -- Item serialization ---------------------------------------------------- QJsonObject Document::serializeItem(QGraphicsItem *item) const { // Groups use a different template instantiation (AbstractShapeType), // so qgraphicsitem_cast will not match them. // Check for groups first. GraphicsItemGroup *group = dynamic_cast(item); if (group) { QJsonObject obj = serializeCommon(item, "group"); obj["pen"] = serializePen(group->pen()); obj["brush"] = serializeBrush(group->brush()); QJsonArray children; const auto kids = group->childItems(); for (QGraphicsItem *child : kids) { if (qgraphicsitem_cast(child)) continue; QJsonObject childObj = serializeItem(child); if (!childObj.isEmpty()) children.append(childObj); } obj["children"] = children; return obj; } AbstractShape *shape = qgraphicsitem_cast(item); if (!shape) return {}; // Use dynamic_cast for type dispatch — GraphicsBaseItem subclasses do not // override QGraphicsItem::type(), so the GIT_* enum values are never returned. if (GraphicsRectItem *rectItem = dynamic_cast(item)) { bool isRound = rectItem->isRound(); QJsonObject obj = serializeCommon(item, isRound ? "roundRect" : "rect"); obj["width"] = shape->width(); obj["height"] = shape->height(); if (isRound) { obj["ratioX"] = rectItem->ratioX(); obj["ratioY"] = rectItem->ratioY(); } obj["pen"] = serializePen(shape->pen()); obj["brush"] = serializeBrush(shape->brush()); return obj; } if (GraphicPolygonItem *poly = dynamic_cast(item)) { QJsonObject obj = serializeCommon(item, "polygon"); QJsonArray pts; for (const QPointF &pt : poly->getPoints()) { pts.append(QJsonObject{{"x", pt.x()}, {"y", pt.y()}}); } obj["points"] = pts; obj["pen"] = serializePen(shape->pen()); obj["brush"] = serializeBrush(shape->brush()); return obj; } qWarning("Document::serializeItem: unrecognized item"); return {}; } // -- Item deserialization -------------------------------------------------- static void applyCommon(QGraphicsItem *item, const QJsonObject &obj) { QJsonObject pos = obj["pos"].toObject(); item->setPos(pos["x"].toDouble(), pos["y"].toDouble()); item->setRotation(obj["rotation"].toDouble()); item->setZValue(obj["zValue"].toDouble()); } template static void applyPen(AbstractShapeType *shape, const QJsonObject &penObj) { QPen pen; pen.setColor(QColor(penObj["color"].toString())); pen.setWidthF(penObj["width"].toDouble()); pen.setStyle(penStyleFromString(penObj["style"].toString())); shape->setPen(pen); } template static void applyBrush(AbstractShapeType *shape, const QJsonObject &brushObj) { shape->setBrushColor(QColor(brushObj["color"].toString())); } QGraphicsItem *Document::deserializeItem(const QJsonObject &obj) { // Note: deserializeItem creates the item but does NOT add it to the scene. // The caller (load() or a parent group) is responsible for scene addition. QString type = obj["type"].toString(); if (type == "group") { GraphicsItemGroup *group = new GraphicsItemGroup(); applyCommon(group, obj); QJsonArray children = obj["children"].toArray(); QList childItems; for (const QJsonValue &val : children) { QGraphicsItem *child = deserializeItem(val.toObject()); if (child) childItems.append(child); } // Child positions in JSON are group-relative, but addToGroup() // treats child pos() as scene-absolute. Convert before grouping. for (QGraphicsItem *child : childItems) child->setPos(group->mapToScene(child->pos())); group->addItems(childItems); applyPen(group, obj["pen"].toObject()); applyBrush(group, obj["brush"].toObject()); group->setHandleVisible(false); return group; } if (type == "rect" || type == "roundRect") { double w = obj["width"].toDouble(); double h = obj["height"].toDouble(); bool isRound = (type == "roundRect"); GraphicsRectItem *item = new GraphicsRectItem( QRectF(-w / 2, -h / 2, w, h).toRect(), isRound); applyCommon(item, obj); applyPen(item, obj["pen"].toObject()); applyBrush(item, obj["brush"].toObject()); if (isRound) { item->setRatioX(obj["ratioX"].toDouble(0.1)); item->setRatioY(obj["ratioY"].toDouble(0.1)); } item->updateCoordinate(); item->setHandleVisible(false); return item; } if (type == "polygon") { GraphicPolygonItem *item = new GraphicPolygonItem(); QJsonArray pts = obj["points"].toArray(); for (const QJsonValue &val : pts) { QJsonObject pt = val.toObject(); item->addPoint(QPointF(pt["x"].toDouble(), pt["y"].toDouble())); } item->endDrawing(); applyCommon(item, obj); applyPen(item, obj["pen"].toObject()); applyBrush(item, obj["brush"].toObject()); item->updateCoordinate(); item->setHandleVisible(false); return item; } return nullptr; } // -- Save / Load ----------------------------------------------------------- bool Document::save() { if (m_sFilePath.isEmpty()) return false; return saveAs(m_sFilePath); } bool Document::saveAs(const QString &filePath) { QJsonArray items; const auto sceneItems = m_pScene->items(); for (QGraphicsItem *item : sceneItems) { // Only serialize top-level items (children of groups are serialized recursively) if (item->parentItem()) continue; QJsonObject obj = serializeItem(item); if (!obj.isEmpty()) items.append(obj); } QJsonObject root; root["version"] = "1.0"; root["items"] = items; QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) return false; file.write(QJsonDocument(root).toJson()); file.close(); m_sFilePath = filePath; m_nSavedIndex = m_pUndoStack->index(); setModified(false); emit filePathChanged(m_sFilePath); emit saved(); return true; } bool Document::load(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) return false; QByteArray data = file.readAll(); file.close(); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) return false; QJsonObject root = doc.object(); // Clear current state without triggering undo m_nSavedIndex = 0; m_pUndoStack->clear(); setupNewScene(); QJsonArray items = root["items"].toArray(); for (const QJsonValue &val : items) { QGraphicsItem *item = deserializeItem(val.toObject()); if (item) m_pScene->addItem(item); } m_pScene->clearSelection(); m_sFilePath = filePath; m_nSavedIndex = 0; setModified(false); emit filePathChanged(m_sFilePath); emit loaded(); return true; }