Physics Simulation & Visualization Tool 0.1
A C++ physics simulation engine with real-time 3D visualization
Loading...
Searching...
No Matches
MainWindow.cpp
Go to the documentation of this file.
1#include "MainWindow.h"
2
3#include <iostream>
4
5#include "OpenGLWindow.h"
6#include <QDockWidget>
7#include <QStatusBar>
8#include <QLineEdit>
9#include <QScrollArea>
10#include <QMenuBar>
11#include <QFileDialog>
12#include <QDir>
13#include <QFrame>
14#include <QHeaderView>
15#include <QTableView>
16#include <QTabWidget>
17#include <QVBoxLayout>
18#include <QFormLayout>
19#include <map>
20#include <memory>
21
22#include "HierarchyWidget.h"
25#include "SolverDialog.h"
26#include "AppSettings.h"
32
33MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
36
37 glWindow = new OpenGLWindow(this);
38
39 setWindowTitle("Physics Engine");
40
41 setCentralWidget(glWindow);
42 glWindow->setFocus();
43
44 fpsLabel = new QLabel(this);
45 fpsLabel->setText("FPS: 0.0");
46 statusBar()->addPermanentWidget(fpsLabel);
47
48 connect(glWindow, &OpenGLWindow::fpsUpdated, this, [this](double fps) {
49 fpsLabel->setText(QString("FPS: %1").arg(fps, 0, 'f', 1));
50 updateStatusPanel();
51 });
52
53 connect(glWindow, &OpenGLWindow::glInitialized, this, &MainWindow::onGLInitialized);
54}
55
56void MainWindow::onGLInitialized() {
57 auto scene = std::make_unique<Scene>(glWindow);
58 Scene* scenePtr = scene.get();
59 sceneManager = std::make_unique<SceneManager>(glWindow, scenePtr);
60 glWindow->setScene(std::move(scene));
61 glWindow->setSceneManager(sceneManager.get());
62 setupMenuBar();
63 setupDockWidgets();
64 sceneManager->defaultSetup();
65 loadAppSettings();
66 connect(sceneManager.get(), &SceneManager::contextMenuRequested, this, &MainWindow::showObjectContextMenu);
67}
68
69void MainWindow::setupDockWidgets() {
70 auto* infoDock = new QDockWidget(tr("Scene Info"), this);
71 infoDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
72 infoDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
73
74 auto* infoPanel = new QWidget(infoDock);
75 auto* infoLayout = new QFormLayout(infoPanel);
76 infoLayout->setContentsMargins(8, 8, 8, 8);
77 infoLayout->setSpacing(6);
78
79 cameraPositionLabel = new QLabel("0, 0, 0", infoPanel);
80 cameraPositionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
81 selectedObjectLabel = new QLabel("None", infoPanel);
82 selectedObjectPositionLabel = new QLabel("-", infoPanel);
83 selectedObjectPositionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
84 selectedObjectDistanceLabel = new QLabel("-", infoPanel);
85 simulationStateLabel = new QLabel("Paused", infoPanel);
86 renderClockStateLabel = new QLabel("Idle", infoPanel);
87 cameraFollowLabel = new QLabel("Off", infoPanel);
88
89 infoLayout->addRow("Camera position", cameraPositionLabel);
90 infoLayout->addRow("Selected", selectedObjectLabel);
91 infoLayout->addRow("Selected position", selectedObjectPositionLabel);
92 infoLayout->addRow("Camera distance", selectedObjectDistanceLabel);
93 infoLayout->addRow("Camera follow", cameraFollowLabel);
94 infoLayout->addRow("Physics", simulationStateLabel);
95 infoLayout->addRow("Render clock", renderClockStateLabel);
96
97 auto* sceneInfoScrollArea = new QScrollArea(infoDock);
98 sceneInfoScrollArea->setWidgetResizable(true);
99 sceneInfoScrollArea->setFrameShape(QFrame::NoFrame);
100 sceneInfoScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
101 sceneInfoScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
102 sceneInfoScrollArea->setWidget(infoPanel);
103
104 infoDock->setWidget(sceneInfoScrollArea);
105 infoDock->setMinimumHeight(80);
106 infoDock->resize(300, 80);
107 addDockWidget(Qt::LeftDockWidgetArea, infoDock);
108 viewMenu->addAction(infoDock->toggleViewAction());
109
110 auto* hierarchyDock = new QDockWidget(tr("Objects"), this);
111 hierarchyDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
112 hierarchy = new HierarchyWidget(this);
113 hierarchyDock->setWidget(hierarchy);
114 addDockWidget(Qt::LeftDockWidgetArea, hierarchyDock);
115 splitDockWidget(infoDock, hierarchyDock, Qt::Vertical);
116 resizeDocks({infoDock, hierarchyDock}, {120, 600}, Qt::Vertical);
117 viewMenu->addAction(hierarchyDock->toggleViewAction());
118
119 connect(hierarchy, &HierarchyWidget::selectionChanged, this, &MainWindow::onHierarchySelectionChanged);
120 connect(hierarchy, &HierarchyWidget::focusObjectRequested, this, [this](SceneObject* obj) {
121 sceneManager->focusObject(obj);
122 glWindow->setFocus();
123 });
124 connect(hierarchy, &HierarchyWidget::followObjectRequested, this, [this](SceneObject* obj) {
125 sceneManager->setCameraTarget(obj);
126 updateStatusPanel();
127 glWindow->setFocus();
128 });
129 connect(hierarchy, &HierarchyWidget::clearCameraFollowRequested, this, [this]() {
130 sceneManager->clearCameraTarget();
131 updateStatusPanel();
132 glWindow->setFocus();
133 });
134 connect(hierarchy, &HierarchyWidget::createObjectRequested, this, [this](const CreationOptions& options) {
135 SceneObject* createdObj = sceneManager->createObject("prim_sphere", ResourceManager::getShader("basic"), options);
136 hierarchy->selectObject(createdObj);
137 });
138 connect(hierarchy, &HierarchyWidget::renameObjectRequested, this, [this](SceneObject* obj, const QString& requestedName) {
139 std::string requested = requestedName.toStdString();
140 if (requested.empty()) {
141 hierarchy->setObjectName(obj, obj->getName().data());
142 return;
143 }
144 std::string finalName = requested;
145
146 if (!sceneManager->isNameUnique(requested, obj)) {
147 finalName = sceneManager->makeUniqueName(requested);
148 }
149
150 sceneManager->setObjectName(obj, finalName);
151 });
152 connect(hierarchy, &HierarchyWidget::deleteObjectRequested, this, [this](SceneObject* obj) {
153 sceneManager->deleteObject(obj);
154 });
155 connect(sceneManager.get(), &SceneManager::objectAdded, this, [this](SceneObject* obj) { hierarchy->addObject(obj); inspector->unloadObject(); });
156 connect(sceneManager.get(), &SceneManager::objectRemoved, this, [this](SceneObject* obj) {
157 if (selectedInfoObject == obj)
158 selectedInfoObject = nullptr;
159 hierarchy->removeObject(obj);
160 inspector->unloadObject();
161 updateStatusPanel();
162 });
163 connect(sceneManager.get(), &SceneManager::objectRenamed, this, [this](SceneObject* obj, const QString& newName) { hierarchy->setObjectName(obj, newName); });
164 connect(sceneManager.get(), &SceneManager::selectedItem, hierarchy, &HierarchyWidget::selectObject);
165
166 inspector = new InspectorWidget(sceneManager.get(), this);
167 auto* inspectorDock = new QDockWidget(tr("Inspector"), this);
168 inspectorDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
169 QScrollArea* scrollArea = new QScrollArea;
170 scrollArea->setWidgetResizable(true);
171 scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
172 scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
173 scrollArea->setWidget(inspector);
174 scrollArea->setMinimumWidth(350);
175 scrollArea->setMinimumHeight(200);
176
177 inspectorDock->setWidget(scrollArea);
178 addDockWidget(Qt::LeftDockWidgetArea, inspectorDock);
179 viewMenu->addAction(inspectorDock->toggleViewAction());
180
181 auto* historyDock = new QDockWidget(tr("Frame History"), this);
182 historyDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
183
184 auto* tabs = new QTabWidget(historyDock);
185
186 auto* tableView = new QTableView(tabs);
187 tableView->horizontalHeader()->setStretchLastSection(true);
188 snapshotModel = new SnapshotTableModel(this);
189
190 tableView->setModel(snapshotModel);
191 tabs->addTab(tableView, tr("History"));
192
193 frameGraphPanel = new FrameGraphPanel(tabs);
194 tabs->addTab(frameGraphPanel, tr("Graphs"));
195
196 historyDock->setWidget(tabs);
197 addDockWidget(Qt::RightDockWidgetArea, historyDock);
198 viewMenu->addAction(historyDock->toggleViewAction());
199}
200
201void MainWindow::setupFileMenu() {
202 QMenu *fileMenu = menuBar()->addMenu("File");
203 QAction *saveAction = new QAction("Save", this);
204 fileMenu->addAction(saveAction);
205 QAction *saveAsAction = new QAction("Save As", this);
206 fileMenu->addAction(saveAsAction);
207 QAction *loadAction = new QAction("Load", this);
208 fileMenu->addAction(loadAction);
209 QAction *loadFromAction = new QAction("Load From", this);
210 fileMenu->addAction(loadFromAction);
211
212 connect(saveAction, &QAction::triggered, this, [this](){
213 if (sceneManager->saveScene("scene.json")) {
214 std::cout << "Save Success!" << std::endl;
215 statusBar()->showMessage("Scene saved to scene.json", 3000);
216 } else {
217 std::cout << "Save Failed!" << std::endl;
218 statusBar()->showMessage("Failed to save scene to scene.json", 3000);
219 }
220 });
221 connect(loadAction, &QAction::triggered, this, [this](){
222 if (sceneManager->loadScene("scene.json")) {
223 std::cout << "Load Success!" << std::endl;
224 statusBar()->showMessage("Scene loaded from scene.json", 3000);
225 } else {
226 std::cout << "Load Failed!" << std::endl;
227 statusBar()->showMessage("Failed to load scene from scene.json", 3000);
228 }
229 });
230 connect(saveAsAction, &QAction::triggered, this, [this](){
231 QFileDialog dialog(this, "Save Scene", QDir::currentPath(), "JSON Files (*.json)");
232 dialog.setOption(QFileDialog::DontUseNativeDialog, true);
233 dialog.setAcceptMode(QFileDialog::AcceptSave);
234 dialog.setDefaultSuffix("json");
235
236 if (dialog.exec() == QDialog::Accepted) {
237 const QString fileName = dialog.selectedFiles().value(0);
238 if (sceneManager->saveScene(fileName)) {
239 std::cout << "Save Success!" << std::endl;
240 statusBar()->showMessage(QString("Scene saved to %1").arg(fileName), 3000);
241 }
242 else {
243 std::cout << "Save Failed!" << std::endl;
244 statusBar()->showMessage(QString("Failed to save scene to %1").arg(fileName), 3000);
245 }
246 }
247 });
248 connect(loadFromAction, &QAction::triggered, this, [this](){
249 QFileDialog dialog(this, "Load Scene", QDir::currentPath(), "JSON Files (*.json)");
250 dialog.setOption(QFileDialog::DontUseNativeDialog, true);
251 dialog.setFileMode(QFileDialog::ExistingFile);
252
253 if (dialog.exec() == QDialog::Accepted) {
254 const QString fileName = dialog.selectedFiles().value(0);
255 if (sceneManager->loadScene(fileName)) {
256 std::cout << "Load Success!" << std::endl;
257 statusBar()->showMessage(QString("Scene loaded from %1").arg(fileName), 3000);
258 } else {
259 std::cout << "Load Failed!" << std::endl;
260 statusBar()->showMessage(QString("Failed to load scene from %1").arg(fileName), 3000);
261 }
262 }
263 });
264}
265
266void MainWindow::setupPresetMenu() {
267 QMenu* presetMenu = menuBar()->addMenu("Presets");
268 std::map<QString, QMenu*> categoryMenus;
269
270 for (const auto& preset : ScenePresets::all()) {
271 const QString category = QString::fromUtf8(preset.category);
272 QMenu*& categoryMenu = categoryMenus[category];
273 if (!categoryMenu) {
274 categoryMenu = presetMenu->addMenu(category);
275 }
276
277 QAction* action = categoryMenu->addAction(QString::fromUtf8(preset.name));
278 action->setToolTip(QString::fromUtf8(preset.description));
279 const ScenePresets::PresetDescriptor* presetPtr = &preset;
280 connect(action, &QAction::triggered, this, [this, presetPtr]() {
281 if (sceneManager->loadPreset(*presetPtr)) {
282 snapshotModel->setSnapshots({});
283 frameGraphPanel->clear();
284 updateStatusPanel();
285 statusBar()->showMessage(QString("Loaded preset: %1").arg(QString::fromUtf8(presetPtr->name)), 3000);
286 } else {
287 statusBar()->showMessage(QString("Failed to load preset: %1").arg(QString::fromUtf8(presetPtr->name)), 3000);
288 }
289 });
290 }
291}
292
293void MainWindow::setupSettingMenu() {
294 QMenu *settingMenu = menuBar()->addMenu("Settings");
295 QAction *preferencesAction = new QAction("Preferences", this);
296 settingMenu->addAction(preferencesAction);
297 connect(preferencesAction, &QAction::triggered, this, [this]() {
298 SettingsDialog dialog(this);
299 connect(&dialog, &SettingsDialog::settingsSaved, this, [this]() {
300 // Push saved settings down to Camera and debug drawables
302 Camera* camera = sceneManager->scene->getCamera();
303 camera->movementSpeed = camGroup.movementSpeed;
304 camera->mouseSensitivity = camGroup.mouseSensitivity;
305 camera->fov = camGroup.fov;
306
307 sceneManager->applyDebugSettings();
308 });
309 dialog.exec();
310 });
311}
312
313void MainWindow::setupMenuBar() {
314 MainWindow::setupFileMenu();
315 MainWindow::setupPresetMenu();
316 viewMenu = menuBar()->addMenu("View");
317 MainWindow::setupSettingMenu();
318}
319
320void MainWindow::loadAppSettings() {
321 QSettings settings;
322 AppSettings::getInstance().load(settings);
323
324 // Push loaded settings down to Camera
326 Camera* camera = sceneManager->scene->getCamera();
327 camera->movementSpeed = camGroup.movementSpeed;
328 camera->mouseSensitivity = camGroup.mouseSensitivity;
329 camera->fov = camGroup.fov;
330
331 // Push loaded settings down to debug drawables
332 sceneManager->applyDebugSettings();
333 updateStatusPanel();
334}
335
336void MainWindow::updateStatusPanel() {
337 if (!sceneManager || !sceneManager->scene || !sceneManager->scene->getCamera())
338 return;
339
340 const glm::vec3 pos = sceneManager->scene->getCamera()->position;
341 cameraPositionLabel->setText(QString("x %1, y %2, z %3")
342 .arg(pos.x, 0, 'g', 6)
343 .arg(pos.y, 0, 'g', 6)
344 .arg(pos.z, 0, 'g', 6));
345
346 if (selectedInfoObject) {
347 const glm::vec3 selectedPos = selectedInfoObject->getPosition();
348 selectedObjectLabel->setText(QString::fromStdString(selectedInfoObject->getName()));
349 selectedObjectPositionLabel->setText(QString("x %1, y %2, z %3")
350 .arg(selectedPos.x, 0, 'g', 6)
351 .arg(selectedPos.y, 0, 'g', 6)
352 .arg(selectedPos.z, 0, 'g', 6));
353 selectedObjectDistanceLabel->setText(QString("%1 m").arg(glm::distance(pos, selectedPos), 0, 'g', 6));
354 } else {
355 selectedObjectLabel->setText("None");
356 selectedObjectPositionLabel->setText("-");
357 selectedObjectDistanceLabel->setText("-");
358 }
359
360 simulationStateLabel->setText(sceneManager->isPhysicsRunning() ? QString("Running") : QString("Paused"));
361 renderClockStateLabel->setText(glWindow->isRenderClockRunning() ? QString("Running") : QString("Idle"));
362 if (const SceneObject* followed = sceneManager->getCameraTarget()) {
363 cameraFollowLabel->setText(QString::fromStdString(followed->getName()));
364 } else {
365 cameraFollowLabel->setText("Off");
366 }
367}
368
369void MainWindow::showObjectContextMenu(const QPoint &pos, SceneObject *obj) {
370 if (!obj) return;
371
372 QMenu contextMenu;
373 QAction* solveAction = contextMenu.addAction("Open Solver...");
374 QAction* followAction = contextMenu.addAction("Follow Camera");
375 QAction* clearFollowAction = contextMenu.addAction("Stop Camera Follow");
376
377 connect(solveAction, &QAction::triggered, [this, obj]() {
379
380 if (body) {
381 const ProblemRouter *router = sceneManager->physicsSystem->getRouter();
382 // Create and show the dialog
383 SolverDialog dialog(router, body, this);
384 if (dialog.exec() == QDialog::Accepted) {
385 auto knowns = dialog.getCollectedKnowns();
386 std::string unknown = dialog.getTargetUnknown();
387
388 sceneManager->physicsSystem->solveProblem(body, knowns, unknown);
389 }
390 } else {
391 qDebug() << "Selected object has no physics body attached.";
392 }
393 });
394 connect(followAction, &QAction::triggered, [this, obj]() {
395 sceneManager->setCameraTarget(obj);
396 updateStatusPanel();
397 });
398 connect(clearFollowAction, &QAction::triggered, [this]() {
399 sceneManager->clearCameraTarget();
400 updateStatusPanel();
401 });
402
403 contextMenu.exec(pos);
404}
405void MainWindow::onHierarchySelectionChanged(SceneObject *previous, SceneObject *current) {
406 selectedInfoObject = current;
407 updateStatusPanel();
408
409 if (previous) {
410 sceneManager->setSelectFor(previous, false);
411 }
412 if (current) {
413 sceneManager->setSelectFor(current, true);
414 sceneManager->setGizmoFor(current, true);
415 inspector->loadObject(current);
416
417 if (auto* selectedBody = current->getPhysicsBody()) {
418 selectedBody->withFrames(BodyLock::LOCK, [this](const std::vector<ObjectSnapshot>& snapshots) {
419 snapshotModel->setSnapshots(snapshots);
420 frameGraphPanel->loadSnapshots(snapshots);
421 });
422 } else {
423 snapshotModel->setSnapshots({});
424 frameGraphPanel->clear();
425 }
426 } else {
427 inspector->unloadObject();
428 snapshotModel->setSnapshots({});
429 frameGraphPanel->clear();
430 }
431}
432
std::variant< ObjectOptions, PointMassOptions, RigidBodyOptions > CreationOptions
void load(QSettings &settings)
static AppSettings & getInstance()
Definition AppSettings.h:11
T & getGroup() const
Definition AppSettings.h:28
T * registerGroup(Args &&... args)
Definition AppSettings.h:17
void loadSnapshots(const std::vector< ObjectSnapshot > &snapshots)
void renameObjectRequested(SceneObject *obj, const QString &newName)
void selectObject(SceneObject *obj)
void focusObjectRequested(SceneObject *obj)
void deleteObjectRequested(SceneObject *obj)
void clearCameraFollowRequested()
void followObjectRequested(SceneObject *obj)
void selectionChanged(SceneObject *previous, SceneObject *current)
void createObjectRequested(const CreationOptions &options)
void loadObject(SceneObject *obj)
MainWindow(QWidget *parent=nullptr)
void setScene(std::unique_ptr< Scene > sc)
void fpsUpdated(double fps)
void glInitialized()
void setSceneManager(SceneManager *scm)
bool isRenderClockRunning() const
static Shader * getShader(const std::string &name)
void contextMenuRequested(const QPoint &globalPos, SceneObject *object)
void objectRemoved(SceneObject *obj)
void selectedItem(SceneObject *object)
void objectAdded(SceneObject *obj)
void objectRenamed(SceneObject *obj, const QString &newName)
glm::vec3 getPosition() const
Physics::PhysicsBody * getPhysicsBody() const
Definition SceneObject.h:39
const std::string & getName() const
Definition SceneObject.h:48
Definition Scene.h:11
void settingsSaved()
void setSnapshots(const std::vector< ObjectSnapshot > &snaps)
std::span< const PresetDescriptor > all()