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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/domain/AgentComponents.h
Original file line number Diff line number Diff line change
@@ -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
121 changes: 121 additions & 0 deletions src/domain/CompressionSystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#include "domain/CompressionSystem.h"

#include "domain/AgentComponents.h"
#include "domain/FacilityLayout2D.h"
#include "domain/Metrics.h"

#include <algorithm>
#include <cmath>

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<float>(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<Position, Agent, CompressionData>();
const auto barrierEntities = query.view<Barrier2D>();

for (const auto entity : agentEntities) {
const auto& position = query.get<Position>(entity);
const auto& agent = query.get<Agent>(entity);
auto& compression = query.get<CompressionData>(entity);

double currentForce = 0.0;

for (const auto otherEntity : agentEntities) {
if (otherEntity == entity) {
continue;
}

const auto& otherPosition = query.get<Position>(otherEntity);
const auto& otherAgent = query.get<Agent>(otherEntity);
const double distance = distanceBetween(position.value, otherPosition.value);
const double combinedRadius = static_cast<double>(agent.radius + otherAgent.radius);

if (distance < combinedRadius) {
currentForce += combinedRadius - distance;
}
}

for (const auto barrierEntity : barrierEntities) {
currentForce += barrierCompression(
query.get<Barrier2D>(barrierEntity),
position.value,
static_cast<double>(agent.radius));
}

compression.force = static_cast<float>(currentForce);
if (compression.force > kForceThreshold) {
compression.exposure += timeStepSeconds_;
}

compression.isCritical =
compression.force > kForceThreshold &&
compression.exposure >= kExposureThreshold;
}
}

} // namespace safecrowd::domain
18 changes: 18 additions & 0 deletions src/domain/CompressionSystem.h
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions src/domain/Metrics.h
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions src/domain/SafeCrowdDomain.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
#include "domain/SafeCrowdDomain.h"

#include <memory>

#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<CompressionSystem>(runtime_.config().fixedDeltaTime),
{
.phase = engine::UpdatePhase::FixedSimulation,
.order = 0,
.triggerPolicy = engine::TriggerPolicy::FixedStep,
});
}

void SafeCrowdDomain::start() {
Expand Down Expand Up @@ -32,6 +46,19 @@ SimulationSummary SafeCrowdDomain::summary() const {
};
}

SimulationSnapshot SafeCrowdDomain::snapshot() const {
const auto& stats = runtime_.stats();
const double simulationTime =
(static_cast<double>(stats.fixedStepIndex) + stats.alpha) *
runtime_.config().fixedDeltaTime;

return buildSnapshot(
runtime_.world().query(),
stats.frameIndex,
stats.fixedStepIndex,
simulationTime);
}

engine::EngineRuntime& SafeCrowdDomain::runtime() noexcept {
return runtime_;
}
Expand Down
2 changes: 2 additions & 0 deletions src/domain/SafeCrowdDomain.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <cstdint>

#include "domain/Snapshot.h"
#include "engine/EngineRuntime.h"
#include "engine/EngineState.h"

Expand All @@ -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;

Expand Down
98 changes: 98 additions & 0 deletions src/domain/Snapshot.cpp
Original file line number Diff line number Diff line change
@@ -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<std::uint64_t>(entity.generation) << 32U) |
static_cast<std::uint64_t>(entity.index);
}

bool hasCompressionMetricsForAllAgents(const engine::WorldQuery& query,
const std::vector<engine::Entity>& agentEntities) {
if (agentEntities.empty()) {
return false;
}

for (const auto entity : agentEntities) {
if (!query.contains<CompressionData>(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<Position, Agent>();
snapshot.agentCount = static_cast<std::uint32_t>(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<Position>(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<CompressionData>(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
Loading
Loading