From 5ace0f5011e88ecf6b982f8d2fbbd3f788e99318 Mon Sep 17 00:00:00 2001 From: 95x8x9 Date: Sun, 12 Apr 2026 19:56:38 +0900 Subject: [PATCH 1/8] [Domain] issue 16,17 --- CMakeLists.txt | 5 ++ src/domain/AgentComponents.h | 21 ++++++ src/domain/CompressionSystem.cpp | 99 +++++++++++++++++++++++++++++ src/domain/CompressionSystem.h | 14 ++++ src/domain/Metrics.h | 16 +++++ src/domain/Snapshot.cpp | 40 ++++++++++++ src/domain/Snapshot.h | 29 +++++++++ src/engine/PackedComponentStorage.h | 4 ++ 8 files changed, 228 insertions(+) create mode 100644 src/domain/AgentComponents.h create mode 100644 src/domain/CompressionSystem.cpp create mode 100644 src/domain/CompressionSystem.h create mode 100644 src/domain/Metrics.h create mode 100644 src/domain/Snapshot.cpp create mode 100644 src/domain/Snapshot.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 749365c..c2e8805 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,11 @@ add_library(safecrowd_domain STATIC src/domain/ImportValidationService.h src/domain/ImportValidationService.cpp src/domain/ImportContracts.h + src/domain/AgentComponents.h + src/domain/CompressionSystem.cpp + src/domain/Metrics.h + src/domain/Snapshot.cpp + src/domain/Snapshot.h ) target_include_directories(safecrowd_domain diff --git a/src/domain/AgentComponents.h b/src/domain/AgentComponents.h new file mode 100644 index 0000000..e89ceb7 --- /dev/null +++ b/src/domain/AgentComponents.h @@ -0,0 +1,21 @@ +#pragma once +#include "domain/Geometry2D.h" + +namespace safecrowd::domain { + + // Ʈ ġ Ÿ Ʈ + struct Position { + Point2D value; + }; + + // Ʈ Ư Ÿ Ʈ + struct Agent { + float radius = 0.25f; + float maxSpeed = 1.5f; + }; + + struct Velocity { + Point2D value; + }; + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/domain/CompressionSystem.cpp b/src/domain/CompressionSystem.cpp new file mode 100644 index 0000000..17b82f0 --- /dev/null +++ b/src/domain/CompressionSystem.cpp @@ -0,0 +1,99 @@ +#include "domain/CompressionSystem.h" +#include "domain/FacilityLayout2D.h" +#include "domain/AgentComponents.h" +#include "domain/metrics.h" +#include +#include + +namespace safecrowd::domain { + + static float distanceBetween(const Point2D& p1, const Point2D& p2) { + float dx = static_cast(p1.x - p2.x); + float dy = static_cast(p1.y - p2.y); + return std::sqrt(dx * dx + dy * dy); + } + + static float distancePointToSegment(const Point2D& p, const Point2D& a, const Point2D& b) { + float l2 = static_cast(std::pow(b.x - a.x, 2) + std::pow(b.y - a.y, 2)); + if (l2 == 0.0f) return distanceBetween(p, a); + + float t = std::clamp(static_cast(((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2), 0.0f, 1.0f); + Point2D projection = { a.x + t * (b.x - a.x), a.y + t * (b.y - a.y) }; + return distanceBetween(p, projection); + } + + void CompressionSystem::update(engine::ComponentRegistry& registry, float dt) { + // ʿ 丮 ε + auto& posStorage = registry.storageFor(); + auto& agentStorage = registry.storageFor(); + auto& compStorage = registry.storageFor(); + + // Barrier2D 丮 ִ Ȯ + if (!registry.isRegistered()) return; + auto& barrierStorage = registry.storageFor(); + + // Position ƼƼ ȸ + for (const auto& entity : posStorage.getEntities()) { + // Agent CompressionData Ʈ ִ Ȯ + if (!agentStorage.contains(entity) || !compStorage.contains(entity)) continue; + + const auto& pos = posStorage.get(entity); + const auto& agent = agentStorage.get(entity); + auto& compression = compStorage.get(entity); + + float currentForce = 0.0f; + + // [ й] + for (const auto& otherEntity : posStorage.getEntities()) { + if (entity.index == otherEntity.index && entity.generation == otherEntity.generation) continue; + if (!agentStorage.contains(otherEntity)) continue; + + const auto& otherPos = posStorage.get(otherEntity); + const auto& otherAgent = agentStorage.get(otherEntity); + + float dist = distanceBetween(pos.value, otherPos.value); + float combinedRadius = agent.radius + otherAgent.radius; + + if (dist < combinedRadius) { + currentForce += (combinedRadius - dist); + } + } + + // [/ֹ й] + for (const auto& barrierEntity : barrierStorage.getEntities()) { + const auto& barrier = barrierStorage.get(barrierEntity); + const auto& vertices = barrier.geometry.vertices; + if (vertices.size() < 2) continue; + + for (size_t i = 0; i < vertices.size() - 1; ++i) { + float distToWall = distancePointToSegment(pos.value, vertices[i], vertices[i + 1]); + if (distToWall < agent.radius) { + currentForce += (agent.radius - distToWall); + } + } + if (barrier.geometry.closed) { + float distToWall = distancePointToSegment(pos.value, vertices.back(), vertices.front()); + if (distToWall < agent.radius) { + currentForce += (agent.radius - distToWall); + } + } + } + + compression.force = currentForce; + + // [ Ʈ] + const float FORCE_THRESHOLD = 0.5f; + if (compression.force > FORCE_THRESHOLD) { + compression.exposure += dt; + } + else { + compression.exposure = std::max(0.0f, compression.exposure - dt * 0.5f); + } + + const float EXPOSURE_THRESHOLD = 2.0f; + compression.isCritical = (compression.force > FORCE_THRESHOLD) && + (compression.exposure > EXPOSURE_THRESHOLD); + } + } + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/domain/CompressionSystem.h b/src/domain/CompressionSystem.h new file mode 100644 index 0000000..eee882c --- /dev/null +++ b/src/domain/CompressionSystem.h @@ -0,0 +1,14 @@ +#pragma once + +#include "engine/ComponentRegistry.h" + +namespace safecrowd::domain { + + // ý Ŭ + class CompressionSystem { + public: + // Ʈ Լ + void update(engine::ComponentRegistry& registry, float dt); + }; + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/domain/Metrics.h b/src/domain/Metrics.h new file mode 100644 index 0000000..fc84cfb --- /dev/null +++ b/src/domain/Metrics.h @@ -0,0 +1,16 @@ +#pragma once + +namespace safecrowd::domain { + // Ư Ʈ ޴ /ð й ¸ + struct CompressionData { + // CompressionForce: ֺ ü ߻ϴ ﰢ + float force = 0.0f; + + // CompressionExposure: Ӱ谪 ̻ й ӵ ð (: sec) + float exposure = 0.0f; + + // CompressionCriticalState: ߰ ӽð ÷ + bool isCritical = false; + }; + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/domain/Snapshot.cpp b/src/domain/Snapshot.cpp new file mode 100644 index 0000000..1778993 --- /dev/null +++ b/src/domain/Snapshot.cpp @@ -0,0 +1,40 @@ +#include "domain/snapshot.h" +#include "domain/AgentComponents.h" +#include "domain/metrics.h" +#include "engine/ComponentRegistry.h" + +namespace safecrowd::domain { + + // Ÿ safecrowd::engine::ComponentRegistry Ȯ (E0276 ذ) + SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time) { + SimulationSnapshot snapshot; + snapshot.frameIndex = frame; + snapshot.simulationTime = time; + + // 츮 (Storage) ͸ ε + auto& posStorage = registry.storageFor(); + auto& compStorage = registry.storageFor(); + + snapshot.agentCount = static_cast(posStorage.size()); + snapshot.agents.reserve(snapshot.agentCount); + + // Position ƼƼ ȸϸ + for (const auto& entity : posStorage.getEntities()) { + // ش ƼƼ й ǥ ͵ ִ Ȯ + if (!compStorage.contains(entity)) continue; + + const auto& pos = posStorage.get(entity); + const auto& metrics = compStorage.get(entity); + + // AgentSnapshot ü + snapshot.agents.push_back({ + static_cast(entity.index), // id + pos.value, // position (Point2D) + metrics // metrics (CompressionData) + }); + } + + return snapshot; + } + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/domain/Snapshot.h b/src/domain/Snapshot.h new file mode 100644 index 0000000..3804c50 --- /dev/null +++ b/src/domain/Snapshot.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include "domain/Geometry2D.h" +#include "domain/metrics.h" + +namespace safecrowd::engine { + class ComponentRegistry; +} + +namespace safecrowd::domain { + + struct AgentSnapshot { + uint32_t id; + Point2D position; + CompressionData metrics; + }; + + struct SimulationSnapshot { + uint64_t frameIndex = 0; + float simulationTime = 0.0f; + uint32_t agentCount = 0; + std::vector agents; + }; + + // ӽ̽ safecrowd::engine + SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time); + +} // namespace safecrowd::domain \ No newline at end of file diff --git a/src/engine/PackedComponentStorage.h b/src/engine/PackedComponentStorage.h index 362be98..e2e3a46 100644 --- a/src/engine/PackedComponentStorage.h +++ b/src/engine/PackedComponentStorage.h @@ -61,6 +61,10 @@ class PackedComponentStorage final : public IComponentStorage { return components_.size(); } + [[nodiscard]] const std::vector& getEntities() const noexcept { + return entities_; + } + private: struct EntityHash { [[nodiscard]] std::size_t operator()(const Entity& entity) const noexcept { From 13f361e68bdce4422213d8108748d12105110b5e Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:03:50 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/Snapshot.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/domain/Snapshot.cpp b/src/domain/Snapshot.cpp index 1778993..e891645 100644 --- a/src/domain/Snapshot.cpp +++ b/src/domain/Snapshot.cpp @@ -1,32 +1,32 @@ #include "domain/snapshot.h" #include "domain/AgentComponents.h" -#include "domain/metrics.h" +#include "domain/Metrics.h" #include "engine/ComponentRegistry.h" namespace safecrowd::domain { - // Ÿ safecrowd::engine::ComponentRegistry Ȯ (E0276 ذ) + // 인자 타입을 safecrowd::engine::ComponentRegistry로 명확히 지정 (E0276 해결) SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time) { SimulationSnapshot snapshot; snapshot.frameIndex = frame; snapshot.simulationTime = time; - // 츮 (Storage) ͸ ε + // 우리 엔진의 저장소(Storage)에서 데이터를 로드 auto& posStorage = registry.storageFor(); auto& compStorage = registry.storageFor(); snapshot.agentCount = static_cast(posStorage.size()); snapshot.agents.reserve(snapshot.agentCount); - // Position ƼƼ ȸϸ + // Position을 가진 모든 엔티티를 순회하며 스냅샷 생성 for (const auto& entity : posStorage.getEntities()) { - // ش ƼƼ й ǥ ͵ ִ Ȯ + // 해당 엔티티에 압박 지표 데이터도 있는지 확인 if (!compStorage.contains(entity)) continue; const auto& pos = posStorage.get(entity); const auto& metrics = compStorage.get(entity); - // AgentSnapshot ü + // AgentSnapshot 구조체에 맞춰 데이터 삽입 snapshot.agents.push_back({ static_cast(entity.index), // id pos.value, // position (Point2D) @@ -37,4 +37,4 @@ namespace safecrowd::domain { return snapshot; } -} // namespace safecrowd::domain \ No newline at end of file +} // namespace safecrowd::domain From 3e6469f1ddb44f8be888d5c0959368282d3fa953 Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:04:23 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/CompressionSystem.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/domain/CompressionSystem.cpp b/src/domain/CompressionSystem.cpp index 17b82f0..4f3656d 100644 --- a/src/domain/CompressionSystem.cpp +++ b/src/domain/CompressionSystem.cpp @@ -1,7 +1,7 @@ #include "domain/CompressionSystem.h" #include "domain/FacilityLayout2D.h" #include "domain/AgentComponents.h" -#include "domain/metrics.h" +#include "domain/Metrics.h" #include #include @@ -23,18 +23,18 @@ namespace safecrowd::domain { } void CompressionSystem::update(engine::ComponentRegistry& registry, float dt) { - // ʿ 丮 ε + // 필요한 스토리지들을 로드 auto& posStorage = registry.storageFor(); auto& agentStorage = registry.storageFor(); auto& compStorage = registry.storageFor(); - // Barrier2D 丮 ִ Ȯ + // Barrier2D 스토리지가 있는지 확인 if (!registry.isRegistered()) return; auto& barrierStorage = registry.storageFor(); - // Position ƼƼ ȸ + // Position을 가진 모든 엔티티를 순회 for (const auto& entity : posStorage.getEntities()) { - // Agent CompressionData Ʈ ִ Ȯ + // Agent와 CompressionData 컴포넌트가 모두 있는지 확인 if (!agentStorage.contains(entity) || !compStorage.contains(entity)) continue; const auto& pos = posStorage.get(entity); @@ -43,7 +43,7 @@ namespace safecrowd::domain { float currentForce = 0.0f; - // [ й] + // [군중 간 압박] for (const auto& otherEntity : posStorage.getEntities()) { if (entity.index == otherEntity.index && entity.generation == otherEntity.generation) continue; if (!agentStorage.contains(otherEntity)) continue; @@ -59,7 +59,7 @@ namespace safecrowd::domain { } } - // [/ֹ й] + // [벽/장애물 압박] for (const auto& barrierEntity : barrierStorage.getEntities()) { const auto& barrier = barrierStorage.get(barrierEntity); const auto& vertices = barrier.geometry.vertices; @@ -81,7 +81,7 @@ namespace safecrowd::domain { compression.force = currentForce; - // [ Ʈ] + // [고위험 상태 업데이트] const float FORCE_THRESHOLD = 0.5f; if (compression.force > FORCE_THRESHOLD) { compression.exposure += dt; @@ -96,4 +96,4 @@ namespace safecrowd::domain { } } -} // namespace safecrowd::domain \ No newline at end of file +} // namespace safecrowd::domain From b0636b8b21a672f491e3f3ae198cdd9fcc08217f Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:05:40 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/Snapshot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/Snapshot.cpp b/src/domain/Snapshot.cpp index e891645..99e910e 100644 --- a/src/domain/Snapshot.cpp +++ b/src/domain/Snapshot.cpp @@ -1,4 +1,4 @@ -#include "domain/snapshot.h" +#include "domain/Snapshot.h" #include "domain/AgentComponents.h" #include "domain/Metrics.h" #include "engine/ComponentRegistry.h" From 9d592edb3f1f6ba0f145aa77913d9856143b4e2d Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:07:08 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/Snapshot.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/Snapshot.h b/src/domain/Snapshot.h index 3804c50..c162747 100644 --- a/src/domain/Snapshot.h +++ b/src/domain/Snapshot.h @@ -2,7 +2,7 @@ #include #include #include "domain/Geometry2D.h" -#include "domain/metrics.h" +#include "domain/Metrics.h" namespace safecrowd::engine { class ComponentRegistry; @@ -23,7 +23,7 @@ namespace safecrowd::domain { std::vector agents; }; - // ӽ̽ safecrowd::engine + // 네임스페이스를 safecrowd::engine으로 명시 SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time); -} // namespace safecrowd::domain \ No newline at end of file +} // namespace safecrowd::domain From 775eb85dd581d02c1d4d0b9f979156365555f664 Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:08:54 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2e8805..6d61c00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ add_library(safecrowd_domain STATIC src/domain/ImportContracts.h src/domain/AgentComponents.h src/domain/CompressionSystem.cpp + src/domain/CompressionSystem.h src/domain/Metrics.h src/domain/Snapshot.cpp src/domain/Snapshot.h From fcb0a2b456ca09842f58efa9198787789ce9da56 Mon Sep 17 00:00:00 2001 From: learncold Date: Mon, 13 Apr 2026 19:24:46 +0900 Subject: [PATCH 7/8] [Domain] fix compression snapshot review findings --- CMakeLists.txt | 2 + ...4\353\260\234 \355\231\230\352\262\275.md" | 4 +- src/domain/AgentComponents.h | 25 ++- src/domain/CompressionSystem.cpp | 162 ++++++++++-------- src/domain/CompressionSystem.h | 20 ++- src/domain/Metrics.h | 17 +- src/domain/SafeCrowdDomain.cpp | 27 +++ src/domain/SafeCrowdDomain.h | 2 + src/domain/Snapshot.cpp | 116 +++++++++---- src/domain/Snapshot.h | 54 ++++-- src/engine/PackedComponentStorage.h | 4 - tests/CompressionSystemTests.cpp | 100 +++++++++++ tests/SafeCrowdDomainTests.cpp | 70 ++++++++ tests/SnapshotTests.cpp | 107 ++++++++++++ 14 files changed, 556 insertions(+), 154 deletions(-) create mode 100644 tests/CompressionSystemTests.cpp create mode 100644 tests/SnapshotTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d61c00..6c722d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,8 @@ if (BUILD_TESTING) tests/EngineRuntimeTests.cpp tests/PackedComponentStorageTests.cpp tests/SafeCrowdDomainTests.cpp + tests/CompressionSystemTests.cpp + tests/SnapshotTests.cpp tests/EcsCoreTests.cpp tests/ImportContractsTests.cpp tests/DxfImportServiceTests.cpp diff --git "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" index e5b6d73..8cb4e2f 100644 --- "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" @@ -373,8 +373,8 @@ Project/ | US-07 인원 배치 기반 실행 제어 | `MainWindow`, `SafeCrowdDomain`, `EngineRuntime::play/pause/stop/stepFrame` | Start/Pause/Stop 중심의 application-domain-engine 연결 | | US-09 실시간 진행 상태 확인 | `SimulationSummary`, `EngineStats`, `MainWindow::refreshStatusLabel` | 상태, frame index, fixed step index, alpha 표시 | | US-10 병목·정체 탐지 | `FacilityLayout2D.connections/zones`, 향후 `LocalDensityField`, `FlowMeasurementSystem`, `CongestionStateSystem` | 현재 레이아웃 기반 입력을 마련했고, 위험 탐지 로직의 상세 확장은 `docs/product/고급 위험 모델.md`를 기준으로 설계 | -| US-11 압력 집중 위험 탐지 | 향후 `CompressionForce`, `CompressionExposure`, `CompressionLoadSystem`, `AsphyxiationRiskSystem` | 압박 위험 모델은 도메인 확장 설계 항목으로 정의됨 | -| US-15~US-17 결과 시각화 및 비교 | 향후 Result UI, 히트맵 레이어, 비교 요약 뷰 | 현재 계층 구조는 Sprint 2 비교/시각화 기능을 application layer에 추가할 수 있게 설계됨 | +| US-11 압력 집중 위험 탐지 | `CompressionSystem`, `CompressionData`, 향후 hotspot/zone 집계 | 최소 agent 압박 하중과 누적 노출 계산은 domain system으로 연결됐고, hotspot 집계와 고급 판정은 후속 확장으로 남겨 둠 | +| US-15~US-17 결과 시각화 및 비교 | `SimulationSnapshot`, 향후 Result UI, 히트맵 레이어, 비교 요약 뷰 | live snapshot 읽기 경로는 마련됐고, persisted 결과 뷰와 비교 UI는 application layer 후속 작업으로 유지 | | US-18~US-19 운영 대안 추천 | 향후 Recommendation module, scenario diff, rationale model | 현재 문서 구조와 위험 지표 정의를 기반으로 Sprint 3에서 연결 예정 | ### Traceability Note diff --git a/src/domain/AgentComponents.h b/src/domain/AgentComponents.h index e89ceb7..2f78861 100644 --- a/src/domain/AgentComponents.h +++ b/src/domain/AgentComponents.h @@ -1,21 +1,20 @@ #pragma once + #include "domain/Geometry2D.h" namespace safecrowd::domain { - // Ʈ ġ Ÿ Ʈ - struct Position { - Point2D value; - }; +struct Position { + Point2D value; +}; - // Ʈ Ư Ÿ Ʈ - struct Agent { - float radius = 0.25f; - float maxSpeed = 1.5f; - }; +struct Agent { + float radius{0.25f}; + float maxSpeed{1.5f}; +}; - struct Velocity { - Point2D value; - }; +struct Velocity { + Point2D value; +}; -} // namespace safecrowd::domain \ No newline at end of file +} // namespace safecrowd::domain diff --git a/src/domain/CompressionSystem.cpp b/src/domain/CompressionSystem.cpp index 4f3656d..6e022b3 100644 --- a/src/domain/CompressionSystem.cpp +++ b/src/domain/CompressionSystem.cpp @@ -1,99 +1,121 @@ #include "domain/CompressionSystem.h" + +#include "domain/AgentComponents.h" #include "domain/FacilityLayout2D.h" -#include "domain/AgentComponents.h" #include "domain/Metrics.h" + #include #include namespace safecrowd::domain { +namespace { - static float distanceBetween(const Point2D& p1, const Point2D& p2) { - float dx = static_cast(p1.x - p2.x); - float dy = static_cast(p1.y - p2.y); - return std::sqrt(dx * dx + dy * dy); - } +constexpr float kForceThreshold = 0.5f; +constexpr float kExposureThreshold = 2.0f; - static float distancePointToSegment(const Point2D& p, const Point2D& a, const Point2D& b) { - float l2 = static_cast(std::pow(b.x - a.x, 2) + std::pow(b.y - a.y, 2)); - if (l2 == 0.0f) return distanceBetween(p, a); +double distanceBetween(const Point2D& lhs, const Point2D& rhs) { + const double dx = lhs.x - rhs.x; + const double dy = lhs.y - rhs.y; + return std::sqrt(dx * dx + dy * dy); +} - float t = std::clamp(static_cast(((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2), 0.0f, 1.0f); - Point2D projection = { a.x + t * (b.x - a.x), a.y + t * (b.y - a.y) }; - return distanceBetween(p, projection); +double distancePointToSegment(const Point2D& point, const Point2D& start, const Point2D& end) { + const double dx = end.x - start.x; + const double dy = end.y - start.y; + const double lengthSquared = (dx * dx) + (dy * dy); + + if (lengthSquared == 0.0) { + return distanceBetween(point, start); } - void CompressionSystem::update(engine::ComponentRegistry& registry, float dt) { - // 필요한 스토리지들을 로드 - auto& posStorage = registry.storageFor(); - auto& agentStorage = registry.storageFor(); - auto& compStorage = registry.storageFor(); + const double t = std::clamp( + (((point.x - start.x) * dx) + ((point.y - start.y) * dy)) / lengthSquared, + 0.0, + 1.0); + const Point2D projection{ + .x = start.x + (t * dx), + .y = start.y + (t * dy), + }; + return distanceBetween(point, projection); +} + +double barrierCompression(const Barrier2D& barrier, const Point2D& position, double radius) { + if (!barrier.blocksMovement || barrier.geometry.vertices.size() < 2) { + return 0.0; + } - // Barrier2D 스토리지가 있는지 확인 - if (!registry.isRegistered()) return; - auto& barrierStorage = registry.storageFor(); + double force = 0.0; + const auto& vertices = barrier.geometry.vertices; - // Position을 가진 모든 엔티티를 순회 - for (const auto& entity : posStorage.getEntities()) { - // Agent와 CompressionData 컴포넌트가 모두 있는지 확인 - if (!agentStorage.contains(entity) || !compStorage.contains(entity)) continue; + for (std::size_t index = 0; index + 1 < vertices.size(); ++index) { + const double distance = distancePointToSegment(position, vertices[index], vertices[index + 1]); + if (distance < radius) { + force += radius - distance; + } + } - const auto& pos = posStorage.get(entity); - const auto& agent = agentStorage.get(entity); - auto& compression = compStorage.get(entity); + if (barrier.geometry.closed) { + const double distance = distancePointToSegment(position, vertices.back(), vertices.front()); + if (distance < radius) { + force += radius - distance; + } + } - float currentForce = 0.0f; + return force; +} - // [군중 간 압박] - for (const auto& otherEntity : posStorage.getEntities()) { - if (entity.index == otherEntity.index && entity.generation == otherEntity.generation) continue; - if (!agentStorage.contains(otherEntity)) continue; +} // namespace - const auto& otherPos = posStorage.get(otherEntity); - const auto& otherAgent = agentStorage.get(otherEntity); +CompressionSystem::CompressionSystem(double timeStepSeconds) + : timeStepSeconds_(static_cast(std::max(0.0, timeStepSeconds))) { +} - float dist = distanceBetween(pos.value, otherPos.value); - float combinedRadius = agent.radius + otherAgent.radius; +void CompressionSystem::update(engine::EngineWorld& world, + const engine::EngineStepContext& step) { + (void)step; - if (dist < combinedRadius) { - currentForce += (combinedRadius - dist); - } - } + auto& query = world.query(); + const auto agentEntities = query.view(); + const auto barrierEntities = query.view(); - // [벽/장애물 압박] - for (const auto& barrierEntity : barrierStorage.getEntities()) { - const auto& barrier = barrierStorage.get(barrierEntity); - const auto& vertices = barrier.geometry.vertices; - if (vertices.size() < 2) continue; - - for (size_t i = 0; i < vertices.size() - 1; ++i) { - float distToWall = distancePointToSegment(pos.value, vertices[i], vertices[i + 1]); - if (distToWall < agent.radius) { - currentForce += (agent.radius - distToWall); - } - } - if (barrier.geometry.closed) { - float distToWall = distancePointToSegment(pos.value, vertices.back(), vertices.front()); - if (distToWall < agent.radius) { - currentForce += (agent.radius - distToWall); - } - } - } + for (const auto entity : agentEntities) { + const auto& position = query.get(entity); + const auto& agent = query.get(entity); + auto& compression = query.get(entity); - compression.force = currentForce; + double currentForce = 0.0; - // [고위험 상태 업데이트] - const float FORCE_THRESHOLD = 0.5f; - if (compression.force > FORCE_THRESHOLD) { - compression.exposure += dt; + for (const auto otherEntity : agentEntities) { + if (otherEntity == entity) { + continue; } - else { - compression.exposure = std::max(0.0f, compression.exposure - dt * 0.5f); + + const auto& otherPosition = query.get(otherEntity); + const auto& otherAgent = query.get(otherEntity); + const double distance = distanceBetween(position.value, otherPosition.value); + const double combinedRadius = static_cast(agent.radius + otherAgent.radius); + + if (distance < combinedRadius) { + currentForce += combinedRadius - distance; } + } + + for (const auto barrierEntity : barrierEntities) { + currentForce += barrierCompression( + query.get(barrierEntity), + position.value, + static_cast(agent.radius)); + } - const float EXPOSURE_THRESHOLD = 2.0f; - compression.isCritical = (compression.force > FORCE_THRESHOLD) && - (compression.exposure > EXPOSURE_THRESHOLD); + compression.force = static_cast(currentForce); + if (compression.force > kForceThreshold) { + compression.exposure += timeStepSeconds_; } + + compression.isCritical = + compression.force > kForceThreshold && + compression.exposure >= kExposureThreshold; } +} -} // namespace safecrowd::domain +} // namespace safecrowd::domain diff --git a/src/domain/CompressionSystem.h b/src/domain/CompressionSystem.h index eee882c..656f9ca 100644 --- a/src/domain/CompressionSystem.h +++ b/src/domain/CompressionSystem.h @@ -1,14 +1,18 @@ #pragma once -#include "engine/ComponentRegistry.h" +#include "engine/EngineSystem.h" namespace safecrowd::domain { - // ý Ŭ - class CompressionSystem { - public: - // Ʈ Լ - void update(engine::ComponentRegistry& registry, float dt); - }; +class CompressionSystem final : public engine::EngineSystem { +public: + explicit CompressionSystem(double timeStepSeconds); -} // namespace safecrowd::domain \ No newline at end of file + void update(engine::EngineWorld& world, + const engine::EngineStepContext& step) override; + +private: + float timeStepSeconds_{0.0f}; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/Metrics.h b/src/domain/Metrics.h index fc84cfb..b203bc8 100644 --- a/src/domain/Metrics.h +++ b/src/domain/Metrics.h @@ -1,16 +1,11 @@ #pragma once namespace safecrowd::domain { - // Ư Ʈ ޴ /ð й ¸ - struct CompressionData { - // CompressionForce: ֺ ü ߻ϴ ﰢ - float force = 0.0f; - // CompressionExposure: Ӱ谪 ̻ й ӵ ð (: sec) - float exposure = 0.0f; +struct CompressionData { + float force{0.0f}; + float exposure{0.0f}; + bool isCritical{false}; +}; - // CompressionCriticalState: ߰ ӽð ÷ - bool isCritical = false; - }; - -} // namespace safecrowd::domain \ No newline at end of file +} // namespace safecrowd::domain diff --git a/src/domain/SafeCrowdDomain.cpp b/src/domain/SafeCrowdDomain.cpp index 14b2dd6..a73d848 100644 --- a/src/domain/SafeCrowdDomain.cpp +++ b/src/domain/SafeCrowdDomain.cpp @@ -1,9 +1,23 @@ #include "domain/SafeCrowdDomain.h" +#include + +#include "domain/CompressionSystem.h" +#include "engine/SystemDescriptor.h" +#include "engine/TriggerPolicy.h" +#include "engine/UpdatePhase.h" + namespace safecrowd::domain { SafeCrowdDomain::SafeCrowdDomain(engine::EngineRuntime& runtime) : runtime_(runtime) { + runtime_.addSystem( + std::make_unique(runtime_.config().fixedDeltaTime), + { + .phase = engine::UpdatePhase::FixedSimulation, + .order = 0, + .triggerPolicy = engine::TriggerPolicy::FixedStep, + }); } void SafeCrowdDomain::start() { @@ -32,6 +46,19 @@ SimulationSummary SafeCrowdDomain::summary() const { }; } +SimulationSnapshot SafeCrowdDomain::snapshot() const { + const auto& stats = runtime_.stats(); + const double simulationTime = + (static_cast(stats.fixedStepIndex) + stats.alpha) * + runtime_.config().fixedDeltaTime; + + return buildSnapshot( + runtime_.world().query(), + stats.frameIndex, + stats.fixedStepIndex, + simulationTime); +} + engine::EngineRuntime& SafeCrowdDomain::runtime() noexcept { return runtime_; } diff --git a/src/domain/SafeCrowdDomain.h b/src/domain/SafeCrowdDomain.h index 458110b..d2c2b24 100644 --- a/src/domain/SafeCrowdDomain.h +++ b/src/domain/SafeCrowdDomain.h @@ -2,6 +2,7 @@ #include +#include "domain/Snapshot.h" #include "engine/EngineRuntime.h" #include "engine/EngineState.h" @@ -24,6 +25,7 @@ class SafeCrowdDomain { void update(double deltaSeconds); SimulationSummary summary() const; + SimulationSnapshot snapshot() const; engine::EngineRuntime& runtime() noexcept; const engine::EngineRuntime& runtime() const noexcept; diff --git a/src/domain/Snapshot.cpp b/src/domain/Snapshot.cpp index 99e910e..562bf76 100644 --- a/src/domain/Snapshot.cpp +++ b/src/domain/Snapshot.cpp @@ -1,40 +1,98 @@ #include "domain/Snapshot.h" + #include "domain/AgentComponents.h" #include "domain/Metrics.h" -#include "engine/ComponentRegistry.h" +#include "engine/Entity.h" +#include "engine/WorldQuery.h" namespace safecrowd::domain { +namespace { + +std::uint64_t packEntityId(engine::Entity entity) { + return (static_cast(entity.generation) << 32U) | + static_cast(entity.index); +} + +bool hasCompressionMetricsForAllAgents(const engine::WorldQuery& query, + const std::vector& agentEntities) { + if (agentEntities.empty()) { + return false; + } - // 인자 타입을 safecrowd::engine::ComponentRegistry로 명확히 지정 (E0276 해결) - SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time) { - SimulationSnapshot snapshot; - snapshot.frameIndex = frame; - snapshot.simulationTime = time; - - // 우리 엔진의 저장소(Storage)에서 데이터를 로드 - auto& posStorage = registry.storageFor(); - auto& compStorage = registry.storageFor(); - - snapshot.agentCount = static_cast(posStorage.size()); - snapshot.agents.reserve(snapshot.agentCount); - - // Position을 가진 모든 엔티티를 순회하며 스냅샷 생성 - for (const auto& entity : posStorage.getEntities()) { - // 해당 엔티티에 압박 지표 데이터도 있는지 확인 - if (!compStorage.contains(entity)) continue; - - const auto& pos = posStorage.get(entity); - const auto& metrics = compStorage.get(entity); - - // AgentSnapshot 구조체에 맞춰 데이터 삽입 - snapshot.agents.push_back({ - static_cast(entity.index), // id - pos.value, // position (Point2D) - metrics // metrics (CompressionData) - }); + for (const auto entity : agentEntities) { + if (!query.contains(entity)) { + return false; } + } + + return true; +} + +} // namespace +const SnapshotScalarChannel* SimulationSnapshot::findScalarChannel(std::string_view key) const noexcept { + for (const auto& channel : scalarChannels) { + if (channel.key == key) { + return &channel; + } + } + + return nullptr; +} + +const SnapshotFlagChannel* SimulationSnapshot::findFlagChannel(std::string_view key) const noexcept { + for (const auto& channel : flagChannels) { + if (channel.key == key) { + return &channel; + } + } + + return nullptr; +} + +SimulationSnapshot buildSnapshot(const engine::WorldQuery& query, + std::uint64_t frame, + std::uint64_t fixedStep, + double simulationTime) { + SimulationSnapshot snapshot; + snapshot.frameIndex = frame; + snapshot.fixedStepIndex = fixedStep; + snapshot.simulationTime = simulationTime; + + const auto agentEntities = query.view(); + snapshot.agentCount = static_cast(agentEntities.size()); + snapshot.agentIds.reserve(snapshot.agentCount); + snapshot.positions.reserve(snapshot.agentCount); + + for (const auto entity : agentEntities) { + snapshot.agentIds.push_back(packEntityId(entity)); + snapshot.positions.push_back(query.get(entity).value); + } + + if (!hasCompressionMetricsForAllAgents(query, agentEntities)) { return snapshot; } -} // namespace safecrowd::domain + SnapshotScalarChannel forceChannel{std::string(kCompressionForceChannelName), {}}; + SnapshotScalarChannel exposureChannel{std::string(kCompressionExposureChannelName), {}}; + SnapshotFlagChannel criticalChannel{std::string(kCompressionCriticalChannelName), {}}; + + forceChannel.values.reserve(snapshot.agentCount); + exposureChannel.values.reserve(snapshot.agentCount); + criticalChannel.values.reserve(snapshot.agentCount); + + for (const auto entity : agentEntities) { + const auto& metrics = query.get(entity); + forceChannel.values.push_back(metrics.force); + exposureChannel.values.push_back(metrics.exposure); + criticalChannel.values.push_back(metrics.isCritical ? 1U : 0U); + } + + snapshot.scalarChannels.push_back(std::move(forceChannel)); + snapshot.scalarChannels.push_back(std::move(exposureChannel)); + snapshot.flagChannels.push_back(std::move(criticalChannel)); + + return snapshot; +} + +} // namespace safecrowd::domain diff --git a/src/domain/Snapshot.h b/src/domain/Snapshot.h index c162747..f45d266 100644 --- a/src/domain/Snapshot.h +++ b/src/domain/Snapshot.h @@ -1,29 +1,49 @@ #pragma once -#include + #include +#include +#include +#include + #include "domain/Geometry2D.h" -#include "domain/Metrics.h" namespace safecrowd::engine { - class ComponentRegistry; +class WorldQuery; } namespace safecrowd::domain { - struct AgentSnapshot { - uint32_t id; - Point2D position; - CompressionData metrics; - }; +inline constexpr std::string_view kCompressionForceChannelName = "compression.force"; +inline constexpr std::string_view kCompressionExposureChannelName = "compression.exposure"; +inline constexpr std::string_view kCompressionCriticalChannelName = "compression.critical"; + +struct SnapshotScalarChannel { + std::string key; + std::vector values; +}; + +struct SnapshotFlagChannel { + std::string key; + std::vector values; +}; + +struct SimulationSnapshot { + std::uint64_t frameIndex{0}; + std::uint64_t fixedStepIndex{0}; + double simulationTime{0.0}; + std::uint32_t agentCount{0}; + std::vector agentIds; + std::vector positions; + std::vector scalarChannels; + std::vector flagChannels; - struct SimulationSnapshot { - uint64_t frameIndex = 0; - float simulationTime = 0.0f; - uint32_t agentCount = 0; - std::vector agents; - }; + [[nodiscard]] const SnapshotScalarChannel* findScalarChannel(std::string_view key) const noexcept; + [[nodiscard]] const SnapshotFlagChannel* findFlagChannel(std::string_view key) const noexcept; +}; - // 네임스페이스를 safecrowd::engine으로 명시 - SimulationSnapshot buildSnapshot(const safecrowd::engine::ComponentRegistry& registry, uint64_t frame, float time); +SimulationSnapshot buildSnapshot(const engine::WorldQuery& query, + std::uint64_t frame, + std::uint64_t fixedStep, + double simulationTime); -} // namespace safecrowd::domain +} // namespace safecrowd::domain diff --git a/src/engine/PackedComponentStorage.h b/src/engine/PackedComponentStorage.h index e2e3a46..362be98 100644 --- a/src/engine/PackedComponentStorage.h +++ b/src/engine/PackedComponentStorage.h @@ -61,10 +61,6 @@ class PackedComponentStorage final : public IComponentStorage { return components_.size(); } - [[nodiscard]] const std::vector& getEntities() const noexcept { - return entities_; - } - private: struct EntityHash { [[nodiscard]] std::size_t operator()(const Entity& entity) const noexcept { diff --git a/tests/CompressionSystemTests.cpp b/tests/CompressionSystemTests.cpp new file mode 100644 index 0000000..2ebaf01 --- /dev/null +++ b/tests/CompressionSystemTests.cpp @@ -0,0 +1,100 @@ +#include "TestSupport.h" + +#include + +#include "domain/AgentComponents.h" +#include "domain/CompressionSystem.h" +#include "domain/FacilityLayout2D.h" +#include "domain/Metrics.h" +#include "engine/CommandBuffer.h" +#include "engine/EcsCore.h" +#include "engine/internal/EngineWorldFactory.h" + +namespace { + +using safecrowd::domain::Agent; +using safecrowd::domain::Barrier2D; +using safecrowd::domain::CompressionData; +using safecrowd::domain::CompressionSystem; +using safecrowd::domain::Point2D; +using safecrowd::domain::Position; +using safecrowd::engine::CommandBuffer; +using safecrowd::engine::EcsCore; +using safecrowd::engine::Entity; + +Entity addAgent(EcsCore& core, double x, double y) { + const Entity entity = core.createEntity(); + core.addComponent(entity, Position{Point2D{x, y}}); + core.addComponent(entity, Agent{}); + core.addComponent(entity, CompressionData{}); + return entity; +} + +void addBarrier(EcsCore& core, + std::vector vertices, + bool closed = false, + bool blocksMovement = true) { + Barrier2D barrier; + barrier.geometry.vertices = std::move(vertices); + barrier.geometry.closed = closed; + barrier.blocksMovement = blocksMovement; + + const Entity entity = core.createEntity(); + core.addComponent(entity, std::move(barrier)); +} + +} // namespace + +SC_TEST(CompressionSystem_UpdatesAgentOverlapWithoutBarrierEntitiesAndPreservesExposure) { + EcsCore core; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + + const Entity first = addAgent(core, 0.0, 0.0); + const Entity second = addAgent(core, 0.0, 0.0); + const Entity third = addAgent(core, 0.0, 0.0); + + CompressionSystem system(0.5); + system.update(world, {}); + + const auto& clusteredMetrics = world.query().get(first); + SC_EXPECT_TRUE(clusteredMetrics.force > 0.5f); + SC_EXPECT_NEAR(clusteredMetrics.exposure, 0.5, 1e-6); + SC_EXPECT_TRUE(!clusteredMetrics.isCritical); + + world.query().get(second).value = Point2D{10.0, 0.0}; + world.query().get(third).value = Point2D{-10.0, 0.0}; + system.update(world, {}); + + const auto& separatedMetrics = world.query().get(first); + SC_EXPECT_NEAR(separatedMetrics.force, 0.0, 1e-6); + SC_EXPECT_NEAR(separatedMetrics.exposure, 0.5, 1e-6); + SC_EXPECT_TRUE(!separatedMetrics.isCritical); +} + +SC_TEST(CompressionSystem_CombinesExposureWithCurrentForceForCriticalState) { + EcsCore core; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + + const Entity first = addAgent(core, 0.0, 0.0); + addAgent(core, 0.0, 0.0); + addAgent(core, 0.0, 0.0); + addBarrier(core, {Point2D{-0.1, -1.0}, Point2D{-0.1, 1.0}}); + + CompressionSystem system(1.0); + system.update(world, {}); + system.update(world, {}); + + const auto& highRiskMetrics = world.query().get(first); + SC_EXPECT_TRUE(highRiskMetrics.force > 0.5f); + SC_EXPECT_NEAR(highRiskMetrics.exposure, 2.0, 1e-6); + SC_EXPECT_TRUE(highRiskMetrics.isCritical); + + world.query().get(first).value = Point2D{10.0, 0.0}; + system.update(world, {}); + + const auto& recoveredMetrics = world.query().get(first); + SC_EXPECT_NEAR(recoveredMetrics.exposure, 2.0, 1e-6); + SC_EXPECT_TRUE(!recoveredMetrics.isCritical); +} diff --git a/tests/SafeCrowdDomainTests.cpp b/tests/SafeCrowdDomainTests.cpp index c5e65e6..d309659 100644 --- a/tests/SafeCrowdDomainTests.cpp +++ b/tests/SafeCrowdDomainTests.cpp @@ -1,7 +1,38 @@ #include "TestSupport.h" +#include + +#include "domain/AgentComponents.h" +#include "domain/Metrics.h" #include "domain/SafeCrowdDomain.h" #include "engine/EngineRuntime.h" +#include "engine/EngineSystem.h" +#include "engine/SystemDescriptor.h" +#include "engine/TriggerPolicy.h" +#include "engine/UpdatePhase.h" + +namespace { + +class StartupSeedCrowdSystem final : public safecrowd::engine::EngineSystem { +public: + void update(safecrowd::engine::EngineWorld& world, + const safecrowd::engine::EngineStepContext&) override { + world.commands().spawnEntity( + safecrowd::domain::Position{{0.0, 0.0}}, + safecrowd::domain::Agent{}, + safecrowd::domain::CompressionData{}); + world.commands().spawnEntity( + safecrowd::domain::Position{{0.0, 0.0}}, + safecrowd::domain::Agent{}, + safecrowd::domain::CompressionData{}); + world.commands().spawnEntity( + safecrowd::domain::Position{{0.0, 0.0}}, + safecrowd::domain::Agent{}, + safecrowd::domain::CompressionData{}); + } +}; + +} // namespace SC_TEST(SafeCrowdDomainExposesRuntimeSummary) { safecrowd::engine::EngineRuntime runtime({ @@ -38,3 +69,42 @@ SC_TEST(SafeCrowdDomainStopResetsSummary) { SC_EXPECT_EQ(summary.frameIndex, 0ULL); SC_EXPECT_EQ(summary.fixedStepIndex, 0ULL); } + +SC_TEST(SafeCrowdDomainExposesRuntimeSnapshotWithCompressionChannels) { + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.5, + .maxCatchUpSteps = 4, + .baseSeed = 99, + }); + + runtime.addSystem( + std::make_unique(), + { + .phase = safecrowd::engine::UpdatePhase::Startup, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame, + }); + + safecrowd::domain::SafeCrowdDomain domain(runtime); + domain.start(); + domain.update(0.5); + + const auto snapshot = domain.snapshot(); + const auto* forceChannel = + snapshot.findScalarChannel(safecrowd::domain::kCompressionForceChannelName); + const auto* exposureChannel = + snapshot.findScalarChannel(safecrowd::domain::kCompressionExposureChannelName); + + SC_EXPECT_EQ(snapshot.frameIndex, 1ULL); + SC_EXPECT_EQ(snapshot.fixedStepIndex, 1ULL); + SC_EXPECT_NEAR(snapshot.simulationTime, 0.5, 1e-9); + SC_EXPECT_EQ(snapshot.agentCount, 3U); + SC_EXPECT_EQ(snapshot.agentIds.size(), std::size_t{3}); + SC_EXPECT_EQ(snapshot.positions.size(), std::size_t{3}); + SC_EXPECT_TRUE(forceChannel != nullptr); + SC_EXPECT_TRUE(exposureChannel != nullptr); + SC_EXPECT_EQ(forceChannel->values.size(), std::size_t{3}); + SC_EXPECT_EQ(exposureChannel->values.size(), std::size_t{3}); + SC_EXPECT_TRUE(forceChannel->values[0] > 0.5f); + SC_EXPECT_NEAR(exposureChannel->values[0], 0.5, 1e-6); +} diff --git a/tests/SnapshotTests.cpp b/tests/SnapshotTests.cpp new file mode 100644 index 0000000..3ed81a6 --- /dev/null +++ b/tests/SnapshotTests.cpp @@ -0,0 +1,107 @@ +#include "TestSupport.h" + +#include + +#include "domain/AgentComponents.h" +#include "domain/Metrics.h" +#include "domain/Snapshot.h" +#include "engine/CommandBuffer.h" +#include "engine/EcsCore.h" +#include "engine/internal/EngineWorldFactory.h" + +namespace { + +using safecrowd::domain::Agent; +using safecrowd::domain::CompressionData; +using safecrowd::domain::Point2D; +using safecrowd::domain::Position; +using safecrowd::engine::CommandBuffer; +using safecrowd::engine::EcsCore; +using safecrowd::engine::Entity; + +Entity addAgent(EcsCore& core, double x, double y, bool withMetrics) { + const Entity entity = core.createEntity(); + core.addComponent(entity, Position{Point2D{x, y}}); + core.addComponent(entity, Agent{}); + + if (withMetrics) { + core.addComponent(entity, CompressionData{}); + } + + return entity; +} + +std::uint64_t packEntityId(Entity entity) { + return (static_cast(entity.generation) << 32U) | + static_cast(entity.index); +} + +} // namespace + +SC_TEST(SimulationSnapshot_OmitsCompressionChannelsWhenMetricsAreIncomplete) { + EcsCore core; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + + addAgent(core, 0.0, 0.0, true); + addAgent(core, 1.0, 1.0, false); + + const auto snapshot = safecrowd::domain::buildSnapshot(world.query(), 7, 11, 3.5); + + SC_EXPECT_EQ(snapshot.frameIndex, 7ULL); + SC_EXPECT_EQ(snapshot.fixedStepIndex, 11ULL); + SC_EXPECT_NEAR(snapshot.simulationTime, 3.5, 1e-9); + SC_EXPECT_EQ(snapshot.agentCount, 2U); + SC_EXPECT_EQ(snapshot.agentIds.size(), std::size_t{2}); + SC_EXPECT_EQ(snapshot.positions.size(), std::size_t{2}); + SC_EXPECT_TRUE(snapshot.findScalarChannel(safecrowd::domain::kCompressionForceChannelName) == nullptr); + SC_EXPECT_TRUE(snapshot.findScalarChannel(safecrowd::domain::kCompressionExposureChannelName) == nullptr); + SC_EXPECT_TRUE(snapshot.findFlagChannel(safecrowd::domain::kCompressionCriticalChannelName) == nullptr); +} + +SC_TEST(SimulationSnapshot_BuildsAlignedCompressionChannelsWhenMetricsExistForAllAgents) { + EcsCore core; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + + const Entity first = addAgent(core, 0.0, 0.0, true); + const Entity second = addAgent(core, 2.0, 3.0, true); + + world.query().get(first) = CompressionData{.force = 1.25f, .exposure = 0.75f, .isCritical = true}; + world.query().get(second) = CompressionData{.force = 0.25f, .exposure = 0.0f, .isCritical = false}; + + const auto snapshot = safecrowd::domain::buildSnapshot(world.query(), 2, 5, 1.25); + const auto* forceChannel = snapshot.findScalarChannel(safecrowd::domain::kCompressionForceChannelName); + const auto* exposureChannel = snapshot.findScalarChannel(safecrowd::domain::kCompressionExposureChannelName); + const auto* criticalChannel = snapshot.findFlagChannel(safecrowd::domain::kCompressionCriticalChannelName); + + SC_EXPECT_EQ(snapshot.agentCount, 2U); + SC_EXPECT_TRUE(forceChannel != nullptr); + SC_EXPECT_TRUE(exposureChannel != nullptr); + SC_EXPECT_TRUE(criticalChannel != nullptr); + SC_EXPECT_EQ(forceChannel->values.size(), std::size_t{2}); + SC_EXPECT_EQ(exposureChannel->values.size(), std::size_t{2}); + SC_EXPECT_EQ(criticalChannel->values.size(), std::size_t{2}); + SC_EXPECT_NEAR(forceChannel->values[0], 1.25, 1e-6); + SC_EXPECT_NEAR(exposureChannel->values[0], 0.75, 1e-6); + SC_EXPECT_EQ(criticalChannel->values[0], static_cast(1)); +} + +SC_TEST(SimulationSnapshot_PacksEntityGenerationIntoStableIds) { + EcsCore core(1); + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + + const Entity original = addAgent(core, 0.0, 0.0, false); + core.destroyEntity(original); + const Entity recycled = addAgent(core, 5.0, 6.0, false); + + const auto snapshot = safecrowd::domain::buildSnapshot(world.query(), 1, 1, 0.5); + + SC_EXPECT_EQ(original.index, recycled.index); + SC_EXPECT_TRUE(original.generation != recycled.generation); + SC_EXPECT_EQ(snapshot.agentCount, 1U); + SC_EXPECT_EQ(snapshot.agentIds.size(), std::size_t{1}); + SC_EXPECT_EQ(snapshot.agentIds[0], packEntityId(recycled)); + SC_EXPECT_TRUE(snapshot.agentIds[0] != packEntityId(original)); +} From 82fc958288c8e278c120282aff4df72ba21bd815 Mon Sep 17 00:00:00 2001 From: learncold Date: Mon, 13 Apr 2026 19:28:07 +0900 Subject: [PATCH 8/8] [Build] fix tests after main merge --- tests/CompressionSystemTests.cpp | 7 +++++-- tests/SnapshotTests.cpp | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/CompressionSystemTests.cpp b/tests/CompressionSystemTests.cpp index 2ebaf01..46663e5 100644 --- a/tests/CompressionSystemTests.cpp +++ b/tests/CompressionSystemTests.cpp @@ -8,6 +8,7 @@ #include "domain/Metrics.h" #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" +#include "engine/ResourceStore.h" #include "engine/internal/EngineWorldFactory.h" namespace { @@ -47,8 +48,9 @@ void addBarrier(EcsCore& core, SC_TEST(CompressionSystem_UpdatesAgentOverlapWithoutBarrierEntitiesAndPreservesExposure) { EcsCore core; + safecrowd::engine::ResourceStore resources; CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const Entity first = addAgent(core, 0.0, 0.0); const Entity second = addAgent(core, 0.0, 0.0); @@ -74,8 +76,9 @@ SC_TEST(CompressionSystem_UpdatesAgentOverlapWithoutBarrierEntitiesAndPreservesE SC_TEST(CompressionSystem_CombinesExposureWithCurrentForceForCriticalState) { EcsCore core; + safecrowd::engine::ResourceStore resources; CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const Entity first = addAgent(core, 0.0, 0.0); addAgent(core, 0.0, 0.0); diff --git a/tests/SnapshotTests.cpp b/tests/SnapshotTests.cpp index 3ed81a6..02ee39d 100644 --- a/tests/SnapshotTests.cpp +++ b/tests/SnapshotTests.cpp @@ -7,6 +7,7 @@ #include "domain/Snapshot.h" #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" +#include "engine/ResourceStore.h" #include "engine/internal/EngineWorldFactory.h" namespace { @@ -40,8 +41,9 @@ std::uint64_t packEntityId(Entity entity) { SC_TEST(SimulationSnapshot_OmitsCompressionChannelsWhenMetricsAreIncomplete) { EcsCore core; + safecrowd::engine::ResourceStore resources; CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); addAgent(core, 0.0, 0.0, true); addAgent(core, 1.0, 1.0, false); @@ -61,8 +63,9 @@ SC_TEST(SimulationSnapshot_OmitsCompressionChannelsWhenMetricsAreIncomplete) { SC_TEST(SimulationSnapshot_BuildsAlignedCompressionChannelsWhenMetricsExistForAllAgents) { EcsCore core; + safecrowd::engine::ResourceStore resources; CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const Entity first = addAgent(core, 0.0, 0.0, true); const Entity second = addAgent(core, 2.0, 3.0, true); @@ -89,8 +92,9 @@ SC_TEST(SimulationSnapshot_BuildsAlignedCompressionChannelsWhenMetricsExistForAl SC_TEST(SimulationSnapshot_PacksEntityGenerationIntoStableIds) { EcsCore core(1); + safecrowd::engine::ResourceStore resources; CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const Entity original = addAgent(core, 0.0, 0.0, false); core.destroyEntity(original);