Files
recordingtest/docs/hmeg-api-survey.md
minsung f6b6e7449e 3-tier split (step 1) + engine-bridge v3 scaffold + HmegDirectStateProvider
Lays down the Generic / HmEG-aware / App-specific separation that lets us
target other HmEG-hosting WPF applications later, and lands the v3 engine
state provider on top of it.

Architecture rule (CLAUDE.md §8.1, new): every module belongs to exactly one
of three tiers — Generic / HmEG-aware / App-specific (e.g. EgBim). Dependency
direction is strictly App-specific → HmEG-aware → Generic. Generic must not
reference HmEG.dll; HmEG-aware must not reference any per-app assembly.

This commit is the first incremental step:

  + src/Recordingtest.Bridge.Abstractions/  (Generic, new csproj)
      IEngineStateProvider, CameraSnapshot, SceneSnapshot,
      NullEngineStateProvider — extracted from EgPlugin so the generic core
      owns the contract. Zero SUT references.

  + src/Hmeg/Recordingtest.Hmeg.Bridge/      (HmEG-aware, new csproj)
      HmegDirectStateProvider — IEngineStateProvider implemented against
      the HmEG public API (Space, HmEGViewport, ISelectable, ModelBase.Uid).
      Decoupled from any specific host app via Func<Space?>/Func<HmEGViewport?>
      lambdas; the EgBim plugin host supplies them. Reusable for any other
      WPF application that hosts HmEG.

      Selection traversal walks Space.Children and collects ModelBase.Uid
      for nodes whose ISelectable.IsSelected is true. We deliberately type
      nodes as object + late-bound Uid lookup to avoid pulling MemoryPack
      into the dependency graph.

  + tests/Hmeg/Recordingtest.Hmeg.Bridge.Tests/
      5 unit tests covering null lambdas, throwing lambdas, document path
      provider, and constructor null arg validation.

  + src/Recordingtest.EgPlugin/ChainedEngineStateProvider.cs
      Wraps two providers; falls back from Hmeg.Direct to the existing
      Reflection accessor when the primary returns empty/default. Lets us
      land the new wire-up before the EgBim adapter Q1~Q7 lookups are
      filled in. 7 new tests.

  + src/Recordingtest.EgPlugin/IAppManagerAccessor.cs
      Reflection accessor abstraction (preserved as the v3 fallback). Looks
      up Editor.AppManager.AppManager via well-known Instance/Current
      property names. Unit-testable through a fake.

  ~ src/Recordingtest.EgPlugin/IEngineStateProvider.cs
      Type definitions removed (now in Bridge.Abstractions); only the
      reflection-based provider remains. ReflectionEngineStateProvider
      delegates everything to IAppManagerAccessor.

  ~ src/Recordingtest.EgPlugin/HmEgBridgePlugin.cs
      BuildProvider() picks ChainedEngineStateProvider(Hmeg.Direct,
      Reflection). The HmEG-aware lambdas are stubs (return null) until the
      next step wires the EgBim adapter; the chain falls through to the
      reflection path so behaviour matches v2 for now.

  + docs/contracts/engine-bridge-v3.md       — Sprint Contract
  + docs/contracts/generic-sut-split.md      — Sprint Contract for the
      remaining mass-rename / folder move (step 2, deferred).
  + docs/hmeg-api-survey.md                  — Read-only survey of the HmEG
      public API (Space, ModelBase, HmEGViewport, IHmCamera, IPlugin) used
      to design HmegDirectStateProvider. Open Q1~Q7 listed.

Tests: 94 → 115 passing, 0 failing. The new HmEG-aware test project copies
HmEG.dll next to its output (Private=true) since it runs out-of-process.

Step 2 (deferred to next session): mass-rename
  src/Recordingtest.EgPlugin → src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost + .Adapter
  src/Recordingtest.EngineBridge → src/Hmeg/Recordingtest.Hmeg.Catalog
  src/Recordingtest.EngineBridge.Client → split (Generic + Hmeg)
plus Recordingtest.Architecture.Tests to enforce the §8.1 dependency rule.

Ref: #10 follow-up, #14 follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:53:27 +09:00

10 KiB

HmEG Public API Survey (engine-bridge v3 source)

본 문서는 D:\GiteaAll\HmEngine\HmEG\HmEG 소스를 read-only로 훑어 정리한 결과다. 목적: HmEgDirectStateProvider와 SUT-side bridge 추가 엔드포인트(/focus, /hit-test, /command) 설계 근거. 수정 금지 — 본 폴더는 우리 저장소 바깥의 SUT 소스다. 참조만.

식별 마커 — <public-hmeg-api />

HmEG는 <public-hmeg-api /> XML doc 태그로 공식 공개 surface를 표시한다. v3에서 우리가 의존할 모든 멤버는 이 태그가 붙은 것만 사용한다. (난독화/리네이밍 시 해당 마커가 안전 목록 역할을 할 가능성이 큼.)

검색 (read-only):

grep -rn 'public-hmeg-api' D:\GiteaAll\HmEngine\HmEG\HmEG

