Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/UI.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ Sprint 1 시연에서 가장 중요한 기준은 `도면 불러오기 -> 검토/

### 3.1 Project Navigator

앱을 처음 켰을 때 보이는 시작 화면이다. 예시 프로젝트 목록을 두지 않고, 실제 저장된 프로젝트 목록과 built-in `Demo` 프로젝트를 보여준다.
앱을 처음 켰을 때 보이는 시작 화면이다. 예시 프로젝트 목록을 두지 않고, 실제 저장된 프로젝트 목록과 built-in 데모 프로젝트를 보여준다.

현재 기능:

- 최근 프로젝트 목록 표시
- built-in `Demo` 프로젝트 표시
- built-in `Demo`, `Evacuation Scenario Demo`, `Two-floor Evacuation Demo` 프로젝트 표시
- 프로젝트 목록이 없을 때 빈 상태 표시
- `+ New Project`로 새 프로젝트 생성 화면 진입
- 최근 프로젝트 row 클릭 시 해당 프로젝트 열기
- 일반 프로젝트 row의 삭제 버튼으로 프로젝트 삭제
- 삭제 전 확인 다이얼로그 표시
- 삭제 확인 시 실제 프로젝트 폴더 삭제 및 최근 목록 갱신
- built-in `Demo` 프로젝트 삭제 방지
- built-in 데모 프로젝트 삭제 방지

현재 UI 요소:

Expand Down
11 changes: 11 additions & 0 deletions docs/demo/시연 평면 가이드.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Sprint 1 시연에서 사용할 도면 자산과 각 도면이 강조하는 결과 지표를 정리합니다.
도면 파일은 [`assets/demo-layouts/`](../../assets/demo-layouts/) 에 있습니다.
앱 시작 화면의 built-in 프로젝트에는 코드 fixture 기반의 `Two-floor Evacuation Demo`도 포함됩니다.

## 시연 흐름 요약

Expand Down Expand Up @@ -47,6 +48,16 @@ Sprint 1 시연에서 사용할 도면 자산과 각 도면이 강조하는 결
- `DensitySummary.peakField` 의 피크 셀이 복도 중앙 또는 로비 출구 직전에 형성
- **보여줄 화면**: 존별 완료 시간 표, 출구 사용 막대그래프, 복도/로비 영역의 밀도 히트맵, 리플레이로 군중이 복도를 통해 모이는 흐름

### 4. 앱 내장 `Two-floor Evacuation Demo` — 2층 대피 시나리오 시연

- **구조**: 2층의 서측 교육실, 중앙 브리핑룸, 동측 교육실에서 복도로 나온 뒤 양쪽 U자형 계단을 통해 1층 로비와 전실을 거쳐 서측/동측 출구로 대피하는 내장 layout.
- **메시지**: "다층 평면에서 층 선택, 계단 전환, 출구 유도 시나리오가 앱에서 바로 실행되는가."
- **강조 지표**:
- 2층 보행자가 계단 연결을 지나 1층 출구로 이동
- baseline과 우측 출구 유도 alternative를 같은 run 화면에서 실행 가능
- 결과 화면의 floor selector로 1층/2층 상태를 나누어 확인 가능
- **보여줄 화면**: Project Navigator에서 `Two-floor Evacuation Demo` 열기, Run Workspace 실행, Result Summary의 층별 canvas와 출구 사용 지표

## 시연 셋업 권장값 (참고)

| 항목 | 권장 |
Expand Down
96 changes: 84 additions & 12 deletions src/application/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <iterator>
#include <string>
#include <utility>

Expand Down Expand Up @@ -35,12 +36,9 @@ void applySavedReviewState(const ProjectMetadata& metadata, safecrowd::domain::I
ProjectPersistence::loadProjectReview(metadata, importResult);
}

safecrowd::domain::ImportResult makeDemoImportResult() {
safecrowd::domain::DemoFixtureService fixtureService;
const auto fixture = fixtureService.createSprint1DemoFixture();

safecrowd::domain::ImportResult makeImportResultForDemoLayout(safecrowd::domain::FacilityLayout2D layout) {
safecrowd::domain::ImportResult result;
result.layout = fixture.layout;
result.layout = std::move(layout);

safecrowd::domain::ImportValidationService validator;
result.issues = validator.validate(*result.layout);
Expand All @@ -50,6 +48,18 @@ safecrowd::domain::ImportResult makeDemoImportResult() {
return result;
}

safecrowd::domain::ImportResult makeSprint1DemoImportResult() {
safecrowd::domain::DemoFixtureService fixtureService;
const auto fixture = fixtureService.createSprint1DemoFixture();
return makeImportResultForDemoLayout(fixture.layout);
}

safecrowd::domain::ImportResult makeTwoFloorEvacuationDemoImportResult() {
safecrowd::domain::DemoFixtureService fixtureService;
const auto fixture = fixtureService.createTwoFloorEvacuationDemoFixture();
return makeImportResultForDemoLayout(fixture.layout);
}

ProjectWorkspaceState makeEvacuationScenarioDemoWorkspace() {
using namespace safecrowd::domain;

Expand Down Expand Up @@ -90,6 +100,33 @@ ProjectWorkspaceState makeEvacuationScenarioDemoWorkspace() {
return workspace;
}

ProjectWorkspaceState makeTwoFloorEvacuationDemoWorkspace() {
using namespace safecrowd::domain;

safecrowd::domain::DemoFixtureService fixtureService;
auto fixture = fixtureService.createTwoFloorEvacuationDemoFixture();

SavedScenarioAuthoringState authoring;
authoring.scenarios.push_back({
.draft = fixture.baselineScenario,
.baseScenarioId = {},
.stagedForRun = true,
});
authoring.scenarios.push_back({
.draft = fixture.alternativeScenario,
.baseScenarioId = fixture.baselineScenario.scenarioId,
.stagedForRun = true,
});
authoring.currentScenarioIndex = 1;
authoring.navigationView = SavedNavigationView::Events;
authoring.rightPanelMode = SavedRightPanelMode::Scenario;

ProjectWorkspaceState workspace;
workspace.activeView = ProjectWorkspaceView::ScenarioAuthoring;
workspace.authoring = std::move(authoring);
return workspace;
}

safecrowd::domain::ImportResult makeBlankImportResult(const QString& projectName) {
safecrowd::domain::ImportResult result;
result.layout = safecrowd::domain::FacilityLayout2D{
Expand All @@ -111,8 +148,11 @@ safecrowd::domain::ImportResult makeBlankImportResult(const QString& projectName
}

safecrowd::domain::ImportResult importProjectLayout(const ProjectMetadata& metadata) {
if (metadata.isBuiltInDemo()) {
return makeDemoImportResult();
if (metadata.isBuiltInTwoFloorEvacuationDemo()) {
return makeTwoFloorEvacuationDemoImportResult();
}
if (metadata.layoutPath == builtInDemoLayoutPath() || metadata.isBuiltInEvacuationScenarioDemo()) {
return makeSprint1DemoImportResult();
}
if (metadata.isBlankLayoutProject()) {
return makeBlankImportResult(metadata.name);
Expand Down Expand Up @@ -270,6 +310,29 @@ SavedScenarioAuthoringState savedStateFromInitial(const ScenarioAuthoringWidget:
return saved;
}

int selectedRunIndexFor(
const std::vector<safecrowd::domain::ScenarioDraft>& scenarios,
const std::optional<safecrowd::domain::ScenarioDraft>& selectedScenario) {
if (!selectedScenario.has_value()) {
return 0;
}

if (!selectedScenario->scenarioId.empty()) {
const auto idIt = std::find_if(scenarios.begin(), scenarios.end(), [&](const auto& scenario) {
return scenario.scenarioId == selectedScenario->scenarioId;
});
if (idIt != scenarios.end()) {
return static_cast<int>(std::distance(scenarios.begin(), idIt));
}
}

const auto nameIt = std::find_if(scenarios.begin(), scenarios.end(), [&](const auto& scenario) {
return scenario.name == selectedScenario->name
&& scenario.role == selectedScenario->role;
});
return nameIt == scenarios.end() ? 0 : static_cast<int>(std::distance(scenarios.begin(), nameIt));
}

template <typename Widget>
Widget* visibleChild(QWidget* root) {
if (root == nullptr) {
Expand Down Expand Up @@ -404,6 +467,8 @@ void MainWindow::openProject(const ProjectMetadata& metadata) {
ProjectWorkspaceState workspace;
if (metadata.isBuiltInEvacuationScenarioDemo()) {
workspace = makeEvacuationScenarioDemoWorkspace();
} else if (metadata.isBuiltInTwoFloorEvacuationDemo()) {
workspace = makeTwoFloorEvacuationDemoWorkspace();
} else if (!ProjectPersistence::loadProjectWorkspace(metadata, &workspace)) {
showLayoutReview(metadata, std::move(importResult));
return;
Expand All @@ -419,12 +484,17 @@ void MainWindow::openProject(const ProjectMetadata& metadata) {
break;
case ProjectWorkspaceView::ScenarioRun:
if (!workspace.runningScenarios.empty()) {
auto returnAuthoringState = workspace.authoring.has_value()
? std::make_optional(initialStateFromSaved(*workspace.authoring, *importResult.layout))
: std::optional<ScenarioAuthoringWidget::InitialState>{};
const auto initialSelectedRunIndex = selectedRunIndexFor(
workspace.runningScenarios,
workspace.runningScenario);
showScenarioRun(
*importResult.layout,
std::move(workspace.runningScenarios),
workspace.authoring.has_value()
? std::make_optional(initialStateFromSaved(*workspace.authoring, *importResult.layout))
: std::nullopt);
std::move(returnAuthoringState),
initialSelectedRunIndex);
return;
}
if (workspace.runningScenario.has_value()) {
Expand Down Expand Up @@ -715,7 +785,8 @@ void MainWindow::showScenarioRun(
void MainWindow::showScenarioRun(
const safecrowd::domain::FacilityLayout2D& layout,
std::vector<safecrowd::domain::ScenarioDraft> scenarios,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState) {
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState,
int initialSelectedRunIndex) {
setCentralWidget(new ScenarioRunWidget(
currentProject_.name,
layout,
Expand All @@ -736,7 +807,8 @@ void MainWindow::showScenarioRun(
}
},
std::move(returnAuthoringState),
this));
this,
initialSelectedRunIndex));
}

void MainWindow::showScenarioBatchResult(
Expand Down
3 changes: 2 additions & 1 deletion src/application/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class MainWindow : public QMainWindow {
void showScenarioRun(
const safecrowd::domain::FacilityLayout2D& layout,
std::vector<safecrowd::domain::ScenarioDraft> scenarios,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState = std::nullopt);
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState = std::nullopt,
int initialSelectedRunIndex = 0);
void showScenarioBatchResult(
const safecrowd::domain::FacilityLayout2D& layout,
std::vector<SavedScenarioResultState> results,
Expand Down
19 changes: 18 additions & 1 deletion src/application/ProjectMetadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,30 @@ inline QString builtInEvacuationScenarioDemoLayoutPath() {
return QStringLiteral("safecrowd://demo/evacuation-scenario");
}

inline QString builtInTwoFloorEvacuationDemoLayoutPath() {
return QStringLiteral("safecrowd://demo/two-floor-evacuation");
}

struct ProjectMetadata {
QString name{};
QString folderPath{};
QString layoutPath{};
QString savedAt{};

bool isBuiltInDemo() const noexcept {
return layoutPath == builtInDemoLayoutPath() || isBuiltInEvacuationScenarioDemo();
return layoutPath == builtInDemoLayoutPath()
|| isBuiltInEvacuationScenarioDemo()
|| isBuiltInTwoFloorEvacuationDemo();
}

bool isBuiltInEvacuationScenarioDemo() const noexcept {
return layoutPath == builtInEvacuationScenarioDemoLayoutPath();
}

bool isBuiltInTwoFloorEvacuationDemo() const noexcept {
return layoutPath == builtInTwoFloorEvacuationDemoLayoutPath();
}

bool isBlankLayoutProject() const noexcept {
return layoutPath.isEmpty();
}
Expand All @@ -52,4 +62,11 @@ inline ProjectMetadata makeBuiltInEvacuationScenarioDemoProject() {
};
}

inline ProjectMetadata makeBuiltInTwoFloorEvacuationDemoProject() {
return {
.name = QStringLiteral("Two-floor Evacuation Demo"),
.layoutPath = builtInTwoFloorEvacuationDemoLayoutPath(),
};
}

} // namespace safecrowd::application
1 change: 1 addition & 0 deletions src/application/ProjectPersistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,7 @@ QList<ProjectMetadata> ProjectPersistence::loadRecentProjects() {
QList<ProjectMetadata> projects;
projects.push_back(makeBuiltInDemoProject());
projects.push_back(makeBuiltInEvacuationScenarioDemoProject());
projects.push_back(makeBuiltInTwoFloorEvacuationDemoProject());

const auto document = readJsonDocument(recentProjectsPath());
if (!document.isObject()) {
Expand Down
32 changes: 25 additions & 7 deletions src/application/ScenarioRunWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ namespace {
constexpr double kSimulationDeltaSeconds = 1.0 / 30.0;
constexpr int kPlaybackTimerIntervalMs = 33;

int normalizedRunIndex(int index, std::size_t runCount) {
if (runCount == 0) {
return 0;
}
return std::clamp(index, 0, static_cast<int>(runCount) - 1);
}

enum class TransportIconKind {
Play,
Pause,
Expand Down Expand Up @@ -232,7 +239,8 @@ ScenarioRunWidget::ScenarioRunWidget(
std::move(openProjectHandler),
std::move(backToLayoutReviewHandler),
std::move(returnAuthoringState),
parent) {}
parent,
0) {}

ScenarioRunWidget::ScenarioRunWidget(
const QString& projectName,
Expand Down Expand Up @@ -260,7 +268,8 @@ ScenarioRunWidget::ScenarioRunWidget(
std::move(openProjectHandler),
std::move(backToLayoutReviewHandler),
std::move(returnAuthoringState),
parent) {}
parent,
0) {}

ScenarioRunWidget::ScenarioRunWidget(
const QString& projectName,
Expand All @@ -270,7 +279,8 @@ ScenarioRunWidget::ScenarioRunWidget(
std::function<void()> openProjectHandler,
std::function<void()> backToLayoutReviewHandler,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState,
QWidget* parent)
QWidget* parent,
int initialSelectedRunIndex)
: ScenarioRunWidget(
projectName,
layout,
Expand All @@ -280,7 +290,8 @@ ScenarioRunWidget::ScenarioRunWidget(
std::move(openProjectHandler),
std::move(backToLayoutReviewHandler),
std::move(returnAuthoringState),
parent) {}
parent,
initialSelectedRunIndex) {}

ScenarioRunWidget::ScenarioRunWidget(
const QString& projectName,
Expand All @@ -291,18 +302,24 @@ ScenarioRunWidget::ScenarioRunWidget(
std::function<void()> openProjectHandler,
std::function<void()> backToLayoutReviewHandler,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState,
QWidget* parent)
QWidget* parent,
int initialSelectedRunIndex)
: QWidget(parent),
projectName_(projectName),
layout_(layout),
scenario_(scenarios.empty() ? safecrowd::domain::ScenarioDraft{} : scenarios.front()),
scenario_({}),
scenarios_(std::move(scenarios)),
cachedResults_(std::move(cachedResults)),
batchRunner_(layout_, scenarios_),
returnAuthoringState_(std::move(returnAuthoringState)),
saveProjectHandler_(std::move(saveProjectHandler)),
openProjectHandler_(std::move(openProjectHandler)),
backToLayoutReviewHandler_(std::move(backToLayoutReviewHandler)) {
selectedRunIndex_ = normalizedRunIndex(initialSelectedRunIndex, scenarios_.size());
if (!scenarios_.empty()) {
scenario_ = scenarios_[static_cast<std::size_t>(selectedRunIndex_)];
}

auto* rootLayout = new QVBoxLayout(this);
rootLayout->setContentsMargins(0, 0, 0, 0);
rootLayout->setSpacing(0);
Expand Down Expand Up @@ -398,7 +415,8 @@ QWidget* ScenarioRunWidget::createRunCanvas() {
canvas_ = new SimulationCanvasWidget(layout_, container);
canvas_->setMinimumHeight(360);
if (!batchRunner_.empty()) {
const auto& run = batchRunner_.run(0);
const auto& run = batchRunner_.run(static_cast<std::size_t>(
normalizedRunIndex(selectedRunIndex_, batchRunner_.size())));
canvas_->setConnectionBlocks(run.scenario.control.connectionBlocks);
canvas_->setEnvironmentHazards(run.scenario.environment.hazards);
canvas_->setRouteGuidances(run.scenario.control.routeGuidances);
Expand Down
6 changes: 4 additions & 2 deletions src/application/ScenarioRunWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class ScenarioRunWidget : public QWidget {
std::function<void()> openProjectHandler,
std::function<void()> backToLayoutReviewHandler,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState = std::nullopt,
QWidget* parent = nullptr);
QWidget* parent = nullptr,
int initialSelectedRunIndex = 0);

explicit ScenarioRunWidget(
const QString& projectName,
Expand All @@ -67,7 +68,8 @@ class ScenarioRunWidget : public QWidget {
std::function<void()> openProjectHandler,
std::function<void()> backToLayoutReviewHandler,
std::optional<ScenarioAuthoringWidget::InitialState> returnAuthoringState = std::nullopt,
QWidget* parent = nullptr);
QWidget* parent = nullptr,
int initialSelectedRunIndex = 0);

const safecrowd::domain::ScenarioDraft& scenario() const noexcept;
const std::vector<safecrowd::domain::ScenarioDraft>& scenarios() const noexcept;
Expand Down
Loading
Loading