diff --git a/CMakeLists.txt b/CMakeLists.txt index e1d16d5..c3ffb95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,12 @@ 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/CompressionSystem.h + src/domain/Metrics.h + src/domain/Snapshot.cpp + src/domain/Snapshot.h ) target_include_directories(safecrowd_domain @@ -120,6 +126,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 new file mode 100644 index 0000000..2f78861 --- /dev/null +++ b/src/domain/AgentComponents.h @@ -0,0 +1,20 @@ +#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 diff --git a/src/domain/CompressionSystem.cpp b/src/domain/CompressionSystem.cpp new file mode 100644 index 0000000..6e022b3 --- /dev/null +++ b/src/domain/CompressionSystem.cpp @@ -0,0 +1,121 @@ +#include "domain/CompressionSystem.h" + +#include "domain/AgentComponents.h" +#include "domain/FacilityLayout2D.h" +#include "domain/Metrics.h" + +#include +#include + +namespace safecrowd::domain { +namespace { + +constexpr float kForceThreshold = 0.5f; +constexpr float kExposureThreshold = 2.0f; + +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); +} + +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); + } + + 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; + } + + double force = 0.0; + const auto& vertices = barrier.geometry.vertices; + + 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; + } + } + + if (barrier.geometry.closed) { + const double distance = distancePointToSegment(position, vertices.back(), vertices.front()); + if (distance < radius) { + force += radius - distance; + } + } + + return force; +} + +} // namespace + +CompressionSystem::CompressionSystem(double timeStepSeconds) + : timeStepSeconds_(static_cast(std::max(0.0, timeStepSeconds))) { +} + +void CompressionSystem::update(engine::EngineWorld& world, + const engine::EngineStepContext& step) { + (void)step; + + auto& query = world.query(); + const auto agentEntities = query.view(); + const auto barrierEntities = query.view(); + + for (const auto entity : agentEntities) { + const auto& position = query.get(entity); + const auto& agent = query.get(entity); + auto& compression = query.get(entity); + + double currentForce = 0.0; + + for (const auto otherEntity : agentEntities) { + if (otherEntity == entity) { + continue; + } + + 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)); + } + + compression.force = static_cast(currentForce); + if (compression.force > kForceThreshold) { + compression.exposure += timeStepSeconds_; + } + + compression.isCritical = + compression.force > kForceThreshold && + compression.exposure >= kExposureThreshold; + } +} + +} // namespace safecrowd::domain diff --git a/src/domain/CompressionSystem.h b/src/domain/CompressionSystem.h new file mode 100644 index 0000000..656f9ca --- /dev/null +++ b/src/domain/CompressionSystem.h @@ -0,0 +1,18 @@ +#pragma once + +#include "engine/EngineSystem.h" + +namespace safecrowd::domain { + +class CompressionSystem final : public engine::EngineSystem { +public: + explicit CompressionSystem(double timeStepSeconds); + + 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 new file mode 100644 index 0000000..b203bc8 --- /dev/null +++ b/src/domain/Metrics.h @@ -0,0 +1,11 @@ +#pragma once + +namespace safecrowd::domain { + +struct CompressionData { + float force{0.0f}; + float exposure{0.0f}; + bool isCritical{false}; +}; + +} // 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 new file mode 100644 index 0000000..562bf76 --- /dev/null +++ b/src/domain/Snapshot.cpp @@ -0,0 +1,98 @@ +#include "domain/Snapshot.h" + +#include "domain/AgentComponents.h" +#include "domain/Metrics.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; + } + + 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; + } + + 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 new file mode 100644 index 0000000..f45d266 --- /dev/null +++ b/src/domain/Snapshot.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "domain/Geometry2D.h" + +namespace safecrowd::engine { +class WorldQuery; +} + +namespace safecrowd::domain { + +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; + + [[nodiscard]] const SnapshotScalarChannel* findScalarChannel(std::string_view key) const noexcept; + [[nodiscard]] const SnapshotFlagChannel* findFlagChannel(std::string_view key) const noexcept; +}; + +SimulationSnapshot buildSnapshot(const engine::WorldQuery& query, + std::uint64_t frame, + std::uint64_t fixedStep, + double simulationTime); + +} // namespace safecrowd::domain diff --git a/tests/CompressionSystemTests.cpp b/tests/CompressionSystemTests.cpp new file mode 100644 index 0000000..46663e5 --- /dev/null +++ b/tests/CompressionSystemTests.cpp @@ -0,0 +1,103 @@ +#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/ResourceStore.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; + safecrowd::engine::ResourceStore resources; + CommandBuffer 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); + 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; + safecrowd::engine::ResourceStore resources; + CommandBuffer 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); + 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..02ee39d --- /dev/null +++ b/tests/SnapshotTests.cpp @@ -0,0 +1,111 @@ +#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/ResourceStore.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; + safecrowd::engine::ResourceStore resources; + CommandBuffer 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); + + 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; + safecrowd::engine::ResourceStore resources; + CommandBuffer 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); + + 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); + safecrowd::engine::ResourceStore resources; + CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, 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)); +}