핵심 타입 (확인 완료)

1. ModelBase — 모든 모델 엔티티의 추상 base

경로: Model\Scene\ModelData\ModelBase.cs

멤버 타입 비고
Uid Guid 결정성 핵심 — 영구 고유 ID. golden file에 그대로 쓸 수 있음
Name string 사용자 보이는 이름
ModelType ModelType (enum) Mesh / Line / 빌보드 등
GeoType HmGEntityType HmGeometry 공통
Tag object 사용자 커스텀
Label string dwg export XData용
ModelMatrix HmMatrix3D 변환 행렬
ItemsChanged event EventHandler<OnChildModelChangedArgs> 자식 변경 알림 — wait_for 후보

2. Space : ModelBase — 문서 컨테이너 (= "Space" 트리의 루트)

경로: Model\Scene\ModelData\Space.cs, Space.Functions.cs

멤버 타입 비고
Children EgObservableFastList<ModelBase> scene 트리 자식 노드 (= 객체 리스트)
ItemsCount int /scene 의 ObjectCount 가 직접 매핑
Viewports List<EGViewport> 이 Space에 연결된 뷰포트들
SplitRow / SplitCol int 뷰포트 분할
EnableGroupSelection bool
Add(ModelBase) / DeleteModel(...) / AddSpace(...) / Clear() mutator read-only로만 사용
ImportFileModels / ImportInstancingModels event 파일 import 알림 — wait_for 후보

3. HmModel : ModelBase — 실제 형상 객체

경로: Model\Scene\ModelData\HmModel.cs

멤버 타입 비고
GEntity HmGEntity HmGeometry 공통
EGgEntity EgObject EG geometry 객체
BlockName string
LayerName string
LineTypeName string
LineTypeScale double
AttributeReferences HmAttributeReferenceCollection
MouseEnter / MouseLeave / MouseDown event 마우스 hit-test 결과 통지 — recorder에서 element 식별에 사용 가능

4. HmEGViewport (interface) — 뷰포트 공개 인터페이스

경로: Interface\IHmEGViewport.cs (namespace HmEG)

멤버 타입 비고
CameraCore CameraCore (Model\Camera\CameraCore.cs) 카메라 진입점
Renderables IEnumerable<SceneNode> 3D 씬 그래프 루트 노드 enumerable
D2DRenderables IEnumerable<SceneNode2D> 2D 노드
RenderHost IRenderHost
EffectsManager IEffectsManager
ViewportRectangle EGRectangleI 픽셀 단위
Attach(IRenderHost) / Detach() mutator
Update(TimeSpan) / InvalidateRender(...) / InvalidateSceneGraph(...) mutator

5. IHmCamera — 카메라 표준 인터페이스

경로: Interface\IHmCamera.cs

멤버 타입 매핑
Position HmVector3D CameraSnapshot.Eye
LookDirection HmVector3D Eye + LookDir = Target
UpDirection HmVector3D CameraSnapshot.Up
CreateLeftHandSystem bool 좌표계 정규화 시 필요
CreateViewMatrix(HmMatrix3D) method
CreateProjectionMatrix(double aspectRatio) method fov는 별도 추출 (PerspectiveCamera에)

주의: IHmCamera에는 Fov/FieldOfView가 없다. PerspectiveCamera : ProjectionCamera : CameraCore 쪽에 있을 것으로 추정. v3 작성 시 CameraCorePerspectiveCamera cast로 fov를 꺼낸다.

6. ISelectable — 노드 선택 상태

경로: Interface\Interfaces.cs:235

public interface ISelectable
{
    bool IsSelected { get; set; }
}

중앙 집중 selection 리스트는 HmEG core에 없음. SelectedIds를 얻으려면 Space 트리를 walk하면서 ISelectable.IsSelected == true인 노드를 모아야 한다. 또는 SUT 측 AppManager가 selection 리스트를 따로 들고 있을 수 있음 — 확인 필요.

7. HmSceneNode : MaterialGeometryNode, IDynamicReflectable, ISelectable

경로: Model\Scene\HmSceneNode.cs

씬 그래프의 실제 렌더링 노드. IsSelected를 가짐. Renderables에서 흘러나옴.

8. HmEG.IPlugin (interface)

경로: PlugIns\IPlugin.cs

public interface IPlugin
{
    string Name { get; }
    EGViewport View { get; set; }       // ← 플러그인에 직접 주입되는 뷰포트
    bool RethrowException { get; set; }
    object Run(params object[] args);
}

중요: HmEG가 플러그인을 로드할 때 View를 직접 set 해준다. 즉 플러그인은 따로 Space/AppManager를 찾아갈 필요 없이 this.View로 즉시 뷰포트에 도달한다. 거기서 CameraCore, Renderables (씬 노드 enumerable) 모두 접근 가능. Renderables를 walk하면서 ISelectable.IsSelected로 선택된 노드 추출.

본 저장소의 HmEgBridgePluginEditorPlugin (SUT-side Editor03.PluginInterface) 베이스를 쓴다. EditorPlugin이 내부적으로 HmEG.IPlugin.View를 set 해주는지, 아니면 다른 경로(예: AppManager)를 통해 Space에 접근하는지는 Editor03.PluginInterface.dll 디컴파일 또는 SUT-side 소스가 있어야 확정 가능.

