From 300d8db6a8a15c14af876be15aa43ab269b47c8f Mon Sep 17 00:00:00 2001 From: 95x8x9 Date: Wed, 22 Apr 2026 01:35:07 +0900 Subject: [PATCH 1/6] [Domain] Provide runtime demo FacilityLayout2D --- CMakeLists.txt | 2 + src/domain/DemoFixtureService.cpp | 68 +++-------------------- src/domain/DemoLayouts.cpp | 91 +++++++++++++++++++++++++++++++ src/domain/DemoLayouts.h | 20 +++++++ tests/DemoFixtureServiceTests.cpp | 64 +++++++++++++++++++--- 5 files changed, 176 insertions(+), 69 deletions(-) create mode 100644 src/domain/DemoLayouts.cpp create mode 100644 src/domain/DemoLayouts.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a3a870b..40dd30d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,8 @@ configure_project_target(ecs_engine) add_library(safecrowd_domain STATIC src/domain/SafeCrowdDomain.h src/domain/SafeCrowdDomain.cpp + src/domain/DemoLayouts.h + src/domain/DemoLayouts.cpp src/domain/Geometry2D.h src/domain/PopulationSpec.h src/domain/ScenarioAuthoring.h diff --git a/src/domain/DemoFixtureService.cpp b/src/domain/DemoFixtureService.cpp index 0bde4f6..3bade0c 100644 --- a/src/domain/DemoFixtureService.cpp +++ b/src/domain/DemoFixtureService.cpp @@ -1,76 +1,22 @@ #include "domain/DemoFixtureService.h" +#include "domain/DemoLayouts.h" + namespace safecrowd::domain { DemoFixture DemoFixtureService::createSprint1DemoFixture() const { DemoFixture fixture; - auto& layout = fixture.layout; - layout.id = "demo-fixture-01"; - layout.name = "Sprint 1 Demo Layout"; - layout.levelId = "L1"; - - Zone2D mainRoom; - mainRoom.id = "zone-room-1"; - mainRoom.kind = ZoneKind::Room; - mainRoom.label = "Main Demo Room"; - mainRoom.area = Polygon2D{ - .outline = { - {0.0, 0.0}, - {20.0, 0.0}, - {20.0, 20.0}, - {0.0, 20.0}, - }, - }; - mainRoom.defaultCapacity = 200; - layout.zones.push_back(mainRoom); - - Zone2D exitZone; - exitZone.id = "zone-exit-1"; - exitZone.kind = ZoneKind::Exit; - exitZone.label = "Main Exit"; - exitZone.area = Polygon2D{ - .outline = { - {18.0, 20.0}, - {20.0, 20.0}, - {20.0, 22.0}, - {18.0, 22.0}, - }, - }; - exitZone.defaultCapacity = 20; - layout.zones.push_back(exitZone); - - Connection2D exitConnection; - exitConnection.id = "conn-exit-1"; - exitConnection.kind = ConnectionKind::Exit; - exitConnection.fromZoneId = mainRoom.id; - exitConnection.toZoneId = exitZone.id; - exitConnection.effectiveWidth = 2.0; - exitConnection.centerSpan = LineSegment2D{{18.0, 20.0}, {20.0, 20.0}}; - layout.connections.push_back(exitConnection); - - Barrier2D centerObstacle; - centerObstacle.id = "barrier-1"; - centerObstacle.blocksMovement = true; - centerObstacle.geometry = Polyline2D{ - .vertices = { - {8.0, 10.0}, - {12.0, 10.0}, - {12.0, 11.0}, - {8.0, 11.0}, - }, - .closed = true, - }; - layout.barriers.push_back(centerObstacle); + fixture.layout = DemoLayouts::demoFacility(); fixture.population.initialPlacements.push_back({ .id = "placement-1", - .zoneId = mainRoom.id, + .zoneId = DemoLayouts::Sprint1FacilityIds::MainRoomZoneId, .area = { .outline = { {1.0, 1.0}, - {5.0, 1.0}, - {5.0, 5.0}, - {1.0, 5.0}, + {4.0, 1.0}, + {4.0, 4.0}, + {1.0, 4.0}, }, }, .targetAgentCount = 100, diff --git a/src/domain/DemoLayouts.cpp b/src/domain/DemoLayouts.cpp new file mode 100644 index 0000000..dea816d --- /dev/null +++ b/src/domain/DemoLayouts.cpp @@ -0,0 +1,91 @@ +#include "domain/DemoLayouts.h" + +namespace safecrowd::domain::DemoLayouts { + +FacilityLayout2D demoFacility() { + FacilityLayout2D layout{}; + layout.id = Sprint1FacilityIds::LayoutId; + layout.name = "Sprint 1 Demo Layout"; + layout.levelId = "L1"; + + Zone2D mainRoom; + mainRoom.id = Sprint1FacilityIds::MainRoomZoneId; + mainRoom.kind = ZoneKind::Room; + mainRoom.label = "Main Demo Room"; + mainRoom.area = Polygon2D{ + .outline = { + {0.0, 0.0}, + {10.0, 0.0}, + {10.0, 10.0}, + {0.0, 10.0}, + }, + }; + mainRoom.defaultCapacity = 200; + layout.zones.push_back(mainRoom); + + Zone2D sideRoom; + sideRoom.id = Sprint1FacilityIds::SideRoomZoneId; + sideRoom.kind = ZoneKind::Room; + sideRoom.label = "Side Demo Room"; + sideRoom.area = Polygon2D{ + .outline = { + {10.0, 0.0}, + {20.0, 0.0}, + {20.0, 10.0}, + {10.0, 10.0}, + }, + }; + sideRoom.defaultCapacity = 80; + layout.zones.push_back(sideRoom); + + Zone2D exitZone; + exitZone.id = Sprint1FacilityIds::ExitZoneId; + exitZone.kind = ZoneKind::Exit; + exitZone.label = "Main Exit"; + exitZone.area = Polygon2D{ + .outline = { + {18.0, 10.0}, + {20.0, 10.0}, + {20.0, 12.0}, + {18.0, 12.0}, + }, + }; + exitZone.defaultCapacity = 20; + layout.zones.push_back(exitZone); + + Connection2D roomConnection; + roomConnection.id = Sprint1FacilityIds::OpeningConnectionId; + roomConnection.kind = ConnectionKind::Opening; + roomConnection.fromZoneId = mainRoom.id; + roomConnection.toZoneId = sideRoom.id; + roomConnection.effectiveWidth = 3.0; + roomConnection.centerSpan = LineSegment2D{{10.0, 4.0}, {10.0, 6.0}}; + layout.connections.push_back(roomConnection); + + Connection2D exitConnection; + exitConnection.id = Sprint1FacilityIds::ExitConnectionId; + exitConnection.kind = ConnectionKind::Exit; + exitConnection.fromZoneId = sideRoom.id; + exitConnection.toZoneId = exitZone.id; + exitConnection.effectiveWidth = 2.0; + exitConnection.centerSpan = LineSegment2D{{18.0, 10.0}, {20.0, 10.0}}; + layout.connections.push_back(exitConnection); + + Barrier2D centerObstacle; + centerObstacle.id = Sprint1FacilityIds::BarrierId; + centerObstacle.blocksMovement = true; + centerObstacle.geometry = Polyline2D{ + .vertices = { + {4.0, 4.0}, + {6.0, 4.0}, + {6.0, 5.0}, + {4.0, 5.0}, + }, + .closed = true, + }; + layout.barriers.push_back(centerObstacle); + + return layout; +} + +} // namespace safecrowd::domain::DemoLayouts diff --git a/src/domain/DemoLayouts.h b/src/domain/DemoLayouts.h new file mode 100644 index 0000000..da5ae8f --- /dev/null +++ b/src/domain/DemoLayouts.h @@ -0,0 +1,20 @@ +#pragma once + +#include "domain/FacilityLayout2D.h" + +namespace safecrowd::domain::DemoLayouts { + +struct Sprint1FacilityIds { + static constexpr const char* LayoutId = "demo-fixture-01"; + static constexpr const char* MainRoomZoneId = "zone-room-1"; + static constexpr const char* SideRoomZoneId = "zone-room-2"; + static constexpr const char* ExitZoneId = "zone-exit-1"; + static constexpr const char* OpeningConnectionId = "conn-opening-1"; + static constexpr const char* ExitConnectionId = "conn-exit-1"; + static constexpr const char* BarrierId = "barrier-1"; +}; + +FacilityLayout2D demoFacility(); + +} // namespace safecrowd::domain::DemoLayouts + diff --git a/tests/DemoFixtureServiceTests.cpp b/tests/DemoFixtureServiceTests.cpp index d156b14..102a8ca 100644 --- a/tests/DemoFixtureServiceTests.cpp +++ b/tests/DemoFixtureServiceTests.cpp @@ -1,32 +1,63 @@ +#include #include #include "TestSupport.h" +#include "domain/DemoLayouts.h" #include "domain/DemoFixtureService.h" #include "domain/ImportIssue.h" #include "domain/ImportValidationService.h" +namespace { + +bool containsConnectionKind( + const std::vector& connections, + safecrowd::domain::ConnectionKind kind) { + return std::any_of(connections.begin(), connections.end(), [&](const auto& connection) { + return connection.kind == kind; + }); +} + +bool containsZoneId( + const std::vector& zones, + const std::string& id) { + return std::any_of(zones.begin(), zones.end(), [&](const auto& zone) { + return zone.id == id; + }); +} + +bool containsConnectionId( + const std::vector& connections, + const std::string& id) { + return std::any_of(connections.begin(), connections.end(), [&](const auto& connection) { + return connection.id == id; + }); +} + +} // namespace + SC_TEST(DemoFixtureServiceBuildsSprint1Fixture) { safecrowd::domain::DemoFixtureService service; const auto fixture = service.createSprint1DemoFixture(); const auto& layout = fixture.layout; const auto& population = fixture.population; - SC_EXPECT_EQ(layout.id, std::string("demo-fixture-01")); + SC_EXPECT_EQ(layout.id, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::LayoutId)); SC_EXPECT_EQ(layout.name, std::string("Sprint 1 Demo Layout")); SC_EXPECT_EQ(layout.levelId, std::string("L1")); - SC_EXPECT_EQ(layout.zones.size(), std::size_t{2}); - SC_EXPECT_EQ(layout.zones.front().kind, safecrowd::domain::ZoneKind::Room); - SC_EXPECT_EQ(layout.zones.back().kind, safecrowd::domain::ZoneKind::Exit); + SC_EXPECT_EQ(layout.zones.size(), std::size_t{3}); + SC_EXPECT_EQ(layout.zones.at(0).kind, safecrowd::domain::ZoneKind::Room); + SC_EXPECT_EQ(layout.zones.at(1).kind, safecrowd::domain::ZoneKind::Room); + SC_EXPECT_EQ(layout.zones.at(2).kind, safecrowd::domain::ZoneKind::Exit); - SC_EXPECT_EQ(layout.connections.size(), std::size_t{1}); - SC_EXPECT_EQ(layout.connections.front().kind, safecrowd::domain::ConnectionKind::Exit); - SC_EXPECT_NEAR(layout.connections.front().effectiveWidth, 2.0, 1e-9); + SC_EXPECT_EQ(layout.connections.size(), std::size_t{2}); + SC_EXPECT_TRUE(containsConnectionKind(layout.connections, safecrowd::domain::ConnectionKind::Opening)); + SC_EXPECT_TRUE(containsConnectionKind(layout.connections, safecrowd::domain::ConnectionKind::Exit)); SC_EXPECT_EQ(layout.barriers.size(), std::size_t{1}); SC_EXPECT_TRUE(layout.barriers.front().geometry.closed); SC_EXPECT_EQ(population.initialPlacements.size(), std::size_t{1}); - SC_EXPECT_EQ(population.initialPlacements.front().zoneId, std::string("zone-room-1")); + SC_EXPECT_EQ(population.initialPlacements.front().zoneId, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::MainRoomZoneId)); SC_EXPECT_EQ(population.initialPlacements.front().targetAgentCount, std::size_t{100}); SC_EXPECT_EQ(population.initialPlacements.front().area.outline.size(), std::size_t{4}); @@ -34,3 +65,20 @@ SC_TEST(DemoFixtureServiceBuildsSprint1Fixture) { const auto issues = validator.validate(layout); SC_EXPECT_TRUE(!safecrowd::domain::hasBlockingImportIssue(issues)); } + +SC_TEST(DemoLayoutsProvidesRuntimeFacilityLayout) { + const auto layout = safecrowd::domain::DemoLayouts::demoFacility(); + + SC_EXPECT_EQ(layout.id, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::LayoutId)); + SC_EXPECT_EQ(layout.zones.size(), std::size_t{3}); + SC_EXPECT_EQ(layout.connections.size(), std::size_t{2}); + SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::MainRoomZoneId)); + SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::SideRoomZoneId)); + SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitZoneId)); + SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::OpeningConnectionId)); + SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitConnectionId)); + + safecrowd::domain::ImportValidationService validator; + const auto issues = validator.validate(layout); + SC_EXPECT_TRUE(!safecrowd::domain::hasBlockingImportIssue(issues)); +} From 492df0897ae6f081c60abf0ba6ea1c3d56f31eb7 Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 22 Apr 2026 18:38:36 +0900 Subject: [PATCH 2/6] [Application] Show built-in demo project --- src/application/MainWindow.cpp | 43 +++++++++++++++++++++----- src/application/ProjectMetadata.h | 18 +++++++++++ src/application/ProjectPersistence.cpp | 11 +++++++ src/domain/DemoLayouts.cpp | 2 +- tests/DemoFixtureServiceTests.cpp | 10 ++++++ 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/application/MainWindow.cpp b/src/application/MainWindow.cpp index 8479e54..46370f6 100644 --- a/src/application/MainWindow.cpp +++ b/src/application/MainWindow.cpp @@ -8,11 +8,29 @@ #include "application/NewProjectWidget.h" #include "application/ProjectPersistence.h" #include "application/ProjectNavigatorWidget.h" +#include "domain/DemoLayouts.h" #include "domain/DxfImportService.h" +#include "domain/ImportIssue.h" #include "domain/ImportOrchestrator.h" +#include "domain/ImportValidationService.h" #include "domain/SafeCrowdDomain.h" namespace safecrowd::application { +namespace { + +safecrowd::domain::ImportResult makeDemoImportResult() { + safecrowd::domain::ImportResult result; + result.layout = safecrowd::domain::DemoLayouts::demoFacility(); + + safecrowd::domain::ImportValidationService validator; + result.issues = validator.validate(*result.layout); + result.reviewStatus = safecrowd::domain::hasBlockingImportIssue(result.issues) + ? safecrowd::domain::ImportReviewStatus::Pending + : safecrowd::domain::ImportReviewStatus::NotRequired; + return result; +} + +} // namespace MainWindow::MainWindow(safecrowd::domain::SafeCrowdDomain& domain, QWidget* parent) : QMainWindow(parent), @@ -76,6 +94,11 @@ void MainWindow::saveCurrentProject() { return; } + if (currentProject_.isBuiltInDemo()) { + QMessageBox::information(this, "Save Project", "Built-in demo projects do not need to be saved."); + return; + } + QString errorMessage; if (!ProjectPersistence::saveProject(currentProject_, &errorMessage)) { QMessageBox::warning(this, "Save Project", errorMessage); @@ -90,14 +113,18 @@ void MainWindow::showLayoutReview(const ProjectMetadata& metadata) { currentProject_ = metadata; hasCurrentProject_ = true; - safecrowd::domain::DxfImportService importer; - const safecrowd::domain::ImportRequest importRequest{ - .sourcePath = std::filesystem::path(metadata.layoutPath.toStdWString()), - .requestedFormat = safecrowd::domain::ImportedFileFormat::Dxf, - .preserveRawModel = true, - .runValidation = true, - }; - auto importResult = importer.importFile(importRequest); + auto importResult = metadata.isBuiltInDemo() + ? makeDemoImportResult() + : [&metadata]() { + safecrowd::domain::DxfImportService importer; + const safecrowd::domain::ImportRequest importRequest{ + .sourcePath = std::filesystem::path(metadata.layoutPath.toStdWString()), + .requestedFormat = safecrowd::domain::ImportedFileFormat::Dxf, + .preserveRawModel = true, + .runValidation = true, + }; + return importer.importFile(importRequest); + }(); setCentralWidget(new LayoutReviewWidget(metadata.name, importResult, [this]() { saveCurrentProject(); diff --git a/src/application/ProjectMetadata.h b/src/application/ProjectMetadata.h index 8f79ce7..7a4e4fb 100644 --- a/src/application/ProjectMetadata.h +++ b/src/application/ProjectMetadata.h @@ -4,15 +4,33 @@ namespace safecrowd::application { +inline QString builtInDemoLayoutPath() { + return QStringLiteral("safecrowd://demo/sprint1-facility"); +} + struct ProjectMetadata { QString name{}; QString folderPath{}; QString layoutPath{}; QString savedAt{}; + bool isBuiltInDemo() const noexcept { + return layoutPath == builtInDemoLayoutPath(); + } + bool isValid() const noexcept { + if (isBuiltInDemo()) { + return !name.isEmpty(); + } return !name.isEmpty() && !folderPath.isEmpty() && !layoutPath.isEmpty(); } }; +inline ProjectMetadata makeBuiltInDemoProject() { + return { + .name = QStringLiteral("Demo"), + .layoutPath = builtInDemoLayoutPath(), + }; +} + } // namespace safecrowd::application diff --git a/src/application/ProjectPersistence.cpp b/src/application/ProjectPersistence.cpp index fdc85d6..0a440bf 100644 --- a/src/application/ProjectPersistence.cpp +++ b/src/application/ProjectPersistence.cpp @@ -115,6 +115,7 @@ bool copyLayoutIntoProject(ProjectMetadata& metadata, QString* errorMessage) { QList ProjectPersistence::loadRecentProjects() { QList projects; + projects.push_back(makeBuiltInDemoProject()); const auto document = readJsonDocument(recentProjectsPath()); if (!document.isObject()) { @@ -123,6 +124,9 @@ QList ProjectPersistence::loadRecentProjects() { for (const auto& value : document.object().value("projects").toArray()) { const auto indexed = fromJson(value.toObject()); + if (indexed.isBuiltInDemo()) { + continue; + } const auto loaded = loadProject(indexed.folderPath); if (loaded.isValid()) { projects.push_back(loaded); @@ -142,6 +146,13 @@ ProjectMetadata ProjectPersistence::loadProject(const QString& folderPath) { } bool ProjectPersistence::saveProject(ProjectMetadata metadata, QString* errorMessage) { + if (metadata.isBuiltInDemo()) { + if (errorMessage != nullptr) { + *errorMessage = "Built-in demo projects do not need to be saved."; + } + return false; + } + if (!metadata.isValid()) { if (errorMessage != nullptr) { *errorMessage = "Project name, folder, and layout path are required."; diff --git a/src/domain/DemoLayouts.cpp b/src/domain/DemoLayouts.cpp index dea816d..8716341 100644 --- a/src/domain/DemoLayouts.cpp +++ b/src/domain/DemoLayouts.cpp @@ -59,7 +59,7 @@ FacilityLayout2D demoFacility() { roomConnection.fromZoneId = mainRoom.id; roomConnection.toZoneId = sideRoom.id; roomConnection.effectiveWidth = 3.0; - roomConnection.centerSpan = LineSegment2D{{10.0, 4.0}, {10.0, 6.0}}; + roomConnection.centerSpan = LineSegment2D{{10.0, 3.5}, {10.0, 6.5}}; layout.connections.push_back(roomConnection); Connection2D exitConnection; diff --git a/tests/DemoFixtureServiceTests.cpp b/tests/DemoFixtureServiceTests.cpp index 102a8ca..ab43e44 100644 --- a/tests/DemoFixtureServiceTests.cpp +++ b/tests/DemoFixtureServiceTests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "TestSupport.h" @@ -34,6 +35,12 @@ bool containsConnectionId( }); } +double spanLength(const safecrowd::domain::LineSegment2D& span) { + const auto dx = span.end.x - span.start.x; + const auto dy = span.end.y - span.start.y; + return std::sqrt(dx * dx + dy * dy); +} + } // namespace SC_TEST(DemoFixtureServiceBuildsSprint1Fixture) { @@ -77,6 +84,9 @@ SC_TEST(DemoLayoutsProvidesRuntimeFacilityLayout) { SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitZoneId)); SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::OpeningConnectionId)); SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitConnectionId)); + for (const auto& connection : layout.connections) { + SC_EXPECT_NEAR(connection.effectiveWidth, spanLength(connection.centerSpan), 1e-9); + } safecrowd::domain::ImportValidationService validator; const auto issues = validator.validate(layout); From 3689097451af9746016d20712e4cb31786be6ec0 Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 22 Apr 2026 18:46:05 +0900 Subject: [PATCH 3/6] [Application] Close rendered polylines --- src/application/LayoutPreviewWidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/application/LayoutPreviewWidget.cpp b/src/application/LayoutPreviewWidget.cpp index ae5fdef..2c6aafd 100644 --- a/src/application/LayoutPreviewWidget.cpp +++ b/src/application/LayoutPreviewWidget.cpp @@ -154,6 +154,9 @@ QPolygonF polylinePath(const safecrowd::domain::Polyline2D& polyline, const Layo for (const auto& point : polyline.vertices) { path.append(transform.map(point)); } + if (polyline.closed && path.size() > 2) { + path.append(path.front()); + } return path; } From 0a4bba2d58b4ab6c67e59b5aded40928e38216df Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 22 Apr 2026 18:52:24 +0900 Subject: [PATCH 4/6] [Application] Render closed barriers as polygons --- src/application/LayoutPreviewWidget.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/application/LayoutPreviewWidget.cpp b/src/application/LayoutPreviewWidget.cpp index 2c6aafd..d16772b 100644 --- a/src/application/LayoutPreviewWidget.cpp +++ b/src/application/LayoutPreviewWidget.cpp @@ -154,9 +154,6 @@ QPolygonF polylinePath(const safecrowd::domain::Polyline2D& polyline, const Layo for (const auto& point : polyline.vertices) { path.append(transform.map(point)); } - if (polyline.closed && path.size() > 2) { - path.append(path.front()); - } return path; } @@ -228,10 +225,15 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) { } painter.setPen(QPen(QColor(70, 70, 70), 3)); + painter.setBrush(Qt::NoBrush); for (const auto& barrier : importResult_.layout->barriers) { const auto path = polylinePath(barrier.geometry, transform); if (path.size() > 1) { - painter.drawPolyline(path); + if (barrier.geometry.closed && path.size() > 2) { + painter.drawPolygon(path); + } else { + painter.drawPolyline(path); + } } } } From a5e7fa36763f301b6a1645a8c571fce08b59481a Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 22 Apr 2026 19:48:31 +0900 Subject: [PATCH 5/6] [Domain] Enrich sprint demo layout --- src/domain/DemoLayouts.cpp | 87 ++++++++++++++++++++++--------- src/domain/DemoLayouts.h | 11 ++++ tests/DemoFixtureServiceTests.cpp | 21 +++++--- 3 files changed, 87 insertions(+), 32 deletions(-) diff --git a/src/domain/DemoLayouts.cpp b/src/domain/DemoLayouts.cpp index 8716341..f608833 100644 --- a/src/domain/DemoLayouts.cpp +++ b/src/domain/DemoLayouts.cpp @@ -1,6 +1,22 @@ #include "domain/DemoLayouts.h" +#include + namespace safecrowd::domain::DemoLayouts { +namespace { + +Barrier2D makeBarrier(const char* id, std::vector vertices, bool closed = false) { + Barrier2D barrier; + barrier.id = id; + barrier.blocksMovement = true; + barrier.geometry = Polyline2D{ + .vertices = std::move(vertices), + .closed = closed, + }; + return barrier; +} + +} // namespace FacilityLayout2D demoFacility() { FacilityLayout2D layout{}; @@ -15,12 +31,12 @@ FacilityLayout2D demoFacility() { mainRoom.area = Polygon2D{ .outline = { {0.0, 0.0}, - {10.0, 0.0}, - {10.0, 10.0}, + {12.0, 0.0}, + {12.0, 10.0}, {0.0, 10.0}, }, }; - mainRoom.defaultCapacity = 200; + mainRoom.defaultCapacity = 120; layout.zones.push_back(mainRoom); Zone2D sideRoom; @@ -29,25 +45,40 @@ FacilityLayout2D demoFacility() { sideRoom.label = "Side Demo Room"; sideRoom.area = Polygon2D{ .outline = { - {10.0, 0.0}, + {12.0, 0.0}, {20.0, 0.0}, {20.0, 10.0}, - {10.0, 10.0}, + {12.0, 10.0}, }, }; sideRoom.defaultCapacity = 80; layout.zones.push_back(sideRoom); + Zone2D exitCorridor; + exitCorridor.id = Sprint1FacilityIds::ExitCorridorZoneId; + exitCorridor.kind = ZoneKind::Corridor; + exitCorridor.label = "Exit Corridor"; + exitCorridor.area = Polygon2D{ + .outline = { + {20.0, 4.0}, + {24.0, 4.0}, + {24.0, 8.0}, + {20.0, 8.0}, + }, + }; + exitCorridor.defaultCapacity = 24; + layout.zones.push_back(exitCorridor); + Zone2D exitZone; exitZone.id = Sprint1FacilityIds::ExitZoneId; exitZone.kind = ZoneKind::Exit; exitZone.label = "Main Exit"; exitZone.area = Polygon2D{ .outline = { - {18.0, 10.0}, - {20.0, 10.0}, - {20.0, 12.0}, - {18.0, 12.0}, + {24.0, 5.0}, + {26.0, 5.0}, + {26.0, 7.0}, + {24.0, 7.0}, }, }; exitZone.defaultCapacity = 20; @@ -59,31 +90,37 @@ FacilityLayout2D demoFacility() { roomConnection.fromZoneId = mainRoom.id; roomConnection.toZoneId = sideRoom.id; roomConnection.effectiveWidth = 3.0; - roomConnection.centerSpan = LineSegment2D{{10.0, 3.5}, {10.0, 6.5}}; + roomConnection.centerSpan = LineSegment2D{{12.0, 3.5}, {12.0, 6.5}}; layout.connections.push_back(roomConnection); + Connection2D corridorConnection; + corridorConnection.id = Sprint1FacilityIds::DoorwayConnectionId; + corridorConnection.kind = ConnectionKind::Doorway; + corridorConnection.fromZoneId = sideRoom.id; + corridorConnection.toZoneId = exitCorridor.id; + corridorConnection.effectiveWidth = 2.0; + corridorConnection.centerSpan = LineSegment2D{{20.0, 5.0}, {20.0, 7.0}}; + layout.connections.push_back(corridorConnection); + Connection2D exitConnection; exitConnection.id = Sprint1FacilityIds::ExitConnectionId; exitConnection.kind = ConnectionKind::Exit; - exitConnection.fromZoneId = sideRoom.id; + exitConnection.fromZoneId = exitCorridor.id; exitConnection.toZoneId = exitZone.id; exitConnection.effectiveWidth = 2.0; - exitConnection.centerSpan = LineSegment2D{{18.0, 10.0}, {20.0, 10.0}}; + exitConnection.centerSpan = LineSegment2D{{24.0, 5.0}, {24.0, 7.0}}; layout.connections.push_back(exitConnection); - Barrier2D centerObstacle; - centerObstacle.id = Sprint1FacilityIds::BarrierId; - centerObstacle.blocksMovement = true; - centerObstacle.geometry = Polyline2D{ - .vertices = { - {4.0, 4.0}, - {6.0, 4.0}, - {6.0, 5.0}, - {4.0, 5.0}, - }, - .closed = true, - }; - layout.barriers.push_back(centerObstacle); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::WestWallId, {{0.0, 0.0}, {0.0, 10.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::NorthWallId, {{0.0, 10.0}, {20.0, 10.0}, {20.0, 8.0}, {24.0, 8.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::SouthWallId, {{24.0, 4.0}, {20.0, 4.0}, {20.0, 0.0}, {0.0, 0.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::CorridorExitWallUpperId, {{24.0, 8.0}, {24.0, 7.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::CorridorExitWallLowerId, {{24.0, 5.0}, {24.0, 4.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::MainSideWallLowerId, {{12.0, 0.0}, {12.0, 3.5}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::MainSideWallUpperId, {{12.0, 6.5}, {12.0, 10.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::SideCorridorWallLowerId, {{20.0, 0.0}, {20.0, 5.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::SideCorridorWallUpperId, {{20.0, 7.0}, {20.0, 10.0}})); + layout.barriers.push_back(makeBarrier(Sprint1FacilityIds::BarrierId, {{5.0, 3.5}, {7.0, 3.5}, {7.0, 5.0}, {5.0, 5.0}}, true)); return layout; } diff --git a/src/domain/DemoLayouts.h b/src/domain/DemoLayouts.h index da5ae8f..6949412 100644 --- a/src/domain/DemoLayouts.h +++ b/src/domain/DemoLayouts.h @@ -8,10 +8,21 @@ struct Sprint1FacilityIds { static constexpr const char* LayoutId = "demo-fixture-01"; static constexpr const char* MainRoomZoneId = "zone-room-1"; static constexpr const char* SideRoomZoneId = "zone-room-2"; + static constexpr const char* ExitCorridorZoneId = "zone-corridor-1"; static constexpr const char* ExitZoneId = "zone-exit-1"; static constexpr const char* OpeningConnectionId = "conn-opening-1"; + static constexpr const char* DoorwayConnectionId = "conn-doorway-1"; static constexpr const char* ExitConnectionId = "conn-exit-1"; static constexpr const char* BarrierId = "barrier-1"; + static constexpr const char* WestWallId = "barrier-wall-west"; + static constexpr const char* NorthWallId = "barrier-wall-north"; + static constexpr const char* SouthWallId = "barrier-wall-south"; + static constexpr const char* CorridorExitWallUpperId = "barrier-wall-corridor-exit-upper"; + static constexpr const char* CorridorExitWallLowerId = "barrier-wall-corridor-exit-lower"; + static constexpr const char* MainSideWallLowerId = "barrier-wall-main-side-lower"; + static constexpr const char* MainSideWallUpperId = "barrier-wall-main-side-upper"; + static constexpr const char* SideCorridorWallLowerId = "barrier-wall-side-corridor-lower"; + static constexpr const char* SideCorridorWallUpperId = "barrier-wall-side-corridor-upper"; }; FacilityLayout2D demoFacility(); diff --git a/tests/DemoFixtureServiceTests.cpp b/tests/DemoFixtureServiceTests.cpp index ab43e44..4584f53 100644 --- a/tests/DemoFixtureServiceTests.cpp +++ b/tests/DemoFixtureServiceTests.cpp @@ -52,16 +52,20 @@ SC_TEST(DemoFixtureServiceBuildsSprint1Fixture) { SC_EXPECT_EQ(layout.id, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::LayoutId)); SC_EXPECT_EQ(layout.name, std::string("Sprint 1 Demo Layout")); SC_EXPECT_EQ(layout.levelId, std::string("L1")); - SC_EXPECT_EQ(layout.zones.size(), std::size_t{3}); + SC_EXPECT_EQ(layout.zones.size(), std::size_t{4}); SC_EXPECT_EQ(layout.zones.at(0).kind, safecrowd::domain::ZoneKind::Room); SC_EXPECT_EQ(layout.zones.at(1).kind, safecrowd::domain::ZoneKind::Room); - SC_EXPECT_EQ(layout.zones.at(2).kind, safecrowd::domain::ZoneKind::Exit); + SC_EXPECT_EQ(layout.zones.at(2).kind, safecrowd::domain::ZoneKind::Corridor); + SC_EXPECT_EQ(layout.zones.at(3).kind, safecrowd::domain::ZoneKind::Exit); - SC_EXPECT_EQ(layout.connections.size(), std::size_t{2}); + SC_EXPECT_EQ(layout.connections.size(), std::size_t{3}); SC_EXPECT_TRUE(containsConnectionKind(layout.connections, safecrowd::domain::ConnectionKind::Opening)); + SC_EXPECT_TRUE(containsConnectionKind(layout.connections, safecrowd::domain::ConnectionKind::Doorway)); SC_EXPECT_TRUE(containsConnectionKind(layout.connections, safecrowd::domain::ConnectionKind::Exit)); - SC_EXPECT_EQ(layout.barriers.size(), std::size_t{1}); - SC_EXPECT_TRUE(layout.barriers.front().geometry.closed); + SC_EXPECT_EQ(layout.barriers.size(), std::size_t{10}); + SC_EXPECT_TRUE(std::any_of(layout.barriers.begin(), layout.barriers.end(), [](const auto& barrier) { + return barrier.geometry.closed; + })); SC_EXPECT_EQ(population.initialPlacements.size(), std::size_t{1}); SC_EXPECT_EQ(population.initialPlacements.front().zoneId, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::MainRoomZoneId)); @@ -77,12 +81,15 @@ SC_TEST(DemoLayoutsProvidesRuntimeFacilityLayout) { const auto layout = safecrowd::domain::DemoLayouts::demoFacility(); SC_EXPECT_EQ(layout.id, std::string(safecrowd::domain::DemoLayouts::Sprint1FacilityIds::LayoutId)); - SC_EXPECT_EQ(layout.zones.size(), std::size_t{3}); - SC_EXPECT_EQ(layout.connections.size(), std::size_t{2}); + SC_EXPECT_EQ(layout.zones.size(), std::size_t{4}); + SC_EXPECT_EQ(layout.connections.size(), std::size_t{3}); + SC_EXPECT_EQ(layout.barriers.size(), std::size_t{10}); SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::MainRoomZoneId)); SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::SideRoomZoneId)); + SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitCorridorZoneId)); SC_EXPECT_TRUE(containsZoneId(layout.zones, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitZoneId)); SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::OpeningConnectionId)); + SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::DoorwayConnectionId)); SC_EXPECT_TRUE(containsConnectionId(layout.connections, safecrowd::domain::DemoLayouts::Sprint1FacilityIds::ExitConnectionId)); for (const auto& connection : layout.connections) { SC_EXPECT_NEAR(connection.effectiveWidth, spanLength(connection.centerSpan), 1e-9); From 5ac221f2215431ea37e33b53e03c60b2854fd456 Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 22 Apr 2026 19:53:13 +0900 Subject: [PATCH 6/6] [Application] Reuse closed polyline rendering for focus --- src/application/LayoutPreviewWidget.cpp | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/application/LayoutPreviewWidget.cpp b/src/application/LayoutPreviewWidget.cpp index ea64ff1..58bdaeb 100644 --- a/src/application/LayoutPreviewWidget.cpp +++ b/src/application/LayoutPreviewWidget.cpp @@ -167,6 +167,20 @@ void drawLine(QPainter& painter, const safecrowd::domain::LineSegment2D& line, c painter.drawLine(transform.map(line.start), transform.map(line.end)); } +void drawPolyline(QPainter& painter, const safecrowd::domain::Polyline2D& polyline, const LayoutTransform& transform) { + const auto path = polylinePath(polyline, transform); + if (path.size() <= 1) { + return; + } + + if (polyline.closed && path.size() > 2) { + painter.drawPolygon(path); + return; + } + + painter.drawPolyline(path); +} + bool stringListContains(const std::vector& values, const QString& target) { return std::any_of(values.begin(), values.end(), [&](const std::string& value) { return QString::fromStdString(value) == target; @@ -382,14 +396,7 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) { painter.setPen(QPen(QColor(70, 70, 70), 3)); painter.setBrush(Qt::NoBrush); for (const auto& barrier : importResult_.layout->barriers) { - const auto path = polylinePath(barrier.geometry, transform); - if (path.size() > 1) { - if (barrier.geometry.closed && path.size() > 2) { - painter.drawPolygon(path); - } else { - painter.drawPolyline(path); - } - } + drawPolyline(painter, barrier.geometry, transform); } } @@ -410,10 +417,7 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) { } for (const auto& barrier : importResult_.layout->barriers) { if (QString::fromStdString(barrier.id) == focusedTargetId_ || traceMatches(barrier.provenance, focusedTargetId_)) { - const auto path = polylinePath(barrier.geometry, transform); - if (path.size() > 1) { - painter.drawPolyline(path); - } + drawPolyline(painter, barrier.geometry, transform); } } }