diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index ed5b4e31..7c8008ca 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -1462,11 +1462,6 @@ bool Window::stepFrame() { this->uiRenderables.erase(std::remove(this->uiRenderables.begin(), this->uiRenderables.end(), obj), this->uiRenderables.end()); - if (auto *fluid = dynamic_cast(obj)) { - this->lateFluids.erase(std::remove(this->lateFluids.begin(), - this->lateFluids.end(), fluid), - this->lateFluids.end()); - } } this->pendingRemovals.clear(); @@ -3122,32 +3117,43 @@ void Window::removeObject(Renderable *obj) { editorActiveGizmoAxis = 0; } - if (this->physicsWorld != nullptr) { - this->pendingRemovals.push_back(obj); + const auto pendingObject = + std::find(this->pendingObjects.begin(), this->pendingObjects.end(), obj); + const bool wasPending = pendingObject != this->pendingObjects.end(); + if (wasPending) { + this->pendingObjects.erase(pendingObject); + } + + this->lateForwardRenderables.erase( + std::remove(this->lateForwardRenderables.begin(), + this->lateForwardRenderables.end(), obj), + this->lateForwardRenderables.end()); + this->preferenceRenderables.erase( + std::remove(this->preferenceRenderables.begin(), + this->preferenceRenderables.end(), obj), + this->preferenceRenderables.end()); + this->firstRenderables.erase(std::remove(this->firstRenderables.begin(), + this->firstRenderables.end(), obj), + this->firstRenderables.end()); + this->uiRenderables.erase(std::remove(this->uiRenderables.begin(), + this->uiRenderables.end(), obj), + this->uiRenderables.end()); + if (auto *fluid = dynamic_cast(obj)) { + this->lateFluids.erase(std::remove(this->lateFluids.begin(), + this->lateFluids.end(), fluid), + this->lateFluids.end()); + } + + if (this->physicsWorld != nullptr && !wasPending) { + if (std::find(this->pendingRemovals.begin(), + this->pendingRemovals.end(), obj) == + this->pendingRemovals.end()) { + this->pendingRemovals.push_back(obj); + } } else { this->renderables.erase(std::remove(this->renderables.begin(), this->renderables.end(), obj), this->renderables.end()); - this->lateForwardRenderables.erase( - std::remove(this->lateForwardRenderables.begin(), - this->lateForwardRenderables.end(), obj), - this->lateForwardRenderables.end()); - this->preferenceRenderables.erase( - std::remove(this->preferenceRenderables.begin(), - this->preferenceRenderables.end(), obj), - this->preferenceRenderables.end()); - this->firstRenderables.erase(std::remove(this->firstRenderables.begin(), - this->firstRenderables.end(), - obj), - this->firstRenderables.end()); - this->uiRenderables.erase(std::remove(this->uiRenderables.begin(), - this->uiRenderables.end(), obj), - this->uiRenderables.end()); - if (auto *fluid = dynamic_cast(obj)) { - this->lateFluids.erase(std::remove(this->lateFluids.begin(), - this->lateFluids.end(), fluid), - this->lateFluids.end()); - } } this->shadowMapsDirty = true; this->shadowUpdateCooldown = 0.0f; diff --git a/editor/core/dockManager.cpp b/editor/core/dockManager.cpp index aaaa4725..30fd01ee 100644 --- a/editor/core/dockManager.cpp +++ b/editor/core/dockManager.cpp @@ -34,15 +34,18 @@ ads::DockWidgetArea EditorDockManager::toAdsArea(EditorDockArea area) const { ads::CDockWidget* EditorDockManager::addPanel(const EditorDockPanelDesc& desc) { auto* dock = new ads::CDockWidget(desc.title); dock->setWidget(desc.widget); + dock->setFeature(ads::CDockWidget::DockWidgetMovable, true); + dock->setFeature(ads::CDockWidget::DockWidgetFloatable, true); if (!desc.icon.isNull()) { dock->setIcon(desc.icon); } if (desc.area == EditorDockArea::Center) { - centerArea = dockManager->setCentralWidget(dock); + centerArea = dockManager->addDockWidget(ads::CenterDockWidgetArea, + dock); if (centerArea != nullptr) { - centerArea->setAllowedAreas(ads::OuterDockAreas); + centerArea->setAllowedAreas(ads::AllDockAreas); } } else if (centerArea) { dockManager->addDockWidget(toAdsArea(desc.area), dock, centerArea); diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index dba9a163..29fdcc2c 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -56,7 +57,7 @@ void EditorWindow::setupWindow() { ads::CDockManager::setConfigFlag( ads::CDockManager::DockAreaHasTabsMenuButton, false); ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, - false); + true); ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, false); @@ -72,7 +73,14 @@ void EditorWindow::setupMenus() { "Open"); auto *saveAction = fileMenu->addAction( style()->standardIcon(QStyle::SP_DialogSaveButton), "Save Scene"); + saveAction->setShortcut(QKeySequence::Save); + saveAction->setShortcutContext(Qt::ApplicationShortcut); connect(saveAction, &QAction::triggered, this, [this] { + if (materialEditorPanel != nullptr && + materialEditorPanel->isVisible()) { + materialEditorPanel->saveMaterial(); + return; + } if (viewportPanel != nullptr) { viewportPanel->saveRuntimeScene(); } @@ -82,8 +90,30 @@ void EditorWindow::setupMenus() { "Quit", this, &QWidget::close); auto *editMenu = menuBar()->addMenu("Edit"); - editMenu->addAction(style()->standardIcon(QStyle::SP_ArrowBack), "Undo"); - editMenu->addAction(style()->standardIcon(QStyle::SP_ArrowForward), "Redo"); + auto *undoAction = editMenu->addAction( + style()->standardIcon(QStyle::SP_ArrowBack), "Undo"); + undoAction->setShortcut(QKeySequence::Undo); + undoAction->setShortcutContext(Qt::ApplicationShortcut); + connect(undoAction, &QAction::triggered, this, [this] { + if (materialEditorPanel != nullptr && + materialEditorPanel->isVisible()) { + materialEditorPanel->undo(); + } else if (viewportPanel != nullptr) { + viewportPanel->undo(); + } + }); + auto *redoAction = editMenu->addAction( + style()->standardIcon(QStyle::SP_ArrowForward), "Redo"); + redoAction->setShortcut(QKeySequence::Redo); + redoAction->setShortcutContext(Qt::ApplicationShortcut); + connect(redoAction, &QAction::triggered, this, [this] { + if (materialEditorPanel != nullptr && + materialEditorPanel->isVisible()) { + materialEditorPanel->redo(); + } else if (viewportPanel != nullptr) { + viewportPanel->redo(); + } + }); editMenu->addSeparator(); editMenu->addAction("Preferences"); @@ -106,7 +136,7 @@ void EditorWindow::setupMenus() { void EditorWindow::setupDocks() { viewportPanel = new ViewportPanel(projectFile); auto *viewportTools = new ViewportTools(viewportPanel); - dockManager->addPanel( + auto *viewportDock = dockManager->addPanel( {.id = "viewport", .title = "Viewport", .widget = viewportTools, @@ -121,7 +151,7 @@ void EditorWindow::setupDocks() { .area = EditorDockArea::Left, .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); - inspectorPanel = new InspectorPanel(viewportPanel); + inspectorPanel = new InspectorPanel(viewportPanel, projectFile); dockManager->addPanel( {.id = "inspector", .title = "Inspector", @@ -137,13 +167,16 @@ void EditorWindow::setupDocks() { .area = EditorDockArea::Bottom, .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); - auto *materialEditor = new MaterialEditorPanel(viewportPanel); + materialEditorPanel = new MaterialEditorPanel(viewportPanel); auto *materialDock = dockManager->addPanel( {.id = "materialEditor", .title = "Material Editor", - .widget = materialEditor, + .widget = materialEditorPanel, .area = EditorDockArea::Right, .icon = style()->standardIcon(QStyle::SP_FileDialogContentsView)}); + coreManager->addDockWidgetTabToArea(materialDock, + viewportDock->dockAreaWidget()); + viewportDock->setAsCurrentTab(); connect(hierarchyPanel, &HierarchyPanel::objectActivated, inspectorPanel, &InspectorPanel::inspectRuntimeObject); @@ -163,8 +196,8 @@ void EditorWindow::setupDocks() { this->inspectorPanel->inspectFile(path); }); connect(contentBrowser, &ContentBrowserPanel::assetActivated, this, - [materialEditor, materialDock](const QString &path) { - materialEditor->openMaterial(path); + [this, materialDock](const QString &path) { + materialEditorPanel->openMaterial(path); materialDock->toggleView(true); materialDock->raise(); }); @@ -175,7 +208,7 @@ void EditorWindow::saveLayout() { settings.setValue("window/geometry", saveGeometry()); settings.setValue("window/state", saveState()); - settings.setValue("docking/state/v2", coreManager->saveState(2)); + settings.setValue("docking/state/v4", coreManager->saveState(4)); if (inspectorPanel != nullptr) { settings.setValue("panels/inspector/width", inspectorPanel->width()); } @@ -199,10 +232,10 @@ void EditorWindow::restoreLayout() { restoreState(settings.value("window/state").toByteArray()); const QByteArray dockState = - settings.value("docking/state/v2").toByteArray(); + settings.value("docking/state/v4").toByteArray(); if (!dockState.isEmpty()) { - coreManager->restoreState(dockState, 2); + coreManager->restoreState(dockState, 4); } const int inspectorWidth = diff --git a/editor/views/editor/hierarchy.cpp b/editor/views/editor/hierarchy.cpp index 4103bfb4..fbf4fbe9 100644 --- a/editor/views/editor/hierarchy.cpp +++ b/editor/views/editor/hierarchy.cpp @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +131,10 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) treeView->setSelectionMode(QAbstractItemView::SingleSelection); treeView->setContextMenuPolicy(Qt::CustomContextMenu); treeView->setUniformRowHeights(true); + treeView->setAcceptDrops(true); + treeView->setDragDropMode(QAbstractItemView::DropOnly); + treeView->viewport()->setAcceptDrops(true); + treeView->viewport()->installEventFilter(this); layout->addWidget(treeView); auto *addMenu = new QMenu(addButton); @@ -256,7 +263,46 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, root->setData(-1, ObjectIdRole); root->setData("scene", ObjectTypeRole); root->setEditable(false); - appendObjects(root, objects); + QJsonArray regularObjects; + QJsonArray cameraObjects; + QJsonArray lightObjects; + for (const QJsonValue &value : objects) { + const QString type = value.toObject().value("type").toString().toLower(); + if (type.contains("light") || type == "sun") { + lightObjects.append(value); + } else if (type.contains("camera")) { + cameraObjects.append(value); + } else { + regularObjects.append(value); + } + } + appendObjects(root, regularObjects); + + auto *cameras = + new QStandardItem(hierarchyIcon(this, "camera"), "Cameras"); + cameras->setData(-1, ObjectIdRole); + cameras->setEditable(false); + cameras->setSelectable(false); + auto *mainCamera = + new QStandardItem(hierarchyIcon(this, "camera"), "Main Camera"); + mainCamera->setData(-1, ObjectIdRole); + mainCamera->setData("camera", ObjectTypeRole); + mainCamera->setToolTip("Scene camera"); + mainCamera->setEditable(false); + mainCamera->setSelectable(false); + cameras->appendRow(mainCamera); + appendObjects(cameras, cameraObjects); + root->appendRow(cameras); + + if (!lightObjects.isEmpty()) { + auto *lights = + new QStandardItem(hierarchyIcon(this, "light"), "Lights"); + lights->setData(-1, ObjectIdRole); + lights->setEditable(false); + lights->setSelectable(false); + appendObjects(lights, lightObjects); + root->appendRow(lights); + } model->appendRow(root); treeView->expandAll(); @@ -268,6 +314,42 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, applyingSnapshot = false; } +bool HierarchyPanel::eventFilter(QObject *watched, QEvent *event) { + if (treeView != nullptr && watched == treeView->viewport() && + (event->type() == QEvent::DragEnter || + event->type() == QEvent::DragMove || event->type() == QEvent::Drop)) { + auto *drop = static_cast(event); + const QModelIndex index = treeView->indexAt(drop->position().toPoint()); + const int objectId = index.data(ObjectIdRole).toInt(); + if (drop->mimeData()->hasUrls() && objectId >= 0) { + const QString suffix = + QFileInfo(drop->mimeData()->urls().constFirst().toLocalFile()) + .suffix() + .toLower(); + const bool supported = suffix == "amat" || suffix == "material" || + suffix == "ts" || suffix == "js"; + if (supported && event->type() == QEvent::Drop && + viewport != nullptr && + viewport->attachRuntimeAsset( + objectId, + drop->mimeData()->urls().constFirst().toLocalFile())) { + treeView->setCurrentIndex(index); + viewport->selectRuntimeObject(objectId, false); + emit objectActivated(objectId); + drop->acceptProposedAction(); + return true; + } + if (supported && event->type() != QEvent::Drop) { + drop->acceptProposedAction(); + return true; + } + } + drop->ignore(); + return true; + } + return QWidget::eventFilter(watched, event); +} + void HierarchyPanel::appendObjects(QStandardItem *parent, const QJsonArray &objects) { for (const QJsonValue &value : objects) { diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index 2b86170a..54504800 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -9,13 +9,17 @@ #include +#include #include #include #include #include #include #include +#include #include +#include +#include #include #include #include @@ -30,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +44,7 @@ #include #include #include +#include #include #include @@ -709,9 +715,13 @@ QJsonObject findObjectInArray(const QJsonArray &objects, int id) { } } // namespace -InspectorPanel::InspectorPanel(ViewportPanel *viewport, QWidget *parent) +InspectorPanel::InspectorPanel(ViewportPanel *viewport, + const QString &projectFile, QWidget *parent) : QWidget(parent), viewport(viewport) { setObjectName("inspectorPanel"); + setAcceptDrops(true); + const QFileInfo projectInfo(projectFile); + projectRoot = projectInfo.absoluteDir().absolutePath(); auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); scrollArea = new QScrollArea(this); @@ -748,6 +758,9 @@ void InspectorPanel::applySceneSnapshot(const QString &snapshot) { const QJsonObject updated = findObject(inspectedObjectId); const bool contentChanged = updated.value("name") != inspectedObject.value("name") || + updated.value("properties").toObject().value("material") != + inspectedObject.value("properties").toObject().value( + "material") || componentShape(updated.value("components").toArray()) != componentShape(inspectedObject.value("components").toArray()); inspectedObject = updated; @@ -855,6 +868,8 @@ void InspectorPanel::showObject(const QJsonObject &object) { content)); QJsonObject objectProperties = object.value("properties").toObject(); + const QString materialPath = objectProperties.value("material").toString(); + objectProperties.remove("material"); const QStringList hidden{"id", "name", "type", "position", "rotation", "scale", "parent", "components", "objects"}; @@ -869,6 +884,18 @@ void InspectorPanel::showObject(const QJsonObject &object) { content)); } + if (!materialPath.isEmpty()) { + contentLayout->addWidget(componentCard( + "Material", QJsonObject{{"source", materialPath}}, QString(), + [this, objectId](const QString &path, const QJsonValue &value) { + if (path == "/source" && value.isString() && + viewport != nullptr) { + viewport->applyRuntimeMaterial(objectId, value.toString()); + } + }, + content)); + } + const QJsonArray components = object.value("components").toArray(); for (int index = 0; index < components.size(); ++index) { const QJsonObject raw = components.at(index).toObject(); @@ -887,6 +914,15 @@ void InspectorPanel::showObject(const QJsonObject &object) { addComponent->setText("+ Add Component"); addComponent->setPopupMode(QToolButton::InstantPopup); auto *componentMenu = new QMenu(addComponent); + auto *searchAction = new QWidgetAction(componentMenu); + auto *componentSearch = new QLineEdit(componentMenu); + componentSearch->setPlaceholderText("Search components, scripts, materials"); + componentSearch->setClearButtonEnabled(true); + componentSearch->setMinimumWidth(280); + searchAction->setDefaultWidget(componentSearch); + componentMenu->addAction(searchAction); + componentMenu->addSeparator(); + QList searchableActions; const QList> componentTypes{ {"Rigidbody", "rigidbody"}, {"Audio Player", "audio_player"}, @@ -898,29 +934,96 @@ void InspectorPanel::showObject(const QJsonObject &object) { {"Trait Script", "trait_script"}, }; for (const auto &[label, componentType] : componentTypes) { - componentMenu->addAction(label, this, [this, componentType, objectId] { - QPointer runtime(viewport); - QPointer inspector(this); - QTimer::singleShot(0, [runtime, inspector, componentType, objectId] { - if (runtime == nullptr || inspector == nullptr) { - return; - } - if (runtime->addRuntimeObjectComponent( - objectId, componentType, - componentSchema(componentType)) < 0) { - QMessageBox::information( - inspector.data(), "Add Component", - "This component is already attached or cannot be added " - "to the selected object."); - } + QAction *action = componentMenu->addAction( + label, this, [this, componentType, objectId] { + QPointer runtime(viewport); + QPointer inspector(this); + QTimer::singleShot( + 0, [runtime, inspector, componentType, objectId] { + if (runtime == nullptr || inspector == nullptr) { + return; + } + if (runtime->addRuntimeObjectComponent( + objectId, componentType, + componentSchema(componentType)) < 0) { + QMessageBox::information( + inspector.data(), "Add Component", + "This component is already attached or cannot " + "be added to the selected object."); + } + }); }); - }); + action->setProperty("searchText", label.toLower()); + searchableActions.append(action); } + QDirIterator assets(projectRoot, + {"*.ts", "*.js", "*.amat", "*.material"}, + QDir::Files, QDirIterator::Subdirectories); + while (assets.hasNext()) { + const QFileInfo info(assets.next()); + const QString suffix = info.suffix().toLower(); + const bool material = suffix == "amat" || suffix == "material"; + const QString label = + QStringLiteral("%1 · %2") + .arg(material ? "Material" : "Script", info.completeBaseName()); + QAction *action = componentMenu->addAction( + label, this, [this, objectId, path = info.absoluteFilePath()] { + attachAsset(path, objectId); + }); + action->setIcon(inspectorIcon(this, material ? "material" : "script")); + action->setProperty("searchText", + (label + ' ' + info.absoluteFilePath()).toLower()); + searchableActions.append(action); + } + connect(componentSearch, &QLineEdit::textChanged, componentMenu, + [searchableActions](const QString &text) { + const QString query = text.trimmed().toLower(); + for (QAction *action : searchableActions) { + action->setVisible( + query.isEmpty() || + action->property("searchText").toString().contains(query)); + } + }); + connect(componentMenu, &QMenu::aboutToShow, componentSearch, + [componentSearch] { + componentSearch->clear(); + componentSearch->setFocus(); + }); addComponent->setMenu(componentMenu); contentLayout->addWidget(addComponent); contentLayout->addStretch(); } +bool InspectorPanel::attachAsset(const QString &path, int objectId) { + return viewport != nullptr && objectId >= 0 && + viewport->attachRuntimeAsset(objectId, path); +} + +void InspectorPanel::dragEnterEvent(QDragEnterEvent *event) { + if (inspectedObjectId >= 0 && event->mimeData()->hasUrls()) { + const QString suffix = + QFileInfo(event->mimeData()->urls().constFirst().toLocalFile()) + .suffix() + .toLower(); + if (suffix == "amat" || suffix == "material" || suffix == "ts" || + suffix == "js") { + event->acceptProposedAction(); + return; + } + } + event->ignore(); +} + +void InspectorPanel::dropEvent(QDropEvent *event) { + if (event->mimeData()->hasUrls() && inspectedObjectId >= 0 && + attachAsset(event->mimeData()->urls().constFirst().toLocalFile(), + inspectedObjectId)) { + event->acceptProposedAction(); + return; + } + event->ignore(); +} + void InspectorPanel::showFile() { rebuildBody(); const QFileInfo info(inspectedFile); diff --git a/editor/views/editor/materialEditor.cpp b/editor/views/editor/materialEditor.cpp index d91a8ad7..081be8e3 100644 --- a/editor/views/editor/materialEditor.cpp +++ b/editor/views/editor/materialEditor.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +150,11 @@ class MaterialPreviewWidget : public QWidget { update(); } + void setEnvironmentMode(int mode) { + environmentMode = mode; + update(); + } + protected: void paintEvent(QPaintEvent *) override { const qreal scale = devicePixelRatioF(); @@ -188,14 +195,14 @@ class MaterialPreviewWidget : public QWidget { for (int y = 0; y < heightPixels; ++y) { QRgb *line = reinterpret_cast(rendered.scanLine(y)); for (int x = 0; x < widthPixels; ++x) { - const int checker = ((x / 20) + (y / 20)) & 1; - const double background = checker ? 0.105 : 0.135; + const QColor background = environmentAt( + (static_cast(x) / widthPixels) * 2.0 - 1.0, + 1.0 - (static_cast(y) / heightPixels) * 2.0); const double px = (x - cx) / radius; const double py = (cy - y) / radius; const double rr = px * px + py * py; if (rr > 1.0) { - const int c = static_cast(background * 255.0); - line[x] = qRgba(c, c, c, 255); + line[x] = background.rgba(); continue; } @@ -249,27 +256,43 @@ class MaterialPreviewWidget : public QWidget { diffuse * (0.83 - localMetallic * 0.38); const double edgeTransmission = transmission * (0.2 + fresnel * 0.55); + const double rx = 2.0 * nx * nz; + const double ry = 2.0 * ny * nz; + const QColor reflected = environmentAt(rx, ry); + const double reflectionWeight = std::clamp( + reflectivity * (0.12 + localMetallic * 0.88) * + (1.0 - localRoughness * 0.72) + + fresnel * 0.24, + 0.0, 0.92); auto output = [&](double base, double texture, - double emitted) { - const double surface = base * texture * light + specular + - fresnel * reflectivity * 0.18; + double emitted, double environment, + double behind) { + double surface = base * texture * light + specular + + fresnel * reflectivity * 0.18; + surface = surface * (1.0 - reflectionWeight) + + environment * reflectionWeight; return std::clamp(surface * (1.0 - edgeTransmission) + - background * edgeTransmission + + behind * edgeTransmission + emitted * emissionStrength, 0.0, 1.0); }; line[x] = qRgba( static_cast(output(albedo.redF(), sampledAlbedo.redF(), - emission.redF()) * + emission.redF(), reflected.redF(), + background.redF()) * 255.0), static_cast(output(albedo.greenF(), sampledAlbedo.greenF(), - emission.greenF()) * + emission.greenF(), + reflected.greenF(), + background.greenF()) * 255.0), static_cast(output(albedo.blueF(), sampledAlbedo.blueF(), - emission.blueF()) * + emission.blueF(), + reflected.blueF(), + background.blueF()) * 255.0), 255); } @@ -281,6 +304,34 @@ class MaterialPreviewWidget : public QWidget { } private: + QColor environmentAt(double x, double y) const { + const double horizon = std::clamp((y + 1.0) * 0.5, 0.0, 1.0); + if (environmentMode == 1) { + const double sun = std::pow( + std::max(0.0, 1.0 - std::hypot(x + 0.38, y - 0.08)), 12.0); + return QColor::fromRgbF( + std::clamp(0.16 + horizon * 0.58 + sun, 0.0, 1.0), + std::clamp(0.07 + horizon * 0.27 + sun * 0.55, 0.0, 1.0), + std::clamp(0.12 + horizon * 0.24 + sun * 0.18, 0.0, 1.0)); + } + if (environmentMode == 2) { + const double cloud = + std::pow(std::max(0.0, std::sin(x * 8.0 + y * 3.0)), 6.0) * + 0.22; + return QColor::fromRgbF( + std::clamp(0.12 + horizon * 0.3 + cloud, 0.0, 1.0), + std::clamp(0.24 + horizon * 0.42 + cloud, 0.0, 1.0), + std::clamp(0.39 + horizon * 0.48 + cloud, 0.0, 1.0)); + } + const double strip = std::pow(std::max(0.0, 1.0 - std::abs(y)), 24.0); + const double panel = + std::pow(std::max(0.0, std::cos(x * 5.5)), 18.0) * 0.58; + const double value = 0.055 + horizon * 0.12 + strip * (0.34 + panel); + return QColor::fromRgbF(std::clamp(value * 0.92, 0.0, 1.0), + std::clamp(value * 0.98, 0.0, 1.0), + std::clamp(value, 0.0, 1.0)); + } + QJsonObject material; QString baseDir; QImage albedoImage; @@ -288,6 +339,7 @@ class MaterialPreviewWidget : public QWidget { QImage metallicImage; QImage roughnessImage; QImage aoImage; + int environmentMode = 0; }; MaterialEditorPanel::MaterialEditorPanel(ViewportPanel *viewport, @@ -319,6 +371,7 @@ MaterialEditorPanel::MaterialEditorPanel(ViewportPanel *viewport, auto *scroll = new QScrollArea(this); scroll->setObjectName("materialEditorScroll"); scroll->setWidgetResizable(true); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); body = new QWidget(scroll); body->setObjectName("materialEditorBody"); bodyLayout = new QVBoxLayout(body); @@ -392,6 +445,8 @@ void MaterialEditorPanel::openMaterial(const QString &path) { } materialPath = QFileInfo(path).absoluteFilePath(); assignedObjectId = -1; + undoHistory.clear(); + redoHistory.clear(); const QJsonObject root = document.object(); material = normalizedMaterial(root.value("material").isObject() ? root.value("material").toObject() @@ -428,11 +483,46 @@ void MaterialEditorPanel::showMaterial() { titleLabel->setText(QFileInfo(materialPath).completeBaseName()); statusLabel->setText("Ready"); - preview = new MaterialPreviewWidget(body); + auto *splitter = new QSplitter(Qt::Horizontal, body); + splitter->setChildrenCollapsible(false); + auto *previewPane = new QWidget(splitter); + previewPane->setMinimumWidth(300); + auto *previewLayout = new QVBoxLayout(previewPane); + previewLayout->setContentsMargins(0, 0, 5, 0); + previewLayout->setSpacing(8); + preview = new MaterialPreviewWidget(previewPane); preview->setMaterial(material, QFileInfo(materialPath).absolutePath()); - bodyLayout->addWidget(preview); - - auto *surface = new QGroupBox("Surface", body); + auto *previewOptions = new QWidget(previewPane); + auto *previewOptionsLayout = new QHBoxLayout(previewOptions); + previewOptionsLayout->setContentsMargins(0, 0, 0, 0); + auto *previewLabel = new QLabel("Preview Environment", previewOptions); + auto *environment = new QComboBox(previewOptions); + environment->addItems({"Studio", "Sunset", "Open Sky"}); + previewOptionsLayout->addWidget(previewLabel); + previewOptionsLayout->addStretch(); + previewOptionsLayout->addWidget(environment); + previewLayout->addWidget(preview, 1); + previewLayout->addWidget(previewOptions); + connect(environment, &QComboBox::currentIndexChanged, preview, + &MaterialPreviewWidget::setEnvironmentMode); + + auto *propertiesScroll = new QScrollArea(splitter); + propertiesScroll->setObjectName("materialPropertiesScroll"); + propertiesScroll->setWidgetResizable(true); + propertiesScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + auto *properties = new QWidget(propertiesScroll); + properties->setMinimumWidth(340); + auto *propertiesLayout = new QVBoxLayout(properties); + propertiesLayout->setContentsMargins(5, 0, 0, 0); + propertiesLayout->setSpacing(9); + propertiesScroll->setWidget(properties); + splitter->addWidget(previewPane); + splitter->addWidget(propertiesScroll); + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 2); + bodyLayout->addWidget(splitter, 1); + + auto *surface = new QGroupBox("Surface", properties); auto *surfaceForm = new QFormLayout(surface); albedoButton = new QPushButton(surface); displayColor(albedoButton, @@ -450,9 +540,9 @@ void MaterialEditorPanel::showMaterial() { surfaceForm->addRow("Roughness", roughnessField); surfaceForm->addRow("Ambient Occlusion", aoField); surfaceForm->addRow("Reflectivity", reflectivityField); - bodyLayout->addWidget(surface); + propertiesLayout->addWidget(surface); - auto *emission = new QGroupBox("Emission", body); + auto *emission = new QGroupBox("Emission", properties); auto *emissionForm = new QFormLayout(emission); emissiveButton = new QPushButton(emission); displayColor(emissiveButton, @@ -462,9 +552,9 @@ void MaterialEditorPanel::showMaterial() { material.value("emissiveIntensity").toDouble()); emissionForm->addRow("Color", emissiveButton); emissionForm->addRow("Strength", emissiveIntensityField); - bodyLayout->addWidget(emission); + propertiesLayout->addWidget(emission); - auto *volume = new QGroupBox("Transmission", body); + auto *volume = new QGroupBox("Transmission", properties); auto *volumeForm = new QFormLayout(volume); transmittanceField = scalarField(0.0, 1.0, 0.01, volume); iorField = scalarField(1.0, 3.0, 0.01, volume); @@ -473,9 +563,9 @@ void MaterialEditorPanel::showMaterial() { iorField->setValue(material.value("ior").toDouble()); volumeForm->addRow("Weight", transmittanceField); volumeForm->addRow("IOR", iorField); - bodyLayout->addWidget(volume); + propertiesLayout->addWidget(volume); - auto *normal = new QGroupBox("Normal", body); + auto *normal = new QGroupBox("Normal", properties); auto *normalForm = new QFormLayout(normal); normalMapField = new QCheckBox(normal); normalMapField->setChecked(material.value("useNormalMap").toBool()); @@ -484,9 +574,9 @@ void MaterialEditorPanel::showMaterial() { material.value("normalMapStrength").toDouble()); normalForm->addRow("Use Normal Map", normalMapField); normalForm->addRow("Strength", normalStrengthField); - bodyLayout->addWidget(normal); + propertiesLayout->addWidget(normal); - auto *textures = new QGroupBox("Texture Slots", body); + auto *textures = new QGroupBox("Texture Slots", properties); auto *textureLayout = new QVBoxLayout(textures); const QList> materialSlots{ {"Base Color", "albedoTexture"}, {"Normal", "normalTexture"}, @@ -531,8 +621,8 @@ void MaterialEditorPanel::showMaterial() { [this, key] { clearTexture(key); }); updateTextureField(key); } - bodyLayout->addWidget(textures); - bodyLayout->addStretch(); + propertiesLayout->addWidget(textures); + propertiesLayout->addStretch(); connect(albedoButton, &QPushButton::clicked, this, [this] { setColor("albedo", albedoButton); }); @@ -558,9 +648,11 @@ void MaterialEditorPanel::setColor(const QString &key, QPushButton *button) { initial, this, "Choose Material Color", QColorDialog::ShowAlphaChannel); if (!color.isValid()) return; + const QJsonObject previous = material; displayColor(button, color); material.insert(key, colorJson(color)); - materialChanged(); + recordHistory(previous); + refreshEditedMaterial(); } void MaterialEditorPanel::chooseTexture(const QString &key) { @@ -569,18 +661,22 @@ void MaterialEditorPanel::chooseTexture(const QString &key) { "Images (*.png *.jpg *.jpeg *.tga *.bmp *.hdr *.exr);;All Files (*)"); if (selected.isEmpty()) return; + const QJsonObject previous = material; const QDir materialDir(QFileInfo(materialPath).absolutePath()); material.insert(key, materialDir.relativeFilePath(selected)); updateTextureField(key); - materialChanged(); + recordHistory(previous); + refreshEditedMaterial(); } void MaterialEditorPanel::clearTexture(const QString &key) { if (!material.contains(key)) return; + const QJsonObject previous = material; material.remove(key); updateTextureField(key); - materialChanged(); + recordHistory(previous); + refreshEditedMaterial(); } void MaterialEditorPanel::updateTextureField(const QString &key) { @@ -605,6 +701,7 @@ void MaterialEditorPanel::updateTextureField(const QString &key) { void MaterialEditorPanel::materialChanged() { if (loading || materialPath.isEmpty()) return; + const QJsonObject previous = material; material.insert("metallic", metallicField->value()); material.insert("roughness", roughnessField->value()); material.insert("ao", aoField->value()); @@ -614,11 +711,27 @@ void MaterialEditorPanel::materialChanged() { material.insert("useNormalMap", normalMapField->isChecked()); material.insert("transmittance", transmittanceField->value()); material.insert("ior", iorField->value()); + if (material == previous) + return; + recordHistory(previous); + refreshEditedMaterial(); +} + +void MaterialEditorPanel::refreshEditedMaterial() { preview->setMaterial(material, QFileInfo(materialPath).absolutePath()); statusLabel->setText("Saving…"); saveTimer->start(); } +void MaterialEditorPanel::recordHistory(const QJsonObject &previous) { + if (previous == material) + return; + undoHistory.append(previous); + while (undoHistory.size() > 100) + undoHistory.removeFirst(); + redoHistory.clear(); +} + void MaterialEditorPanel::saveMaterial() { if (materialPath.isEmpty()) return; @@ -663,3 +776,21 @@ void MaterialEditorPanel::assignToSelectedObject() { assignedObjectId = objectId; statusLabel->setText("Assigned · live updates enabled"); } + +void MaterialEditorPanel::undo() { + if (undoHistory.isEmpty() || materialPath.isEmpty()) + return; + redoHistory.append(material); + material = undoHistory.takeLast(); + showMaterial(); + saveMaterial(); +} + +void MaterialEditorPanel::redo() { + if (redoHistory.isEmpty() || materialPath.isEmpty()) + return; + undoHistory.append(material); + material = redoHistory.takeLast(); + showMaterial(); + saveMaterial(); +} diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 29e4e52b..dadc3b78 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -24,13 +27,17 @@ #include #include #include +#include #include +#include #include #include #include #include #include #include +#include +#include #include #include @@ -38,6 +45,7 @@ #include #include #include +#include namespace { constexpr int RuntimeEditorCameraKeyForward = 0; @@ -79,25 +87,17 @@ int activeRuntimeMouseButton(Qt::MouseButtons buttons) { int editorCameraKey(int key) { switch (key) { - case Qt::Key_W: case Qt::Key_Up: return RuntimeEditorCameraKeyForward; - case Qt::Key_S: case Qt::Key_Down: return RuntimeEditorCameraKeyBackward; - case Qt::Key_A: case Qt::Key_Left: return RuntimeEditorCameraKeyLeft; - case Qt::Key_D: case Qt::Key_Right: return RuntimeEditorCameraKeyRight; - case Qt::Key_E: case Qt::Key_PageUp: - case Qt::Key_Space: return RuntimeEditorCameraKeyUp; - case Qt::Key_Q: case Qt::Key_PageDown: - case Qt::Key_C: return RuntimeEditorCameraKeyDown; default: return -1; @@ -108,10 +108,126 @@ float widgetScale(QWidget *widget) { const qreal scale = widget != nullptr ? widget->devicePixelRatioF() : 1.0; return scale > 0.0 ? static_cast(scale) : 1.0f; } + +QJsonObject findSnapshotObject(const QJsonArray &objects, int id) { + for (const QJsonValue &entry : objects) { + const QJsonObject object = entry.toObject(); + if (object.value("id").toInt(-1) == id) { + return object; + } + const QJsonObject child = + findSnapshotObject(object.value("children").toArray(), id); + if (!child.isEmpty()) { + return child; + } + } + return {}; +} + +QString decodePointerSegment(QString segment) { + return segment.replace("~1", "/").replace("~0", "~"); +} + +QJsonValue snapshotValueAt(QJsonValue value, const QString &path) { + for (const QString &raw : path.split('/', Qt::SkipEmptyParts)) { + const QString segment = decodePointerSegment(raw); + if (value.isObject()) { + value = value.toObject().value(segment); + } else if (value.isArray()) { + bool valid = false; + const int index = segment.toInt(&valid); + const QJsonArray array = value.toArray(); + if (!valid || index < 0 || index >= array.size()) { + return QJsonValue(QJsonValue::Undefined); + } + value = array.at(index); + } else { + return QJsonValue(QJsonValue::Undefined); + } + } + return value; +} + +class RuntimePropertyCommand : public QUndoCommand { + public: + RuntimePropertyCommand(ViewportPanel *viewport, int objectId, + QString component, int componentIndex, QString path, + QJsonValue before, QJsonValue after) + : viewport(viewport), objectId(objectId), + component(std::move(component)), componentIndex(componentIndex), + path(std::move(path)), before(std::move(before)), + after(std::move(after)) { + setText(QStringLiteral("Edit %1").arg(this->component)); + } + + void undo() override { + if (viewport != nullptr) { + viewport->applyRuntimeObjectProperty( + objectId, component, componentIndex, path, before); + } + } + + void redo() override { + if (viewport != nullptr) { + viewport->applyRuntimeObjectProperty( + objectId, component, componentIndex, path, after); + } + } + + int id() const override { return 0x415450; } + + bool mergeWith(const QUndoCommand *other) override { + const auto *command = dynamic_cast(other); + if (command == nullptr || command->viewport != viewport || + command->objectId != objectId || command->component != component || + command->componentIndex != componentIndex || command->path != path) { + return false; + } + after = command->after; + return true; + } + + private: + QPointer viewport; + int objectId; + QString component; + int componentIndex; + QString path; + QJsonValue before; + QJsonValue after; +}; + +class RuntimeRenameCommand : public QUndoCommand { + public: + RuntimeRenameCommand(ViewportPanel *viewport, int objectId, QString before, + QString after) + : viewport(viewport), objectId(objectId), before(std::move(before)), + after(std::move(after)) { + setText("Rename Object"); + } + + void undo() override { + if (viewport != nullptr) + viewport->renameRuntimeObjectDirect(objectId, before); + } + + void redo() override { + if (viewport != nullptr) + viewport->renameRuntimeObjectDirect(objectId, after); + } + + private: + QPointer viewport; + int objectId; + QString before; + QString after; +}; } ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) : QWidget(parent), projectFile(projectFile) { + setAcceptDrops(true); + setAttribute(Qt::WA_DontCreateNativeAncestors); setAttribute(Qt::WA_NativeWindow); setAttribute(Qt::WA_NoSystemBackground); setAttribute(Qt::WA_OpaquePaintEvent); @@ -123,6 +239,7 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); frameTimer = new QTimer(this); + undoStack = new QUndoStack(this); frameTimer->setTimerType(Qt::PreciseTimer); connect(frameTimer, &QTimer::timeout, this, [this] { stepRuntime(); }); if (auto *app = QCoreApplication::instance()) { @@ -161,6 +278,34 @@ void ViewportPanel::closeEvent(QCloseEvent *event) { QWidget::closeEvent(event); } +void ViewportPanel::dragEnterEvent(QDragEnterEvent *event) { + if (selectedRuntimeObjectId() >= 0 && event->mimeData()->hasUrls()) { + const QString suffix = + QFileInfo(event->mimeData()->urls().constFirst().toLocalFile()) + .suffix() + .toLower(); + if (suffix == "amat" || suffix == "material" || suffix == "ts" || + suffix == "js") { + event->acceptProposedAction(); + return; + } + } + event->ignore(); +} + +void ViewportPanel::dropEvent(QDropEvent *event) { + const int objectId = selectedRuntimeObjectId(); + if (objectId >= 0 && event->mimeData()->hasUrls() && + attachRuntimeAsset( + objectId, + event->mimeData()->urls().constFirst().toLocalFile())) { + event->acceptProposedAction(); + emit runtimeObjectActivated(objectId); + return; + } + event->ignore(); +} + void ViewportPanel::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); if (runtimeContext == nullptr) { @@ -321,6 +466,8 @@ void ViewportPanel::stopRuntime() { } auto context = std::move(runtimeContext); lastSceneSnapshot.clear(); + if (undoStack != nullptr) + undoStack->clear(); emit runtimeAvailabilityChanged(false); playbackState = 0; emit playbackStateChanged(playbackState); @@ -399,6 +546,21 @@ bool ViewportPanel::selectRuntimeObject(int id, bool focusCamera) { } bool ViewportPanel::renameRuntimeObject(int id, const QString &name) { + const QJsonObject object = findSnapshotObject( + QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) + .object() + .value("objects") + .toArray(), + id); + const QString previous = object.value("name").toString(); + if (previous.isEmpty() || previous == name) { + return previous == name; + } + undoStack->push(new RuntimeRenameCommand(this, id, previous, name)); + return true; +} + +bool ViewportPanel::renameRuntimeObjectDirect(int id, const QString &name) { if (runtimeContext == nullptr || !runtimeContext->renameObject(id, name.toUtf8().toStdString())) { return false; @@ -408,6 +570,23 @@ bool ViewportPanel::renameRuntimeObject(int id, const QString &name) { } bool ViewportPanel::setRuntimeObjectProperty( + int id, const QString &component, int componentIndex, + const QString &propertyPath, const QJsonValue &value) { + const QJsonValue previous = runtimeObjectProperty( + id, component, componentIndex, propertyPath); + if (previous.isUndefined()) { + return applyRuntimeObjectProperty(id, component, componentIndex, + propertyPath, value); + } + if (previous == value) { + return true; + } + undoStack->push(new RuntimePropertyCommand( + this, id, component, componentIndex, propertyPath, previous, value)); + return true; +} + +bool ViewportPanel::applyRuntimeObjectProperty( int id, const QString &component, int componentIndex, const QString &propertyPath, const QJsonValue &value) { if (runtimeContext == nullptr) { @@ -466,7 +645,14 @@ bool ViewportPanel::deleteRuntimeObject(int id) { if (runtimeContext == nullptr || !runtimeContext->deleteObject(id)) { return false; } + if (undoStack != nullptr) { + undoStack->clear(); + } + if (!runtimeContext->saveCurrentScene()) { + qWarning() << "Atlas editor could not persist the deleted object"; + } refreshSceneSnapshot(); + emit runtimeObjectActivated(-1); return true; } @@ -492,6 +678,10 @@ int ViewportPanel::selectedRuntimeObjectId() const { } bool ViewportPanel::applyRuntimeMaterial(int id, const QString &path) { + return applyRuntimeMaterialDirect(id, path); +} + +bool ViewportPanel::applyRuntimeMaterialDirect(int id, const QString &path) { if (runtimeContext == nullptr || id < 0 || path.isEmpty() || !runtimeContext->setObjectMaterial(id, path.toStdString())) { return false; @@ -500,6 +690,60 @@ bool ViewportPanel::applyRuntimeMaterial(int id, const QString &path) { return true; } +bool ViewportPanel::attachRuntimeAsset(int id, const QString &path) { + const QFileInfo info(path); + const QString suffix = info.suffix().toLower(); + if (suffix == "amat" || suffix == "material") { + return applyRuntimeMaterial(id, info.absoluteFilePath()); + } + if (suffix == "ts" || suffix == "js") { + return addRuntimeObjectComponent( + id, "script", + QJsonObject{{"name", info.completeBaseName()}, + {"source", info.absoluteFilePath()}, + {"variables", QJsonObject{}}}) >= 0; + } + return false; +} + +void ViewportPanel::undo() { + if (undoStack != nullptr) { + undoStack->undo(); + const int selected = selectedRuntimeObjectId(); + if (selected >= 0) + emit runtimeObjectActivated(selected); + } +} + +void ViewportPanel::redo() { + if (undoStack != nullptr) { + undoStack->redo(); + const int selected = selectedRuntimeObjectId(); + if (selected >= 0) + emit runtimeObjectActivated(selected); + } +} + +QJsonValue ViewportPanel::runtimeObjectProperty( + int id, const QString &component, int componentIndex, + const QString &propertyPath) const { + const QJsonDocument document = + QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()); + const QJsonObject object = findSnapshotObject( + document.object().value("objects").toArray(), id); + if (object.isEmpty()) + return QJsonValue(QJsonValue::Undefined); + const QString normalized = component.toLower(); + if (normalized == "transform") + return snapshotValueAt(object, propertyPath); + if (normalized == "object") + return snapshotValueAt(object.value("properties"), propertyPath); + const QJsonArray components = object.value("components").toArray(); + if (componentIndex < 0 || componentIndex >= components.size()) + return QJsonValue(QJsonValue::Undefined); + return snapshotValueAt(components.at(componentIndex), propertyPath); +} + void ViewportPanel::playRuntime() { if (runtimeContext == nullptr) { return; @@ -513,7 +757,9 @@ void ViewportPanel::pauseRuntime() { if (runtimeContext == nullptr || playbackState == 0) { return; } + saveRuntimeScene(); runtimeContext->setEditorSimulationEnabled(false); + refreshSceneSnapshot(); playbackState = 2; emit playbackStateChanged(playbackState); } @@ -525,6 +771,7 @@ void ViewportPanel::stepRuntimeOnce() { runtimeContext->setEditorSimulationEnabled(true); stepRuntime(); if (runtimeContext != nullptr) { + saveRuntimeScene(); runtimeContext->setEditorSimulationEnabled(false); playbackState = 2; emit playbackStateChanged(playbackState); diff --git a/editor/views/general/contentBrowser.cpp b/editor/views/general/contentBrowser.cpp index 0d9c7f55..a6599711 100644 --- a/editor/views/general/contentBrowser.cpp +++ b/editor/views/general/contentBrowser.cpp @@ -163,6 +163,9 @@ ContentBrowserPanel::ContentBrowserPanel(const QString &projectFile, gridView->setWordWrap(true); gridView->setTextElideMode(Qt::ElideNone); gridView->setSelectionMode(QAbstractItemView::ExtendedSelection); + gridView->setDragEnabled(true); + gridView->setDragDropMode(QAbstractItemView::DragOnly); + gridView->setDefaultDropAction(Qt::CopyAction); gridView->setContextMenuPolicy(Qt::CustomContextMenu); gridView->setUniformItemSizes(true); gridView->setSpacing(4); diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index d9cfa7a1..dc21c3e7 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -80,6 +80,7 @@ class Context { std::unique_ptr window; std::vector> objects; + std::vector> retiredObjects; std::unordered_map objectReferences; std::unordered_map objectNames; std::unordered_map objectSceneReferences; diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index 487ab340..d4fe4954 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -22,6 +22,7 @@ namespace ads { class ViewportPanel; class InspectorPanel; +class MaterialEditorPanel; class EditorWindow : public QMainWindow { Q_OBJECT @@ -42,6 +43,7 @@ class EditorWindow : public QMainWindow { ads::CDockManager* coreManager = nullptr; ViewportPanel* viewportPanel = nullptr; InspectorPanel* inspectorPanel = nullptr; + MaterialEditorPanel* materialEditorPanel = nullptr; QString projectFile; bool closing = false; diff --git a/include/editor/views/hierarchyPanel.h b/include/editor/views/hierarchyPanel.h index a72a5dc4..29afc2b6 100644 --- a/include/editor/views/hierarchyPanel.h +++ b/include/editor/views/hierarchyPanel.h @@ -15,6 +15,7 @@ #include class QJsonArray; +class QEvent; class QMenu; class QPoint; class QStandardItem; @@ -32,6 +33,9 @@ class HierarchyPanel : public QWidget { signals: void objectActivated(int id); + protected: + bool eventFilter(QObject *watched, QEvent *event) override; + private: void applySceneSnapshot(const QString &snapshot); void rebuildScene(const QString &sceneName, const QJsonArray &objects, diff --git a/include/editor/views/inspectorView.h b/include/editor/views/inspectorView.h index f3c65a2c..fcd4d3ad 100644 --- a/include/editor/views/inspectorView.h +++ b/include/editor/views/inspectorView.h @@ -16,6 +16,8 @@ class QLabel; class QLineEdit; +class QDragEnterEvent; +class QDropEvent; class QScrollArea; class QVBoxLayout; class ViewportPanel; @@ -24,13 +26,18 @@ class InspectorPanel : public QWidget { Q_OBJECT public: - explicit InspectorPanel(ViewportPanel *viewport, QWidget *parent = nullptr); + explicit InspectorPanel(ViewportPanel *viewport, const QString &projectFile, + QWidget *parent = nullptr); public slots: void applySceneSnapshot(const QString &snapshot); void inspectRuntimeObject(int id); void inspectFile(const QString &path); + protected: + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; + private: void showEmptyState(); void showObject(const QJsonObject &object); @@ -38,6 +45,7 @@ class InspectorPanel : public QWidget { void rebuildBody(); void commitHeaderName(); QJsonObject findObject(int id) const; + bool attachAsset(const QString &path, int objectId); ViewportPanel *viewport = nullptr; QScrollArea *scrollArea = nullptr; @@ -49,6 +57,7 @@ class InspectorPanel : public QWidget { QJsonObject scene; QJsonObject inspectedObject; QString inspectedFile; + QString projectRoot; int inspectedObjectId = -1; int lastRuntimeSelection = -1; bool fileTarget = false; diff --git a/include/editor/views/materialEditor.h b/include/editor/views/materialEditor.h index 7e0ac8b9..c35192c6 100644 --- a/include/editor/views/materialEditor.h +++ b/include/editor/views/materialEditor.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -26,6 +27,9 @@ class MaterialEditorPanel : public QWidget { public slots: void openMaterial(const QString &path); + void saveMaterial(); + void undo(); + void redo(); signals: void materialSaved(const QString &path); @@ -39,7 +43,8 @@ class MaterialEditorPanel : public QWidget { void clearTexture(const QString &key); void updateTextureField(const QString &key); void materialChanged(); - void saveMaterial(); + void refreshEditedMaterial(); + void recordHistory(const QJsonObject &previous); void assignToSelectedObject(); QJsonObject normalizedMaterial(const QJsonObject &source) const; @@ -65,6 +70,8 @@ class MaterialEditorPanel : public QWidget { ViewportPanel *viewport = nullptr; QString materialPath; QJsonObject material; + QList undoHistory; + QList redoHistory; int assignedObjectId = -1; bool loading = false; }; diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 39b7f5ab..96d5a2ad 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -17,6 +17,8 @@ class Context; class QCloseEvent; +class QDragEnterEvent; +class QDropEvent; class QHideEvent; class QKeyEvent; class QJsonObject; @@ -27,6 +29,7 @@ class QResizeEvent; class QSize; class QShowEvent; class QTimer; +class QUndoStack; class QWheelEvent; class ViewportPanel : public QWidget { @@ -41,10 +44,15 @@ class ViewportPanel : public QWidget { void shutdownRuntime(); bool selectRuntimeObject(int id, bool focusCamera = true); bool renameRuntimeObject(int id, const QString &name); + bool renameRuntimeObjectDirect(int id, const QString &name); bool setRuntimeObjectProperty(int id, const QString &component, int componentIndex, const QString &propertyPath, const QJsonValue &value); + bool applyRuntimeObjectProperty(int id, const QString &component, + int componentIndex, + const QString &propertyPath, + const QJsonValue &value); int addRuntimeObjectComponent(int id, const QString &type, const QJsonObject &properties); bool setRuntimeObjectParent(int childId, int parentId); @@ -53,6 +61,10 @@ class ViewportPanel : public QWidget { bool saveRuntimeScene(); int selectedRuntimeObjectId() const; bool applyRuntimeMaterial(int id, const QString &path); + bool applyRuntimeMaterialDirect(int id, const QString &path); + bool attachRuntimeAsset(int id, const QString &path); + void undo(); + void redo(); void playRuntime(); void pauseRuntime(); void stepRuntimeOnce(); @@ -73,6 +85,8 @@ class ViewportPanel : public QWidget { void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; void closeEvent(QCloseEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; @@ -89,8 +103,12 @@ class ViewportPanel : public QWidget { void resizeRuntime(); void sendPointerEvent(int action, float x, float y, int button); void refreshSceneSnapshot(); + QJsonValue runtimeObjectProperty(int id, const QString &component, + int componentIndex, + const QString &propertyPath) const; QTimer *frameTimer = nullptr; + QUndoStack *undoStack = nullptr; QString projectFile; std::shared_ptr runtimeContext; int runtimeWidth = 0; diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index d78838a8..846a8402 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -3939,7 +3940,16 @@ bool Context::stepFrame() { } } window->setScene(scene.get()); - return window->stepFrame(); + auto previousRetiredObjects = std::move(retiredObjects); + try { + return window->stepFrame(); + } catch (...) { + retiredObjects.insert( + retiredObjects.end(), + std::make_move_iterator(previousRetiredObjects.begin()), + std::make_move_iterator(previousRetiredObjects.end())); + throw; + } } bool Context::resize(int width, int height, float scale) { @@ -4635,12 +4645,15 @@ bool Context::deleteObject(int id) { window->removeObject(object); } - objects.erase(std::remove_if(objects.begin(), objects.end(), + auto objectIt = std::find_if(objects.begin(), objects.end(), [&](const auto &renderable) { return renderable != nullptr && renderable.get() == object; - }), - objects.end()); + }); + if (objectIt != objects.end()) { + retiredObjects.push_back(std::move(*objectIt)); + objects.erase(objectIt); + } return true; } @@ -4936,11 +4949,22 @@ Context::~Context() { } if (context != nullptr) { runtime::scripting::clearSceneBindings(context, scriptHost); + editorRuntimeComponents.clear(); + objects.clear(); + renderTargets.clear(); + directionalLights.clear(); + pointLights.clear(); + spotlights.clear(); + areaLights.clear(); + if (runtime != nullptr) { + JS_RunGC(runtime); + } JS_SetContextOpaque(context, nullptr); JS_FreeContext(context); context = nullptr; } if (runtime != nullptr) { + JS_RunGC(runtime); JS_FreeRuntime(runtime); runtime = nullptr; } @@ -5122,6 +5146,9 @@ void Context::loadScene(Window &window, const json &sceneData) { } } + retiredObjects.insert(retiredObjects.end(), + std::make_move_iterator(objects.begin()), + std::make_move_iterator(objects.end())); objects.clear(); objectReferences.clear(); objectNames.clear(); diff --git a/runtime/lib/scripting.cpp b/runtime/lib/scripting.cpp index a1baefe9..066cf3f9 100644 --- a/runtime/lib/scripting.cpp +++ b/runtime/lib/scripting.cpp @@ -15335,6 +15335,47 @@ void runtime::scripting::clearSceneBindings(JSContext *ctx, ScriptHost &host) { host.springJointPrototype = JS_UNDEFINED; } + auto freeHostValue = [ctx](JSValue &value) { + if (!JS_IsUndefined(value)) { + JS_FreeValue(ctx, value); + value = JS_UNDEFINED; + } + }; + freeHostValue(host.atlasNamespace); + freeHostValue(host.atlasInputNamespace); + freeHostValue(host.atlasUnitsNamespace); + freeHostValue(host.atlasGraphicsNamespace); + freeHostValue(host.componentPrototype); + freeHostValue(host.gameObjectPrototype); + freeHostValue(host.coreObjectPrototype); + freeHostValue(host.modelPrototype); + freeHostValue(host.materialPrototype); + freeHostValue(host.instancePrototype); + freeHostValue(host.coreVertexPrototype); + freeHostValue(host.resourcePrototype); + freeHostValue(host.windowPrototype); + freeHostValue(host.monitorPrototype); + freeHostValue(host.gamepadPrototype); + freeHostValue(host.joystickPrototype); + freeHostValue(host.cameraPrototype); + freeHostValue(host.scenePrototype); + freeHostValue(host.texturePrototype); + freeHostValue(host.cubemapPrototype); + freeHostValue(host.skyboxPrototype); + freeHostValue(host.renderTargetPrototype); + freeHostValue(host.pointLightPrototype); + freeHostValue(host.directionalLightPrototype); + freeHostValue(host.spotLightPrototype); + freeHostValue(host.areaLightPrototype); + freeHostValue(host.position3dPrototype); + freeHostValue(host.position2dPrototype); + freeHostValue(host.colorPrototype); + freeHostValue(host.size2dPrototype); + freeHostValue(host.quaternionPrototype); + freeHostValue(host.triggerPrototype); + freeHostValue(host.axisTriggerPrototype); + freeHostValue(host.inputActionPrototype); + for (JSValue &interactive : host.interactiveValues) { JS_FreeValue(ctx, interactive); }