아직 모르는 것 (확인 대기)

항목 어디서 찾아야 하는가
Q1 "활성 Space" 진입점 — EGViewport.Space? AppManager.Instance.ActiveSpace? Editor03.PluginInterface / SUT-side AppManager
Q2 "활성 Viewport" 가 여러 개일 때 어느 것이 active 인가 동일
Q3 중앙 selection 리스트 (예: AppManager.Selection) 또는 selection-changed 이벤트 동일
Q4 명령 (Command) 생명주기 이벤트 — CommandStarted / CommandFinished 같은 것 동일 (Editor.AppManager.AppModeManager 후보)
Q5 PerspectiveCamera에서 FieldOfView 정확한 프로퍼티 이름 Model\Camera\PerspectiveCamera.cs (read-only로 한 번만 더 확인하면 됨)
Q6 문서 파일 경로 — 저장 후의 *.hmeg 경로 보유처 Editor03.PluginInterface 또는 AppManager.ActiveDocument
Q7 EGViewportHmEGViewport 관계 — EGViewport가 후자를 구현? 아니면 별개 SUT-side 클래스? Editor03.PluginInterface

v3 구현 방향 (Direct Provider)

// src/Recordingtest.EgPlugin/HmEgDirectStateProvider.cs
public sealed class HmEgDirectStateProvider : IEngineStateProvider
{
    private readonly Func<HmEG.HmEGViewport?> _getViewport;  // plugin이 주입
    private readonly Func<Space?> _getSpace;                  // 동일

    public IReadOnlyList<string> GetSelectedIds()
    {
        var sp = _getSpace();
        if (sp is null) return Array.Empty<string>();
        var ids = new List<string>();
        Walk(sp, ids);
        return ids;
        static void Walk(ModelBase node, List<string> ids)
        {
            if (node is ISelectable s && s.IsSelected) ids.Add(node.Uid.ToString());
            if (node is Space space)
                foreach (var child in space.Children) Walk(child, ids);
        }
    }

    public CameraSnapshot GetCamera()
    {
        var vp = _getViewport();
        if (vp is null) return Default;
        var cam = vp.CameraCore;
        // CameraCore → IHmCamera 캐스트, 또는 Position/Look/Up 직접 접근
        // PerspectiveCamera 캐스트로 FOV
        ...
    }

    public SceneSnapshot GetScene()
    {
        var sp = _getSpace();
        return new SceneSnapshot(
            sp?.ItemsCount ?? 0,
            DocumentPathFromSomewhere());  // Q6
    }

    public bool GetRenderComplete() => true; // 후속 작업
}

_getViewport / _getSpace 람다는 HmEgBridgePlugin이 자기 환경(EditorPlugin이 노출하는 진입점)에서 캡처해 넘긴다. 람다 형태로 두면 다음과 같은 이점이 있다:

  • 플러그인 base 클래스가 진입점을 어떻게 노출하는지가 바뀌어도 v3 provider는 영향 없음
  • 단위 테스트는 fake 람다를 넘겨서 검증

SUT-side bridge 추가 엔드포인트 (Gap I 우회)

BridgeHttpServer / StateRouter에 추가:

엔드포인트 응답 사용처
GET /focus {"path": "...", "type": "...", "name": "..."} recorder가 key_down 시 polling. Keyboard.FocusedElement (WPF)를 Dispatcher 위에서 호출
GET /hit-test?x=&y= {"hit": "HmModel#guid", "type": "...", "name": "..."} recorder가 click 시 호출. Space/Renderables walk + VisualTreeHelper.HitTest + (있으면) HmEG의 pick API
GET /command {"running": "BOX", "phase": "awaiting_first_corner"} player의 wait_for. Q4 이벤트 구독 결과 캐시

이건 engine-bridge v3 본 contract와 별도 contract 권장 (/contract sut-side-bridge).

다음 액션

  1. engine-bridge-v3.md 계약 갱신 — reflection 항목 제거, 위 타입/멤버 이름으로 D2/D3/D4 고정
  2. Q1~Q7 확인 — 사용자가 SUT-side(Editor03.PluginInterface 또는 AppManager) 소스/경로 알려주면 read-only로 1~2회 더 확인
  3. Q1/Q3 채워지면 HmEgDirectStateProvider 구현 + 플러그인 wire-up
  4. CI fallback — 기존 ReflectionEngineStateProvider + IAppManagerAccessor는 fake-friendly 형태로 유지 (HmEG 어셈블리 없는 단위 테스트 환경에서 빌드/테스트 가능해야 함)
  5. 라이브 검증 → curl 로 /state 확인 → 보정 → 커밋

라이선스 / 위생

본 저장소는 HmEG 소스 사본을 보관하지 않는다. 본 문서는 외부 소스에 대한 인터페이스 추출 메모일 뿐. 코드 발췌도 시그니처/주석 수준으로만 인용한다.