From d5122a2fa1d10cae60eb2a7d2d48672cd14491ae Mon Sep 17 00:00:00 2001 From: Silversupplier Date: Wed, 3 Jun 2026 04:53:48 +0900 Subject: [PATCH 1/4] [Application] add run-screen execution controls --- src/application/ScenarioRunWidget.cpp | 119 ++++++++++++++++++++++++++ src/application/ScenarioRunWidget.h | 9 ++ 2 files changed, 128 insertions(+) diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index 6f3c955..645ec9a 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -20,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -657,6 +661,49 @@ QWidget* ScenarioRunWidget::createRunPanel() { layout->addWidget(congestionLabel_); layout->addWidget(bottleneckLabel_); + auto* settingsTitle = createLabel("Run settings", panel, ui::FontRole::Caption); + settingsTitle->setStyleSheet(ui::mutedTextStyleSheet()); + layout->addWidget(settingsTitle); + + auto* settingsGroup = new QWidget(panel); + auto* settingsForm = new QFormLayout(settingsGroup); + settingsForm->setContentsMargins(0, 4, 0, 0); + settingsForm->setSpacing(6); + + timeLimitSpin_ = new QDoubleSpinBox(settingsGroup); + timeLimitSpin_->setRange(1.0, 3600.0); + timeLimitSpin_->setDecimals(0); + timeLimitSpin_->setSuffix(" s"); + timeLimitSpin_->setToolTip("Simulation time limit"); + settingsForm->addRow("Time limit", timeLimitSpin_); + + sampleIntervalSpin_ = new QDoubleSpinBox(settingsGroup); + sampleIntervalSpin_->setRange(0.1, 10.0); + sampleIntervalSpin_->setSingleStep(0.1); + sampleIntervalSpin_->setDecimals(2); + sampleIntervalSpin_->setSuffix(" s"); + sampleIntervalSpin_->setToolTip("Result sample interval"); + settingsForm->addRow("Sample interval", sampleIntervalSpin_); + + repeatSpin_ = new QSpinBox(settingsGroup); + repeatSpin_->setRange(1, static_cast(safecrowd::domain::kScenarioExecutionMaxRepeatCount)); + repeatSpin_->setSuffix(" runs"); + repeatSpin_->setToolTip("Repeat count"); + settingsForm->addRow("Repeats", repeatSpin_); + + seedSpin_ = new QSpinBox(settingsGroup); + seedSpin_->setRange(1, 1000000); + seedSpin_->setToolTip("Base random seed"); + settingsForm->addRow("Seed", seedSpin_); + + applySettingsButton_ = new QPushButton("Apply & restart", settingsGroup); + applySettingsButton_->setFont(ui::font(ui::FontRole::Caption)); + applySettingsButton_->setStyleSheet(ui::secondaryButtonStyleSheet()); + settingsForm->addRow(applySettingsButton_); + + layout->addWidget(settingsGroup); + syncRunSettingsControls(); + layout->addStretch(1); auto* transportLayout = new QHBoxLayout(); @@ -704,6 +751,9 @@ QWidget* ScenarioRunWidget::createRunPanel() { connect(resultButton_, &QPushButton::clicked, this, [this]() { showResults(); }); + connect(applySettingsButton_, &QPushButton::clicked, this, [this]() { + applyRunSettings(); + }); return panel; } @@ -917,7 +967,76 @@ void ScenarioRunWidget::selectRun(int index) { } selectedRunIndex_ = index; scenario_ = batchRunner_.run(static_cast(selectedRunIndex_)).scenario; + syncRunSettingsControls(); + refreshStatus(); +} + +void ScenarioRunWidget::syncRunSettingsControls() { + if (scenarios_.empty()) { + return; + } + const auto sourceIndex = selectedSourceScenarioIndex(); + if (sourceIndex >= scenarios_.size()) { + return; + } + const auto& execution = scenarios_[sourceIndex].execution; + if (timeLimitSpin_ != nullptr) { + timeLimitSpin_->setValue(execution.timeLimitSeconds > 0.0 ? execution.timeLimitSeconds : 600.0); + } + if (sampleIntervalSpin_ != nullptr) { + sampleIntervalSpin_->setValue(execution.sampleIntervalSeconds > 0.0 ? execution.sampleIntervalSeconds : 0.5); + } + if (repeatSpin_ != nullptr) { + repeatSpin_->setValue(std::clamp( + static_cast(execution.repeatCount), + 1, + static_cast(safecrowd::domain::kScenarioExecutionMaxRepeatCount))); + } + if (seedSpin_ != nullptr) { + seedSpin_->setValue(execution.baseSeed == 0 + ? 1 + : static_cast(std::min(execution.baseSeed, 1000000U))); + } +} + +void ScenarioRunWidget::applyRunSettings() { + if (scenarios_.empty() + || timeLimitSpin_ == nullptr + || sampleIntervalSpin_ == nullptr + || repeatSpin_ == nullptr + || seedSpin_ == nullptr) { + return; + } + + const auto timeLimitSeconds = timeLimitSpin_->value(); + const auto sampleIntervalSeconds = sampleIntervalSpin_->value(); + const auto repeatCount = static_cast(repeatSpin_->value()); + const auto baseSeed = static_cast(seedSpin_->value()); + for (auto& scenario : scenarios_) { + scenario.execution.timeLimitSeconds = timeLimitSeconds; + scenario.execution.sampleIntervalSeconds = sampleIntervalSeconds; + scenario.execution.repeatCount = repeatCount; + scenario.execution.baseSeed = baseSeed; + } + + playbackSpeedMultiplier_ = 1; + paused_ = false; + cachedResults_.clear(); + if (timer_ != nullptr) { + timer_->stop(); + } + batchRunner_.reset(layout_, scenarios_); + selectedRunIndex_ = normalizedRunIndex(selectedRunIndex_, batchRunner_.size()); + if (!batchRunner_.empty()) { + scenario_ = batchRunner_.run(static_cast(selectedRunIndex_)).scenario; + } + if (shell_ != nullptr) { + shell_->setCanvas(createRunCanvas()); + } refreshStatus(); + if (timer_ != nullptr) { + timer_->start(); + } } void ScenarioRunWidget::cycleFastForwardMode() { diff --git a/src/application/ScenarioRunWidget.h b/src/application/ScenarioRunWidget.h index cf5155d..b52e915 100644 --- a/src/application/ScenarioRunWidget.h +++ b/src/application/ScenarioRunWidget.h @@ -13,9 +13,11 @@ #include "domain/ScenarioAuthoring.h" #include "domain/ScenarioBatchRunner.h" +class QDoubleSpinBox; class QLabel; class QProgressBar; class QPushButton; +class QSpinBox; class QTimer; namespace safecrowd::application { @@ -89,6 +91,8 @@ class ScenarioRunWidget : public QWidget { void refreshStatus(); void selectRun(int index); std::size_t selectedSourceScenarioIndex() const; + void applyRunSettings(); + void syncRunSettingsControls(); void showResults(); void setPlaybackSpeedMultiplier(int multiplier); void stopRun(); @@ -126,6 +130,11 @@ class ScenarioRunWidget : public QWidget { QPushButton* speed3Button_{nullptr}; QPushButton* speed5Button_{nullptr}; QPushButton* resultButton_{nullptr}; + QDoubleSpinBox* timeLimitSpin_{nullptr}; + QDoubleSpinBox* sampleIntervalSpin_{nullptr}; + QSpinBox* repeatSpin_{nullptr}; + QSpinBox* seedSpin_{nullptr}; + QPushButton* applySettingsButton_{nullptr}; int selectedRunIndex_{0}; int playbackSpeedMultiplier_{1}; bool paused_{false}; From 99cabe4f2a5e61bf79a9cdfb66c6c5c5b6ba38d1 Mon Sep 17 00:00:00 2001 From: Silversupplier Date: Wed, 3 Jun 2026 05:57:33 +0900 Subject: [PATCH 2/4] Scope run settings to selected scenario --- src/application/ScenarioRunWidget.cpp | 57 +++++++++++++++++++++------ src/application/ScenarioRunWidget.h | 1 + 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index 645ec9a..e3339a2 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -49,6 +50,18 @@ int normalizedRunIndex(int index, std::size_t runCount) { return std::clamp(index, 0, static_cast(runCount) - 1); } +int firstRunIndexForSourceScenario( + const safecrowd::domain::ScenarioBatchRunner& batchRunner, + std::size_t sourceScenarioIndex, + int fallbackIndex) { + for (std::size_t index = 0; index < batchRunner.size(); ++index) { + if (batchRunner.run(index).sourceScenarioIndex == sourceScenarioIndex) { + return static_cast(index); + } + } + return normalizedRunIndex(fallbackIndex, batchRunner.size()); +} + enum class TransportIconKind { Play, Pause, @@ -696,7 +709,7 @@ QWidget* ScenarioRunWidget::createRunPanel() { seedSpin_->setToolTip("Base random seed"); settingsForm->addRow("Seed", seedSpin_); - applySettingsButton_ = new QPushButton("Apply & restart", settingsGroup); + applySettingsButton_ = new QPushButton("Apply selected & restart", settingsGroup); applySettingsButton_->setFont(ui::font(ui::FontRole::Caption)); applySettingsButton_->setStyleSheet(ui::secondaryButtonStyleSheet()); settingsForm->addRow(applySettingsButton_); @@ -1008,17 +1021,18 @@ void ScenarioRunWidget::applyRunSettings() { return; } - const auto timeLimitSeconds = timeLimitSpin_->value(); - const auto sampleIntervalSeconds = sampleIntervalSpin_->value(); - const auto repeatCount = static_cast(repeatSpin_->value()); - const auto baseSeed = static_cast(seedSpin_->value()); - for (auto& scenario : scenarios_) { - scenario.execution.timeLimitSeconds = timeLimitSeconds; - scenario.execution.sampleIntervalSeconds = sampleIntervalSeconds; - scenario.execution.repeatCount = repeatCount; - scenario.execution.baseSeed = baseSeed; + const auto sourceIndex = selectedSourceScenarioIndex(); + if (sourceIndex >= scenarios_.size()) { + return; } + auto& execution = scenarios_[sourceIndex].execution; + execution.timeLimitSeconds = timeLimitSpin_->value(); + execution.sampleIntervalSeconds = sampleIntervalSpin_->value(); + execution.repeatCount = static_cast(repeatSpin_->value()); + execution.baseSeed = static_cast(seedSpin_->value()); + syncReturnAuthoringScenarioExecution(sourceIndex); + playbackSpeedMultiplier_ = 1; paused_ = false; cachedResults_.clear(); @@ -1026,7 +1040,7 @@ void ScenarioRunWidget::applyRunSettings() { timer_->stop(); } batchRunner_.reset(layout_, scenarios_); - selectedRunIndex_ = normalizedRunIndex(selectedRunIndex_, batchRunner_.size()); + selectedRunIndex_ = firstRunIndexForSourceScenario(batchRunner_, sourceIndex, selectedRunIndex_); if (!batchRunner_.empty()) { scenario_ = batchRunner_.run(static_cast(selectedRunIndex_)).scenario; } @@ -1039,6 +1053,27 @@ void ScenarioRunWidget::applyRunSettings() { } } +void ScenarioRunWidget::syncReturnAuthoringScenarioExecution(std::size_t sourceIndex) { + if (!returnAuthoringState_.has_value() || sourceIndex >= scenarios_.size()) { + return; + } + + const auto& sourceScenario = scenarios_[sourceIndex]; + auto& authoringScenarios = returnAuthoringState_->scenarios; + auto it = std::find_if(authoringScenarios.begin(), authoringScenarios.end(), [&](const auto& scenario) { + return scenario.draft.scenarioId == sourceScenario.scenarioId; + }); + if (it == authoringScenarios.end() && sourceIndex < authoringScenarios.size()) { + it = std::next(authoringScenarios.begin(), static_cast(sourceIndex)); + } + if (it == authoringScenarios.end()) { + return; + } + + it->draft.execution = sourceScenario.execution; + it->stagedForRun = true; +} + void ScenarioRunWidget::cycleFastForwardMode() { if (batchRunner_.empty()) { refreshStatus(); diff --git a/src/application/ScenarioRunWidget.h b/src/application/ScenarioRunWidget.h index b52e915..e96d10d 100644 --- a/src/application/ScenarioRunWidget.h +++ b/src/application/ScenarioRunWidget.h @@ -92,6 +92,7 @@ class ScenarioRunWidget : public QWidget { void selectRun(int index); std::size_t selectedSourceScenarioIndex() const; void applyRunSettings(); + void syncReturnAuthoringScenarioExecution(std::size_t sourceIndex); void syncRunSettingsControls(); void showResults(); void setPlaybackSpeedMultiplier(int multiplier); From 70bb53dffe7248d18eefa7e68e91a1754ef90774 Mon Sep 17 00:00:00 2001 From: Silversupplier Date: Wed, 3 Jun 2026 10:08:45 +0900 Subject: [PATCH 3/4] [Application] confirm before discarding results on run settings apply --- src/application/ScenarioRunWidget.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index e3339a2..4d8c431 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -1026,6 +1027,18 @@ void ScenarioRunWidget::applyRunSettings() { return; } + const bool wouldDiscardResults = hasCachedResults() || (batchRunner_.complete() && !batchRunner_.empty()); + if (wouldDiscardResults + && QMessageBox::question( + this, + "Apply run settings", + "Re-running with the new settings discards the current simulation results.\n\nContinue?", + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel) + != QMessageBox::Yes) { + return; + } + auto& execution = scenarios_[sourceIndex].execution; execution.timeLimitSeconds = timeLimitSpin_->value(); execution.sampleIntervalSeconds = sampleIntervalSpin_->value(); From 11bb528bbc938c15db557b0fbc0509109ed9ec55 Mon Sep 17 00:00:00 2001 From: Silversupplier Date: Wed, 3 Jun 2026 14:10:13 +0900 Subject: [PATCH 4/4] Align run seed limit with authoring --- src/application/ScenarioRunWidget.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index 4d8c431..0825d20 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -43,6 +43,7 @@ namespace { constexpr double kSimulationDeltaSeconds = 1.0 / 30.0; constexpr double kResultCalculationChunkSeconds = 1.0; constexpr int kPlaybackTimerIntervalMs = 33; +constexpr int kMaxUiSeed = 2147483647; int normalizedRunIndex(int index, std::size_t runCount) { if (runCount == 0) { @@ -706,7 +707,7 @@ QWidget* ScenarioRunWidget::createRunPanel() { settingsForm->addRow("Repeats", repeatSpin_); seedSpin_ = new QSpinBox(settingsGroup); - seedSpin_->setRange(1, 1000000); + seedSpin_->setRange(1, kMaxUiSeed); seedSpin_->setToolTip("Base random seed"); settingsForm->addRow("Seed", seedSpin_); @@ -1009,7 +1010,9 @@ void ScenarioRunWidget::syncRunSettingsControls() { if (seedSpin_ != nullptr) { seedSpin_->setValue(execution.baseSeed == 0 ? 1 - : static_cast(std::min(execution.baseSeed, 1000000U))); + : static_cast(std::min( + execution.baseSeed, + static_cast(kMaxUiSeed)))); } }