From e90ce643988f6002819916d00ed58f4034008170 Mon Sep 17 00:00:00 2001 From: learncold <166647091+learncold@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:06:20 +0900 Subject: [PATCH 1/4] tighten EngineWorld query boundary (#109) --- CMakeLists.txt | 2 + ...5\355\212\270 \352\265\254\354\241\260.md" | 2 +- ...4\353\260\234 \355\231\230\352\262\275.md" | 4 +- src/engine/EngineRuntime.cpp | 2 +- src/engine/EngineRuntime.h | 1 + src/engine/EngineSystem.h | 18 +-------- src/engine/EngineWorld.h | 38 +++++++++++++++++++ src/engine/WorldQuery.h | 8 +++- src/engine/internal/EngineWorldFactory.h | 16 ++++++++ tests/SystemSchedulerTests.cpp | 13 ++++--- tests/WorldQueryTests.cpp | 36 ++++++++++++------ uml/engine-ecs-core.puml | 3 ++ ...ecs-core.puml \355\225\264\354\204\244.md" | 2 + uml/engine-runtime-core.puml | 8 ++++ ...ime-core.puml \355\225\264\354\204\244.md" | 2 + 15 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 src/engine/EngineWorld.h create mode 100644 src/engine/internal/EngineWorldFactory.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0094216..580661b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ add_library(ecs_engine STATIC src/engine/EcsCore.h src/engine/EngineConfig.h src/engine/EngineRuntime.h + src/engine/EngineWorld.h src/engine/EngineState.h src/engine/EngineStats.h src/engine/EngineStepContext.h @@ -58,6 +59,7 @@ add_library(ecs_engine STATIC src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp + src/engine/internal/EngineWorldFactory.h src/engine/SystemScheduler.cpp ) diff --git "a/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" "b/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" index 8f38620..b2c8f7c 100644 --- "a/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" +++ "b/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" @@ -67,7 +67,7 @@ Project/ - `EngineRuntime` : 엔진 상태와 실행 루프를 관리하는 진입점 - `Entity` : `index + generation` 기반 식별자. 재사용된 entity ID의 stale reference를 막기 위한 기본 핸들 - `EngineWorld` : query / resources / commands를 통해 월드 상태에 접근하는 공유 파사드 -- `WorldQuery`, `WorldResources`, `WorldCommands` : 컴포넌트 조회·값 접근 / 리소스 접근 / 구조 변경 요청을 분리한 접근면 +- `WorldQuery`, `WorldResources`, `WorldCommands` : 컴포넌트 조회·값 접근 / 리소스 접근 / 구조 변경 요청을 분리한 접근면. 상위 계층은 `WorldQuery`를 직접 생성하지 않고 `EngineWorld::query()`로 획득한다. - `WorldCommands` : 엔티티 생성/삭제와 컴포넌트 추가/제거 같은 구조 변경을 즉시 ECS에 반영하지 않고 command buffer에 기록하며, 실제 반영은 runtime이 phase 경계에서 수행 - `FrameClock` : 고정 timestep 누적과 catch-up step 계산 담당 - `EngineSystem` : `update(world, stepContext)` 형태로 월드와 프레임 컨텍스트를 받는 범용 시스템 인터페이스 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 ea7b441..e5b6d73 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" @@ -105,7 +105,7 @@ flowchart LR FC["FrameClock"] ER["EntityRegistry"] PCS["PackedComponentStorage"] - SYS["EngineSystem / WorldQuery / CommandBuffer"] + SYS["EngineWorld / EngineSystem / CommandBuffer"] end QT["Qt6 Widgets"] --> Application @@ -130,7 +130,7 @@ flowchart LR | --- | --- | --- | --- | | `application` | `MainWindow`, Qt signal/slot, 향후 import/scenario/result UI | 사용자 입력 수집, 도메인 서비스 호출, 상태 표시 | ECS 내부 로직과 위험 계산식을 직접 가지지 않음 | | `domain` | `SafeCrowdDomain`, `DxfImportService`, `FacilityLayoutBuilder`, `ImportValidationService`, 향후 시나리오/위험/추천 모듈 | SafeCrowd 문제 영역 규칙, 공간 구조 정규화, 실행 가능성 검토, 위험 지표 정의 | Qt UI 의존 금지 | -| `engine` | `EngineRuntime`, `FrameClock`, `EntityRegistry`, `ComponentRegistry`, `PackedComponentStorage`, `WorldQuery`, `CommandBuffer` | 범용 ECS runtime, fixed timestep, world 상태 관리, 시스템 실행 기반 제공 | SafeCrowd 도메인 지식 의존 금지 | +| `engine` | `EngineRuntime`, `EngineWorld`, `FrameClock`, `EntityRegistry`, `ComponentRegistry`, `PackedComponentStorage`, `WorldQuery`(`EngineWorld::query()` 경유), `CommandBuffer` | 범용 ECS runtime, fixed timestep, world 상태 관리, 시스템 실행 기반 제공 | SafeCrowd 도메인 지식 의존 금지 | 핵심 의존 방향은 아래 한 줄로 요약된다. diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 5a3803e..bc99e88 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -24,7 +24,7 @@ EngineConfig normalizeConfig(EngineConfig config) { EngineRuntime::EngineRuntime(EngineConfig config) : config_(normalizeConfig(config)), scheduler_(core_, buffer_), - world_(core_, buffer_), + world_(EngineWorld::ConstructionToken{}, core_, buffer_), frameClock_(config_) { } diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 340e7d1..407e781 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -8,6 +8,7 @@ #include "engine/EngineConfig.h" #include "engine/EngineStats.h" #include "engine/EngineSystem.h" +#include "engine/EngineWorld.h" #include "engine/FrameClock.h" #include "engine/SystemDescriptor.h" #include "engine/SystemScheduler.h" diff --git a/src/engine/EngineSystem.h b/src/engine/EngineSystem.h index c8d09d6..b0c6196 100644 --- a/src/engine/EngineSystem.h +++ b/src/engine/EngineSystem.h @@ -1,26 +1,10 @@ #pragma once -#include "engine/CommandBuffer.h" +#include "engine/EngineWorld.h" #include "engine/EngineStepContext.h" -#include "engine/WorldQuery.h" namespace safecrowd::engine { -class EngineWorld { -public: - EngineWorld() = delete; - explicit EngineWorld(EcsCore& core, CommandBuffer& buffer) - : query_(core), commands_(buffer) {} - - [[nodiscard]] WorldQuery& query() noexcept { return query_; } - [[nodiscard]] const WorldQuery& query() const noexcept { return query_; } - [[nodiscard]] WorldCommands& commands() noexcept { return commands_; } - -private: - WorldQuery query_; - WorldCommands commands_; -}; - class EngineSystem { public: virtual ~EngineSystem() = default; diff --git a/src/engine/EngineWorld.h b/src/engine/EngineWorld.h new file mode 100644 index 0000000..c31aab5 --- /dev/null +++ b/src/engine/EngineWorld.h @@ -0,0 +1,38 @@ +#pragma once + +#include "engine/CommandBuffer.h" +#include "engine/WorldQuery.h" + +namespace safecrowd::engine { + +namespace internal { +class EngineWorldFactory; +} + +class EngineWorld { +public: + [[nodiscard]] WorldQuery& query() noexcept { return query_; } + [[nodiscard]] const WorldQuery& query() const noexcept { return query_; } + [[nodiscard]] WorldCommands& commands() noexcept { return commands_; } + +private: + class ConstructionToken { + private: + ConstructionToken() = default; + + friend class EngineRuntime; + friend class internal::EngineWorldFactory; + }; + + EngineWorld() = delete; + explicit EngineWorld(ConstructionToken, EcsCore& core, CommandBuffer& buffer) + : query_(core), commands_(buffer) {} + + friend class EngineRuntime; + friend class internal::EngineWorldFactory; + + WorldQuery query_; + WorldCommands commands_; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/WorldQuery.h b/src/engine/WorldQuery.h index cc3885a..e4ad440 100644 --- a/src/engine/WorldQuery.h +++ b/src/engine/WorldQuery.h @@ -6,10 +6,10 @@ namespace safecrowd::engine { +class EngineWorld; + class WorldQuery { public: - explicit WorldQuery(EcsCore& core) : core_(core) {} - template [[nodiscard]] std::vector view() const { Signature required{}; @@ -50,6 +50,10 @@ class WorldQuery { } private: + friend class EngineWorld; + + explicit WorldQuery(EcsCore& core) : core_(core) {} + EcsCore& core_; }; diff --git a/src/engine/internal/EngineWorldFactory.h b/src/engine/internal/EngineWorldFactory.h new file mode 100644 index 0000000..4f0d001 --- /dev/null +++ b/src/engine/internal/EngineWorldFactory.h @@ -0,0 +1,16 @@ +#pragma once + +#include "engine/EngineWorld.h" + +namespace safecrowd::engine::internal { + +class EngineWorldFactory { +public: + EngineWorldFactory() = delete; + + [[nodiscard]] static EngineWorld create(EcsCore& core, CommandBuffer& buffer) { + return EngineWorld(EngineWorld::ConstructionToken{}, core, buffer); + } +}; + +} // namespace safecrowd::engine::internal diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp index a40d6a7..5cc00f0 100644 --- a/tests/SystemSchedulerTests.cpp +++ b/tests/SystemSchedulerTests.cpp @@ -8,6 +8,7 @@ #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" #include "engine/SystemScheduler.h" +#include "engine/internal/EngineWorldFactory.h" namespace { @@ -67,7 +68,7 @@ SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { safecrowd::engine::EcsCore dummyCore; safecrowd::engine::CommandBuffer dummyBuffer; - safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -105,7 +106,7 @@ SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { safecrowd::engine::EcsCore dummyCore; safecrowd::engine::CommandBuffer dummyBuffer; - safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -134,7 +135,7 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { safecrowd::engine::EcsCore dummyCore; safecrowd::engine::CommandBuffer dummyBuffer; - safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -157,7 +158,7 @@ SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) { safecrowd::engine::EcsCore core; safecrowd::engine::CommandBuffer buffer; safecrowd::engine::SystemScheduler scheduler{core, buffer}; - safecrowd::engine::EngineWorld world{core, buffer}; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); std::size_t configuredCount = 0; scheduler.registerSystem(std::make_unique(), {}); @@ -174,7 +175,7 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { safecrowd::engine::CommandBuffer buffer; safecrowd::engine::SystemScheduler scheduler{core, buffer}; - safecrowd::engine::EngineWorld world{core, buffer}; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); scheduler.registerSystem( std::make_unique(), @@ -187,7 +188,7 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { world, ctx); - const auto entities = safecrowd::engine::WorldQuery{core}.view(); + const auto entities = world.query().view(); SC_EXPECT_EQ(entities.size(), std::size_t{1}); } diff --git a/tests/WorldQueryTests.cpp b/tests/WorldQueryTests.cpp index 2bf469a..9c88492 100644 --- a/tests/WorldQueryTests.cpp +++ b/tests/WorldQueryTests.cpp @@ -1,6 +1,8 @@ #include "TestSupport.h" -#include "engine/WorldQuery.h" +#include + +#include "engine/internal/EngineWorldFactory.h" namespace { @@ -16,9 +18,15 @@ struct Velocity { } // namespace +static_assert(!std::is_constructible_v); +static_assert(!std::is_constructible_v); + SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) { safecrowd::engine::EcsCore core; - safecrowd::engine::WorldQuery query{core}; + safecrowd::engine::CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); const auto e1 = core.createEntity(); const auto e2 = core.createEntity(); @@ -29,24 +37,26 @@ SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) { core.addComponent(e2, Position{}); core.addComponent(e3, Velocity{}); - const auto result = query.view(); + const auto result = world.query().view(); SC_EXPECT_EQ(result.size(), std::size_t{1}); SC_EXPECT_TRUE(result[0] == e1); } SC_TEST(WorldQuery_ViewReturnsEmptyIfTypeNotRegistered) { safecrowd::engine::EcsCore core; - safecrowd::engine::WorldQuery query{core}; + safecrowd::engine::CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); static_cast(core.createEntity()); - const auto result = query.view(); + const auto result = world.query().view(); SC_EXPECT_TRUE(result.empty()); } SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { safecrowd::engine::EcsCore core; - safecrowd::engine::WorldQuery query{core}; + safecrowd::engine::CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); const auto e1 = core.createEntity(); core.addComponent(e1, Position{}); @@ -55,30 +65,32 @@ SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { const auto e2 = core.createEntity(); core.addComponent(e2, Position{}); - const auto result = query.view(); + const auto result = world.query().view(); SC_EXPECT_EQ(result.size(), std::size_t{1}); SC_EXPECT_TRUE(result[0] == e2); } SC_TEST(WorldQuery_ContainsReflectsComponentPresence) { safecrowd::engine::EcsCore core; - safecrowd::engine::WorldQuery query{core}; + safecrowd::engine::CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); const auto e = core.createEntity(); core.addComponent(e, Position{}); - SC_EXPECT_TRUE(query.contains(e)); - SC_EXPECT_TRUE(!query.contains(e)); + SC_EXPECT_TRUE(world.query().contains(e)); + SC_EXPECT_TRUE(!world.query().contains(e)); } SC_TEST(WorldQuery_GetReturnsComponentRef) { safecrowd::engine::EcsCore core; - safecrowd::engine::WorldQuery query{core}; + safecrowd::engine::CommandBuffer buffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); const auto e = core.createEntity(); core.addComponent(e, Position{3.0f, 4.0f}); - const auto& pos = query.get(e); + const auto& pos = world.query().get(e); SC_EXPECT_NEAR(pos.x, 3.0f, 1e-6); SC_EXPECT_NEAR(pos.y, 4.0f, 1e-6); } diff --git a/uml/engine-ecs-core.puml b/uml/engine-ecs-core.puml index dd311a7..e592d23 100644 --- a/uml/engine-ecs-core.puml +++ b/uml/engine-ecs-core.puml @@ -18,6 +18,7 @@ package "engine::api" { +view() +contains(entity: Entity): bool +get(entity: Entity): T& + -WorldQuery(core: EcsCore&) } class WorldCommands { @@ -111,6 +112,8 @@ end note note bottom of WorldQuery The first implementation may iterate live entities and filter by signature without extra caches. + Upper layers obtain it through EngineWorld::query() + instead of constructing it directly. Higher-level query caching can be added later without changing the facade. end note diff --git "a/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" "b/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" index e961ca6..a5783d0 100644 --- "a/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" +++ "b/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" @@ -12,12 +12,14 @@ ## `EngineWorld` - 개요: ECS 코어에 접근하는 상위 파사드다. - 목적: runtime과 상위 계층이 raw registry 대신 동일한 진입점을 쓰도록 만든다. +- 접근 경계: `WorldQuery` 같은 ECS 읽기 면도 직접 생성하지 않고 `EngineWorld::query()`를 통해서만 꺼내 쓴다. - 유의사항: 편의 객체처럼 보이더라도, 실제로는 계층 경계를 지키는 핵심 요소다. - 후속 개선 사항: 읽기/쓰기 분리 facade나 phase-aware facade로 나눌 수 있다. ## `WorldQuery` - 개요: ECS 컴포넌트 조회와 값 접근용 API다. - 목적: 필요한 컴포넌트 조합을 읽고 개별 엔티티의 컴포넌트 값을 조회하거나 갱신하게 한다. +- 접근 경계: 상위 계층은 raw `EcsCore`를 받아 `WorldQuery`를 직접 만들지 않고, `EngineWorld`가 제공하는 query facade를 사용한다. - 유의사항: 초기에는 단순 signature 필터로 충분하지만, query 중 엔티티 생성/삭제나 컴포넌트 추가/제거 같은 구조 변경은 허용하지 않는 원칙이 중요하다. - 후속 개선 사항: cached query, typed iterator, optional access 지원을 확장할 수 있다. diff --git a/uml/engine-runtime-core.puml b/uml/engine-runtime-core.puml index e02fa3f..300ec94 100644 --- a/uml/engine-runtime-core.puml +++ b/uml/engine-runtime-core.puml @@ -28,6 +28,7 @@ package "engine::api" { +view() +contains(entity: Entity): bool +get(entity: Entity): T& + -WorldQuery(core: EcsCore&) } class WorldResources { @@ -180,6 +181,13 @@ note bottom of WorldCommands while iterating queries. end note +note bottom of WorldQuery + Runtime and systems access query capabilities + through EngineWorld::query(). + Raw ECS-backed construction stays behind + the engine facade boundary. +end note + note right of DeterministicRng Repeatable seed streams are derived per run so scenario batches can be reproduced. diff --git "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" index 26f247a..5ac673b 100644 --- "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" +++ "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" @@ -18,12 +18,14 @@ ## `EngineWorld` - 개요: query, resources, commands를 묶은 월드 파사드다. - 목적: 외부 계층이 raw storage를 직접 만지지 않고도 월드에 접근할 수 있게 한다. +- 접근 경계: `WorldQuery`는 외부에서 raw `EcsCore`로 직접 생성하지 않고 `EngineWorld::query()`를 통해 획득한다. - 유의사항: 단순 편의 객체가 아니라 엔진 접근 규칙을 강제하는 경계로 다뤄야 한다. - 후속 개선 사항: 읽기/쓰기 권한 분리 facade로 확장할 수 있다. ## `WorldQuery` - 개요: 엔티티와 컴포넌트 조회 및 값 접근 면이다. - 목적: 시스템이 필요한 컴포넌트 집합을 안전하게 순회하고 조회하거나 기존 컴포넌트 값을 갱신하게 한다. +- 접근 경계: runtime과 시스템은 `EngineWorld`가 가진 query facade를 사용하고, raw ECS 기반 생성 경로는 엔진 내부 구현 세부로 남긴다. - 유의사항: 초기 구현은 전체 live entity 순회로 시작해도 되지만, query 중 entity 생성/삭제나 component 추가/제거 같은 구조 변경은 허용하면 안 된다. - 후속 개선 사항: query cache, signature index, iterator 최적화를 추가할 수 있다. From 5f8897a9b3c18c0d97b4e7e3abad351f4c908289 Mon Sep 17 00:00:00 2001 From: learncold Date: Thu, 9 Apr 2026 03:08:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Pathfinder=20=EC=A1=B0=EC=82=AC=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\355\221\234_\354\264\210\354\225\210.md" | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 "docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" 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/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" new file mode 100644 index 0000000..8cfc328 --- /dev/null +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" @@ -0,0 +1,113 @@ +# Pathfinder 반영 발표 초안 + +## 발표 전략 +- 이번 발표에서는 Pathfinder 조사 결과가 SafeCrowd 설계와 구현 계획에 어떻게 반영됐는지를 설명한다. +- 설명은 기능 반영 내용, backlog 수정, 관리 체계, 현재 구현 상태 순서로 간다. +- Pathfinder 조사를 통해 제품 구조와 구현 계획이 더 구체화되었다는 흐름으로 말한다. + +## 전체 슬라이드 흐름 +1. Pathfinder 조사 후 우리 주제에 맞게 반영한 기능 +2. Pathfinder 반영 결과 Backlog가 어떻게 바뀌었는가 +3. Sprint와 우선순위가 왜 다시 배치되었는가 +4. GitHub Project, UML, Issue 세팅 반영 상태 +5. 현재 구현 완료 범위 + +--- + +## 슬라이드 1. Pathfinder 조사 후 반영한 기능 + +### 슬라이드에 넣을 문구 +- 공간 구조를 실행 가능한 토폴로지로 정리 +- 연결 구조와 이동 제약 반영 +- 시나리오 제어와 행동 전환 반영 +- 반복 실행과 재현성 반영 +- 결과 아티팩트와 비교 분석 구조 반영 + +### 슬라이드에 넣을 세부 bullet +- `Floor / Room / Wall / Door / Obstruction / Obstacle` 기준으로 공간 요소를 나눠 다루는 구조 +- `stairs / ramp / escalator / walkway`를 공통 connector + modifier로 다루는 방식 +- 사람 유형별 이동 제약, 사용할 수 있는 연결 경로 제한, 속도/가속/간격 분포 파라미터 +- 단순 최단거리 대신 `국소 이동 시간 + 대기 + 잔여 경로`를 반영하는 기본 출구 선택 비용 +- 행동 순서, 조건에 따른 행동 전환, 인원 그룹별 제어 +- 초기 배치 + 동적 인원 유입 + 반복 실행과 실행별 비교 구조 +- 출입구별 통과 인원과 시간, 공간별 인원 변화, 특정 구역 혼잡도, 전체 실행 요약을 남기는 결과 구조 +- heatmap, 핵심 지표 요약, 시나리오 비교, 추천 연결을 위한 결과 파이프라인 + +### 발표 대본 +교수님 피드백 이후 저희는 Pathfinder를 기능을 공식 문서를 통해 조사했고, 그 조사 결과를 반영했습니다. +이 슬라이드에서는 그 반영 내용 중 중요한 부분들을 입력부터 결과 분석 흐름 순서로 설명드리겠습니다. +먼저 공간 구조 쪽에서는 단순히 도면을 불러오는 수준이 아니라, Floor, Room, Wall, Door, Obstruction, Obstacle처럼 실행 가능한 토폴로지로 나눠 다루는 구조를 반영했습니다. +또 계단, 램프, 에스컬레이터, 무빙워크처럼 이동 연결에 해당하는 요소는 각각 따로 보기보다 connector와 modifier 관점으로 정리했습니다. +인원 모델 쪽에서는 사람 유형별 이동 제약과 사용할 수 있는 연결 경로 제한, 그리고 속도나 간격 같은 분포형 파라미터를 반영했습니다. +경로 선택도 단순 최단거리 대신, 국소 이동 시간과 대기, 남은 경로를 함께 고려하는 출구 선택 비용 구조로 정리했습니다. +시나리오 제어 쪽에서는 행동 순서, 조건에 따른 행동 전환, 인원 그룹별 제어 같은 방식을 반영했고, 실행 쪽에서는 초기 배치뿐 아니라 시간대별 인원 유입, 반복 실행, 반복 실험 간 비교가 가능한 구조를 반영했습니다. +마지막으로 결과 쪽에서는 출입구별 통과 기록, 공간별 인원 변화 기록, 특정 구역의 혼잡도 측정 결과, 전체 실행 요약 같은 결과를 남기고, 이걸 heatmap, 핵심 지표 요약, 시나리오 비교, 그리고 이후 추천 기능으로 연결하는 흐름을 반영했습니다. + +### 발표 포인트 +- 조사한 기능을 `공간`, `연결`, `인원`, `경로`, `제어`, `실행`, `결과` 순서로 소개하면 흐름이 자연스럽다 + +--- + +## 슬라이드 2. Pathfinder 반영 결과 Backlog 수정 내용 (Product Backlog - Pathfinder 반영안.md의 (수정) 이라는 텍스트가 붙은 부분들 참고) + +### 슬라이드에 넣을 문구 +- Acceptance Criteria 보강 + +### 슬라이드에 넣을 세부 bullet +- 공간 import 결과에 `Room`, `Door`, `Obstruction`, `Obstacle`, `Connector` 검토 기준 추가 +- 시나리오 생성에 인원 배치 조건, 연결 경로 사용 제한, 경로 선택 규칙 기준 추가 +- 운영 이벤트에 행동 전환, 인원 그룹 구분, 발동 조건 추가 +- 실행과 반복 실행에 시간대별 인원 유입, seed, 반복 실행 기록 기준 추가 +- 결과 분석에 출입구별 통과 기록, 공간별 인원 변화 기록, 특정 구역 측정 결과, 반복 실행/전체 실행 요약 기준 추가 + +### 발표 대본 +Backlog에서 변화는 acceptance criteria를 구체적으로 늘린 부분입니다. +먼저 공간 입력 쪽에서는 import 결과를 단순 도면 불러오기로 두지 않고, Room, Door, Obstruction, Obstacle, Connector 후보를 구분해서 검토할 수 있도록 기준을 추가했습니다. +시나리오 생성 쪽에서는 인원 배치 조건, 어떤 연결 경로를 사용할 수 있는지, 어떤 기준으로 출구를 선택하는지 같은 설정 요소를 acceptance criteria에 넣었습니다. +운영 이벤트 쪽도 단순한 상태 변경이 아니라, 행동 전환, 인원 그룹 구분, 어떤 조건에서 이벤트가 발동하는지까지 포함하도록 구체화했습니다. +실행 쪽에서는 초기 배치뿐 아니라 시간대별 인원 유입, seed, 반복 실행 기록 기준을 추가해서 반복 실행과 재현성을 더 명확히 적었습니다. +마지막으로 결과 쪽에서는 출입구별 통과 기록, 공간별 인원 변화 기록, 특정 구역 측정 결과, 반복 실행 요약, 전체 실행 요약 같은 결과 단위를 acceptance criteria에 반영했습니다. +--- + +## 슬라이드 3. Sprint와 구현 순서 재배치 (Product_Backlog_2팀.docx 후반 참고) + +### 슬라이드에 넣을 문구 +- Sprint 1: 공간 토폴로지와 기본 실행 +- Sprint 2: 핵심 위험 분석과 결과 파이프라인 +- Sprint 3: 추가 기능과 추천 연결 + +### 슬라이드에 넣을 세부 bullet +- Sprint 1: E1, E2 기본, E3 기본 실행 +- Sprint 2: E4 핵심 위험 축, E5 요약/비교 +- Sprint 3: 템플릿, 추가 위험 축, 저장/내보내기, 추천 +- 후속 단계 기능과 추가 조사 항목은 분리 유지 + +### 발표 대본 +Backlog를 수정하면서 Sprint 배치도 같이 바꿨습니다. +Sprint 1은 공간 토폴로지와 기본 실행입니다. 즉, 도면을 실행 가능한 구조로 만들고, 기준 시나리오와 운영 이벤트를 정의하고, 실제로 실행이 돌아가는 기반을 먼저 확보하는 단계입니다. +Sprint 2는 핵심 위험 분석과 결과 파이프라인입니다. 여기서는 병목과 정체, 근접도와 압박 전조, 운영 갈등 같은 핵심 위험 축을 잡고, heatmap과 핵심 지표, 기준안 대비 대안 비교까지 연결합니다. +Sprint 3은 추가 기능과 추천 연결 단계입니다. 시나리오 템플릿, 추가 위험 축, 저장과 내보내기, 그리고 운영 대안 추천을 이 단계에 둡니다. + +--- + +## 슬라이드 4. GitHub Project, UML, Issue 세팅 반영 상태 + +### 슬라이드에 넣을 문구 +- UML, Issue, Project를 같은 기준으로 동기화 + +### 슬라이드에 넣을 세부 bullet +- UML 5종 추가 +- Github Issue +- Project + +### 발표 대본 +실제로 구현이 따라갈 수 있도록 UML, Issue, GitHub Project 운영 방식까지 같이 맞췄습니다. +수정된 Backlog 문서의 기준에 맞춰 UML에 반영했습니다. +다음으로 이 설계를 GitHub 작업 체계에 연결했습니다. +Project에서는 진행상황을 한 눈에 보이게 추적할 수 있습니다. + +--- + +## 슬라이드 5. 현재 구현 완료 범위 (예정) + +--- From 980385b3311b7f1418d27bbd0c6b150ea2bcc728 Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:46:51 +0900 Subject: [PATCH 3/4] [Domain] implement demo layout fixture for Sprint 1 (#14) Reflect review follow-ups: build the fixture service, keep initial placement in PopulationSpec, and add fixture coverage. --- CMakeLists.txt | 4 ++ src/domain/DemoFixtureService.cpp | 82 +++++++++++++++++++++++++++++++ src/domain/DemoFixtureService.h | 18 +++++++ src/domain/PopulationSpec.h | 22 +++++++++ tests/DemoFixtureServiceTests.cpp | 36 ++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 src/domain/DemoFixtureService.cpp create mode 100644 src/domain/DemoFixtureService.h create mode 100644 src/domain/PopulationSpec.h create mode 100644 tests/DemoFixtureServiceTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 580661b..9b80702 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,8 +74,11 @@ add_library(safecrowd_domain STATIC src/domain/SafeCrowdDomain.h src/domain/SafeCrowdDomain.cpp src/domain/Geometry2D.h + src/domain/PopulationSpec.h src/domain/RawImportModel.h src/domain/CanonicalGeometry.h + src/domain/DemoFixtureService.h + src/domain/DemoFixtureService.cpp src/domain/DxfImportService.h src/domain/DxfImportService.cpp src/domain/FacilityLayout2D.h @@ -116,6 +119,7 @@ if (BUILD_TESTING) tests/EcsCoreTests.cpp tests/ImportContractsTests.cpp tests/DxfImportServiceTests.cpp + tests/DemoFixtureServiceTests.cpp tests/FacilityLayoutBuilderTests.cpp tests/WorldQueryTests.cpp tests/CommandBufferTests.cpp diff --git a/src/domain/DemoFixtureService.cpp b/src/domain/DemoFixtureService.cpp new file mode 100644 index 0000000..0bde4f6 --- /dev/null +++ b/src/domain/DemoFixtureService.cpp @@ -0,0 +1,82 @@ +#include "domain/DemoFixtureService.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.population.initialPlacements.push_back({ + .id = "placement-1", + .zoneId = mainRoom.id, + .area = { + .outline = { + {1.0, 1.0}, + {5.0, 1.0}, + {5.0, 5.0}, + {1.0, 5.0}, + }, + }, + .targetAgentCount = 100, + }); + + return fixture; +} + +} // namespace safecrowd::domain diff --git a/src/domain/DemoFixtureService.h b/src/domain/DemoFixtureService.h new file mode 100644 index 0000000..228da40 --- /dev/null +++ b/src/domain/DemoFixtureService.h @@ -0,0 +1,18 @@ +#pragma once + +#include "domain/FacilityLayout2D.h" +#include "domain/PopulationSpec.h" + +namespace safecrowd::domain { + +struct DemoFixture { + FacilityLayout2D layout{}; + PopulationSpec population{}; +}; + +class DemoFixtureService { +public: + DemoFixture createSprint1DemoFixture() const; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/PopulationSpec.h b/src/domain/PopulationSpec.h new file mode 100644 index 0000000..953f58e --- /dev/null +++ b/src/domain/PopulationSpec.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#include "domain/Geometry2D.h" + +namespace safecrowd::domain { + +struct InitialPlacement2D { + std::string id{}; + std::string zoneId{}; + Polygon2D area{}; + std::size_t targetAgentCount{0}; +}; + +struct PopulationSpec { + std::vector initialPlacements{}; +}; + +} // namespace safecrowd::domain diff --git a/tests/DemoFixtureServiceTests.cpp b/tests/DemoFixtureServiceTests.cpp new file mode 100644 index 0000000..d156b14 --- /dev/null +++ b/tests/DemoFixtureServiceTests.cpp @@ -0,0 +1,36 @@ +#include + +#include "TestSupport.h" + +#include "domain/DemoFixtureService.h" +#include "domain/ImportIssue.h" +#include "domain/ImportValidationService.h" + +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.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.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.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().targetAgentCount, std::size_t{100}); + SC_EXPECT_EQ(population.initialPlacements.front().area.outline.size(), std::size_t{4}); + + safecrowd::domain::ImportValidationService validator; + const auto issues = validator.validate(layout); + SC_EXPECT_TRUE(!safecrowd::domain::hasBlockingImportIssue(issues)); +} From 195a35dcafb484ca25177aed2862edd364e2501e Mon Sep 17 00:00:00 2001 From: gsh <144871327+95x8x9@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:51:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스크립트 부분 약간 다듬었습니다. --- ...\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" index 8cfc328..ae4bad5 100644 --- "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" @@ -61,8 +61,8 @@ - 결과 분석에 출입구별 통과 기록, 공간별 인원 변화 기록, 특정 구역 측정 결과, 반복 실행/전체 실행 요약 기준 추가 ### 발표 대본 -Backlog에서 변화는 acceptance criteria를 구체적으로 늘린 부분입니다. -먼저 공간 입력 쪽에서는 import 결과를 단순 도면 불러오기로 두지 않고, Room, Door, Obstruction, Obstacle, Connector 후보를 구분해서 검토할 수 있도록 기준을 추가했습니다. +Backlog에서 변화는 acceptance criteria를 구체적으로 세분화한 부분입니다. +먼저 공간 입력 쪽에서는 import 결과를 단순 도면 불러오기로 두지 않고, Room, Door, Obstruction, Obstacle, Connector 후보를 구분해서 검토할 수 있도록 기준을 세화했습니다. 시나리오 생성 쪽에서는 인원 배치 조건, 어떤 연결 경로를 사용할 수 있는지, 어떤 기준으로 출구를 선택하는지 같은 설정 요소를 acceptance criteria에 넣었습니다. 운영 이벤트 쪽도 단순한 상태 변경이 아니라, 행동 전환, 인원 그룹 구분, 어떤 조건에서 이벤트가 발동하는지까지 포함하도록 구체화했습니다. 실행 쪽에서는 초기 배치뿐 아니라 시간대별 인원 유입, seed, 반복 실행 기록 기준을 추가해서 반복 실행과 재현성을 더 명확히 적었습니다.