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>
This commit is contained in:
205
docs/hmeg-api-survey.md
Normal file
205
docs/hmeg-api-survey.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 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 작성 시 `CameraCore` → `PerspectiveCamera` cast로 fov를 꺼낸다.
|
||||
|
||||
### 6. `ISelectable` — 노드 선택 상태
|
||||
경로: `Interface\Interfaces.cs:235`
|
||||
|
||||
```csharp
|
||||
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`
|
||||
|
||||
```csharp
|
||||
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`로 선택된 노드 추출.
|
||||
|
||||
> 본 저장소의 `HmEgBridgePlugin`은 `EditorPlugin` (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 | `EGViewport` 와 `HmEGViewport` 관계 — `EGViewport`가 후자를 구현? 아니면 별개 SUT-side 클래스? | `Editor03.PluginInterface` |
|
||||
|
||||
## v3 구현 방향 (Direct Provider)
|
||||
|
||||
```csharp
|
||||
// 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 소스 사본을 보관하지 **않는다**. 본 문서는 외부 소스에 대한 인터페이스 추출 메모일 뿐. 코드 발췌도 시그니처/주석 수준으로만 인용한다.
|
||||
Reference in New Issue
Block a user