From 16a1eb694ed22e59257e44210ff1016ab2310731 Mon Sep 17 00:00:00 2001 From: learncold Date: Fri, 3 Apr 2026 19:54:21 +0900 Subject: [PATCH] define import domain contracts --- CMakeLists.txt | 10 + src/domain/CanonicalGeometry.h | 69 ++++++ src/domain/FacilityLayout2D.h | 97 ++++++++ src/domain/Geometry2D.h | 27 +++ src/domain/ImportContracts.h | 9 + src/domain/ImportIssue.cpp | 55 +++++ src/domain/ImportIssue.h | 40 +++ src/domain/ImportOrchestrator.h | 23 ++ src/domain/ImportResult.h | 42 ++++ src/domain/RawImportModel.h | 90 +++++++ tests/ImportContractsTests.cpp | 228 ++++++++++++++++++ uml/domain-import-module.puml | 4 +- ...t-module.puml \355\225\264\354\204\244.md" | 19 +- 13 files changed, 702 insertions(+), 11 deletions(-) create mode 100644 src/domain/CanonicalGeometry.h create mode 100644 src/domain/FacilityLayout2D.h create mode 100644 src/domain/Geometry2D.h create mode 100644 src/domain/ImportContracts.h create mode 100644 src/domain/ImportIssue.cpp create mode 100644 src/domain/ImportIssue.h create mode 100644 src/domain/ImportOrchestrator.h create mode 100644 src/domain/ImportResult.h create mode 100644 src/domain/RawImportModel.h create mode 100644 tests/ImportContractsTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6daba31..1f37dc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,15 @@ configure_project_target(ecs_engine) add_library(safecrowd_domain STATIC src/domain/SafeCrowdDomain.h src/domain/SafeCrowdDomain.cpp + src/domain/Geometry2D.h + src/domain/RawImportModel.h + src/domain/CanonicalGeometry.h + src/domain/FacilityLayout2D.h + src/domain/ImportIssue.h + src/domain/ImportIssue.cpp + src/domain/ImportResult.h + src/domain/ImportOrchestrator.h + src/domain/ImportContracts.h ) target_include_directories(safecrowd_domain @@ -90,6 +99,7 @@ if (BUILD_TESTING) tests/PackedComponentStorageTests.cpp tests/SafeCrowdDomainTests.cpp tests/EcsCoreTests.cpp + tests/ImportContractsTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/domain/CanonicalGeometry.h b/src/domain/CanonicalGeometry.h new file mode 100644 index 0000000..12921a0 --- /dev/null +++ b/src/domain/CanonicalGeometry.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include "domain/Geometry2D.h" + +namespace safecrowd::domain { + +enum class OpeningKind { + Unknown, + Doorway, + Passage, + Exit, +}; + +enum class VerticalLinkKind { + Unknown, + Stair, + Ramp, + Elevator, +}; + +struct WalkableSurface2D { + std::string id{}; + Polygon2D polygon{}; + std::vector sourceIds{}; +}; + +struct WallSegment2D { + std::string id{}; + LineSegment2D segment{}; + double thickness{0.0}; + std::vector sourceIds{}; +}; + +struct Opening2D { + std::string id{}; + OpeningKind kind{OpeningKind::Unknown}; + LineSegment2D span{}; + double width{0.0}; + std::vector sourceIds{}; +}; + +struct Obstacle2D { + std::string id{}; + Polygon2D footprint{}; + std::vector sourceIds{}; +}; + +struct VerticalLink2D { + std::string id{}; + VerticalLinkKind kind{VerticalLinkKind::Unknown}; + Point2D anchor{}; + std::string targetLevelId{}; + double width{0.0}; + std::vector sourceIds{}; +}; + +struct CanonicalGeometry { + std::string levelId{}; + std::vector walkableAreas{}; + std::vector walls{}; + std::vector openings{}; + std::vector obstacles{}; + std::vector verticalLinks{}; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/FacilityLayout2D.h b/src/domain/FacilityLayout2D.h new file mode 100644 index 0000000..dba2268 --- /dev/null +++ b/src/domain/FacilityLayout2D.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +#include "domain/Geometry2D.h" + +namespace safecrowd::domain { + +enum class ZoneKind { + Unknown, + Room, + Corridor, + Exit, + Intersection, + Stair, +}; + +enum class ConnectionKind { + Unknown, + Doorway, + Opening, + Exit, + Stair, + Ramp, +}; + +enum class TravelDirection { + Bidirectional, + ForwardOnly, + ReverseOnly, + Closed, +}; + +enum class ControlKind { + Unknown, + Gate, + ExitControl, + BarrierToggle, +}; + +struct ElementProvenance { + std::vector sourceIds{}; + std::vector canonicalIds{}; +}; + +struct Zone2D { + std::string id{}; + ZoneKind kind{ZoneKind::Unknown}; + std::string label{}; + Polygon2D area{}; + std::size_t defaultCapacity{0}; + bool isStair{false}; + bool isRamp{false}; + ElementProvenance provenance{}; +}; + +struct Connection2D { + std::string id{}; + ConnectionKind kind{ConnectionKind::Unknown}; + std::string fromZoneId{}; + std::string toZoneId{}; + double effectiveWidth{0.0}; + TravelDirection directionality{TravelDirection::Bidirectional}; + bool isStair{false}; + bool isRamp{false}; + LineSegment2D centerSpan{}; + ElementProvenance provenance{}; +}; + +struct Barrier2D { + std::string id{}; + Polyline2D geometry{}; + bool blocksMovement{true}; + ElementProvenance provenance{}; +}; + +struct ControlPoint2D { + std::string id{}; + ControlKind kind{ControlKind::Unknown}; + std::string targetId{}; + bool defaultOpen{true}; + ElementProvenance provenance{}; +}; + +struct FacilityLayout2D { + std::string id{}; + std::string name{}; + std::string levelId{}; + std::vector zones{}; + std::vector connections{}; + std::vector barriers{}; + std::vector controls{}; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/Geometry2D.h b/src/domain/Geometry2D.h new file mode 100644 index 0000000..5b7e51e --- /dev/null +++ b/src/domain/Geometry2D.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace safecrowd::domain { + +struct Point2D { + double x{0.0}; + double y{0.0}; +}; + +struct LineSegment2D { + Point2D start{}; + Point2D end{}; +}; + +struct Polyline2D { + std::vector vertices{}; + bool closed{false}; +}; + +struct Polygon2D { + std::vector outline{}; + std::vector> holes{}; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/ImportContracts.h b/src/domain/ImportContracts.h new file mode 100644 index 0000000..4b383f7 --- /dev/null +++ b/src/domain/ImportContracts.h @@ -0,0 +1,9 @@ +#pragma once + +#include "domain/CanonicalGeometry.h" +#include "domain/FacilityLayout2D.h" +#include "domain/Geometry2D.h" +#include "domain/ImportIssue.h" +#include "domain/ImportOrchestrator.h" +#include "domain/ImportResult.h" +#include "domain/RawImportModel.h" diff --git a/src/domain/ImportIssue.cpp b/src/domain/ImportIssue.cpp new file mode 100644 index 0000000..64f8f74 --- /dev/null +++ b/src/domain/ImportIssue.cpp @@ -0,0 +1,55 @@ +#include "domain/ImportIssue.h" + +namespace safecrowd::domain { + +bool ImportIssue::blocksSimulation() const noexcept { + return isBlocking || severity == ImportIssueSeverity::Error; +} + +const char* toString(ImportIssueSeverity severity) noexcept { + switch (severity) { + case ImportIssueSeverity::Info: + return "Info"; + case ImportIssueSeverity::Warning: + return "Warning"; + case ImportIssueSeverity::Error: + return "Error"; + } + + return "Unknown"; +} + +const char* toString(ImportIssueCode code) noexcept { + switch (code) { + case ImportIssueCode::Unknown: + return "Unknown"; + case ImportIssueCode::UnsupportedEntity: + return "UnsupportedEntity"; + case ImportIssueCode::MissingSourceGeometry: + return "MissingSourceGeometry"; + case ImportIssueCode::InvalidGeometry: + return "InvalidGeometry"; + case ImportIssueCode::DisconnectedWalkableArea: + return "DisconnectedWalkableArea"; + case ImportIssueCode::MissingExit: + return "MissingExit"; + case ImportIssueCode::WidthBelowMinimum: + return "WidthBelowMinimum"; + case ImportIssueCode::UnmappedElement: + return "UnmappedElement"; + } + + return "Unknown"; +} + +bool hasBlockingImportIssue(const std::vector& issues) noexcept { + for (const auto& issue : issues) { + if (issue.blocksSimulation()) { + return true; + } + } + + return false; +} + +} // namespace safecrowd::domain diff --git a/src/domain/ImportIssue.h b/src/domain/ImportIssue.h new file mode 100644 index 0000000..3875987 --- /dev/null +++ b/src/domain/ImportIssue.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace safecrowd::domain { + +enum class ImportIssueSeverity { + Info, + Warning, + Error, +}; + +enum class ImportIssueCode { + Unknown, + UnsupportedEntity, + MissingSourceGeometry, + InvalidGeometry, + DisconnectedWalkableArea, + MissingExit, + WidthBelowMinimum, + UnmappedElement, +}; + +struct ImportIssue { + ImportIssueSeverity severity{ImportIssueSeverity::Warning}; + ImportIssueCode code{ImportIssueCode::Unknown}; + std::string message{}; + std::string sourceId{}; + std::string targetId{}; + bool isBlocking{false}; + + bool blocksSimulation() const noexcept; +}; + +const char* toString(ImportIssueSeverity severity) noexcept; +const char* toString(ImportIssueCode code) noexcept; +bool hasBlockingImportIssue(const std::vector& issues) noexcept; + +} // namespace safecrowd::domain diff --git a/src/domain/ImportOrchestrator.h b/src/domain/ImportOrchestrator.h new file mode 100644 index 0000000..27c6507 --- /dev/null +++ b/src/domain/ImportOrchestrator.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "domain/ImportResult.h" + +namespace safecrowd::domain { + +struct ImportRequest { + std::filesystem::path sourcePath{}; + ImportedFileFormat requestedFormat{ImportedFileFormat::Unknown}; + bool preserveRawModel{true}; + bool runValidation{true}; +}; + +class ImportOrchestrator { +public: + virtual ~ImportOrchestrator() = default; + + virtual ImportResult importFile(const ImportRequest& request) = 0; +}; + +} // namespace safecrowd::domain diff --git a/src/domain/ImportResult.h b/src/domain/ImportResult.h new file mode 100644 index 0000000..5e2bae6 --- /dev/null +++ b/src/domain/ImportResult.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "domain/CanonicalGeometry.h" +#include "domain/FacilityLayout2D.h" +#include "domain/ImportIssue.h" +#include "domain/RawImportModel.h" + +namespace safecrowd::domain { + +enum class ImportReviewStatus { + NotRequired, + Pending, + Approved, + Rejected, +}; + +struct ImportTraceRef { + std::string targetId{}; + std::vector sourceIds{}; + std::vector canonicalIds{}; +}; + +struct ImportResult { + std::optional rawModel{}; + std::optional canonicalGeometry{}; + std::optional layout{}; + std::vector issues{}; + std::vector traceRefs{}; + ImportReviewStatus reviewStatus{ImportReviewStatus::Pending}; + + bool readyForSimulation() const noexcept { + return layout.has_value() + && !hasBlockingImportIssue(issues) + && (reviewStatus == ImportReviewStatus::NotRequired + || reviewStatus == ImportReviewStatus::Approved); + } +}; + +} // namespace safecrowd::domain diff --git a/src/domain/RawImportModel.h b/src/domain/RawImportModel.h new file mode 100644 index 0000000..1477563 --- /dev/null +++ b/src/domain/RawImportModel.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include + +#include "domain/Geometry2D.h" + +namespace safecrowd::domain { + +enum class ImportedFileFormat { + Unknown, + Dxf, + Ifc, +}; + +enum class ImportUnit { + Unknown, + Millimeter, + Centimeter, + Meter, +}; + +enum class RawEntityKind { + Unknown, + Line, + Polyline, + Polygon, + BlockReference, + IfcElement, + Annotation, +}; + +struct SourceTrace { + std::string sourceId{}; + std::string parentSourceId{}; + std::string layerName{}; + std::string objectName{}; + std::string externalId{}; +}; + +struct RawBlockReference2D { + std::string blockName{}; + Point2D insertionPoint{}; + double rotationRadians{0.0}; + double scaleX{1.0}; + double scaleY{1.0}; + std::vector polylines{}; + std::vector polygons{}; +}; + +struct RawIfcElement2D { + std::string elementType{}; + std::string representationId{}; + std::vector curves{}; + std::vector footprints{}; +}; + +struct RawAnnotation2D { + Point2D anchor{}; + std::string text{}; +}; + +using RawEntityPayload = std::variant< + std::monostate, + LineSegment2D, + Polyline2D, + Polygon2D, + RawBlockReference2D, + RawIfcElement2D, + RawAnnotation2D>; + +struct RawEntity2D { + RawEntityKind kind{RawEntityKind::Unknown}; + SourceTrace trace{}; + RawEntityPayload payload{}; + std::map metadata{}; +}; + +struct RawImportModel { + ImportedFileFormat format{ImportedFileFormat::Unknown}; + ImportUnit unit{ImportUnit::Unknown}; + std::string sourceDocumentId{}; + std::string levelId{}; + std::vector entities{}; + std::map metadata{}; +}; + +} // namespace safecrowd::domain diff --git a/tests/ImportContractsTests.cpp b/tests/ImportContractsTests.cpp new file mode 100644 index 0000000..4fc8cef --- /dev/null +++ b/tests/ImportContractsTests.cpp @@ -0,0 +1,228 @@ +#include +#include +#include + +#include "TestSupport.h" + +#include "domain/ImportContracts.h" + +namespace { + +class FakeImportOrchestrator : public safecrowd::domain::ImportOrchestrator { +public: + safecrowd::domain::ImportResult importFile(const safecrowd::domain::ImportRequest& request) override { + lastRequest = request; + + safecrowd::domain::ImportResult result; + result.layout = safecrowd::domain::FacilityLayout2D{ + .id = "layout-demo", + .name = "Imported Demo Floor", + .levelId = "L1", + }; + return result; + } + + safecrowd::domain::ImportRequest lastRequest{}; +}; + +} // namespace + +SC_TEST(ImportContractsCaptureSprintOneLayoutFields) { + safecrowd::domain::RawImportModel rawModel; + rawModel.format = safecrowd::domain::ImportedFileFormat::Dxf; + rawModel.unit = safecrowd::domain::ImportUnit::Meter; + rawModel.sourceDocumentId = "demo-floor.dxf"; + rawModel.levelId = "L1"; + + safecrowd::domain::RawEntity2D wallEntity; + wallEntity.kind = safecrowd::domain::RawEntityKind::Polyline; + wallEntity.trace.sourceId = "wall-01"; + wallEntity.trace.layerName = "WALL"; + wallEntity.payload = safecrowd::domain::Polyline2D{ + .vertices = { + {0.0, 0.0}, + {10.0, 0.0}, + }, + }; + rawModel.entities.push_back(wallEntity); + + safecrowd::domain::RawEntity2D blockEntity; + blockEntity.kind = safecrowd::domain::RawEntityKind::BlockReference; + blockEntity.trace.sourceId = "block-01"; + blockEntity.payload = safecrowd::domain::RawBlockReference2D{ + .blockName = "STAIR_CORE", + .insertionPoint = {2.0, 1.5}, + .rotationRadians = 0.25, + .scaleX = 1.0, + .scaleY = 1.0, + .polylines = { + { + .vertices = { + {1.5, 1.0}, + {2.5, 1.0}, + {2.5, 2.0}, + {1.5, 2.0}, + }, + .closed = true, + }, + }, + }; + rawModel.entities.push_back(blockEntity); + + safecrowd::domain::CanonicalGeometry canonicalGeometry; + canonicalGeometry.levelId = "L1"; + canonicalGeometry.walkableAreas.push_back({ + .id = "walkable-01", + .polygon = { + .outline = { + {0.0, 0.0}, + {10.0, 0.0}, + {10.0, 6.0}, + {0.0, 6.0}, + }, + }, + .sourceIds = {"wall-01"}, + }); + canonicalGeometry.openings.push_back({ + .id = "opening-01", + .kind = safecrowd::domain::OpeningKind::Exit, + .span = { + .start = {10.0, 2.0}, + .end = {10.0, 3.2}, + }, + .width = 1.2, + .sourceIds = {"wall-01"}, + }); + + safecrowd::domain::FacilityLayout2D layout; + layout.id = "layout-demo"; + layout.name = "Imported Demo Floor"; + layout.levelId = "L1"; + layout.zones.push_back({ + .id = "zone-room-a", + .kind = safecrowd::domain::ZoneKind::Room, + .label = "Room A", + .area = { + .outline = { + {0.0, 0.0}, + {5.0, 0.0}, + {5.0, 4.0}, + {0.0, 4.0}, + }, + }, + .defaultCapacity = 20, + .provenance = { + .sourceIds = {"wall-01"}, + .canonicalIds = {"walkable-01"}, + }, + }); + layout.zones.push_back({ + .id = "zone-exit", + .kind = safecrowd::domain::ZoneKind::Exit, + .label = "Exit", + .area = { + .outline = { + {9.0, 2.0}, + {10.0, 2.0}, + {10.0, 3.2}, + {9.0, 3.2}, + }, + }, + .defaultCapacity = 50, + .provenance = { + .sourceIds = {"wall-01"}, + .canonicalIds = {"opening-01"}, + }, + }); + layout.connections.push_back({ + .id = "conn-exit", + .kind = safecrowd::domain::ConnectionKind::Exit, + .fromZoneId = "zone-room-a", + .toZoneId = "zone-exit", + .effectiveWidth = 1.2, + .directionality = safecrowd::domain::TravelDirection::Bidirectional, + .centerSpan = { + .start = {9.0, 2.6}, + .end = {10.0, 2.6}, + }, + .provenance = { + .sourceIds = {"wall-01"}, + .canonicalIds = {"opening-01"}, + }, + }); + + safecrowd::domain::ImportResult result; + result.rawModel = rawModel; + result.canonicalGeometry = canonicalGeometry; + result.layout = layout; + result.traceRefs.push_back({ + .targetId = "conn-exit", + .sourceIds = {"wall-01"}, + .canonicalIds = {"opening-01"}, + }); + result.reviewStatus = safecrowd::domain::ImportReviewStatus::Approved; + + SC_EXPECT_EQ(result.rawModel->format, safecrowd::domain::ImportedFileFormat::Dxf); + SC_EXPECT_EQ(result.canonicalGeometry->openings.front().kind, safecrowd::domain::OpeningKind::Exit); + SC_EXPECT_EQ(result.layout->zones.front().defaultCapacity, std::size_t{20}); + SC_EXPECT_EQ(result.layout->connections.front().directionality, safecrowd::domain::TravelDirection::Bidirectional); + SC_EXPECT_NEAR(result.layout->connections.front().effectiveWidth, 1.2, 1e-9); + SC_EXPECT_TRUE(std::holds_alternative(result.rawModel->entities.back().payload)); + SC_EXPECT_EQ(result.layout->connections.front().provenance.canonicalIds.front(), std::string("opening-01")); + SC_EXPECT_EQ(result.traceRefs.front().targetId, std::string("conn-exit")); + SC_EXPECT_TRUE(result.readyForSimulation()); +} + +SC_TEST(ImportIssuesBlockSimulationOnlyForBlockingProblems) { + std::vector warnings = { + { + .severity = safecrowd::domain::ImportIssueSeverity::Warning, + .code = safecrowd::domain::ImportIssueCode::WidthBelowMinimum, + .message = "Exit width is below the demo threshold.", + .sourceId = "opening-01", + }, + }; + + SC_EXPECT_TRUE(!safecrowd::domain::hasBlockingImportIssue(warnings)); + SC_EXPECT_EQ(std::string(safecrowd::domain::toString(warnings.front().severity)), std::string("Warning")); + + warnings.push_back({ + .severity = safecrowd::domain::ImportIssueSeverity::Error, + .code = safecrowd::domain::ImportIssueCode::MissingExit, + .message = "No reachable exit was inferred.", + .targetId = "layout-demo", + }); + + SC_EXPECT_TRUE(safecrowd::domain::hasBlockingImportIssue(warnings)); + SC_EXPECT_TRUE(warnings.back().blocksSimulation()); +} + +SC_TEST(ImportResultRequiresApprovedReviewBeforeSimulation) { + safecrowd::domain::ImportResult result; + result.layout = safecrowd::domain::FacilityLayout2D{ + .id = "layout-demo", + .levelId = "L1", + }; + result.reviewStatus = safecrowd::domain::ImportReviewStatus::Pending; + + SC_EXPECT_TRUE(!result.readyForSimulation()); + + result.reviewStatus = safecrowd::domain::ImportReviewStatus::Approved; + SC_EXPECT_TRUE(result.readyForSimulation()); +} + +SC_TEST(ImportOrchestratorUsesAFileBasedDomainEntryPoint) { + FakeImportOrchestrator orchestrator; + + safecrowd::domain::ImportRequest request; + request.sourcePath = std::filesystem::path("sample/demo-floor.dxf"); + request.requestedFormat = safecrowd::domain::ImportedFileFormat::Dxf; + request.preserveRawModel = false; + + const auto result = orchestrator.importFile(request); + + SC_EXPECT_EQ(orchestrator.lastRequest.sourcePath.generic_string(), std::string("sample/demo-floor.dxf")); + SC_EXPECT_EQ(orchestrator.lastRequest.requestedFormat, safecrowd::domain::ImportedFileFormat::Dxf); + SC_EXPECT_TRUE(!orchestrator.lastRequest.preserveRawModel); + SC_EXPECT_TRUE(result.layout.has_value()); +} diff --git a/uml/domain-import-module.puml b/uml/domain-import-module.puml index 63ac4c8..9d933ba 100644 --- a/uml/domain-import-module.puml +++ b/uml/domain-import-module.puml @@ -14,7 +14,7 @@ package "application" { package "domain" { package "api" { [ImportOrchestrator\n(single import entry)] as Orchestrator - [ImportResult\n(layout / issues / trace refs)] as ImportResult + [ImportResult\n(layout / issues /\ntrace refs / review state)] as ImportResult } package "adapters" { @@ -27,7 +27,7 @@ package "domain" { [GeometryNormalizer\n(Clipper2 / Boost.Geometry)] as GeometryNormalizer [CanonicalGeometry\n(floors / walls / portals /\nobstacles / vertical links)] as CanonicalGeometry [FacilityLayoutBuilder\n(zone / connection /\nbarrier inference)] as FacilityLayoutBuilder - [FacilityLayout\n(zones / links / controls /\nvalidation targets)] as FacilityLayout + [FacilityLayout2D\n(zones / links / controls /\nvalidation targets)] as FacilityLayout [ImportValidationService\n(disconnected areas /\nmissing exits / width checks)] as ImportValidation [ImportArtifactStore\n(trace refs / user overrides /\nreimport metadata)] as ImportArtifactStore } diff --git "a/uml/domain-import-module.puml \355\225\264\354\204\244.md" "b/uml/domain-import-module.puml \355\225\264\354\204\244.md" index 9a5cd9e..ececcba 100644 --- "a/uml/domain-import-module.puml \355\225\264\354\204\244.md" +++ "b/uml/domain-import-module.puml \355\225\264\354\204\244.md" @@ -23,9 +23,10 @@ 4. `GeometryNormalizer`가 포맷 차이를 제거하고 `CanonicalGeometry`를 만든다. 5. `FacilityLayoutBuilder`가 zone, connection, barrier 같은 SafeCrowd 공간 구조를 추론한다. 6. `ImportValidationService`가 끊긴 동선, 누락 출구, 비정상 폭 같은 문제를 진단한다. -7. `ImportArtifactStore`가 원본 trace, 사용자 수정, 재import 메타데이터를 저장한다. -8. `NavigationBakeService`가 `FacilityLayout`을 `BakedNavigationData`로 변환한다. -9. `SimulationSession`이 승인된 layout과 baked nav 데이터를 받아 엔진 리소스로 등록한다. +7. `ImportResult`는 layout, issue, trace ref, review 상태를 함께 돌려준다. +8. `ImportArtifactStore`가 원본 trace, 사용자 수정, 재import 메타데이터를 저장한다. +9. `NavigationBakeService`가 `FacilityLayout2D`를 `BakedNavigationData`로 변환한다. +10. `SimulationSession`이 승인된 layout과 baked nav 데이터를 받아 엔진 리소스로 등록한다. --- @@ -37,7 +38,7 @@ ## `DxfImportService` - 개요: `libdxfrw`를 이용한 DXF adapter다. - 목적: DXF entity, layer, unit, block 참조를 SafeCrowd 내부 모델로 끌어오는 역할을 한다. -- 유의사항: DXF 특유의 선/폴리라인 중심 구조는 아직 의미가 약하므로, 여기서 바로 `FacilityLayout`을 만들기보다 `RawImportModel` 단계에 원본 trace를 보존하는 편이 안전하다. +- 유의사항: DXF 특유의 선/폴리라인 중심 구조는 아직 의미가 약하므로, 여기서 바로 `FacilityLayout2D`를 만들기보다 `RawImportModel` 단계에 원본 trace를 보존하는 편이 안전하다. ## `IfcImportService` - 개요: `IfcOpenShell` 기반 IFC adapter다. @@ -61,11 +62,11 @@ ## `FacilityLayoutBuilder` - 개요: SafeCrowd 문제영역의 공간 구조를 추론하는 계층이다. -- 목적: `CanonicalGeometry`를 `Zone`, `Connection`, `Barrier`, `Control` 중심의 `FacilityLayout`으로 바꾼다. +- 목적: `CanonicalGeometry`를 `Zone`, `Connection`, `Barrier`, `Control` 중심의 `FacilityLayout2D`로 바꾼다. - 유의사항: 이 계층은 SafeCrowd 핵심 규칙이 들어가는 곳이므로, 범용 geometry 라이브러리 코드를 engine으로 빼는 것보다 여기서 도메인 의미를 분명히 하는 편이 맞다. -## `FacilityLayout` -- 개요: 시뮬레이션 입력의 기준이 되는 공간 모델이다. +## `FacilityLayout2D` +- 개요: Sprint 1 데모와 import 경로의 기준이 되는 2D 공간 모델이다. - 목적: import 결과를 검수 가능하고 시나리오에 연결 가능한 구조로 정규화한다. - 유의사항: zone ID, 연결 관계, 유효 폭, 방향, 계단 여부, 기본 수용량 같은 필드는 여기서 확정되어야 한다. @@ -81,7 +82,7 @@ ## `NavigationBakeService` - 개요: `Recast`와 `Detour`를 이용해 runtime-friendly navigation 데이터를 만드는 계층이다. -- 목적: `FacilityLayout`을 navmesh, path query, spawn-goal anchor 같은 구조로 bake 한다. +- 목적: `FacilityLayout2D`를 navmesh, path query, spawn-goal anchor 같은 구조로 bake 한다. - 유의사항: 이 단계는 import의 일부이지만 runtime crowd behavior 자체는 아니다. 그래서 `DetourCrowd`는 이 그림에서 의도적으로 제외했다. ## `BakedNavigationData` @@ -103,6 +104,6 @@ ## 설계 핵심 요약 - import 관련 오픈소스 스택은 모두 `domain`에 둔다. -- `engine`은 `FacilityLayout`과 `BakedNavigationData` 같은 domain 결과만 소비한다. +- `engine`은 `FacilityLayout2D`와 `BakedNavigationData` 같은 domain 결과만 소비한다. - `DetourCrowd`는 runtime behavior 계층이므로 import 모듈 UML에는 넣지 않는다. - 재import와 사용자 수정 유지까지 고려하면 `ImportArtifactStore` 같은 추적 계층이 필요하다.