Compare commits
6 Commits
4ba5b3d74b
...
9fe053619f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe053619f | ||
|
|
03fb504eea | ||
|
|
f6b6e7449e | ||
|
|
a771352bcb | ||
|
|
98d801442b | ||
|
|
70bf5703b3 |
85
CLAUDE.md
85
CLAUDE.md
@@ -164,6 +164,91 @@ recordingtest/
|
||||
- 부하·성능 테스트
|
||||
- 단위 테스트 프레임워크 대체
|
||||
|
||||
## 8.1 코드 계층 분리 (의무 규칙)
|
||||
|
||||
본 도구는 **EG-BIM Modeler 외에도 다양한 WPF 응용**을 회귀 자동화 대상으로 삼는다. 또한 사용자 WPF 응용군의 **대다수가 자체 3D 엔진 `HmEG`를 공유**한다. 따라서 코드는 다음 **3개 계층** 중 하나에 명시적으로 속한다.
|
||||
|
||||
| 계층 | 의미 | 의존 가능 | 폴더/네이밍 |
|
||||
|---|---|---|---|
|
||||
| **Generic** | 임의 WPF 데스크톱 응용에 동작 | .NET BCL, FlaUI/UIA3, Win32, YamlDotNet 등 SUT-중립 라이브러리만 | `src/` 직속, 네임스페이스 `Recordingtest.*` |
|
||||
| **HmEG-aware** | HmEG 엔진을 호스팅하는 임의 WPF 응용에 동작 (앱은 고정 안 됨) | Generic + `HmEG.dll` 만 | `src/Hmeg/` 하위, 네임스페이스 `Recordingtest.Hmeg.*` |
|
||||
| **App-specific** | 특정 응용(예: EG-BIM Modeler)에만 동작 | Generic + HmEG-aware + 해당 앱 어셈블리 (`Editor03.PluginInterface.dll` 등) | `src/Sut/<AppName>/` 하위, 네임스페이스 `Recordingtest.Sut.<AppName>.*` |
|
||||
|
||||
### 의존 방향 (단방향)
|
||||
|
||||
```
|
||||
App-specific ──→ HmEG-aware ──→ Generic
|
||||
(e.g. EgBim) (e.g. HmegBridge) (Recorder/Player/...)
|
||||
```
|
||||
|
||||
역참조 금지: Generic은 HmEG-aware/App-specific을 모름. HmEG-aware는 App-specific을 모름.
|
||||
|
||||
### 강제 사항
|
||||
|
||||
1. **Generic 코드는 어떤 SUT 어셈블리도 참조하지 않는다.** `HmEG.dll`도 안 됨.
|
||||
2. **HmEG-aware 코드는 `HmEG.dll`만 참조**한다. 특정 앱(`Editor03.PluginInterface.dll` 등)은 참조 금지.
|
||||
3. **App-specific 코드만이 자기 앱의 어셈블리를 참조**한다.
|
||||
4. **확장 지점은 항상 한 계층 아래에 인터페이스로 둔다.** 예:
|
||||
- `IEngineStateProvider` (Generic) ← `HmegEngineStateProvider` (HmEG-aware) ← `EgBimAppManagerAdapter` (App-specific 진입점만 제공)
|
||||
- `ITargetResolver` (Generic) ← `HmegSceneNodeTargetResolver` (HmEG-aware) ← ...
|
||||
5. **시나리오 포맷, 베이스라인 포맷, 정규화 규칙은 Generic.** HmEG/앱 특화 정규화 규칙이 필요하면 그 계층에서 plugin 식으로 등록한다.
|
||||
6. **새 SUT를 추가할 때 HmEG-aware 코드는 재사용**한다. 새로 작성하지 말 것. 앱마다 다른 부분만 App-specific에 둔다 (보통: 플러그인 호스트 진입점 + 명령 lifecycle 이벤트 어댑터).
|
||||
|
||||
### 폴더 레이아웃 (목표 — 본 규칙 추가 후 점진 마이그레이션)
|
||||
|
||||
```
|
||||
src/
|
||||
├── Recordingtest.Recorder/ # Generic
|
||||
├── Recordingtest.Player/ # Generic
|
||||
├── Recordingtest.Normalizer/ # Generic
|
||||
├── Recordingtest.DiffReporter/ # Generic
|
||||
├── Recordingtest.Runner/ # Generic
|
||||
├── Recordingtest.SutProber/ # Generic
|
||||
├── Recordingtest.Bridge.Abstractions/ # Generic — IEngineStateProvider, ITargetResolver, ...
|
||||
├── Recordingtest.Bridge.Client/ # Generic — HTTP 클라이언트
|
||||
├── Hmeg/
|
||||
│ ├── Recordingtest.Hmeg.Bridge/ # HmEG-aware — Space/Camera/Selection을 HmEG 공개 API로 읽기
|
||||
│ ├── Recordingtest.Hmeg.TargetResolver/ # HmEG-aware — 씬 노드 hit-test/포커스 식별
|
||||
│ └── Recordingtest.Hmeg.Catalog/ # HmEG-aware — 정적 분석/카탈로그 (현 EngineBridge 일부)
|
||||
└── Sut/
|
||||
└── EgBim/ # EG-BIM Modeler 전용
|
||||
├── Recordingtest.Sut.EgBim.PluginHost/ # MEF entry, EditorPlugin base 상속
|
||||
└── Recordingtest.Sut.EgBim.Adapter/ # AppManager 진입점 / command lifecycle 어댑터
|
||||
tests/
|
||||
├── Recordingtest.*.Tests/ # Generic
|
||||
├── Hmeg/Recordingtest.Hmeg.*.Tests/ # HmEG-aware
|
||||
└── Sut/EgBim/Recordingtest.Sut.EgBim.*.Tests/
|
||||
```
|
||||
|
||||
새 SUT(예: 다른 HmEG 호스트 앱)를 추가할 때:
|
||||
```
|
||||
src/Sut/<NewApp>/
|
||||
Recordingtest.Sut.<NewApp>.PluginHost/
|
||||
Recordingtest.Sut.<NewApp>.Adapter/
|
||||
```
|
||||
이 두 개만 새로 만들면 되고 Generic + HmEG-aware는 그대로 재사용된다.
|
||||
|
||||
### 현재 모듈 분류 (2026-04-09 시점, 마이그레이션 전)
|
||||
|
||||
| 모듈 | 분류 | 비고 |
|
||||
|---|---|---|
|
||||
| `Recordingtest.Recorder` | **Generic** | OK |
|
||||
| `Recordingtest.Player` | **Generic** | OK |
|
||||
| `Recordingtest.Normalizer` | **Generic** | OK |
|
||||
| `Recordingtest.DiffReporter` | **Generic** | OK |
|
||||
| `Recordingtest.Runner` | **Generic** | OK |
|
||||
| `Recordingtest.SutProber` | **Generic** | OK |
|
||||
| `Recordingtest.EgPlugin` | **혼합 — 분할 필요** | 본체는 EG-BIM 전용(MEF entry), reflection accessor는 HmEG 비의존 generic, HmEG state는 HmEG-aware. 3개로 split. |
|
||||
| `Recordingtest.EngineBridge` | **HmEG-aware** | HmEG 카탈로그/CandidateFinder. rename → `Recordingtest.Hmeg.Catalog` |
|
||||
| `Recordingtest.EngineBridge.Client` | **혼합 — 분할 필요** | HTTP 호출부 → Generic, `HmEgHttpSnapshot` → HmEG-aware |
|
||||
| `IEngineStateProvider` (현 EgPlugin 안) | → **Generic** | `Recordingtest.Bridge.Abstractions` 로 추출 |
|
||||
| `ReflectionAppManagerAccessor` (현 EgPlugin 안) | → **App-specific** | EG-BIM Modeler의 `Editor.AppManager.AppManager`를 찾는 코드. EgBim 어댑터로 이동. CI fallback 용도로만 유지. |
|
||||
| 향후 `HmegDirectStateProvider` | **HmEG-aware** | `HmEG.dll` 공개 API 직접 호출. 모든 HmEG 호스트 앱에서 재사용 |
|
||||
|
||||
### Sprint Contract 의무
|
||||
|
||||
새 SUT를 추가하거나 기존 모듈을 generic↔SUT 사이에서 옮기는 모든 작업은 `/contract <name>` 으로 Sprint Contract를 먼저 작성한다. DoD에 "분류 라벨", "의존 그래프 검증", "네임스페이스 규칙" 항목 필수.
|
||||
|
||||
## 10. 결정 로그 위치
|
||||
|
||||
주요 기술 결정과 그 근거는 `docs/history/`와 Claude 메모리(`project_recordingtest_*`)에 분산 저장된다. 새 결정 시 반드시 둘 다 갱신한다.
|
||||
|
||||
4
PLAN.md
4
PLAN.md
@@ -10,8 +10,8 @@
|
||||
|
||||
## P1 — 라이브 검증 (사용자 환경 필요)
|
||||
|
||||
4. **라이브 SUT smoke test 실행** — `docs/guides/smoke-test.md` 따라 수동 수행
|
||||
5. **engine-bridge v3** — ReflectionEngineStateProvider 실매핑 (smoke test 이후)
|
||||
4. **engine-bridge v3 라이브 검증** — 코드 쪽 완료 (3-tier 분리 2단계 + EgBim 람다 실 매핑). SUT 환경에서 plugin 배치 후 `curl http://localhost:38080/scene /camera /selection`로 실값 확인. PerspectiveCamera cast로 Fov 추출 여부 검증.
|
||||
5. ~~recorder Gap I-1~~ — **deferred**. UIA poller PoC 결과 본질적 한계 확인 (AutomationPeer 부재 컨트롤은 못 봄). generic WPF DLL injection 또는 AutomationPeer AI 부착 PoC가 선결.
|
||||
|
||||
## Follow-ups (non-blocking)
|
||||
|
||||
|
||||
14
PROGRESS.md
14
PROGRESS.md
@@ -43,14 +43,16 @@
|
||||
| 2026-04-07 | normalizer follow-ups + Evaluator pass — float epsilon 구성화 + JSON-path 마스크 스코핑, 77 tests | commit `eeee3c2` |
|
||||
| 2026-04-08 | **Smoke test 2회차 — 첫 E2E 성공** 🎉 Box geometry 생성 확인 | `docs/history/2026-04-08_smoke-2회차-첫-e2e-성공.md`, `scenarios/box-v5*.yaml` |
|
||||
| 2026-04-08 | 이슈 #13 Gap E/F/G fix — HotkeyParseTests + FocusEventFilter + WindowPointResolver, 94 tests | `docs/history/2026-04-08_이슈13-smoke3-fix-generator.md` |
|
||||
| 2026-04-08 | **이슈 #14 Raw 시나리오 E2E 성공** 🎉 수동 cleanup 없이 box-v6.yaml 재생으로 Box 생성 | player: null-target fallback + foreground switch + leading alt+tab strip + timing preservation, 24 player tests |
|
||||
| 2026-04-09 | engine-bridge v3 D1/D6 scaffold (reflection accessor + 9 tests, EgPlugin) | `IAppManagerAccessor`, `ReflectionEngineStateProvider`, 14 EgPlugin tests |
|
||||
| 2026-04-09 | HmEG 소스 survey + `docs/hmeg-api-survey.md` | Q1~Q7 식별, `HmegDirectStateProvider` 설계 근거 |
|
||||
| 2026-04-09 | **3-tier 분리 1단계 (incremental)** — `Recordingtest.Bridge.Abstractions` (Generic) + `Recordingtest.Hmeg.Bridge` (HmEG-aware) 신설, `HmegDirectStateProvider` + `ChainedEngineStateProvider` wire-up, 115 tests | commit `f6b6e74` |
|
||||
| 2026-04-09 | **3-tier 분리 2단계** — `EgPlugin` → `Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost`, `EngineBridge` → `Hmeg/Recordingtest.Hmeg.Catalog`, `EngineBridge.Client` → `Hmeg/Recordingtest.Hmeg.Bridge.Client`, `EngineBridge.Probe` → `Hmeg/Recordingtest.Hmeg.Catalog.Probe`, 테스트 동일. `Recordingtest.Architecture.Tests` 11건 추가 (의존 그래프 강제). 126 tests | commit pending |
|
||||
| 2026-04-09 | **engine-bridge v3 EgBim 람다 wire-up** — `EditorPlugin` base 직접 사용: `RootSpace`, `View(EGViewport:HmEGViewport)`, `AppManager.FileManager.CurrentFile`. `HmegDirectStateProvider` 이제 실값 가능. `Editor02.HmEGAppManager.dll` 참조 추가. 라이브 검증 대기 | commit pending |
|
||||
|
||||
## In progress
|
||||
|
||||
_(없음 — Smoke 2회차 라이브 검증 대기)_
|
||||
|
||||
## In progress
|
||||
|
||||
_(없음)_
|
||||
- **engine-bridge v3 라이브 검증 대기** — 코드 쪽은 완료 (`EditorPlugin.RootSpace`/`View`/`AppManager.FileManager.CurrentFile` 실 매핑). 사용자 환경에서 `curl /scene /camera /selection`로 실값 확인 필요.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
@@ -62,6 +64,8 @@ _(없음)_
|
||||
- [ ] player: `wait_for` UIA 이벤트 매핑 강화 (현재 host passthrough).
|
||||
- [ ] player: `UiaPlayerHost` uia_path resolver가 마지막 `@AutomationId`만 사용 — 전체 ancestor chain 지원 필요.
|
||||
- [ ] recorder: IME 조합 키 처리 (contract risks).
|
||||
- [x] ~~player: foreground settle 안정화~~ — 능동 대기(`GetForegroundWindow` polling 2s + 100ms settle)로 전환, 1차 재생 성공 확인
|
||||
- [~] recorder Gap I-1 — UIA `Automation.FocusedElement` 폴링 PoC 시도(commit pending). 결과: SUT의 CommandBox 등 AutomationPeer 미부착 컨트롤은 UIA 외부에서 본질적으로 못 봄. **deferred** — generic WPF DLL injection 또는 SUT-side AutomationPeer 부착 PoC가 필요. 현재는 Player fallback(null target → OS 키 입력 / raw_coord 클릭)이 공식 전략.
|
||||
|
||||
## Blocked
|
||||
|
||||
|
||||
71
README.md
71
README.md
@@ -12,18 +12,45 @@
|
||||
[회귀 시점] → 입력 리플레이 → 결과 파일 B → normalize → diff(A, B)
|
||||
```
|
||||
|
||||
## 3계층 아키텍처
|
||||
|
||||
본 도구는 **EG-BIM Modeler 외 다양한 WPF 응용**을 대상으로 하고, 사용자 WPF 응용군 대다수가 자체 3D 엔진 **HmEG**를 공유한다. 따라서 코드는 엄격히 3계층으로 분리된다 (의존 방향 단방향, 자세한 규칙은 [CLAUDE.md §8.1](CLAUDE.md)):
|
||||
|
||||
```
|
||||
App-specific (e.g. EgBim) ──→ HmEG-aware ──→ Generic
|
||||
(특정 앱만) (HmEG 호스팅 앱 공통) (임의 WPF 앱)
|
||||
```
|
||||
|
||||
`Recordingtest.Architecture.Tests` 가 `Assembly.GetReferencedAssemblies()` 검사로 빌드 시점에 규칙을 강제한다. 새 SUT를 추가할 때는 App-specific 계층에 플러그인 진입점 + 어댑터만 작성하고 Generic + HmEG-aware 전부 재사용.
|
||||
|
||||
## 모듈 구성
|
||||
|
||||
### Generic tier — 임의 WPF 응용
|
||||
|
||||
| 모듈 | 책임 | 상태 |
|
||||
|------|------|------|
|
||||
| [Recordingtest.Bridge.Abstractions](src/Recordingtest.Bridge.Abstractions/) | `IEngineStateProvider`/`CameraSnapshot`/`SceneSnapshot` 등 SUT-중립 인터페이스 | ✓ |
|
||||
| [Recordingtest.SutProber](src/Recordingtest.SutProber/) | SUT 정적 probe (plugin/Json/assembly 카탈로그) | PoC pass |
|
||||
| [Recordingtest.Recorder](src/Recordingtest.Recorder/) | 입력 캡처 (UIA element path + offset + 키/마우스/포커스) | PoC pass |
|
||||
| [Recordingtest.Player](src/Recordingtest.Player/) | 시나리오 재생, 비동기 동기화 | PoC pass |
|
||||
| [Recordingtest.Player](src/Recordingtest.Player/) | 시나리오 재생, 비동기 동기화, null-target fallback | PoC pass |
|
||||
| [Recordingtest.Normalizer](src/Recordingtest.Normalizer/) | 결과 파일 정규화 (timestamp/GUID/path/float/order) | PoC pass |
|
||||
| [Recordingtest.DiffReporter](src/Recordingtest.DiffReporter/) | approved vs received diff 리포트 | PoC pass |
|
||||
| [Recordingtest.EngineBridge.Client](src/Recordingtest.EngineBridge.Client/) + [Recordingtest.EgPlugin](src/Recordingtest.EgPlugin/) | HmEG 내부 상태 sidecar (MEF plugin masquerade + HttpListener) | v2 pass |
|
||||
| [Recordingtest.Runner](src/Recordingtest.Runner/) | 5-모듈 E2E 파이프라인 + 실패 triage | PoC pass |
|
||||
|
||||
### HmEG-aware tier — HmEG 호스팅 앱 공통
|
||||
|
||||
| 모듈 | 책임 | 상태 |
|
||||
|------|------|------|
|
||||
| [Recordingtest.Hmeg.Bridge](src/Hmeg/Recordingtest.Hmeg.Bridge/) | `HmegDirectStateProvider` — HmEG 공개 API(Space/HmEGViewport/ISelectable)로 상태 읽기. 앱에 람다로 진입점 주입 | v3 wired |
|
||||
| [Recordingtest.Hmeg.Catalog](src/Hmeg/Recordingtest.Hmeg.Catalog/) | HmEG 정적 분석 카탈로그 | PoC pass |
|
||||
| [Recordingtest.Hmeg.Bridge.Client](src/Hmeg/Recordingtest.Hmeg.Bridge.Client/) | `/scene` `/camera` `/selection` HTTP 클라이언트 | v2 pass |
|
||||
|
||||
### App-specific tier — 현재 SUT: EG-BIM Modeler
|
||||
|
||||
| 모듈 | 책임 | 상태 |
|
||||
|------|------|------|
|
||||
| [Recordingtest.Sut.EgBim.PluginHost](src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/) | MEF 진입점(`EditorPlugin` 상속), `BridgeHttpServer` 부팅, `HmegDirectStateProvider` 람다에 `RootSpace`/`View`/`AppManager.FileManager.CurrentFile` 주입 | v3 wired |
|
||||
|
||||
## 작업 사이클 — Planner / Generator / Evaluator
|
||||
|
||||
Anthropic harness design 원칙을 채택. 같은 에이전트가 생성과 평가를 겸하지 않는다.
|
||||
@@ -39,19 +66,51 @@ Anthropic harness design 원칙을 채택. 같은 에이전트가 생성과 평
|
||||
- **시나리오 포맷**: YAML/JSON (git diff 친화적)
|
||||
- **베이스라인**: `*.approved.{ext}` / `*.received.{ext}`
|
||||
|
||||
## 주요 이정표
|
||||
|
||||
- **2026-04-08 — 첫 E2E 성공** 🎉 Smoke 2회차에서 수동 테스트 시나리오 → 재생 → SUT에 Box geometry 생성 확인 (box-v5-clean.yaml).
|
||||
- **2026-04-08 — Raw 시나리오 E2E 성공** 🎉 수동 cleanup 없이 recorder 원본 `box-v6.yaml`이 그대로 재생되어 Box 생성 (이슈 #14). Player에 null-target fallback, SUT foreground 능동 대기, 선두 alt+tab 노이즈 자동 skip, 스텝 간 타이밍 보존 추가.
|
||||
- **2026-04-09 — 3-tier 분리 완료** — Generic / HmEG-aware / App-specific 물리적 분리, `Recordingtest.Architecture.Tests`가 의존 그래프 강제. 같은 날 engine-bridge v3 EgBim 람다 실 wire-up (코드 쪽 완료, 라이브 검증 대기).
|
||||
- **126 단위 테스트** green (Recorder/Player/Normalizer/DiffReporter/Runner/Hmeg.* /Sut.EgBim.* /Architecture)
|
||||
|
||||
자세한 과정은 [docs/history/](docs/history/), 결정 근거는 [docs/contracts/](docs/contracts/) + [docs/hmeg-api-survey.md](docs/hmeg-api-survey.md) 참고.
|
||||
|
||||
## Gap 현황 (주요)
|
||||
|
||||
- **Gap A~H** (smoke 1~2회차) — 전부 fix (#11/#12/#13/#14)
|
||||
- **Gap I (recorder root-cause)** — EG-BIM Modeler의 CommandBox 등 핵심 입력 컨트롤이 AutomationPeer를 노출하지 않아 UIA 외부에서 본질적으로 식별 불가. **deferred**. 현재는 Player null-target fallback(Type→OS 포커스 / Click→raw_coord)이 공식 우회 전략. 근본 해소는 generic WPF DLL injection 또는 SUT-side AutomationPeer 부착 PoC가 선결.
|
||||
|
||||
## 디렉터리
|
||||
|
||||
```
|
||||
recordingtest/
|
||||
├── src/ # 모듈별 C# 프로젝트
|
||||
├── scenarios/ # 시나리오 YAML
|
||||
├── src/
|
||||
│ ├── Recordingtest.Bridge.Abstractions/ # Generic — 인터페이스
|
||||
│ ├── Recordingtest.Recorder/ # Generic
|
||||
│ ├── Recordingtest.Player/ # Generic
|
||||
│ ├── Recordingtest.Normalizer/ # Generic
|
||||
│ ├── Recordingtest.DiffReporter/ # Generic
|
||||
│ ├── Recordingtest.Runner/ # Generic
|
||||
│ ├── Recordingtest.SutProber/ # Generic
|
||||
│ ├── Hmeg/
|
||||
│ │ ├── Recordingtest.Hmeg.Bridge/ # HmEG-aware
|
||||
│ │ ├── Recordingtest.Hmeg.Catalog/ # HmEG-aware
|
||||
│ │ ├── Recordingtest.Hmeg.Bridge.Client/ # HmEG-aware
|
||||
│ │ └── Recordingtest.Hmeg.Catalog.Probe/ # HmEG-aware CLI
|
||||
│ └── Sut/
|
||||
│ └── EgBim/
|
||||
│ └── Recordingtest.Sut.EgBim.PluginHost/ # App-specific
|
||||
├── tests/ # 같은 계층 구조로 미러링 + Architecture.Tests
|
||||
├── scenarios/ # 시나리오 YAML (box-v*.yaml)
|
||||
├── docs/
|
||||
│ ├── contracts/ # Sprint Contracts + evaluations
|
||||
│ ├── history/ # 작업 히스토리
|
||||
│ ├── sut-catalog/ # sut-prober 산출물
|
||||
│ ├── engine-catalog/ # HmEG 후보 카탈로그 (정적 분석)
|
||||
│ ├── hmeg-api-survey.md # HmEG public API 조사 메모
|
||||
│ └── guides/ # smoke test, deploy 가이드
|
||||
├── CLAUDE.md # 에이전트 운영 지침
|
||||
├── PROGRESS.md # 완료 상태
|
||||
├── CLAUDE.md # 에이전트 운영 지침 + §8.1 3-tier 규칙
|
||||
├── PROGRESS.md # 완료 상태 (세션 간 공유 메모리)
|
||||
└── PLAN.md # 우선순위 큐
|
||||
```
|
||||
|
||||
|
||||
84
docs/contracts/engine-bridge-v3.md
Normal file
84
docs/contracts/engine-bridge-v3.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Sprint Contract — engine-bridge-v3
|
||||
|
||||
## Goal
|
||||
|
||||
`Recordingtest.EgPlugin.ReflectionEngineStateProvider`를 v2의 stub에서 진짜 reflection 기반 매핑으로 격상한다. SUT(EG-BIM Modeler)의 HmEG 엔진 내부 상태(카메라/선택/씬)를 in-process plugin에서 reflection으로 읽어 HTTP `/state` 응답에 실제 값을 채운다. 이 값이 골든파일 sidecar JSON으로 들어가 회귀 비교의 핵심 결정성 신호가 된다.
|
||||
|
||||
## Background
|
||||
|
||||
- v1: 정적 분석으로 HmEG 어셈블리 멤버 후보 8000+ 카탈로그 (`docs/engine-catalog/hmeg-candidates.json`).
|
||||
- v2: MEF plugin masquerade + HttpListener + 8 + 3 tests. `ReflectionEngineStateProvider`는 stub만.
|
||||
- v3: stub을 실매핑으로 교체. SUT 환경에서 라이브 검증 필요.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
각 항목은 객관적으로 검증 가능해야 한다.
|
||||
|
||||
### D1. AppManager 발견
|
||||
- plugin이 SUT 프로세스 안에서 `AppManager`(또는 동등한 root) 인스턴스를 reflection으로 획득한다.
|
||||
- 실패 시 `NullEngineStateProvider`로 안전하게 폴백하고 stderr에 한 번만 경고 로그.
|
||||
|
||||
### D2. CameraSnapshot 실매핑
|
||||
- `GetCamera()`가 활성 뷰포트의 카메라 eye/target/up/fov를 **non-default 값**으로 반환.
|
||||
- 검증: 라이브 SUT에서 카메라 이동 후 두 번 호출 → 적어도 한 필드가 달라짐.
|
||||
|
||||
### D3. SceneSnapshot 실매핑
|
||||
- `GetScene()`가 현재 문서의 객체 수와 (열린 경우) 문서 경로를 반환.
|
||||
- 검증: Box 1개 생성 후 `ObjectCount >= 1`, 새 문서 만들면 0.
|
||||
|
||||
### D4. SelectedIds 실매핑
|
||||
- `GetSelectedIds()`가 현재 선택된 객체의 ID 리스트를 반환 (HmEG 내부 ID 또는 GUID 문자열).
|
||||
- 검증: 객체 선택 → 비어있지 않은 리스트.
|
||||
|
||||
### D5. 결정성 + 정규화
|
||||
- 응답 JSON은 normalizer가 처리 가능한 형태 (정렬된 키, 안정된 부동소수점 표현). normalizer 규칙은 기존 `mask_volatile_settings` / 부동소수점 epsilon으로 충분한지 확인하고 부족하면 신규 규칙 등록.
|
||||
|
||||
### D6. 단위 테스트
|
||||
- `ReflectionEngineStateProvider`의 reflection 경로를 mockable한 `IAppManagerAccessor` 추상화 뒤로 격리.
|
||||
- Fake accessor로 각 D2/D3/D4를 단위 테스트화 (라이브 SUT 없이 CI 가능).
|
||||
- 최소 6 신규 테스트, 전체 suite green (현 94+ → 100+).
|
||||
|
||||
### D7. 라이브 검증
|
||||
- 사용자 SUT 환경에서 plugin 로드 → `/state` GET 응답에 D2/D3/D4 실값 확인.
|
||||
- 결과는 `docs/history/2026-04-08_engine-bridge-v3.md` 에 캡처.
|
||||
|
||||
### D8. 문서
|
||||
- `docs/contracts/engine-bridge-v3.evaluation.md` (Evaluator 산출물)
|
||||
- `docs/guides/engine-bridge-deploy.md` 업데이트 (v3 응답 스키마 변경분)
|
||||
|
||||
## Out of scope
|
||||
|
||||
- HmEG 내부 데이터 변경/쓰기 (read-only)
|
||||
- viewport 픽셀 캡처 (별개 모듈)
|
||||
- 새 HTTP 엔드포인트 (기존 `/state` 라우트만 채움)
|
||||
|
||||
## Interfaces
|
||||
|
||||
```csharp
|
||||
// 새 추상화 (D6)
|
||||
public interface IAppManagerAccessor
|
||||
{
|
||||
object? GetAppManager();
|
||||
object? GetActiveDocument();
|
||||
object? GetActiveViewport();
|
||||
}
|
||||
|
||||
// 기존 IEngineStateProvider 시그니처 유지 — 구현만 교체
|
||||
```
|
||||
|
||||
## Evaluation plan
|
||||
|
||||
1. Evaluator는 `/contract engine-bridge-v3.md` 를 읽고 D1~D8을 차례로 채점.
|
||||
2. D2/D3/D4는 단위 테스트(D6)로 검증 가능 → CI에서 자동 grade.
|
||||
3. D7은 사용자 라이브 결과 첨부로 grade (orchestrator가 캡처 전달).
|
||||
4. fail 1회 → Generator 재작업. 누적 3회 → 자동 중단.
|
||||
|
||||
## Risks
|
||||
|
||||
- HmEG 내부 타입 이름이 obfuscation/난독화 가능 → reflection by-name이 깨질 수 있음. 완화: `hmeg-candidates.json` 카탈로그를 dictionary로 lookup, fallback 체인 다중화.
|
||||
- AppManager singleton 접근 패턴이 SUT 버전마다 다를 수 있음. 완화: D1에서 여러 후보 시도.
|
||||
- 카메라 좌표계가 right-handed/left-handed/up-axis 다양 → 정규화 규칙 필요.
|
||||
|
||||
## Estimated complexity
|
||||
|
||||
중. 단위 테스트만으로는 D2/D3/D4의 실매핑이 옳은지 확인 불가 — 라이브 검증(D7)이 critical path. 1차 사이클은 발견(discovery)에 시간 쏠릴 가능성.
|
||||
140
docs/contracts/generic-sut-split.md
Normal file
140
docs/contracts/generic-sut-split.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Sprint Contract — generic-sut-split (3-tier)
|
||||
|
||||
## Goal
|
||||
|
||||
`CLAUDE.md §8.1` 신규 규칙(**Generic / HmEG-aware / App-specific 3계층 분리**)을 코드베이스에 실제로 반영한다. 향후 EG-BIM Modeler 외에도 HmEG를 호스팅하는 다양한 WPF 응용을 추가할 때 Generic 코어와 HmEG-aware 미들 계층을 재사용하고, 앱마다 다른 부분(플러그인 진입점, 명령 lifecycle 어댑터)만 새로 작성하면 되도록 한다.
|
||||
|
||||
## Background
|
||||
|
||||
지금까지 모든 모듈이 `src/Recordingtest.*` 평면에 있고, `Recordingtest.EgPlugin`/`Recordingtest.EngineBridge`가 "EG-BIM 전용"과 "HmEG 일반"과 "Generic"이 섞인 채로 존재한다. 사용자 디렉티브:
|
||||
1. 처음부터 Generic vs SUT-specific 분리
|
||||
2. **HmEG는 사용자 WPF 앱군의 공통 엔진** — HmEG-aware 미들 계층을 따로 두어 앱 간 재사용
|
||||
|
||||
따라서 분리는 2-tier가 아니라 **3-tier**: Generic → HmEG-aware → App-specific (EgBim).
|
||||
|
||||
## Definition of Done
|
||||
|
||||
### D1. 폴더/csproj 이동·분할
|
||||
|
||||
**Generic 신설**:
|
||||
- `src/Recordingtest.Bridge.Abstractions/` — `IEngineStateProvider`, `CameraSnapshot`, `SceneSnapshot`, 향후 `IFocusProbe`/`IHitTestProbe`/`ICommandLifecycle`. **`HmEG.dll` 참조 금지**.
|
||||
- `src/Recordingtest.Bridge.Client/` — generic HTTP 클라이언트 (`HttpClient` 래퍼, `BridgeClientException`, `IBridgeClient`)
|
||||
|
||||
**HmEG-aware 신설** (`src/Hmeg/` 하위):
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Bridge/` — `HmegDirectStateProvider : IEngineStateProvider` (HmEG `Space`/`HmEGViewport`/`CameraCore` 직접 호출), HTTP 서버는 별도. **`HmEG.dll`만 참조**.
|
||||
- `src/Hmeg/Recordingtest.Hmeg.TargetResolver/` — 씬 노드 hit-test/포커스 식별 (Gap I 우회의 HmEG-aware 레이어)
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Catalog/` — `Recordingtest.EngineBridge`의 정적 분석/CandidateFinder를 이쪽으로 이동
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Bridge.Client/` — `HmEgHttpSnapshot` 등 HmEG-shaped 응답 타입 (현 `Recordingtest.EngineBridge.Client`에서 분리)
|
||||
|
||||
**App-specific (EgBim) 신설** (`src/Sut/EgBim/` 하위):
|
||||
- `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/` — MEF entry. `EditorPlugin` 베이스 상속, `BridgeHttpServer` 부팅. 현재 `Recordingtest.EgPlugin.HmEgBridgePlugin` 이동.
|
||||
- `src/Sut/EgBim/Recordingtest.Sut.EgBim.Adapter/` — EG-BIM Modeler `AppManager` 진입점, command lifecycle 어댑터. 현재 `ReflectionAppManagerAccessor`는 여기로 (CI fallback 용도). EG-BIM 전용 SUT 멤버 이름 후보(예: `Editor.AppManager.AppManager`)는 이 모듈에만 등장.
|
||||
|
||||
**테스트 이동**:
|
||||
- `tests/Recordingtest.Bridge.Abstractions.Tests/` (신규)
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Bridge.Tests/` (현 EgPlugin 테스트의 HmEG 부분)
|
||||
- `tests/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost.Tests/` (현 EgPlugin 테스트의 EgBim 부분)
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests/` (현 EngineBridge.Tests 이동)
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Catalog.IntegrationTests/` (현 EngineBridge.IntegrationTests 이동)
|
||||
|
||||
**제거**:
|
||||
- 기존 `src/Recordingtest.EgPlugin/`, `src/Recordingtest.EngineBridge/`, `src/Recordingtest.EngineBridge.Client/` 디렉터리 (내용물은 위 3계층으로 분배)
|
||||
|
||||
### D2. 네임스페이스 rename
|
||||
- `Recordingtest.Bridge.*` (Generic)
|
||||
- `Recordingtest.Hmeg.*` (HmEG-aware)
|
||||
- `Recordingtest.Sut.EgBim.*` (App-specific)
|
||||
- 모든 `using Recordingtest.EgPlugin;` 정리
|
||||
- `recordingtest.sln` 프로젝트 경로/이름 갱신
|
||||
|
||||
### D3. 인터페이스 추출
|
||||
- `IEngineStateProvider`, `CameraSnapshot`, `SceneSnapshot` → `Recordingtest.Bridge.Abstractions` (Generic)
|
||||
- 현재 `Recordingtest.EgPlugin` 내 정의 제거, 새 위치를 참조
|
||||
|
||||
### D4. HmegDirectStateProvider 골격 신설
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Bridge/HmegDirectStateProvider.cs` 작성 (구현은 람다 주입 형태, `docs/hmeg-api-survey.md` §"v3 구현 방향" 참고)
|
||||
- 단위 테스트 — fake viewport/space 람다로 D2/D3/D4(원래 v3 contract)에 해당하는 동작 검증
|
||||
- **본 contract에서는 골격만**. 실제 HmEG 라이브 검증은 `engine-bridge-v3` 후속 contract.
|
||||
|
||||
### D5. EgBim PluginHost 보존
|
||||
- 현 `HmEgBridgePlugin`의 동작(MEF 발견, BridgeHttpServer 부팅, ReflectionEngineStateProvider 폴백)은 동일해야 함
|
||||
- provider 결정 로직: `HmegDirectStateProvider` 가능하면 사용, 실패 시 `ReflectionEngineStateProvider` 폴백, 최종 실패 시 `NullEngineStateProvider`
|
||||
|
||||
### D6. 의존 그래프 검증
|
||||
- Generic 모듈의 csproj는 `HmEG.dll` 또는 `Editor*PluginInterface.dll` 을 **직간접 참조하지 않음**
|
||||
- HmEG-aware 모듈의 csproj는 `HmEG.dll`만 참조, 특정 앱 어셈블리 참조 금지
|
||||
- App-specific 모듈만이 자기 앱 어셈블리 참조
|
||||
- 신규 `Recordingtest.Architecture.Tests` — `Assembly.GetReferencedAssemblies()` 검사로 위반 검출. 각 계층별 expected reference set assert.
|
||||
|
||||
### D7. 빌드/테스트 green
|
||||
- `dotnet build recordingtest.sln` 성공
|
||||
- `dotnet test recordingtest.sln` 100+ tests 모두 pass
|
||||
- 신규 ArchitectureTests + HmegDirectStateProvider 단위 테스트 통과
|
||||
|
||||
### D6. 빌드/테스트 green
|
||||
- `dotnet build recordingtest.sln` 성공
|
||||
- `dotnet test recordingtest.sln` 100+ tests 모두 pass (현재 상태 유지)
|
||||
- 신규 ArchitectureTests 통과
|
||||
|
||||
### D8. PROGRESS/PLAN 갱신 + history
|
||||
- PROGRESS.md Done에 항목 추가
|
||||
- PLAN.md에서 본 contract 제거
|
||||
- `docs/history/YYYY-MM-DD_generic-sut-split.md` 작성
|
||||
- CLAUDE.md §8.1 표(현재 모듈 분류)를 마이그레이션 후 상태로 갱신
|
||||
|
||||
### D9. 단일 커밋(권장) 또는 2단 커밋
|
||||
- 옵션 A: 단일 커밋 — sln 무결성 보장, BREAKING 명시
|
||||
- 옵션 B: (1) git mv만, (2) 내용 변경 — git rename detection 보존
|
||||
- Generator 판단. 어느 쪽이든 메시지 prefix `BREAKING:`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- 새 SUT(다른 WPF 앱) 추가 — 본 contract는 구조만 만든다
|
||||
- engine-bridge v3의 `HmEgDirectStateProvider` 구현 — 그건 별도 contract `engine-bridge-v3` 후속
|
||||
- generic 코어의 기능 변경 — 순수 rename/이동만
|
||||
|
||||
## Interfaces
|
||||
|
||||
```csharp
|
||||
// src/Recordingtest.Bridge.Abstractions/IEngineStateProvider.cs (generic)
|
||||
namespace Recordingtest.Bridge;
|
||||
|
||||
public interface IEngineStateProvider
|
||||
{
|
||||
IReadOnlyList<string> GetSelectedIds();
|
||||
CameraSnapshot GetCamera();
|
||||
SceneSnapshot GetScene();
|
||||
bool GetRenderComplete();
|
||||
}
|
||||
|
||||
public sealed record CameraSnapshot(double[] Eye, double[] Target, double[] Up, double Fov);
|
||||
public sealed record SceneSnapshot(int ObjectCount, string? DocumentPath);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// src/Sut/EgBim/Recordingtest.Sut.EgBim.Plugin/HmEgDirectStateProvider.cs (SUT)
|
||||
namespace Recordingtest.Sut.EgBim.Plugin;
|
||||
using Recordingtest.Bridge;
|
||||
public sealed class HmEgDirectStateProvider : IEngineStateProvider { ... }
|
||||
```
|
||||
|
||||
## Risks
|
||||
|
||||
- **sln 경로 깨짐**: csproj 이동 시 sln 갱신을 빠뜨리면 빌드 깨짐. 완화: D6에서 솔루션 빌드 + 테스트 강제.
|
||||
- **using 구문 누락**: 네임스페이스 rename 시 다른 프로젝트의 using이 깨짐. 완화: 빌드가 잡아냄.
|
||||
- **engine-bridge v3 진행 중 방해**: scaffold 미커밋 상태(IAppManagerAccessor 등). 본 refactor 전에 v3 scaffold를 먼저 커밋해 두는 게 안전.
|
||||
- **git rename detection**: 폴더 이동 + 내용 변경이 동시에 들어가면 git이 rename 인식을 못 할 수 있음. 완화: 가능한 한 "이동만" 한 번 커밋, "내용 변경"은 후속 커밋. (단일 커밋 vs rename 보존 trade-off — D8과 충돌 가능. Generator 판단.)
|
||||
|
||||
## Estimated complexity
|
||||
|
||||
중. 코드 로직 변경은 거의 없고 mass rename + 폴더 이동 + sln 정리. 시간보다 손실 위험 관리가 핵심.
|
||||
|
||||
## Evaluation plan
|
||||
|
||||
Evaluator는:
|
||||
1. D1~D5를 파일/폴더/csproj 검사로 채점
|
||||
2. D5의 ArchitectureTests 실행
|
||||
3. D6의 전체 build/test 실행
|
||||
4. D8의 git log 단일 커밋 확인
|
||||
|
||||
fail 1회 → Generator 재작업. 누적 3회 → 자동 중단.
|
||||
44
docs/history/2026-04-08_gap-i1-deferred.md
Normal file
44
docs/history/2026-04-08_gap-i1-deferred.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 2026-04-08 — Gap I-1 (recorder focus poller) deferred
|
||||
|
||||
**소요 시간**: ~45분
|
||||
**Context 사용량**: ~25k tokens (Opus 4.6, 동일 세션 누적)
|
||||
|
||||
## 시도
|
||||
|
||||
issue #14의 recorder Gap I-1: type 스텝 target이 항상 null로 남는 문제를 root에서 풀기 위해 시도.
|
||||
|
||||
접근:
|
||||
1. 백그라운드 Task가 100ms 주기로 `automation.FocusedElement()` 폴링
|
||||
2. 결과 path를 `LowLevelHook.CurrentFocusedPath` volatile 필드에 저장
|
||||
3. `KeyboardProc`가 key_down RawEvent 생성 시 이 필드를 `FocusedElementPath`에 stamp
|
||||
4. `DragCollapser`가 type burst 시작 시 `typeFocusPath` 로컬에 캡처, FlushType fallback에서 우선 사용
|
||||
5. 진단 카운터 추가 (success/null_focus/wrong_pid/errors/last_path)
|
||||
|
||||
## 결과
|
||||
|
||||
box-v7.yaml 녹화 → `null_target_steps=13` (이전 12와 사실상 동일). type 스텝의 `target:` 여전히 비어있음.
|
||||
|
||||
## 원인
|
||||
|
||||
UIA `FocusedElement()`는 **AutomationPeer가 부착된 컨트롤만** 보고할 수 있다. EG-BIM Modeler의 CommandBox 등 핵심 입력 컨트롤은 AutomationPeer가 없어서 UIA 트리에 의미 있게 노출되지 않음. 외부 프로세스에서 어떤 UIA API를 호출해도 동일한 한계.
|
||||
|
||||
WPF의 진짜 포커스(`Keyboard.FocusedElement`)는 **in-process API**라 외부 recorder에서는 직접 호출 불가.
|
||||
|
||||
## 결정
|
||||
|
||||
**Gap I-1 deferred.** 현재는 Player fallback이 공식 전략:
|
||||
- Type with null target → OS 레벨 키보드 입력 (SUT의 WPF가 자기 포커스 컨트롤로 라우팅)
|
||||
- Click with null target + raw_coord → 화면 절대좌표 클릭
|
||||
|
||||
이 fallback으로 box-v6/v7 모두 E2E 재생 성공 확인됨. 결정성/진단성은 떨어지지만 실행 자체엔 충분.
|
||||
|
||||
## 향후 옵션 (선결 PoC 필요)
|
||||
|
||||
1. **Generic WPF DLL injection** — CreateRemoteThread + LoadLibrary로 임의 WPF 프로세스에 probe DLL 주입, Dispatcher 위에서 `Keyboard.FocusedElement` 읽어 named pipe로 노출. 권한 이슈 있음.
|
||||
2. **AutomationPeer AI 부착 PoC** (메모리 `project_recordingtest_automationpeer_ai.md`) — SUT fork에 AI로 AutomationPeer 자동 부착하는 별도 PR 트랙. SUT 협조 필요.
|
||||
|
||||
둘 다 본 이슈 범위를 벗어나므로 별도 트랙. PLAN.md에서 제거됨.
|
||||
|
||||
## 남긴 코드
|
||||
|
||||
진단 가능한 형태로 commit (revert하지 않음). 향후 generic injection PoC가 들어올 때 같은 stamping 메커니즘(`LowLevelHook.CurrentFocusedPath` → `RawEvent.FocusedElementPath` → `DragCollapser.typeFocusPath`) 그대로 재사용 가능.
|
||||
20
docs/history/2026-04-08_gitea-mcp-access-qa.md
Normal file
20
docs/history/2026-04-08_gitea-mcp-access-qa.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Gitea MCP 접근 경로 Q&A
|
||||
|
||||
**소요 시간**: 5분
|
||||
**Context 사용량**: input ~30k / output ~1k tokens
|
||||
**이슈**: #0
|
||||
|
||||
## 요약
|
||||
|
||||
- 사용자 질문: "gitea 엑세스 어떻게 하고 있지. 다른 프로젝트에서는 gitea mcp가 안 된다."
|
||||
- 확인 결과: Gitea MCP는 글로벌 user scope (`C:\Users\nbright\.claude.json`의 최상위 `mcpServers.gitea`)에 등록됨.
|
||||
- command: `C:/Users/nbright/bin/gitea-mcp.exe -t stdio`
|
||||
- env: `GITEA_HOST=https://gitea.hmac.kr`, `GITEA_ACCESS_TOKEN=6f6147...`
|
||||
- 본 프로젝트에서 바로 쓸 수 있었던 이유: `.claude/settings.json`의 `permissions.allow`에 `mcp__gitea__*` 명시.
|
||||
|
||||
## 다른 프로젝트에서 안 되는 원인 후보
|
||||
|
||||
1. 프로젝트별 MCP 승인 미처리 (`/mcp`로 enable 필요)
|
||||
2. `disabledMcpjsonServers`에 gitea 포함 또는 `enableAllProjectMcpServers: false`
|
||||
3. 프로젝트 로컬 `.mcp.json`이 잘못된 gitea 정의로 덮어씀
|
||||
4. `permissions.allow`에 `mcp__gitea__*` 누락 → 매번 허용 프롬프트
|
||||
70
docs/history/2026-04-08_이슈14-raw-시나리오-e2e-성공.md
Normal file
70
docs/history/2026-04-08_이슈14-raw-시나리오-e2e-성공.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 2026-04-08 — 이슈 #14 Raw 시나리오 E2E 성공
|
||||
|
||||
**이슈**: #14 Raw 레코딩 시나리오를 수동 cleanup 없이 재생 가능하게
|
||||
**소요 시간**: ~90분
|
||||
**Context 사용량**: ~60k tokens (Opus 4.6)
|
||||
|
||||
## 결과
|
||||
|
||||
🎉 **`scenarios/box-v6.yaml` 원본(AI 후처리 없음)** → Player 재생 → **SUT에 Box geometry 생성 확인**.
|
||||
|
||||
이전 box-v5-clean.yaml E2E 성공은 AI가 focus 이벤트 40+개 제거, target UIA 경로 리타기팅(ItemsControl→CommandBox), 뷰포트 offset 박음질 등 수작업 후편집의 결과였다. 이번 작업으로 **수작업 없이 recorder 원본을 그대로 재생**하는 경로가 처음으로 열렸다.
|
||||
|
||||
## 문제 분해
|
||||
|
||||
레코더 원본은 다음 4가지 이유로 재생 불가였다:
|
||||
|
||||
1. **Type/Click null target** — recorder가 key/mouse 이벤트 발생 시 focused element를 resolve 못 해 `target: null`로 저장. Player는 "#11에서 null skip" 정책이라 아예 실행 안 함.
|
||||
2. **Player 실행 시 포커스** — `dotnet run` 한 PowerShell이 foreground라 첫 type("BOX")이 PowerShell로 들어감.
|
||||
3. **선두 alt+tab 노이즈** — 녹화 시작 시 사용자가 에디터→SUT로 전환하려던 alt+tab 2개가 재생 시 SUT를 오히려 off-foreground로 보냄.
|
||||
4. **스텝 간 타이밍 없음** — 엔진이 즉시 연속 실행 → SUT가 BOX 명령 → 모서리 픽 전환할 틈 없음.
|
||||
|
||||
## 구현 (Player 쪽 포스트프로세싱)
|
||||
|
||||
### 1. Null-target fallback (PlayerEngine)
|
||||
- `Type + null target` → 현재 포커스로 그대로 `host.Type()` (SUT CommandBox 포커스 가정)
|
||||
- `Click + null target + raw_coord` → screen-absolute 좌표로 직접 `host.Click()`
|
||||
- 기타 null target → 여전히 skip (안전)
|
||||
- `Step.RawCoord: int[]?` 추가, YAML `raw_coord` 자동 매핑
|
||||
|
||||
### 2. SUT foreground 강제 (UiaPlayerHost.BringSutToForeground)
|
||||
- `_app.GetMainWindow().SetForeground() + Focus()`
|
||||
- 600ms settle (150 → 600으로 상향; 초기 char 드롭 관찰 후)
|
||||
- Program.cs가 engine.Run 이전에 1회 호출
|
||||
|
||||
### 3. 선두 alt+tab 자동 skip (PlayerEngine.Run)
|
||||
- 녹화 startup 노이즈 제거. SUT가 이미 foreground인 상태에서 alt+tab 실행은 오히려 유해.
|
||||
|
||||
### 4. 스텝 간 타이밍 복원 (PlayerEngine + IPlayerHost.Delay)
|
||||
- `Step.Ts: long?` 추가
|
||||
- `PlayerEngineOptions.PreserveTiming` (기본 on)
|
||||
- `ts_i - ts_{i-1}` 를 150ms~3s로 클램프해 host.Delay 호출
|
||||
- **엔진 내부 `Thread.Sleep` 금지 DoD를 유지하기 위해** 딜레이는 `IPlayerHost.Delay`로 위임. `UiaPlayerHost`만 실제 sleep, `FakePlayerHost`는 기록만.
|
||||
- 첫 스텝도 MinStepDelay 받도록 prevTs 시드
|
||||
|
||||
### 5. 스텝 로그
|
||||
- `[player] step {i} kind={kind} value={value}` — 라이브 디버깅용
|
||||
|
||||
## 테스트
|
||||
|
||||
- `PlayerEngine_NullTarget_SkipsWithoutCalling` → `PlayerEngine_NullTarget_Fallback_Issue14`로 교체
|
||||
- click(null+raw_coord) → clicks[0] 검증, type(null+value) → types[0] 검증
|
||||
- click(null, no raw_coord), drag(null) → 여전히 skip
|
||||
- 전체 suite green: 94+ tests (Player 24, Runner 6, Recorder 26, Normalizer 16, ...)
|
||||
|
||||
## 라이브 검증 (사용자 환경)
|
||||
|
||||
```
|
||||
dotnet run --project src\Recordingtest.Player -- --scenario scenarios\box-v6.yaml --output-dir artifacts\replay-v6-raw --no-launch
|
||||
```
|
||||
|
||||
첫 시도는 BOX 타이핑이 누락 (`[player] step 2 kind=Type value=BOX` 로그는 찍혔지만 SUT command box에 안 들어감). 두 번째 시도에서 Box geometry 생성 성공.
|
||||
|
||||
## 남은 과제 (PLAN.md P1에 등록)
|
||||
|
||||
- **foreground settle 경계선 문제** — 600ms가 가끔 부족. `SetForegroundWindow` 후 능동 대기(`GetForegroundWindow == sut_hwnd` polling) 또는 첫 type 이전에 Keyboard warm-up 필요.
|
||||
- **recorder Gap I-1** — null_target_steps를 근본적으로 줄이려면 recorder가 key_down 시점에 `Automation.FocusedElement`를 직접 쿼리해서 typeRes를 채워야 함. 현재는 player fallback으로 우회 중.
|
||||
|
||||
## 관련 커밋
|
||||
|
||||
- (pending) Player null-target fallback + foreground + alt+tab strip + timing preservation
|
||||
108
docs/history/2026-04-09_3tier-split-step1.md
Normal file
108
docs/history/2026-04-09_3tier-split-step1.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 2026-04-09 — 3-tier 분리 1단계 (incremental)
|
||||
|
||||
**이슈**: #10 follow-up (engine-bridge v3) + 사용자 디렉티브 (Generic / HmEG-aware / App-specific 분리)
|
||||
**소요 시간**: ~70분
|
||||
**Context 사용량**: input ~205k / output ~38k tokens (Opus 4.6, 1M context, 동일 세션 누적)
|
||||
|
||||
## 작업
|
||||
|
||||
CLAUDE.md §8.1 의 3-tier 규칙을 incremental하게 코드에 반영. 기존 `EgPlugin`/`EngineBridge` mass-rename은 2단계로 미루고, **새 계층 모듈을 신설**해 wire-up까지 끝냄.
|
||||
|
||||
### 신설 (Generic 계층)
|
||||
|
||||
- `src/Recordingtest.Bridge.Abstractions/` (csproj, net8.0)
|
||||
- `IEngineStateProvider`, `CameraSnapshot`, `SceneSnapshot`, `NullEngineStateProvider`
|
||||
- SUT 어셈블리 참조 0개. CI 안전.
|
||||
- 기존 `Recordingtest.EgPlugin`의 `IEngineStateProvider`/`CameraSnapshot`/`SceneSnapshot`/`NullEngineStateProvider` 정의 제거 → Bridge.Abstractions로 위임 (`using Recordingtest.Bridge;`)
|
||||
|
||||
### 신설 (HmEG-aware 계층)
|
||||
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Bridge/` (csproj, net8.0-windows + WPF + HmEG.dll 직접 참조)
|
||||
- `HmegDirectStateProvider : IEngineStateProvider`
|
||||
- 람다 주입: `Func<HmEG.Space?>`, `Func<HmEG.HmEGViewport?>`, `Func<string?>?`
|
||||
- `GetSelectedIds`: Space 트리 walk → `ISelectable.IsSelected` 노드의 `Uid` 수집. `ModelBase` 직접 타입 참조 회피 (MemoryPack 의존 차단), 대신 `object` + 패턴 매칭 + 늦은 바인딩 `Uid` 프로퍼티 읽기
|
||||
- `GetCamera`: `viewport.CameraCore`에서 `Position`/`LookDirection`/`UpDirection`/`FieldOfView`를 reflection으로 읽어 `CameraSnapshot`. Target = Eye + LookDir.
|
||||
- `GetScene`: `space.ItemsCount` + 외부 documentPathProvider 람다
|
||||
- 모든 호출 try/catch → safe default 폴백
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Bridge.Tests/` (csproj, HmEG.dll Private=true로 출력 폴더 복사)
|
||||
- `HmegDirectStateProviderTests` 5개 (null lambdas / throwing lambdas / document path / null arg)
|
||||
|
||||
### EgPlugin wire-up
|
||||
|
||||
- `HmEgBridgePlugin.BuildProvider()` 신설:
|
||||
```
|
||||
HmegDirectStateProvider (1순위, lambdas는 일단 null 반환)
|
||||
↓ default →
|
||||
ReflectionEngineStateProvider (2순위, EgBim AppManager 후보 탐색)
|
||||
```
|
||||
체인: `ChainedEngineStateProvider`
|
||||
- `ChainedEngineStateProvider` 신설 — primary 결과가 default/empty면 fallback 호출. signal별 판정:
|
||||
- SelectedIds 빈 리스트
|
||||
- Camera Eye=(0,0,0) AND Target=(0,0,0)
|
||||
- Scene ObjectCount=0 AND DocumentPath=null
|
||||
- RenderComplete: primary always wins
|
||||
- 단위 테스트 7개 (`ChainedEngineStateProviderTests`)
|
||||
|
||||
EgBim adapter(Q1~Q7 답)가 채워지면 `BuildProvider`의 두 람다만 실값으로 바꾸면 라이브 검증으로 이어진다.
|
||||
|
||||
### sln/build/test
|
||||
|
||||
- `dotnet sln add` 로 3개 신규 csproj 등록
|
||||
- `Recordingtest.Bridge.Abstractions`
|
||||
- `Recordingtest.Hmeg.Bridge`
|
||||
- `Recordingtest.Hmeg.Bridge.Tests`
|
||||
- `dotnet build` 성공 (HmEG.dll의 ModelBase가 MemoryPack 의존성을 transitively 요구해서 1차 빌드 실패 → ModelBase 직접 참조 제거로 우회)
|
||||
- `dotnet test recordingtest.sln`: **0 failures, 115 passed** (94 → 115, +21)
|
||||
|
||||
## 분류 라벨 (현재 시점)
|
||||
|
||||
| 모듈 | 계층 | 비고 |
|
||||
|---|---|---|
|
||||
| `Recordingtest.Bridge.Abstractions` ✨ | **Generic** | 신설 |
|
||||
| `Recordingtest.Hmeg.Bridge` ✨ | **HmEG-aware** | 신설, HmEG.dll만 참조 |
|
||||
| `Recordingtest.EgPlugin` | **App-specific (EgBim)** | rename 대기. Bridge.Abstractions + Hmeg.Bridge 참조하도록 갱신됨 |
|
||||
| 기타 generic 모듈 (Recorder/Player/Normalizer/...) | **Generic** | 변경 없음 |
|
||||
|
||||
## 2단계 (다음 세션)
|
||||
|
||||
`docs/contracts/generic-sut-split.md` D1~D9 잔여:
|
||||
- `src/Recordingtest.EgPlugin/` → `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/` + `.Adapter/`
|
||||
- `src/Recordingtest.EngineBridge/` → `src/Hmeg/Recordingtest.Hmeg.Catalog/`
|
||||
- `src/Recordingtest.EngineBridge.Client/` → 분할 (generic HTTP + HmEG-shaped)
|
||||
- 네임스페이스 일괄 rename
|
||||
- `Recordingtest.Architecture.Tests` 추가 (의존 그래프 검증)
|
||||
- 단일 BREAKING 커밋
|
||||
|
||||
## 라이브 검증 (Q1~Q7 후)
|
||||
|
||||
EgBim adapter에서 다음 람다를 실값으로 채운다:
|
||||
```csharp
|
||||
spaceProvider = () => /* AppManager.Instance.ActiveSpace */ ;
|
||||
viewportProvider = () => /* AppManager.Instance.ActiveViewport (HmEGViewport 캐스트) */ ;
|
||||
documentPathProvider = () => /* AppManager.Instance.ActiveDocumentPath */ ;
|
||||
```
|
||||
|
||||
→ `curl http://localhost:38080/scene` 등으로 검증.
|
||||
|
||||
## 미커밋
|
||||
|
||||
본 세션 누적 (다음 단계에서 통합 커밋):
|
||||
- `Recordingtest.Bridge.Abstractions/` (신규)
|
||||
- `Hmeg/Recordingtest.Hmeg.Bridge/` (신규)
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Bridge.Tests/` (신규)
|
||||
- `Recordingtest.EgPlugin/` 갱신 (`HmEgBridgePlugin`, `IEngineStateProvider`, `ChainedEngineStateProvider` 신규, csproj ProjectReference 추가)
|
||||
- `Recordingtest.EgPlugin.Tests/` 갱신 (`ChainedEngineStateProviderTests` 신규, using 정리)
|
||||
- `recordingtest.sln`
|
||||
- `CLAUDE.md` §8.1 (직전 단계)
|
||||
- `docs/contracts/generic-sut-split.md`, `docs/contracts/engine-bridge-v3.md`, `docs/hmeg-api-survey.md`
|
||||
- `PROGRESS.md`, `PLAN.md`
|
||||
- 본 history + `2026-04-09_engine-bridge-v3-scaffold.md`
|
||||
|
||||
## 관련
|
||||
|
||||
- `src/Recordingtest.Bridge.Abstractions/IEngineStateProvider.cs`
|
||||
- `src/Hmeg/Recordingtest.Hmeg.Bridge/HmegDirectStateProvider.cs`
|
||||
- `src/Recordingtest.EgPlugin/HmEgBridgePlugin.cs`
|
||||
- `src/Recordingtest.EgPlugin/ChainedEngineStateProvider.cs`
|
||||
- `tests/Hmeg/Recordingtest.Hmeg.Bridge.Tests/HmegDirectStateProviderTests.cs`
|
||||
- `tests/Recordingtest.EgPlugin.Tests/ChainedEngineStateProviderTests.cs`
|
||||
138
docs/history/2026-04-09_3tier-split-step2-and-v3-wireup.md
Normal file
138
docs/history/2026-04-09_3tier-split-step2-and-v3-wireup.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 2026-04-09 — 3-tier 분리 2단계 + engine-bridge v3 EgBim 람다 wire-up
|
||||
|
||||
**이슈**: #10 follow-up (engine-bridge v3) + `docs/contracts/generic-sut-split.md`
|
||||
**소요 시간**: ~110분 (새 세션 / 동일 날짜 두 번째 블록)
|
||||
**Context 사용량**: input ~80k / output ~18k tokens (Opus 4.6, 1M context, 새 세션)
|
||||
|
||||
## 작업
|
||||
|
||||
### 1. 3-tier 분리 2단계 (mass-rename + move)
|
||||
|
||||
기존 EgPlugin/EngineBridge 모듈을 새 계층 폴더로 이동하고 네임스페이스를 일괄 rename.
|
||||
|
||||
**소스 이동 (git mv)**:
|
||||
| 원본 | 대상 | 계층 |
|
||||
|---|---|---|
|
||||
| `src/Recordingtest.EgPlugin/` | `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/` | App-specific |
|
||||
| `src/Recordingtest.EngineBridge/` | `src/Hmeg/Recordingtest.Hmeg.Catalog/` | HmEG-aware |
|
||||
| `src/Recordingtest.EngineBridge.Client/` | `src/Hmeg/Recordingtest.Hmeg.Bridge.Client/` | HmEG-aware |
|
||||
| `src/Recordingtest.EngineBridge.Probe/` | `src/Hmeg/Recordingtest.Hmeg.Catalog.Probe/` | HmEG-aware |
|
||||
| `tests/Recordingtest.EgPlugin.Tests/` | `tests/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost.Tests/` | App-specific |
|
||||
| `tests/Recordingtest.EngineBridge.Tests/` | `tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests/` | HmEG-aware |
|
||||
| `tests/Recordingtest.EngineBridge.IntegrationTests/` | `tests/Hmeg/Recordingtest.Hmeg.Catalog.IntegrationTests/` | HmEG-aware |
|
||||
|
||||
**네임스페이스 rename**:
|
||||
- `Recordingtest.EgPlugin` → `Recordingtest.Sut.EgBim.PluginHost`
|
||||
- `Recordingtest.EngineBridge` → `Recordingtest.Hmeg.Catalog`
|
||||
- `Recordingtest.EngineBridge.Client` → `Recordingtest.Hmeg.Bridge.Client`
|
||||
- `Recordingtest.EngineBridge.Probe` → `Recordingtest.Hmeg.Catalog.Probe`
|
||||
|
||||
**csproj rename** + `<RootNamespace>` / `<AssemblyName>` + ProjectReference 경로 갱신 + `recordingtest.sln` 에서 remove/add.
|
||||
|
||||
**문제/해결**:
|
||||
- `git mv`가 bin/obj 폴더 때문에 일부 실패 → 해당 폴더 삭제 후 재시도
|
||||
- `EngineBridge.Client` 전체 폴더 이동이 계속 permission denied → 파일 단위로 `git mv`해서 해결
|
||||
- `Directory.Build.props`의 자동 RootNamespace와 csproj의 수동 RootNamespace 정리 (Sut.EgBim.PluginHost는 수동, Hmeg.Catalog는 수동 덮어쓰기)
|
||||
|
||||
### 2. Architecture Tests 추가
|
||||
|
||||
`tests/Recordingtest.Architecture.Tests/` — 3-tier 규칙 강제.
|
||||
|
||||
- Generic 모듈 (`Bridge.Abstractions`, `Recorder`, `Player`, `Normalizer`, `DiffReporter`, `Runner`, `SutProber`) 각각이 `HmEG.dll` 또는 `Editor03.PluginInterface` / `Editor02.HmEGAppManager` / `EditorCore` 를 **참조하지 않음** (Theory, 7건)
|
||||
- HmEG-aware 모듈 (`Hmeg.Bridge`, `Hmeg.Catalog`, `Hmeg.Bridge.Client`) 각각이 app-specific DLL을 **참조하지 않음** (3건)
|
||||
- `Hmeg.Bridge` 는 `HmEG.dll` 을 **참조함** (positive check, 1건)
|
||||
|
||||
총 11건, 모두 pass. 향후 누가 실수로 generic 모듈에 HmEG 참조를 추가하면 여기서 red.
|
||||
|
||||
### 3. engine-bridge v3 EgBim 람다 wire-up (Q1~Q7 답)
|
||||
|
||||
사용자가 `D:\GiteaAll\EG-BIM_Modeler\EditorPluginInterface` 경로 공유 → read-only survey.
|
||||
|
||||
**확정 (single-file find)**: `EditorPluginInterface/EditorPlugin.cs`
|
||||
|
||||
```csharp
|
||||
public HmEGAppManager AppManager { get => TriggerStateService.AppManager; set; }
|
||||
public Space RootSpace { get => AppManager.ViewportManager.RootSpace; }
|
||||
public ViewportManager ViewportManager { get => AppManager.ViewportManager; }
|
||||
public EGViewport View { get; set; } // deprecated but still usable
|
||||
```
|
||||
|
||||
**HmEGAppManager** (`D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\HmEGAppManager.cs`):
|
||||
- `ViewportManager` — RootSpace 진입점
|
||||
- `SelectionManager` — 중앙 선택 상태 (필요 시 hook, 현재는 walk)
|
||||
- `FileManager` — 저장 파일 경로
|
||||
- `AppModeManager` — 명령 lifecycle (Q4 후속)
|
||||
|
||||
**`EGViewport : Control, HmEGViewport`** (`HmEG/Controls/HmEGViewport.cs:43`) — 그대로 HmEG-aware provider에 넘길 수 있음.
|
||||
|
||||
**`FileManager.CurrentFile : string`** — 저장 문서 경로.
|
||||
|
||||
**Q 답 매핑**:
|
||||
- Q1 Space: `this.RootSpace` ✅
|
||||
- Q2 Viewport: `this.View` (EGViewport: HmEGViewport) ✅
|
||||
- Q3 Selection: 중앙 리스트 없음. Space walk + `ISelectable.IsSelected` (이미 구현됨)
|
||||
- Q4 Command lifecycle: `AppModeManager`, `TriggerStateService.TriggerEnded` (별도 contract)
|
||||
- Q5 Fov: `PerspectiveCamera` cast, `HmegDirectStateProvider.GetCamera`의 reflection `FieldOfView` 후보 chain이 이미 잡음
|
||||
- Q6 DocumentPath: `AppManager.FileManager.CurrentFile` ✅
|
||||
- Q7 EGViewport↔HmEGViewport: `EGViewport : Control, HmEGViewport` ✅
|
||||
|
||||
**구현**: `HmEgBridgePlugin.BuildProvider()`가 이제 실 람다 주입:
|
||||
|
||||
```csharp
|
||||
Func<Space?> spaceProvider = () => { try { return RootSpace; } catch { return null; } };
|
||||
Func<HmEGViewport?> viewportProvider = () => { try { return View; } catch { return null; } };
|
||||
Func<string?> documentPathProvider = () =>
|
||||
{
|
||||
try { var p = AppManager?.FileManager?.CurrentFile; return string.IsNullOrEmpty(p) ? null : p; }
|
||||
catch { return null; }
|
||||
};
|
||||
```
|
||||
|
||||
**csproj 추가 참조**: `Editor02.HmEGAppManager.dll` — `HmEGAppManager.FileManager.CurrentFile` 접근을 위해. app-specific tier이므로 허용 (ArchitectureTests는 이 tier를 검사하지 않음).
|
||||
|
||||
### 테스트
|
||||
|
||||
- 전체 suite: **126 tests pass** (115 → 126, +11 ArchitectureTests)
|
||||
- 구성: Recorder 26 / Player 24 / Sut.EgBim.PluginHost 21 / Normalizer 16 / Architecture 11 / DiffReporter 5 / Runner 6 / Hmeg.Catalog 6 / Hmeg.Catalog.Integration 6 / Hmeg.Bridge 5
|
||||
- 빌드/테스트 0 failures
|
||||
|
||||
### 분류 라벨 (2단계 완료 후 현재)
|
||||
|
||||
| 경로 | 계층 |
|
||||
|---|---|
|
||||
| `src/Recordingtest.Bridge.Abstractions/` | Generic |
|
||||
| `src/Recordingtest.Recorder/`, `.Player/`, `.Normalizer/`, `.DiffReporter/`, `.Runner/`, `.SutProber/`, `.DiffReporter.Cli/` | Generic |
|
||||
| `src/Hmeg/Recordingtest.Hmeg.Bridge/`, `.Catalog/`, `.Catalog.Probe/`, `.Bridge.Client/` | HmEG-aware |
|
||||
| `src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost/` | App-specific (EgBim) |
|
||||
|
||||
## 라이브 검증 (대기)
|
||||
|
||||
다음 세션의 P1:
|
||||
|
||||
1. 본 작업물을 빌드해 `EG-BIM Modeler/Plugins/Recordingtest.Sut.EgBim.PluginHost/` 아래 배포
|
||||
2. SUT 실행, 이미 열린 .hmeg 문서가 있으면 Box 등 몇 개 객체 생성 + 선택
|
||||
3. 셸에서:
|
||||
```
|
||||
curl http://localhost:38080/health
|
||||
curl http://localhost:38080/scene # object_count, document_path
|
||||
curl http://localhost:38080/camera # eye/target/up/fov 실값
|
||||
curl http://localhost:38080/selection # selected_ids
|
||||
```
|
||||
4. 실값이 기대와 다르면 `HmegDirectStateProvider.GetCamera`의 reflection 멤버 후보 또는 selection walk 로직 보정 1~2회 반복
|
||||
|
||||
## 미커밋 (다음 커밋에 통합)
|
||||
|
||||
- 3-tier 분리 2단계 전체 (수많은 파일 rename/move)
|
||||
- `Recordingtest.Architecture.Tests` 신규
|
||||
- `HmEgBridgePlugin.BuildProvider` 실 람다
|
||||
- `Editor02.HmEGAppManager.dll` 참조 추가
|
||||
- `recordingtest.sln`, `PROGRESS.md`, `PLAN.md`, 본 history
|
||||
|
||||
## 관련
|
||||
|
||||
- `CLAUDE.md §8.1`
|
||||
- `docs/contracts/generic-sut-split.md`
|
||||
- `docs/hmeg-api-survey.md`
|
||||
- `D:\GiteaAll\EG-BIM_Modeler\EditorPluginInterface\EditorPlugin.cs` (read-only)
|
||||
- `D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\HmEGAppManager.cs` (read-only)
|
||||
- `D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\SubManager\FileManager.cs` (read-only)
|
||||
70
docs/history/2026-04-09_engine-bridge-v3-scaffold.md
Normal file
70
docs/history/2026-04-09_engine-bridge-v3-scaffold.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 2026-04-09 — engine-bridge v3 scaffold (D1/D6)
|
||||
|
||||
**이슈**: #10 follow-up (engine-bridge v3)
|
||||
**소요 시간**: ~95분 (HmEG 소스 survey + 3-tier 분리 디렉티브 반영 포함)
|
||||
**Context 사용량**: input ~165k / output ~30k tokens (Opus 4.6, 1M context, 동일 세션 누적)
|
||||
|
||||
## 작업
|
||||
|
||||
`docs/contracts/engine-bridge-v3.md` Sprint Contract 작성 후 D1/D6 구현:
|
||||
|
||||
- `IAppManagerAccessor` 추상화 신설 — AppManager/ActiveDocument/ActiveViewport/Selection/Camera를 reflection 경계 뒤로 격리
|
||||
- `ReflectionAppManagerAccessor` — loaded assemblies에서 `Editor.AppManager.AppManager` 타입 + `Instance/Current/Default` static 프로퍼티 탐색, well-known 멤버 이름 후보 체인(Selection/SelectedObjects, Camera/ActiveCamera, Position/Eye, …)로 reflection lookup, vector는 `double[]` / `float[]` / `X/Y/Z` 세 가지 shape 모두 시도
|
||||
- `ReflectionEngineStateProvider` v2 stub 제거, 접근자 위임 구조로 재작성. HmEG 부재 환경(= CI)에서는 v2와 동일한 safe default 반환
|
||||
- `ReflectionEngineStateProviderTests` 9 테스트 추가 — FakeAccessor로 정상값/예외/null/HmEG 부재 폴백 커버. EgPlugin 테스트 5 → 14
|
||||
- 전체 suite green (100+ tests)
|
||||
|
||||
## 라이브 검증 대기 (D7)
|
||||
|
||||
reflection 멤버 후보 이름은 `hmeg-candidates.json` 기반 추측. SUT 라이브에서 `curl /scene /camera /selection` 응답 받아 실제 매칭 여부 확인 후 1~2회 보정 필요.
|
||||
|
||||
## 전략 pivot — Reflection → HmEG 직접 참조
|
||||
|
||||
사용자 지적: `Recordingtest.EgPlugin`은 이미 `HmEG.dll` + `Editor03.PluginInterface.dll`을 compile-time 참조 중이다 (`.csproj` 확인). 즉 reflection으로 멤버 추측할 필요가 없고, HmEG public 타입을 직접 호출하면 된다. 이식성(generic WPF)은 포기하지만 이 프로젝트는 EG-BIM Modeler 전용이므로 합리적 trade-off.
|
||||
|
||||
이에 따라 사용자 동의 하에 HmEG 소스(`D:\GiteaAll\HmEngine\HmEG\HmEG`)를 read-only로 surveyed:
|
||||
|
||||
확인된 공개 타입 (`<public-hmeg-api />` 마커 기준):
|
||||
- `ModelBase.Uid : Guid` — 영구 고유 ID, golden file 결정성의 핵심
|
||||
- `Space : ModelBase` — 문서 컨테이너. `Children`/`ItemsCount`/`Viewports`
|
||||
- `HmModel : ModelBase` — 형상 객체. `MouseDown/Enter/Leave` event (recorder hit-test 후보)
|
||||
- `HmEGViewport` (interface, namespace `HmEG`) — `CameraCore`, `Renderables`, `ViewportRectangle`
|
||||
- `IHmCamera` — Position/LookDirection/UpDirection (Fov는 PerspectiveCamera 캐스트 필요)
|
||||
- `ISelectable.IsSelected` — 노드별 (중앙 selection 리스트는 HmEG core에 없음 → Space walk + 필터)
|
||||
- `HmEG.IPlugin.View : EGViewport` — 플러그인이 로드 시 viewport 직접 주입받음
|
||||
|
||||
산출: `docs/hmeg-api-survey.md` — 발견 내용, v3 구현 방향(`HmEgDirectStateProvider` + 람다 주입), SUT-side bridge 추가 엔드포인트(`/focus`, `/hit-test`, `/command`) 설계, 미해결 7개 질문(Q1~Q7) 큐.
|
||||
|
||||
## 곁가지
|
||||
|
||||
- 사용자 질문으로 "SUT 소스 협조 wishlist" 정리 — AutomationPeer 부착, AppManager.Instance 난독화 제외, read-only 상태 API, 명령 생명주기 이벤트 등 7항목을 대화에 남김. 필요 시 `docs/sut-cooperation-wishlist.md`로 문서화.
|
||||
- 미커밋 변경 존재 (engine-bridge v3 scaffold + contract + hmeg-api-survey.md + 본 마지막 단계의 3-tier 분리 작업). 다음 세션에서 분리 refactor 완료 후 통합 커밋 예정.
|
||||
|
||||
## 아키텍처 디렉티브 — 3-tier 분리 (세션 후반)
|
||||
|
||||
사용자 디렉티브 두 가지가 연속해서 들어왔다:
|
||||
1. "처음부터 WPF 일반인지 Modeler 테스트 자동화인지 코드 분리해놓아라"
|
||||
2. "대부분의 WPF는 HmEG(3D 그래픽 엔진)을 사용하고 있으니 이점도 고려해서 테스트 자동화를 설계해라"
|
||||
|
||||
→ 즉 분리는 2-tier(generic vs SUT)가 아니라 **3-tier**:
|
||||
- **Generic** — 임의 WPF 응용
|
||||
- **HmEG-aware** — HmEG를 호스팅하는 임의 WPF 응용 (앱 미고정)
|
||||
- **App-specific** — 특정 응용 (현재 EG-BIM Modeler)
|
||||
|
||||
의존 방향: App-specific → HmEG-aware → Generic. 역참조 금지.
|
||||
|
||||
본 세션에서 한 일:
|
||||
- `CLAUDE.md §8.1` 신규 — 3-tier 규칙, 폴더 레이아웃, 강제 사항, 현재 모듈 분류표
|
||||
- `docs/contracts/generic-sut-split.md` 신규 — D1~D9 명세 (폴더/csproj 분할, 인터페이스 추출, HmegDirectStateProvider 골격, ArchitectureTests, sln 갱신)
|
||||
- `PLAN.md` — 본 refactor를 P0.5로 등록 (engine-bridge v3 진입 전 선결)
|
||||
- `PROGRESS.md` — In progress 에 해당 항목 추가
|
||||
|
||||
본 contract는 다음 세션에서 Generator가 단일 작업 단위로 실행한다. **engine-bridge v3 코드 진입은 본 분리 완료 후**. 이유: `HmegDirectStateProvider`는 HmEG-aware 계층에 들어가야 다른 SUT에서 재사용 가능.
|
||||
|
||||
## 관련
|
||||
|
||||
- `docs/contracts/engine-bridge-v3.md` (갱신 예정)
|
||||
- `docs/hmeg-api-survey.md` (신규 — 본 세션 산출)
|
||||
- `src/Recordingtest.EgPlugin/IAppManagerAccessor.cs` (신규 — CI fallback으로 유지)
|
||||
- `src/Recordingtest.EgPlugin/IEngineStateProvider.cs` (v3 1차 재작성, 다음 단계에서 HmEgDirectStateProvider 추가)
|
||||
- `tests/Recordingtest.EgPlugin.Tests/ReflectionEngineStateProviderTests.cs` (신규)
|
||||
24
docs/history/2026-04-09_readme-refresh-and-push.md
Normal file
24
docs/history/2026-04-09_readme-refresh-and-push.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 2026-04-09 — README 갱신 + 누적 커밋 push
|
||||
|
||||
**이슈**: 없음 (세션 마무리 문서 작업)
|
||||
**소요 시간**: ~5분
|
||||
**Context 사용량**: input ~15k / output ~3k tokens (Opus 4.6)
|
||||
|
||||
## 작업
|
||||
|
||||
- `README.md`를 3-tier 아키텍처 반영해 전면 갱신
|
||||
- 3계층(Generic / HmEG-aware / App-specific) 의존 방향 다이어그램 + 강제 규칙
|
||||
- 모듈 표를 계층별로 재구성 (12개 csproj, 이동 후 경로)
|
||||
- 주요 이정표 섹션: 2026-04-08 첫 E2E, Raw 시나리오 E2E, 2026-04-09 3-tier 분리, 126 tests
|
||||
- Gap 현황 (A~H 완료, Gap I deferred + Player fallback 전략)
|
||||
- 디렉터리 트리를 실제 3-tier 구조로 갱신
|
||||
- 누적 5개 커밋을 origin으로 push:
|
||||
- `70bf570` player: raw scenario replay without manual cleanup (#14)
|
||||
- `98d8014` player: active foreground wait
|
||||
- `a771352` recorder: focus poller PoC for Gap I-1 (deferred)
|
||||
- `f6b6e74` 3-tier split (step 1) + engine-bridge v3 scaffold
|
||||
- `03fb504` BREAKING: 3-tier split step 2 + engine-bridge v3 EgBim lambdas wired
|
||||
|
||||
## 관련
|
||||
|
||||
- `README.md`
|
||||
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 소스 사본을 보관하지 **않는다**. 본 문서는 외부 소스에 대한 인터페이스 추출 메모일 뿐. 코드 발췌도 시그니처/주석 수준으로만 인용한다.
|
||||
@@ -27,19 +27,35 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Runner", "src
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Runner.Tests", "tests\Recordingtest.Runner.Tests\Recordingtest.Runner.Tests.csproj", "{6F9973EA-977A-4185-AF24-4E76D9D851C8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EngineBridge", "src\Recordingtest.EngineBridge\Recordingtest.EngineBridge.csproj", "{938D464B-B810-425F-83B6-52877B584DE2}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Bridge.Abstractions", "src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj", "{E9192225-E9F6-44EB-A18E-7F61F1093DA8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EngineBridge.Probe", "src\Recordingtest.EngineBridge.Probe\Recordingtest.EngineBridge.Probe.csproj", "{B1EAD466-9C07-4C07-907C-3D5794F6689D}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Bridge", "src\Hmeg\Recordingtest.Hmeg.Bridge\Recordingtest.Hmeg.Bridge.csproj", "{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EngineBridge.Tests", "tests\Recordingtest.EngineBridge.Tests\Recordingtest.EngineBridge.Tests.csproj", "{0811AC32-E2A4-4BFD-A29A-6451F5756F10}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EgPlugin", "src\Recordingtest.EgPlugin\Recordingtest.EgPlugin.csproj", "{51D7B803-5F6E-4B78-9A5D-326F28CD934F}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hmeg", "Hmeg", "{FA0FB21B-DC6D-6187-86C3-94DFEB22505D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EngineBridge.Client", "src\Recordingtest.EngineBridge.Client\Recordingtest.EngineBridge.Client.csproj", "{45D80D0C-A8A1-4173-B28C-68F0628EE346}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Bridge.Tests", "tests\Hmeg\Recordingtest.Hmeg.Bridge.Tests\Recordingtest.Hmeg.Bridge.Tests.csproj", "{20FB4AD7-3414-436D-880C-B2D95280DA3D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EngineBridge.IntegrationTests", "tests\Recordingtest.EngineBridge.IntegrationTests\Recordingtest.EngineBridge.IntegrationTests.csproj", "{BA346F72-6F9C-4D68-9CDD-DD05F9687095}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sut", "Sut", "{79DA188A-9C91-3DBA-2827-6072BD5E3D4F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.EgPlugin.Tests", "tests\Recordingtest.EgPlugin.Tests\Recordingtest.EgPlugin.Tests.csproj", "{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EgBim", "EgBim", "{7CC28442-33DD-D811-CEDA-9CC787317768}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Sut.EgBim.PluginHost", "src\Sut\EgBim\Recordingtest.Sut.EgBim.PluginHost\Recordingtest.Sut.EgBim.PluginHost.csproj", "{0A800F25-64B6-4F05-BB8E-68E317862CED}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Catalog", "src\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj", "{23D628DC-D98D-427A-B0C0-470E70CC6DD2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Bridge.Client", "src\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj", "{4E0274C5-39C2-436E-90AA-87DD1C675B4C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Catalog.Probe", "src\Hmeg\Recordingtest.Hmeg.Catalog.Probe\Recordingtest.Hmeg.Catalog.Probe.csproj", "{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Sut.EgBim.PluginHost.Tests", "tests\Sut\EgBim\Recordingtest.Sut.EgBim.PluginHost.Tests\Recordingtest.Sut.EgBim.PluginHost.Tests.csproj", "{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Catalog.Tests", "tests\Hmeg\Recordingtest.Hmeg.Catalog.Tests\Recordingtest.Hmeg.Catalog.Tests.csproj", "{A9894277-E1F3-4B86-AAE4-041116FBBE1D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Hmeg.Catalog.IntegrationTests", "tests\Hmeg\Recordingtest.Hmeg.Catalog.IntegrationTests\Recordingtest.Hmeg.Catalog.IntegrationTests.csproj", "{3D981C63-0D1E-466C-9BD6-3DAF46936A45}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recordingtest.Architecture.Tests", "tests\Recordingtest.Architecture.Tests\Recordingtest.Architecture.Tests.csproj", "{D35B233B-267B-40DB-87EF-689AEE5C9399}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -195,90 +211,138 @@ Global
|
||||
{6F9973EA-977A-4185-AF24-4E76D9D851C8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{6F9973EA-977A-4185-AF24-4E76D9D851C8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6F9973EA-977A-4185-AF24-4E76D9D851C8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{938D464B-B810-425F-83B6-52877B584DE2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10}.Release|x86.Build.0 = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|x64.Build.0 = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095}.Release|x86.Build.0 = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED}.Release|x86.Build.0 = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -295,12 +359,19 @@ Global
|
||||
{7A5C0D53-BDFC-4AF6-8F4D-49E7EB8245F5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{DADF0474-9EF3-4E8D-8139-93504E4F745D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{6F9973EA-977A-4185-AF24-4E76D9D851C8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{938D464B-B810-425F-83B6-52877B584DE2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{B1EAD466-9C07-4C07-907C-3D5794F6689D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{0811AC32-E2A4-4BFD-A29A-6451F5756F10} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{51D7B803-5F6E-4B78-9A5D-326F28CD934F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{45D80D0C-A8A1-4173-B28C-68F0628EE346} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{BA346F72-6F9C-4D68-9CDD-DD05F9687095} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{315F3B4F-BF8F-4DBF-8F06-CAF55152725D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{E9192225-E9F6-44EB-A18E-7F61F1093DA8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{33D35B3C-9572-432F-8675-6AD7CDF1C0EB} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{FA0FB21B-DC6D-6187-86C3-94DFEB22505D} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{20FB4AD7-3414-436D-880C-B2D95280DA3D} = {FA0FB21B-DC6D-6187-86C3-94DFEB22505D}
|
||||
{79DA188A-9C91-3DBA-2827-6072BD5E3D4F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{7CC28442-33DD-D811-CEDA-9CC787317768} = {79DA188A-9C91-3DBA-2827-6072BD5E3D4F}
|
||||
{0A800F25-64B6-4F05-BB8E-68E317862CED} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{23D628DC-D98D-427A-B0C0-470E70CC6DD2} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{4E0274C5-39C2-436E-90AA-87DD1C675B4C} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{A5765A50-21FC-4BC6-97E6-3FE3A1AE6008} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{5D5C57B2-D9BC-4E27-8EB1-49FE2FD78207} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{A9894277-E1F3-4B86-AAE4-041116FBBE1D} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{3D981C63-0D1E-466C-9BD6-3DAF46936A45} = {7CC28442-33DD-D811-CEDA-9CC787317768}
|
||||
{D35B233B-267B-40DB-87EF-689AEE5C9399} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Recordingtest.EngineBridge.Client;
|
||||
namespace Recordingtest.Hmeg.Bridge.Client;
|
||||
|
||||
public sealed class EngineBridgeException : Exception
|
||||
{
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using Recordingtest.Hmeg.Catalog;
|
||||
|
||||
namespace Recordingtest.EngineBridge.Client;
|
||||
namespace Recordingtest.Hmeg.Bridge.Client;
|
||||
|
||||
public sealed class HmEgHttpSnapshot : IEngineSnapshot, IDisposable
|
||||
{
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>Recordingtest.Hmeg.Bridge.Client</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Bridge.Client</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
212
src/Hmeg/Recordingtest.Hmeg.Bridge/HmegDirectStateProvider.cs
Normal file
212
src/Hmeg/Recordingtest.Hmeg.Bridge/HmegDirectStateProvider.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using HmEG;
|
||||
using Recordingtest.Bridge;
|
||||
|
||||
namespace Recordingtest.Hmeg.Bridge;
|
||||
|
||||
/// <summary>
|
||||
/// HmEG-aware <see cref="IEngineStateProvider"/> backed by direct calls into
|
||||
/// the HmEG public API. Reusable across any WPF application that hosts HmEG.
|
||||
///
|
||||
/// The provider is decoupled from the *host* application via two lambdas
|
||||
/// supplied at construction:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><c>spaceProvider</c> — returns the active <see cref="Space"/> tree (root of the scene/document).</item>
|
||||
/// <item><c>viewportProvider</c> — returns the active <see cref="HmEGViewport"/> (camera + renderables).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// App-specific glue (e.g. <c>Recordingtest.Sut.EgBim.*</c>) is responsible for
|
||||
/// resolving those handles from its own <c>AppManager</c> and passing them in.
|
||||
/// This keeps the bridge usable for *any* HmEG-hosting WPF app without
|
||||
/// recompilation.
|
||||
///
|
||||
/// All accessors are best-effort: any exception is swallowed and the method
|
||||
/// returns the same safe default a <see cref="NullEngineStateProvider"/> would.
|
||||
/// The plugin runs in-process inside the SUT and must never throw.
|
||||
/// </summary>
|
||||
public sealed class HmegDirectStateProvider : IEngineStateProvider
|
||||
{
|
||||
private readonly Func<Space?> _spaceProvider;
|
||||
private readonly Func<HmEGViewport?> _viewportProvider;
|
||||
private readonly Func<string?>? _documentPathProvider;
|
||||
|
||||
public HmegDirectStateProvider(
|
||||
Func<Space?> spaceProvider,
|
||||
Func<HmEGViewport?> viewportProvider,
|
||||
Func<string?>? documentPathProvider = null)
|
||||
{
|
||||
_spaceProvider = spaceProvider ?? throw new ArgumentNullException(nameof(spaceProvider));
|
||||
_viewportProvider = viewportProvider ?? throw new ArgumentNullException(nameof(viewportProvider));
|
||||
_documentPathProvider = documentPathProvider;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
try
|
||||
{
|
||||
var space = _spaceProvider();
|
||||
if (space is null) return Array.Empty<string>();
|
||||
var ids = new List<string>();
|
||||
CollectSelectedRecursive(space, ids);
|
||||
return ids;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks the Space tree and collects the <c>Uid</c> of every node whose
|
||||
/// <see cref="HmEG.ISelectable.IsSelected"/> is true. HmEG does not
|
||||
/// expose a centralized selection list in core; this is the canonical
|
||||
/// traversal pattern.
|
||||
///
|
||||
/// We deliberately type the node parameter as <see cref="object"/> rather
|
||||
/// than <c>HmEG.ModelBase</c> so this assembly does not have to reference
|
||||
/// MemoryPack.Core (a serialization dependency that <c>ModelBase</c>
|
||||
/// transitively pulls in via its attributes). The runtime shape we rely
|
||||
/// on is just <c>ISelectable</c> + <c>Uid</c> + <c>Children</c>, all of
|
||||
/// which are read by reflection-free pattern matching.
|
||||
/// </summary>
|
||||
private static void CollectSelectedRecursive(object node, List<string> ids)
|
||||
{
|
||||
if (node is HmEG.ISelectable sel && sel.IsSelected)
|
||||
{
|
||||
// Read Uid via late-bound property access — avoids the ModelBase
|
||||
// type reference and survives any future field-vs-property change.
|
||||
var uid = node.GetType().GetProperty("Uid")?.GetValue(node);
|
||||
if (uid is not null) ids.Add(uid.ToString() ?? string.Empty);
|
||||
}
|
||||
if (node is Space space)
|
||||
{
|
||||
foreach (var child in space.Children)
|
||||
{
|
||||
if (child is not null) CollectSelectedRecursive(child, ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CameraSnapshot GetCamera()
|
||||
{
|
||||
try
|
||||
{
|
||||
var vp = _viewportProvider();
|
||||
var core = vp?.CameraCore;
|
||||
if (core is null) return Default.GetCamera();
|
||||
|
||||
// CameraCore is the abstract base; common shapes (Position, LookDirection,
|
||||
// UpDirection) come from IHmCamera-like contracts. We use late-binding via
|
||||
// reflection on the concrete CameraCore subclass to stay tolerant of
|
||||
// PerspectiveCamera vs OrthographicCamera vs MatrixCamera variants.
|
||||
//
|
||||
// This is the *only* reflection in the HmEG-aware tier and it sits behind
|
||||
// a try/catch — failure mode is a default snapshot.
|
||||
var t = core.GetType();
|
||||
double[] eye = ReadVec3(core, t, new[] { "Position", "Eye" });
|
||||
double[] look = ReadVec3(core, t, new[] { "LookDirection", "Direction" });
|
||||
double[] up = ReadVec3(core, t, new[] { "UpDirection", "Up" });
|
||||
double fov = ReadDouble(core, t, new[] { "FieldOfView", "Fov", "FOV" }, fallback: 45.0);
|
||||
|
||||
// Target = Eye + LookDirection (HmEG stores look as a direction vector,
|
||||
// not as an explicit target point).
|
||||
var target = new double[]
|
||||
{
|
||||
eye[0] + look[0],
|
||||
eye[1] + look[1],
|
||||
eye[2] + look[2],
|
||||
};
|
||||
|
||||
return new CameraSnapshot(eye, target, up, fov);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Default.GetCamera();
|
||||
}
|
||||
}
|
||||
|
||||
public SceneSnapshot GetScene()
|
||||
{
|
||||
try
|
||||
{
|
||||
var space = _spaceProvider();
|
||||
int count = space?.ItemsCount ?? 0;
|
||||
string? path = null;
|
||||
try { path = _documentPathProvider?.Invoke(); } catch { /* best-effort */ }
|
||||
return new SceneSnapshot(count, path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new SceneSnapshot(0, null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetRenderComplete()
|
||||
{
|
||||
// HmEG core does not expose a stable "frame complete" signal we can
|
||||
// poll without subscribing to a render-host event. Treat as always
|
||||
// ready until a Hmeg.Bridge follow-up wires the event.
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly NullEngineStateProvider Default = new();
|
||||
|
||||
private static double[] ReadVec3(object owner, Type t, string[] names)
|
||||
{
|
||||
foreach (var n in names)
|
||||
{
|
||||
object? v = null;
|
||||
try
|
||||
{
|
||||
var p = t.GetProperty(n);
|
||||
if (p is not null) v = p.GetValue(owner);
|
||||
if (v is null)
|
||||
{
|
||||
var f = t.GetField(n);
|
||||
if (f is not null) v = f.GetValue(owner);
|
||||
}
|
||||
}
|
||||
catch { /* try next */ }
|
||||
if (v is null) continue;
|
||||
|
||||
// Common shapes: HmVector3D / Vector3 / double[] / float[]
|
||||
if (v is double[] da && da.Length >= 3) return new[] { da[0], da[1], da[2] };
|
||||
if (v is float[] fa && fa.Length >= 3) return new[] { (double)fa[0], fa[1], fa[2] };
|
||||
|
||||
try
|
||||
{
|
||||
double Read(string memberName)
|
||||
{
|
||||
var vt = v.GetType();
|
||||
var pp = vt.GetProperty(memberName);
|
||||
if (pp is not null) return Convert.ToDouble(pp.GetValue(v));
|
||||
var ff = vt.GetField(memberName);
|
||||
if (ff is not null) return Convert.ToDouble(ff.GetValue(v));
|
||||
return 0;
|
||||
}
|
||||
return new[] { Read("X"), Read("Y"), Read("Z") };
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to next candidate
|
||||
}
|
||||
}
|
||||
return new double[] { 0, 0, 0 };
|
||||
}
|
||||
|
||||
private static double ReadDouble(object owner, Type t, string[] names, double fallback)
|
||||
{
|
||||
foreach (var n in names)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = t.GetProperty(n);
|
||||
if (p is not null) return Convert.ToDouble(p.GetValue(owner));
|
||||
var f = t.GetField(n);
|
||||
if (f is not null) return Convert.ToDouble(f.GetValue(owner));
|
||||
}
|
||||
catch { /* try next */ }
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>Recordingtest.Hmeg.Bridge</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- HmEG-aware tier may reference HmEG.dll only. App-specific assemblies
|
||||
(e.g. Editor03.PluginInterface.dll) are forbidden here — they live
|
||||
in src/Sut/<App>/. -->
|
||||
<Reference Include="HmEG">
|
||||
<HintPath>..\..\..\EG-BIM Modeler\HmEG.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Recordingtest.EngineBridge;
|
||||
using Recordingtest.Hmeg.Catalog;
|
||||
|
||||
namespace Recordingtest.EngineBridge.Probe;
|
||||
namespace Recordingtest.Hmeg.Catalog.Probe;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog.Probe</AssemblyName>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog.Probe</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Recordingtest.EngineBridge;
|
||||
namespace Recordingtest.Hmeg.Catalog;
|
||||
|
||||
public sealed record Candidate(
|
||||
string Category,
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Recordingtest.EngineBridge;
|
||||
namespace Recordingtest.Hmeg.Catalog;
|
||||
|
||||
public sealed record TypeEntry(string Assembly, string TypeName, bool IsPublic, string Namespace);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Recordingtest.EngineBridge;
|
||||
namespace Recordingtest.Hmeg.Catalog;
|
||||
|
||||
/// <summary>
|
||||
/// Skeleton implementation of <see cref="IEngineSnapshot"/> for HmEG.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Recordingtest.EngineBridge;
|
||||
namespace Recordingtest.Hmeg.Catalog;
|
||||
|
||||
public interface IEngineSnapshot
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Recordingtest.EngineBridge;
|
||||
namespace Recordingtest.Hmeg.Catalog;
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around <see cref="MetadataLoadContext"/>. This class is
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Recordingtest.EngineBridge</AssemblyName>
|
||||
<RootNamespace>Recordingtest.EngineBridge</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog</AssemblyName>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Recordingtest.Bridge;
|
||||
|
||||
/// <summary>
|
||||
/// Generic, SUT-neutral abstraction for reading the current engine state of
|
||||
/// a WPF application under test. Implementations may delegate to a SUT-side
|
||||
/// in-process bridge (HmEG-aware tier), to a reflection probe, or to a stub.
|
||||
///
|
||||
/// This interface lives in <c>Recordingtest.Bridge.Abstractions</c> so that
|
||||
/// every higher tier (HmEG-aware / app-specific) can target it without the
|
||||
/// generic core ever seeing a SUT-specific symbol.
|
||||
/// </summary>
|
||||
public interface IEngineStateProvider
|
||||
{
|
||||
IReadOnlyList<string> GetSelectedIds();
|
||||
CameraSnapshot GetCamera();
|
||||
SceneSnapshot GetScene();
|
||||
bool GetRenderComplete();
|
||||
}
|
||||
|
||||
public sealed record CameraSnapshot(double[] Eye, double[] Target, double[] Up, double Fov);
|
||||
public sealed record SceneSnapshot(int ObjectCount, string? DocumentPath);
|
||||
|
||||
/// <summary>
|
||||
/// Safe-default provider used when no real SUT bridge is available
|
||||
/// (CI / unit tests / startup race window).
|
||||
/// </summary>
|
||||
public sealed class NullEngineStateProvider : IEngineStateProvider
|
||||
{
|
||||
public IReadOnlyList<string> GetSelectedIds() => Array.Empty<string>();
|
||||
public CameraSnapshot GetCamera() => new(
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 1 },
|
||||
45.0);
|
||||
public SceneSnapshot GetScene() => new(0, null);
|
||||
public bool GetRenderComplete() => true;
|
||||
}
|
||||
@@ -4,9 +4,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>Recordingtest.EngineBridge.Client</RootNamespace>
|
||||
<RootNamespace>Recordingtest.Bridge</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recordingtest.EngineBridge\Recordingtest.EngineBridge.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,48 +0,0 @@
|
||||
using Editor.PluginInterface;
|
||||
|
||||
namespace Recordingtest.EgPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// MEF/PluginLoader-discovered plugin. Inherits the SUT's <c>EditorPlugin</c>
|
||||
/// abstract base (which itself implements <c>HmEG.IPlugin</c>), and on construction
|
||||
/// boots a localhost HTTP bridge that exposes HmEG state to recordingtest.
|
||||
/// </summary>
|
||||
public sealed class HmEgBridgePlugin : EditorPlugin, IDisposable
|
||||
{
|
||||
private BridgeHttpServer? _server;
|
||||
|
||||
public HmEgBridgePlugin()
|
||||
{
|
||||
StartBridge();
|
||||
}
|
||||
|
||||
public override string Name => "Recordingtest.EgPlugin";
|
||||
public override string Description => "recordingtest engine-bridge v2 (HTTP sidecar)";
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
// Construction already started the bridge; Initialize is a no-op safeguard.
|
||||
}
|
||||
|
||||
private void StartBridge()
|
||||
{
|
||||
try
|
||||
{
|
||||
var port = PortResolver.Resolve();
|
||||
var provider = new ReflectionEngineStateProvider(this);
|
||||
var router = new StateRouter(provider, port);
|
||||
_server = new BridgeHttpServer(router, port);
|
||||
_server.Start();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// never throw out of plugin construction; SUT must remain stable.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { _server?.Dispose(); } catch { }
|
||||
_server = null;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
namespace Recordingtest.EgPlugin;
|
||||
|
||||
public interface IEngineStateProvider
|
||||
{
|
||||
IReadOnlyList<string> GetSelectedIds();
|
||||
CameraSnapshot GetCamera();
|
||||
SceneSnapshot GetScene();
|
||||
bool GetRenderComplete();
|
||||
}
|
||||
|
||||
public sealed record CameraSnapshot(double[] Eye, double[] Target, double[] Up, double Fov);
|
||||
public sealed record SceneSnapshot(int ObjectCount, string? DocumentPath);
|
||||
|
||||
public sealed class NullEngineStateProvider : IEngineStateProvider
|
||||
{
|
||||
public IReadOnlyList<string> GetSelectedIds() => Array.Empty<string>();
|
||||
public CameraSnapshot GetCamera() => new(new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 45.0);
|
||||
public SceneSnapshot GetScene() => new(0, null);
|
||||
public bool GetRenderComplete() => true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skeleton reflection-based provider. v2 returns safe defaults; real HmEG mapping happens in v3 once SUT smoke tests confirm field shapes.
|
||||
/// </summary>
|
||||
public sealed class ReflectionEngineStateProvider : IEngineStateProvider
|
||||
{
|
||||
private readonly object? _appManager;
|
||||
|
||||
public ReflectionEngineStateProvider(object? appManager)
|
||||
{
|
||||
_appManager = appManager;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
try { _ = _appManager; return Array.Empty<string>(); }
|
||||
catch { return Array.Empty<string>(); }
|
||||
}
|
||||
|
||||
public CameraSnapshot GetCamera()
|
||||
{
|
||||
try { return new(new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 45.0); }
|
||||
catch { return new(new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 45.0); }
|
||||
}
|
||||
|
||||
public SceneSnapshot GetScene()
|
||||
{
|
||||
try { return new(0, null); }
|
||||
catch { return new(0, null); }
|
||||
}
|
||||
|
||||
public bool GetRenderComplete()
|
||||
{
|
||||
try { return true; } catch { return false; }
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>Recordingtest.EgPlugin</RootNamespace>
|
||||
<EnableDefaultItems>true</EnableDefaultItems>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Editor03.PluginInterface">
|
||||
<HintPath>..\..\EG-BIM Modeler\Editor03.PluginInterface.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="HmEG">
|
||||
<HintPath>..\..\EG-BIM Modeler\HmEG.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>Recordingtest.EngineBridge.Probe</AssemblyName>
|
||||
<RootNamespace>Recordingtest.EngineBridge.Probe</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recordingtest.EngineBridge\Recordingtest.EngineBridge.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -25,4 +25,9 @@ public interface IPlayerHost
|
||||
|
||||
void CaptureCheckpoint(int afterStep, string saveAs);
|
||||
void CaptureFailureArtifacts(int stepIndex, string reason);
|
||||
|
||||
// Issue #14: delay between steps. Kept on the host (not in the engine)
|
||||
// because PlayerEngine contract forbids fixed sleeps; the host is free
|
||||
// to implement real time or a virtual clock for tests.
|
||||
void Delay(TimeSpan duration);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ public sealed class Step
|
||||
public string? WaitFor { get; set; }
|
||||
public int? AfterStep { get; set; }
|
||||
public string? SaveAs { get; set; }
|
||||
// Issue #14: recorder-captured screen-absolute coordinates used as
|
||||
// fallback when Target is null (Click). Optional; null for non-mouse steps.
|
||||
public int[]? RawCoord { get; set; }
|
||||
// Issue #14: recorder-captured absolute timestamp (ms). Used by the
|
||||
// engine to preserve inter-step pacing during playback.
|
||||
public long? Ts { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Target
|
||||
|
||||
@@ -6,6 +6,12 @@ public sealed class PlayerEngineOptions
|
||||
{
|
||||
public TimeSpan ResolveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public TimeSpan WaitForTimeout { get; set; } = TimeSpan.FromSeconds(15);
|
||||
|
||||
// Issue #14: preserve recorded inter-step delays (clamped). When true the
|
||||
// engine sleeps step.Ts - prevStep.Ts between steps, bounded by Min/Max.
|
||||
public bool PreserveTiming { get; set; } = true;
|
||||
public TimeSpan MinStepDelay { get; set; } = TimeSpan.FromMilliseconds(150);
|
||||
public TimeSpan MaxStepDelay { get; set; } = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
|
||||
public sealed class PlayerEngine
|
||||
@@ -22,9 +28,51 @@ public sealed class PlayerEngine
|
||||
ArgumentNullException.ThrowIfNull(scenario);
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
|
||||
for (int i = 0; i < scenario.Steps.Count; i++)
|
||||
// Issue #14: strip leading alt+tab hotkey steps. These are recording
|
||||
// startup noise (user tabbing from their editor into the SUT at the
|
||||
// start of the session). At replay time the player already puts the
|
||||
// SUT in the foreground, so re-running alt+tab here just switches
|
||||
// focus AWAY from the SUT and breaks subsequent Type steps.
|
||||
int start = 0;
|
||||
while (start < scenario.Steps.Count)
|
||||
{
|
||||
var s = scenario.Steps[start];
|
||||
if (s.Kind == StepKind.Hotkey &&
|
||||
string.Equals(s.Value, "alt+tab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[player] info: skipping leading alt+tab step {start} (issue #14)");
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Seed prevTs so the FIRST executed step also gets a pre-delay
|
||||
// (MinStepDelay). Without this, step 2's Type can fire before the
|
||||
// SUT has fully settled after foreground switch.
|
||||
long? prevTs = start < scenario.Steps.Count && scenario.Steps[start].Ts is long firstTs
|
||||
? firstTs - (long)_options.MinStepDelay.TotalMilliseconds
|
||||
: null;
|
||||
for (int i = start; i < scenario.Steps.Count; i++)
|
||||
{
|
||||
var step = scenario.Steps[i];
|
||||
|
||||
if (_options.PreserveTiming && step.Ts is long ts)
|
||||
{
|
||||
if (prevTs is long p)
|
||||
{
|
||||
var delta = ts - p;
|
||||
if (delta < _options.MinStepDelay.TotalMilliseconds)
|
||||
delta = (long)_options.MinStepDelay.TotalMilliseconds;
|
||||
if (delta > _options.MaxStepDelay.TotalMilliseconds)
|
||||
delta = (long)_options.MaxStepDelay.TotalMilliseconds;
|
||||
host.Delay(TimeSpan.FromMilliseconds(delta));
|
||||
}
|
||||
prevTs = ts;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[player] step {i} kind={step.Kind} value={step.Value ?? ""}");
|
||||
try
|
||||
{
|
||||
ExecuteStep(i, step, host);
|
||||
@@ -70,13 +118,30 @@ public sealed class PlayerEngine
|
||||
}
|
||||
else if (StepRequiresTarget(step.Kind))
|
||||
{
|
||||
// Issue #11: recorder may emit Click/Drag/Type/Focus steps with
|
||||
// null target. Never click/drag/type at (0,0) on the desktop —
|
||||
// skip with a warning instead.
|
||||
// Issue #14: recorder emits Type/Click with null target when the
|
||||
// focused element / UIA path at record time could not be resolved
|
||||
// (e.g. typing into a CommandBox before any mouse click, clicks on
|
||||
// canvas children that don't expose AutomationId). Fall back to:
|
||||
// - Type → send keystrokes to whatever currently has focus
|
||||
// - Click → use recorded raw_coord (screen-absolute) directly
|
||||
// This mirrors the manual cleanup that produced box-v5-clean.yaml.
|
||||
if (step.Kind == StepKind.Type)
|
||||
{
|
||||
}
|
||||
else if (step.Kind == StepKind.Click
|
||||
&& step.RawCoord is { Length: >= 2 })
|
||||
{
|
||||
point = new ScreenPoint(step.RawCoord[0], step.RawCoord[1]);
|
||||
Console.WriteLine(
|
||||
$"[player] warn: skipping step {index} kind={step.Kind} — target is null (issue #11)");
|
||||
$"[player] info: step {index} kind=Click null target — using raw_coord ({point.X},{point.Y}) (issue #14)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[player] warn: skipping step {index} kind={step.Kind} — target is null and no fallback (issue #14)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (step.Kind)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,13 @@ else
|
||||
}
|
||||
|
||||
using var host = new UiaPlayerHost(app, artifactDir);
|
||||
|
||||
// Issue #14: ensure SUT is the foreground window before playback so that
|
||||
// keystrokes (Type/Hotkey) land on the SUT instead of whatever shell the
|
||||
// player was launched from (PowerShell, VS Code, etc.). Without this, the
|
||||
// very first "BOX" type step gets typed into the launching terminal.
|
||||
host.BringSutToForeground();
|
||||
|
||||
var engine = new PlayerEngine();
|
||||
try
|
||||
{
|
||||
|
||||
@@ -206,6 +206,54 @@ public sealed class UiaPlayerHost : IPlayerHost, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Issue #14 — force the SUT main window to foreground and give it keyboard
|
||||
/// focus before playback starts. Handles the common case where the player
|
||||
/// was launched from a shell that still has focus when the first Type/Hotkey
|
||||
/// step fires.
|
||||
/// </summary>
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
public void BringSutToForeground()
|
||||
{
|
||||
try
|
||||
{
|
||||
var w = _app?.GetMainWindow(_automation, TimeSpan.FromSeconds(5));
|
||||
if (w is null) return;
|
||||
var targetHwnd = w.Properties.NativeWindowHandle.ValueOrDefault;
|
||||
try { w.SetForeground(); } catch { /* best-effort */ }
|
||||
try { w.Focus(); } catch { /* best-effort */ }
|
||||
|
||||
// Issue #14 follow-up: active wait instead of fixed 600ms sleep.
|
||||
// Poll until the OS reports the SUT as the foreground window, up
|
||||
// to 2s. Previously a 600ms fixed sleep was threshold-sensitive
|
||||
// and caused the first "BOX" keystroke to get dropped on a cold
|
||||
// first run.
|
||||
var deadline = DateTime.UtcNow.AddSeconds(2);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (targetHwnd != IntPtr.Zero && GetForegroundWindow() == targetHwnd)
|
||||
break;
|
||||
System.Threading.Thread.Sleep(25);
|
||||
}
|
||||
// Tiny additional settle for the OS keyboard-focus IPC to finish
|
||||
// after the foreground transition is observed.
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort; if this fails the user will see the BOX text land
|
||||
// in the wrong window and can re-run with the SUT focused manually.
|
||||
}
|
||||
}
|
||||
|
||||
public void Delay(TimeSpan duration)
|
||||
{
|
||||
if (duration > TimeSpan.Zero)
|
||||
System.Threading.Thread.Sleep(duration);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_automation.Dispose();
|
||||
|
||||
@@ -35,6 +35,11 @@ public sealed class DragCollapser
|
||||
var typeBuf = new System.Text.StringBuilder();
|
||||
RawEvent? typeFirst = null;
|
||||
UiaResolution? typeRes = null;
|
||||
// Issue #14 Gap I-1: path captured directly from the focus poller at
|
||||
// the first key_down of a type burst. Takes precedence over the older
|
||||
// lastFocusPath / lastMousePath fallbacks because it's pinned to the
|
||||
// instant the user actually started typing.
|
||||
string? typeFocusPath = null;
|
||||
// Active modifiers (ctrl/shift/alt/win) held down.
|
||||
var modsDown = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
@@ -69,7 +74,7 @@ public sealed class DragCollapser
|
||||
}
|
||||
else
|
||||
{
|
||||
var fallback = lastFocusPath ?? lastMousePath;
|
||||
var fallback = typeFocusPath ?? lastFocusPath ?? lastMousePath;
|
||||
if (!string.IsNullOrEmpty(fallback))
|
||||
{
|
||||
step.Target = new ScenarioTarget
|
||||
@@ -83,6 +88,7 @@ public sealed class DragCollapser
|
||||
typeBuf.Clear();
|
||||
typeFirst = null;
|
||||
typeRes = null;
|
||||
typeFocusPath = null;
|
||||
}
|
||||
|
||||
foreach (var ev in events)
|
||||
@@ -265,6 +271,9 @@ public sealed class DragCollapser
|
||||
{
|
||||
typeFirst = ev;
|
||||
typeRes = res;
|
||||
// Issue #14 Gap I-1: capture focused-element path
|
||||
// snapshotted by the poller at key_down time.
|
||||
typeFocusPath = ev.FocusedElementPath;
|
||||
}
|
||||
typeBuf.Append(tr.Text);
|
||||
break;
|
||||
|
||||
@@ -27,6 +27,14 @@ public sealed class LowLevelHook : IDisposable
|
||||
/// </summary>
|
||||
public IWindowFilter Filter { get; set; } = new PassThroughWindowFilter();
|
||||
|
||||
/// <summary>
|
||||
/// Issue #14 Gap I-1 — latest UIA focused-element path observed by a
|
||||
/// background poller. Stamped onto key_down RawEvents so the collapser
|
||||
/// can assign a target to the resulting Type step without relying on
|
||||
/// the stale post-hoc Resolve() pass. Null until the first poll.
|
||||
/// </summary>
|
||||
public volatile string? CurrentFocusedPath;
|
||||
|
||||
public LowLevelHook(Channel<RawEvent> channel)
|
||||
{
|
||||
_channel = channel;
|
||||
@@ -83,7 +91,8 @@ public sealed class LowLevelHook : IDisposable
|
||||
NativeMethods.WM_KEYUP or NativeMethods.WM_SYSKEYUP => "key_up",
|
||||
_ => "key",
|
||||
};
|
||||
var ev = new RawEvent(NowMs(), kind, 0, 0, data.vkCode, 0);
|
||||
var ev = new RawEvent(
|
||||
NowMs(), kind, 0, 0, data.vkCode, 0, CurrentFocusedPath);
|
||||
if (Filter.ShouldKeep(ev))
|
||||
{
|
||||
_channel.Writer.TryWrite(ev);
|
||||
|
||||
@@ -155,6 +155,63 @@ public static class Program
|
||||
Console.Error.WriteLine($"[recorder] focus subscribe failed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Issue #14 Gap I-1 — background focus poller. Periodically queries
|
||||
// Automation.FocusedElement() and publishes the element path into
|
||||
// LowLevelHook.CurrentFocusedPath, so key_down events captured on the
|
||||
// hook thread can be stamped with the live focused-element path at
|
||||
// the exact instant the user started typing. This catches custom WPF
|
||||
// controls (e.g. CommandBox) that do NOT raise UIA focus_changed
|
||||
// events reliably.
|
||||
var pollerCts = new CancellationTokenSource();
|
||||
Task? pollerTask = null;
|
||||
int pollerSuccess = 0;
|
||||
int pollerNullFocus = 0;
|
||||
int pollerWrongPid = 0;
|
||||
int pollerErrors = 0;
|
||||
string? pollerLastError = null;
|
||||
if (automation is not null)
|
||||
{
|
||||
var auto = automation;
|
||||
var pid = sutPid;
|
||||
pollerTask = Task.Run(async () =>
|
||||
{
|
||||
while (!pollerCts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var focused = auto.FocusedElement();
|
||||
if (focused is null)
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref pollerNullFocus);
|
||||
}
|
||||
else
|
||||
{
|
||||
int elPid = 0;
|
||||
try { elPid = focused.Properties.ProcessId.ValueOrDefault; }
|
||||
catch { elPid = 0; }
|
||||
if (pid == 0 || elPid == pid)
|
||||
{
|
||||
var snap = new FlaUiSnapshot(focused);
|
||||
hook.CurrentFocusedPath = ElementPathBuilder.Build(snap);
|
||||
System.Threading.Interlocked.Increment(ref pollerSuccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref pollerWrongPid);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Threading.Interlocked.Increment(ref pollerErrors);
|
||||
pollerLastError = ex.GetType().Name + ": " + ex.Message;
|
||||
}
|
||||
try { await Task.Delay(100, pollerCts.Token); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
}
|
||||
}, pollerCts.Token);
|
||||
}
|
||||
|
||||
Console.WriteLine("[recorder] capturing... press Ctrl+C to stop.");
|
||||
int eventCount = 0;
|
||||
int unresolvedPaths = 0; // resolver ran but returned null
|
||||
@@ -174,12 +231,20 @@ public static class Program
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Stop the focus poller before UIA teardown.
|
||||
pollerCts.Cancel();
|
||||
try { pollerTask?.Wait(500); } catch { /* ignore */ }
|
||||
|
||||
// Collapse buffered raw events into scenario steps via DragCollapser.
|
||||
var collapser = new DragCollapser();
|
||||
UiaResolution? Resolve(RawEvent ev)
|
||||
{
|
||||
// Key events have no meaningful coordinate — resolver cannot attempt
|
||||
// a point-based lookup. Count them separately from genuine misses.
|
||||
// Issue #14 Gap I-1 — key events: Resolve() runs at collapse time
|
||||
// (after the recording ended), so querying FocusedElement() HERE
|
||||
// would be stale (focus has already left the SUT). The focused
|
||||
// element path is captured at key_down time by the FocusPoller
|
||||
// and baked into RawEvent.FocusedElementPath. DragCollapser reads
|
||||
// that directly; Resolve() simply returns null for key events.
|
||||
if (ev.Kind == "key_down" || ev.Kind == "key_up")
|
||||
{
|
||||
noResolverAttempt++;
|
||||
@@ -225,6 +290,11 @@ public static class Program
|
||||
$"[recorder] done. events={eventCount} elapsed={sw.Elapsed} " +
|
||||
$"unresolved_paths={unresolvedPaths} no_resolver_attempt={noResolverAttempt} " +
|
||||
$"null_target_steps={nullTargetSteps}");
|
||||
Console.WriteLine(
|
||||
$"[recorder] focus_poller success={pollerSuccess} null_focus={pollerNullFocus} " +
|
||||
$"wrong_pid={pollerWrongPid} errors={pollerErrors} last_path={hook.CurrentFocusedPath ?? "<null>"}");
|
||||
if (pollerLastError is not null)
|
||||
Console.WriteLine($"[recorder] focus_poller last_error: {pollerLastError}");
|
||||
|
||||
automation?.Dispose();
|
||||
return 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace Recordingtest.EgPlugin;
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// Hosts an HttpListener that delegates path routing to <see cref="StateRouter"/>.
|
||||
@@ -0,0 +1,54 @@
|
||||
using Recordingtest.Bridge;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// Tries the primary provider first; if it returns the empty/default value
|
||||
/// (e.g. <c>HmegDirectStateProvider</c> with unresolved lambdas) the fallback
|
||||
/// provider is consulted. This lets us land the HmEG-aware path now while
|
||||
/// still benefiting from the reflection fallback when EgBim AppManager
|
||||
/// entry-point wiring is incomplete.
|
||||
///
|
||||
/// "Empty/default" is detected per signal:
|
||||
/// - SelectedIds: empty list → try fallback
|
||||
/// - Camera : Up == (0,0,1) AND Eye == (0,0,0) → try fallback
|
||||
/// - Scene : ObjectCount == 0 AND DocumentPath == null → try fallback
|
||||
/// - RenderComplete: primary always wins (boolean)
|
||||
/// </summary>
|
||||
public sealed class ChainedEngineStateProvider : IEngineStateProvider
|
||||
{
|
||||
private readonly IEngineStateProvider _primary;
|
||||
private readonly IEngineStateProvider _fallback;
|
||||
|
||||
public ChainedEngineStateProvider(IEngineStateProvider primary, IEngineStateProvider fallback)
|
||||
{
|
||||
_primary = primary;
|
||||
_fallback = fallback;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
var p = _primary.GetSelectedIds();
|
||||
return p.Count > 0 ? p : _fallback.GetSelectedIds();
|
||||
}
|
||||
|
||||
public CameraSnapshot GetCamera()
|
||||
{
|
||||
var p = _primary.GetCamera();
|
||||
return IsDefault(p) ? _fallback.GetCamera() : p;
|
||||
}
|
||||
|
||||
public SceneSnapshot GetScene()
|
||||
{
|
||||
var p = _primary.GetScene();
|
||||
return p.ObjectCount == 0 && p.DocumentPath is null
|
||||
? _fallback.GetScene()
|
||||
: p;
|
||||
}
|
||||
|
||||
public bool GetRenderComplete() => _primary.GetRenderComplete();
|
||||
|
||||
private static bool IsDefault(CameraSnapshot c) =>
|
||||
c.Eye is { Length: >= 3 } e && e[0] == 0 && e[1] == 0 && e[2] == 0 &&
|
||||
c.Target is { Length: >= 3 } t && t[0] == 0 && t[1] == 0 && t[2] == 0;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using Editor.PluginInterface;
|
||||
using HmEG;
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Hmeg.Bridge;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// MEF/PluginLoader-discovered plugin. Inherits the SUT's <c>EditorPlugin</c>
|
||||
/// abstract base (which itself implements <c>HmEG.IPlugin</c>), and on construction
|
||||
/// boots a localhost HTTP bridge that exposes HmEG state to recordingtest.
|
||||
/// </summary>
|
||||
public sealed class HmEgBridgePlugin : EditorPlugin, IDisposable
|
||||
{
|
||||
private BridgeHttpServer? _server;
|
||||
|
||||
public HmEgBridgePlugin()
|
||||
{
|
||||
StartBridge();
|
||||
}
|
||||
|
||||
public override string Name => "Recordingtest.Sut.EgBim.PluginHost";
|
||||
public override string Description => "recordingtest engine-bridge v3 (HTTP sidecar)";
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
// Construction already started the bridge; Initialize is a no-op safeguard.
|
||||
}
|
||||
|
||||
private void StartBridge()
|
||||
{
|
||||
try
|
||||
{
|
||||
var port = PortResolver.Resolve();
|
||||
var provider = BuildProvider();
|
||||
var router = new StateRouter(provider, port);
|
||||
_server = new BridgeHttpServer(router, port);
|
||||
_server.Start();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// never throw out of plugin construction; SUT must remain stable.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Choose the best available <see cref="IEngineStateProvider"/>:
|
||||
///
|
||||
/// 1. <c>HmegDirectStateProvider</c> — HmEG-aware tier, calls into HmEG
|
||||
/// public API. Reusable across any HmEG-hosting WPF app. Active
|
||||
/// space/viewport handles are resolved via lambdas; the EgBim
|
||||
/// app-specific entry-point lookup lives in this method only.
|
||||
/// 2. <c>ReflectionEngineStateProvider</c> — fallback that uses
|
||||
/// <c>IAppManagerAccessor</c> reflection on Editor.AppManager.AppManager.
|
||||
/// Used when the direct provider can't resolve any handle.
|
||||
///
|
||||
/// Both providers are wrapped to never throw; the SUT must remain stable.
|
||||
/// </summary>
|
||||
private IEngineStateProvider BuildProvider()
|
||||
{
|
||||
// EG-BIM Modeler entry points (resolved via EditorPlugin base):
|
||||
// RootSpace = AppManager.ViewportManager.RootSpace
|
||||
// View (EGViewport : HmEGViewport) — plugin-injected active viewport
|
||||
// AppManager.FileManager.CurrentFile — document path on disk
|
||||
//
|
||||
// Each lambda is wrapped in try/catch so plugin construction never
|
||||
// throws even if the host state is in flight at boot time.
|
||||
Func<Space?> spaceProvider = () =>
|
||||
{
|
||||
try { return RootSpace; }
|
||||
catch { return null; }
|
||||
};
|
||||
|
||||
Func<HmEGViewport?> viewportProvider = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// EGViewport implements HmEGViewport; the base class exposes it
|
||||
// as EGViewport on the Obsolete View property. We catch and
|
||||
// return null to survive the obsolete warning at runtime.
|
||||
#pragma warning disable CS0618 // Obsolete API on EditorPlugin.View
|
||||
return View;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
catch { return null; }
|
||||
};
|
||||
|
||||
Func<string?> documentPathProvider = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = AppManager?.FileManager?.CurrentFile;
|
||||
return string.IsNullOrEmpty(path) ? null : path;
|
||||
}
|
||||
catch { return null; }
|
||||
};
|
||||
|
||||
var direct = new HmegDirectStateProvider(spaceProvider, viewportProvider, documentPathProvider);
|
||||
var fallback = new ReflectionEngineStateProvider(this);
|
||||
return new ChainedEngineStateProvider(direct, fallback);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { _server?.Dispose(); } catch { }
|
||||
_server = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// engine-bridge v3 — abstraction over the SUT-side AppManager singleton.
|
||||
/// Lets <see cref="ReflectionEngineStateProvider"/> be unit-tested with a
|
||||
/// fake instead of needing the real HmEG runtime in CI.
|
||||
/// </summary>
|
||||
public interface IAppManagerAccessor
|
||||
{
|
||||
/// <summary>The AppManager root, or null if not yet discovered.</summary>
|
||||
object? GetAppManager();
|
||||
|
||||
/// <summary>The currently active document, or null.</summary>
|
||||
object? GetActiveDocument();
|
||||
|
||||
/// <summary>The currently active viewport (camera host), or null.</summary>
|
||||
object? GetActiveViewport();
|
||||
|
||||
/// <summary>Selection IDs as strings. Empty when no selection or not discoverable.</summary>
|
||||
IReadOnlyList<string> GetSelectedIds();
|
||||
|
||||
/// <summary>Object count in the active document, or 0.</summary>
|
||||
int GetObjectCount();
|
||||
|
||||
/// <summary>Active document path on disk, or null when unsaved.</summary>
|
||||
string? GetDocumentPath();
|
||||
|
||||
/// <summary>
|
||||
/// Camera tuple (eye, target, up, fov). Returns null when no viewport.
|
||||
/// </summary>
|
||||
(double[] Eye, double[] Target, double[] Up, double Fov)? GetCameraTuple();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default in-process reflection accessor. Walks well-known HmEG type and
|
||||
/// member names against a runtime-resolved AppManager handle. All accessors
|
||||
/// are best-effort: any failure (missing type, wrong shape, exception) is
|
||||
/// swallowed and the method returns the safe-default value. The plugin must
|
||||
/// never throw out of these calls because it runs inside the SUT process.
|
||||
///
|
||||
/// Type/member names are intentionally lookup-by-string so the v3 reflection
|
||||
/// shape can be tightened against live SUT inspection without recompilation
|
||||
/// of the plugin's public surface.
|
||||
/// </summary>
|
||||
public sealed class ReflectionAppManagerAccessor : IAppManagerAccessor
|
||||
{
|
||||
private readonly object? _seedRoot;
|
||||
|
||||
/// <param name="seedRoot">
|
||||
/// Any object that can act as the entry point to the AppManager graph.
|
||||
/// In production this is the plugin instance itself; the accessor walks
|
||||
/// out to the AppManager singleton via reflection on loaded assemblies.
|
||||
/// </param>
|
||||
public ReflectionAppManagerAccessor(object? seedRoot)
|
||||
{
|
||||
_seedRoot = seedRoot;
|
||||
}
|
||||
|
||||
private object? _cachedAppManager;
|
||||
private bool _appManagerLookupAttempted;
|
||||
|
||||
public object? GetAppManager()
|
||||
{
|
||||
if (_cachedAppManager is not null) return _cachedAppManager;
|
||||
if (_appManagerLookupAttempted) return null;
|
||||
_appManagerLookupAttempted = true;
|
||||
|
||||
// Strategy: scan loaded assemblies for the well-known AppManager type
|
||||
// and look for a static "Instance" / "Current" property. HmEG ships
|
||||
// Editor.AppManager.* types under the Editor02.HmEGAppManager
|
||||
// assembly (per docs/engine-catalog).
|
||||
try
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Type? t = null;
|
||||
try { t = asm.GetType("Editor.AppManager.AppManager", throwOnError: false); }
|
||||
catch { /* ignore — assembly may not allow GetType */ }
|
||||
if (t is null) continue;
|
||||
|
||||
foreach (var name in new[] { "Instance", "Current", "Default" })
|
||||
{
|
||||
var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.Static);
|
||||
var v = p?.GetValue(null);
|
||||
if (v is not null) { _cachedAppManager = v; return v; }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best-effort; keep _cachedAppManager null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object? GetActiveDocument()
|
||||
{
|
||||
var am = GetAppManager();
|
||||
return TryGetMember(am, new[] { "ActiveDocument", "CurrentDocument", "Document" });
|
||||
}
|
||||
|
||||
public object? GetActiveViewport()
|
||||
{
|
||||
var am = GetAppManager();
|
||||
return TryGetMember(am, new[] { "ActiveViewport", "CurrentViewport", "Viewport" });
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
var doc = GetActiveDocument();
|
||||
var sel = TryGetMember(doc, new[] { "Selection", "SelectedObjects", "Selected" });
|
||||
if (sel is not System.Collections.IEnumerable e) return Array.Empty<string>();
|
||||
var ids = new List<string>();
|
||||
try
|
||||
{
|
||||
foreach (var item in e)
|
||||
{
|
||||
if (item is null) continue;
|
||||
var id = TryGetMember(item, new[] { "Id", "ID", "Guid", "ObjectId" });
|
||||
ids.Add(id?.ToString() ?? item.GetHashCode().ToString());
|
||||
}
|
||||
}
|
||||
catch { /* return what we got so far */ }
|
||||
return ids;
|
||||
}
|
||||
|
||||
public int GetObjectCount()
|
||||
{
|
||||
var doc = GetActiveDocument();
|
||||
var objs = TryGetMember(doc, new[] { "Objects", "Entities", "Nodes" });
|
||||
if (objs is null) return 0;
|
||||
if (objs is System.Collections.ICollection coll) return coll.Count;
|
||||
try
|
||||
{
|
||||
int n = 0;
|
||||
if (objs is System.Collections.IEnumerable e)
|
||||
foreach (var _ in e) n++;
|
||||
return n;
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
public string? GetDocumentPath()
|
||||
{
|
||||
var doc = GetActiveDocument();
|
||||
var p = TryGetMember(doc, new[] { "FilePath", "Path", "FileName", "DocumentPath" });
|
||||
return p?.ToString();
|
||||
}
|
||||
|
||||
public (double[] Eye, double[] Target, double[] Up, double Fov)? GetCameraTuple()
|
||||
{
|
||||
var vp = GetActiveViewport();
|
||||
var cam = TryGetMember(vp, new[] { "Camera", "ActiveCamera" });
|
||||
if (cam is null) return null;
|
||||
try
|
||||
{
|
||||
var eye = AsVec3(TryGetMember(cam, new[] { "Position", "Eye", "From" }));
|
||||
var tgt = AsVec3(TryGetMember(cam, new[] { "Target", "LookAt", "To" }));
|
||||
var up = AsVec3(TryGetMember(cam, new[] { "UpDirection", "Up" }));
|
||||
var fovObj = TryGetMember(cam, new[] { "FieldOfView", "Fov", "FOV" });
|
||||
double fov = fovObj is null ? 45.0 : Convert.ToDouble(fovObj);
|
||||
return (eye, tgt, up, fov);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static object? TryGetMember(object? owner, string[] candidates)
|
||||
{
|
||||
if (owner is null) return null;
|
||||
var t = owner.GetType();
|
||||
foreach (var name in candidates)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (p is not null) return p.GetValue(owner);
|
||||
var f = t.GetField(name, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (f is not null) return f.GetValue(owner);
|
||||
}
|
||||
catch { /* try next candidate */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static double[] AsVec3(object? v)
|
||||
{
|
||||
if (v is null) return new double[] { 0, 0, 0 };
|
||||
// Try common shapes: double[], float[], or X/Y/Z properties.
|
||||
if (v is double[] da && da.Length >= 3) return new[] { da[0], da[1], da[2] };
|
||||
if (v is float[] fa && fa.Length >= 3) return new[] { (double)fa[0], fa[1], fa[2] };
|
||||
var t = v.GetType();
|
||||
try
|
||||
{
|
||||
double Read(string n)
|
||||
{
|
||||
var p = t.GetProperty(n, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (p is not null) return Convert.ToDouble(p.GetValue(v));
|
||||
var f = t.GetField(n, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (f is not null) return Convert.ToDouble(f.GetValue(v));
|
||||
return 0;
|
||||
}
|
||||
return new[] { Read("X"), Read("Y"), Read("Z") };
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new double[] { 0, 0, 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 3-tier split step 2 — this file now holds only the EgBim-specific
|
||||
// ReflectionEngineStateProvider, which probes Editor.AppManager.AppManager
|
||||
// via reflection as the CI / fallback path. The generic IEngineStateProvider
|
||||
// contract lives in Recordingtest.Bridge.Abstractions; the HmEG-aware
|
||||
// HmegDirectStateProvider lives in Recordingtest.Hmeg.Bridge.
|
||||
|
||||
using Recordingtest.Bridge;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// engine-bridge v3 — reflection-backed provider. Delegates all SUT-specific
|
||||
/// lookups to <see cref="IAppManagerAccessor"/>, which is unit-testable via a
|
||||
/// fake accessor (see <c>FakeAccessor</c> in tests). When the real
|
||||
/// AppManager cannot be discovered the provider returns the same safe
|
||||
/// defaults v2 used, so the SUT remains stable even if HmEG type names drift.
|
||||
///
|
||||
/// In the 3-tier model this class is App-specific (EG-BIM Modeler): it
|
||||
/// targets <c>Editor.AppManager.AppManager</c>. It is kept as the CI/fallback
|
||||
/// path; production should prefer <c>HmegDirectStateProvider</c>.
|
||||
/// </summary>
|
||||
public sealed class ReflectionEngineStateProvider : IEngineStateProvider
|
||||
{
|
||||
private readonly IAppManagerAccessor _accessor;
|
||||
|
||||
public ReflectionEngineStateProvider(IAppManagerAccessor accessor)
|
||||
{
|
||||
_accessor = accessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload used by the v2 plugin call site, which only had
|
||||
/// the plugin instance to hand. Wraps the seed in the default reflection
|
||||
/// accessor so existing call sites compile unchanged.
|
||||
/// </summary>
|
||||
public ReflectionEngineStateProvider(object? seedRoot)
|
||||
: this(new ReflectionAppManagerAccessor(seedRoot))
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
try { return _accessor.GetSelectedIds(); }
|
||||
catch { return Array.Empty<string>(); }
|
||||
}
|
||||
|
||||
private static readonly CameraSnapshot _defaultCamera =
|
||||
new(new double[] { 0, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 45.0);
|
||||
|
||||
public CameraSnapshot GetCamera()
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = _accessor.GetCameraTuple();
|
||||
return t is null
|
||||
? _defaultCamera
|
||||
: new CameraSnapshot(t.Value.Eye, t.Value.Target, t.Value.Up, t.Value.Fov);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return _defaultCamera;
|
||||
}
|
||||
}
|
||||
|
||||
public SceneSnapshot GetScene()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new SceneSnapshot(_accessor.GetObjectCount(), _accessor.GetDocumentPath());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new SceneSnapshot(0, null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetRenderComplete()
|
||||
{
|
||||
// v3 still treats render-complete as best-effort true; HmEG does not
|
||||
// expose a stable "frame finished" flag we can poll without an event.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Recordingtest.EgPlugin;
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
public static class PortResolver
|
||||
{
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>Recordingtest.Sut.EgBim.PluginHost</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Sut.EgBim.PluginHost</AssemblyName>
|
||||
<EnableDefaultItems>true</EnableDefaultItems>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Hmeg\Recordingtest.Hmeg.Bridge\Recordingtest.Hmeg.Bridge.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Editor03.PluginInterface">
|
||||
<HintPath>..\..\..\..\EG-BIM Modeler\Editor03.PluginInterface.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Editor02.HmEGAppManager">
|
||||
<HintPath>..\..\..\..\EG-BIM Modeler\Editor02.HmEGAppManager.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="HmEG">
|
||||
<HintPath>..\..\..\..\EG-BIM Modeler\HmEG.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Recordingtest.Bridge;
|
||||
|
||||
namespace Recordingtest.EgPlugin;
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost;
|
||||
|
||||
/// <summary>
|
||||
/// Pure logic router: maps a request path to (status, json body).
|
||||
@@ -0,0 +1,73 @@
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Hmeg.Bridge;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Hmeg.Bridge.Tests;
|
||||
|
||||
public class HmegDirectStateProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullLambdas_Return_SafeDefaults_NoThrow()
|
||||
{
|
||||
var p = new HmegDirectStateProvider(
|
||||
spaceProvider: () => null,
|
||||
viewportProvider: () => null);
|
||||
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
Assert.Equal(new double[] { 0, 0, 1 }, c.Up);
|
||||
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
Assert.Null(s.DocumentPath);
|
||||
|
||||
Assert.True(p.GetRenderComplete());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throwing_Lambdas_Are_Swallowed_Returns_SafeDefaults()
|
||||
{
|
||||
var p = new HmegDirectStateProvider(
|
||||
spaceProvider: () => throw new InvalidOperationException("boom"),
|
||||
viewportProvider: () => throw new InvalidOperationException("boom"));
|
||||
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentPathProvider_Is_Used_For_Scene()
|
||||
{
|
||||
var p = new HmegDirectStateProvider(
|
||||
spaceProvider: () => null,
|
||||
viewportProvider: () => null,
|
||||
documentPathProvider: () => "C:/sample.hmeg");
|
||||
var s = p.GetScene();
|
||||
Assert.Equal("C:/sample.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentPathProvider_Throwing_Is_Swallowed()
|
||||
{
|
||||
var p = new HmegDirectStateProvider(
|
||||
spaceProvider: () => null,
|
||||
viewportProvider: () => null,
|
||||
documentPathProvider: () => throw new InvalidOperationException());
|
||||
var s = p.GetScene();
|
||||
Assert.Null(s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_OnNullProviders()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new HmegDirectStateProvider(null!, () => null));
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new HmegDirectStateProvider(() => null, null!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.Hmeg.Bridge.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Hmeg\Recordingtest.Hmeg.Bridge\Recordingtest.Hmeg.Bridge.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="HmEG">
|
||||
<HintPath>..\..\..\EG-BIM Modeler\HmEG.dll</HintPath>
|
||||
<!-- Test process is standalone (not loaded into the SUT), so the
|
||||
assembly must be copied next to the test dll at runtime. -->
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -2,7 +2,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Recordingtest.EngineBridge.IntegrationTests;
|
||||
namespace Recordingtest.Hmeg.Catalog.IntegrationTests;
|
||||
|
||||
public sealed class FakeBridgeServer : IDisposable
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using Recordingtest.EngineBridge.Client;
|
||||
using Recordingtest.Hmeg.Bridge.Client;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.EngineBridge.IntegrationTests;
|
||||
namespace Recordingtest.Hmeg.Catalog.IntegrationTests;
|
||||
|
||||
public class HmEgHttpSnapshotTests
|
||||
{
|
||||
@@ -5,7 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.EngineBridge.IntegrationTests</RootNamespace>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog.IntegrationTests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog.IntegrationTests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
@@ -13,6 +14,6 @@
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Recordingtest.EngineBridge.Client\Recordingtest.EngineBridge.Client.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Recordingtest.EngineBridge;
|
||||
using Recordingtest.Hmeg.Catalog;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.EngineBridge.Tests;
|
||||
namespace Recordingtest.Hmeg.Catalog.Tests;
|
||||
|
||||
public sealed class EngineBridgeTests
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.EngineBridge.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.EngineBridge.Tests</AssemblyName>
|
||||
<RootNamespace>Recordingtest.Hmeg.Catalog.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Hmeg.Catalog.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
@@ -10,6 +10,6 @@
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Recordingtest.EngineBridge\Recordingtest.EngineBridge.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
115
tests/Recordingtest.Architecture.Tests/DependencyGraphTests.cs
Normal file
115
tests/Recordingtest.Architecture.Tests/DependencyGraphTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Architecture.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the 3-tier separation rule from CLAUDE.md §8.1:
|
||||
///
|
||||
/// Generic → no SUT assembly references at all
|
||||
/// HmEG-aware → HmEG.dll only, no per-app assembly
|
||||
/// App-specific → anything (not checked here; not referenced by this project)
|
||||
///
|
||||
/// These tests walk <see cref="Assembly.GetReferencedAssemblies"/> on each
|
||||
/// compiled output and fail if a forbidden reference appears. The test
|
||||
/// project itself does NOT reference any Sut/* project, so a physical build
|
||||
/// error would catch accidental upward references even before these tests
|
||||
/// run — this file is the explicit contract.
|
||||
/// </summary>
|
||||
public class DependencyGraphTests
|
||||
{
|
||||
private static readonly string[] ForbiddenAppAssemblies =
|
||||
{
|
||||
"Editor03.PluginInterface",
|
||||
"Editor02.HmEGAppManager",
|
||||
"EditorCore",
|
||||
};
|
||||
|
||||
private const string HmegAssembly = "HmEG";
|
||||
|
||||
private static readonly string[] GenericAssemblies =
|
||||
{
|
||||
"Recordingtest.Bridge.Abstractions",
|
||||
"Recordingtest.Recorder",
|
||||
"Recordingtest.Player",
|
||||
"Recordingtest.Normalizer",
|
||||
"Recordingtest.DiffReporter",
|
||||
"Recordingtest.Runner",
|
||||
"Recordingtest.SutProber",
|
||||
};
|
||||
|
||||
private static readonly string[] HmegAwareAssemblies =
|
||||
{
|
||||
"Recordingtest.Hmeg.Bridge",
|
||||
"Recordingtest.Hmeg.Catalog",
|
||||
"Recordingtest.Hmeg.Bridge.Client",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData("Recordingtest.Bridge.Abstractions")]
|
||||
[InlineData("Recordingtest.Recorder")]
|
||||
[InlineData("Recordingtest.Player")]
|
||||
[InlineData("Recordingtest.Normalizer")]
|
||||
[InlineData("Recordingtest.DiffReporter")]
|
||||
[InlineData("Recordingtest.Runner")]
|
||||
[InlineData("Recordingtest.SutProber")]
|
||||
public void Generic_Tier_Does_Not_Reference_Hmeg_Or_AppAssemblies(string assemblyName)
|
||||
{
|
||||
var refs = LoadReferences(assemblyName);
|
||||
Assert.DoesNotContain(HmegAssembly, refs);
|
||||
foreach (var app in ForbiddenAppAssemblies)
|
||||
{
|
||||
Assert.DoesNotContain(app, refs);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Recordingtest.Hmeg.Bridge")]
|
||||
[InlineData("Recordingtest.Hmeg.Catalog")]
|
||||
[InlineData("Recordingtest.Hmeg.Bridge.Client")]
|
||||
public void HmegAware_Tier_Does_Not_Reference_AppAssemblies(string assemblyName)
|
||||
{
|
||||
var refs = LoadReferences(assemblyName);
|
||||
foreach (var app in ForbiddenAppAssemblies)
|
||||
{
|
||||
Assert.DoesNotContain(app, refs);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hmeg_Bridge_References_HmEG_Dll()
|
||||
{
|
||||
// HmEG-aware tier is expected to reference HmEG.dll. This is the
|
||||
// positive check that pairs with the App-specific forbidden list.
|
||||
var refs = LoadReferences("Recordingtest.Hmeg.Bridge");
|
||||
Assert.Contains(HmegAssembly, refs);
|
||||
}
|
||||
|
||||
private static IReadOnlySet<string> LoadReferences(string assemblyName)
|
||||
{
|
||||
// Locate the assembly via a type we know belongs to it. Each tier
|
||||
// member has at least one public type; we use GenericAssemblies/
|
||||
// HmegAwareAssemblies as lookups by name.
|
||||
Assembly? asm = null;
|
||||
foreach (var loaded in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (loaded.GetName().Name == assemblyName)
|
||||
{
|
||||
asm = loaded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (asm is null)
|
||||
{
|
||||
// Force-load by trying to resolve a type: the test project has a
|
||||
// ProjectReference to the target, so the binary is next to ours.
|
||||
var dir = Path.GetDirectoryName(typeof(DependencyGraphTests).Assembly.Location)!;
|
||||
var path = Path.Combine(dir, assemblyName + ".dll");
|
||||
asm = Assembly.LoadFrom(path);
|
||||
}
|
||||
return new HashSet<string>(
|
||||
asm.GetReferencedAssemblies().Select(n => n.Name ?? string.Empty),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.Architecture.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Architecture.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Reference every Generic-tier assembly so we can inspect its dependency
|
||||
graph at test time. Add HmEG-aware here too; do NOT reference any
|
||||
Sut/* project or app-specific DLL. -->
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Recorder\Recordingtest.Recorder.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Player\Recordingtest.Player.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Normalizer\Recordingtest.Normalizer.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.DiffReporter\Recordingtest.DiffReporter.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.Runner\Recordingtest.Runner.csproj" />
|
||||
<ProjectReference Include="..\..\src\Recordingtest.SutProber\Recordingtest.SutProber.csproj" />
|
||||
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Bridge\Recordingtest.Hmeg.Bridge.csproj" />
|
||||
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Catalog\Recordingtest.Hmeg.Catalog.csproj" />
|
||||
<ProjectReference Include="..\..\src\Hmeg\Recordingtest.Hmeg.Bridge.Client\Recordingtest.Hmeg.Bridge.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -35,4 +35,6 @@ internal sealed class FakePlayerHost : IPlayerHost
|
||||
Checkpoints.Add((afterStep, saveAs));
|
||||
public void CaptureFailureArtifacts(int stepIndex, string reason) =>
|
||||
Failures.Add((stepIndex, reason));
|
||||
public List<TimeSpan> Delays { get; } = new();
|
||||
public void Delay(TimeSpan duration) => Delays.Add(duration);
|
||||
}
|
||||
|
||||
@@ -154,25 +154,33 @@ baselines:
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerEngine_NullTarget_SkipsWithoutCalling()
|
||||
public void PlayerEngine_NullTarget_Fallback_Issue14()
|
||||
{
|
||||
// Issue #14: null-target fallbacks.
|
||||
// - Type → send keystrokes to current focus (no target required)
|
||||
// - Click w/ raw_coord → click at raw_coord screen-absolute
|
||||
// - Click w/o raw_coord → still skipped (no way to click safely)
|
||||
// - Drag → still skipped (no raw_coord pair handling yet)
|
||||
var engine = new PlayerEngine();
|
||||
var host = new FakePlayerHost();
|
||||
var scenario = new Scenario
|
||||
{
|
||||
Steps =
|
||||
{
|
||||
new Step { Kind = StepKind.Click, Target = null },
|
||||
new Step { Kind = StepKind.Drag, Target = null },
|
||||
new Step { Kind = StepKind.Click, Target = null }, // skip
|
||||
new Step { Kind = StepKind.Click, Target = null, RawCoord = new[] { 123, 456 } },
|
||||
new Step { Kind = StepKind.Drag, Target = null }, // skip
|
||||
new Step { Kind = StepKind.Type, Target = null, Value = "hello" },
|
||||
},
|
||||
};
|
||||
|
||||
engine.Run(scenario, host);
|
||||
|
||||
Assert.Empty(host.Clicks);
|
||||
Assert.Single(host.Clicks);
|
||||
Assert.Equal(new ScreenPoint(123, 456), host.Clicks[0]);
|
||||
Assert.Empty(host.Drags);
|
||||
Assert.Empty(host.Types);
|
||||
Assert.Single(host.Types);
|
||||
Assert.Equal("hello", host.Types[0]);
|
||||
Assert.Empty(host.Failures);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public sealed class FakePlayerHost : IPlayerHost
|
||||
}
|
||||
public void CaptureCheckpoint(int afterStep, string saveAs) { }
|
||||
public void CaptureFailureArtifacts(int stepIndex, string reason) { }
|
||||
public void Delay(TimeSpan duration) { }
|
||||
}
|
||||
|
||||
public sealed class FakeHostFactory : IRunnerHostFactory
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
|
||||
|
||||
public class ChainedEngineStateProviderTests
|
||||
{
|
||||
private sealed class ScriptedProvider : IEngineStateProvider
|
||||
{
|
||||
public IReadOnlyList<string> SelectedIds = Array.Empty<string>();
|
||||
public CameraSnapshot Camera = new(
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 0 },
|
||||
new double[] { 0, 0, 1 },
|
||||
45.0);
|
||||
public SceneSnapshot Scene = new(0, null);
|
||||
public bool Render = true;
|
||||
|
||||
public IReadOnlyList<string> GetSelectedIds() => SelectedIds;
|
||||
public CameraSnapshot GetCamera() => Camera;
|
||||
public SceneSnapshot GetScene() => Scene;
|
||||
public bool GetRenderComplete() => Render;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_Primary_NonEmpty_Wins()
|
||||
{
|
||||
var p = new ScriptedProvider { SelectedIds = new[] { "a" } };
|
||||
var f = new ScriptedProvider { SelectedIds = new[] { "fallback" } };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(new[] { "a" }, c.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_Primary_Empty_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var f = new ScriptedProvider { SelectedIds = new[] { "fallback" } };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(new[] { "fallback" }, c.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Camera_Primary_Default_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var fc = new CameraSnapshot(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }, new double[] { 0, 0, 1 }, 60);
|
||||
var f = new ScriptedProvider { Camera = fc };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(60.0, c.GetCamera().Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Camera_Primary_NonDefault_Wins()
|
||||
{
|
||||
var pc = new CameraSnapshot(new double[] { 1, 0, 0 }, new double[] { 0, 0, 0 }, new double[] { 0, 0, 1 }, 30);
|
||||
var p = new ScriptedProvider { Camera = pc };
|
||||
var f = new ScriptedProvider();
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.Equal(30.0, c.GetCamera().Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scene_Primary_Empty_Falls_Through()
|
||||
{
|
||||
var p = new ScriptedProvider();
|
||||
var f = new ScriptedProvider { Scene = new(7, "x.hmeg") };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
var s = c.GetScene();
|
||||
Assert.Equal(7, s.ObjectCount);
|
||||
Assert.Equal("x.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scene_Primary_Has_Path_Wins_Even_With_Zero_Objects()
|
||||
{
|
||||
var p = new ScriptedProvider { Scene = new(0, "primary.hmeg") };
|
||||
var f = new ScriptedProvider { Scene = new(99, "fallback.hmeg") };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
var s = c.GetScene();
|
||||
Assert.Equal("primary.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Primary_Always_Wins()
|
||||
{
|
||||
var p = new ScriptedProvider { Render = false };
|
||||
var f = new ScriptedProvider { Render = true };
|
||||
var c = new ChainedEngineStateProvider(p, f);
|
||||
Assert.False(c.GetRenderComplete());
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Recordingtest.EgPlugin.Tests</RootNamespace>
|
||||
<RootNamespace>Recordingtest.Sut.EgBim.PluginHost.Tests</RootNamespace>
|
||||
<AssemblyName>Recordingtest.Sut.EgBim.PluginHost.Tests</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
@@ -13,6 +14,7 @@
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Recordingtest.EgPlugin\Recordingtest.EgPlugin.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Sut\EgBim\Recordingtest.Sut.EgBim.PluginHost\Recordingtest.Sut.EgBim.PluginHost.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Recordingtest.Bridge.Abstractions\Recordingtest.Bridge.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,149 @@
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
|
||||
|
||||
public class ReflectionEngineStateProviderTests
|
||||
{
|
||||
private sealed class FakeAccessor : IAppManagerAccessor
|
||||
{
|
||||
public object? AppManager { get; set; } = new object();
|
||||
public object? ActiveDocument { get; set; }
|
||||
public object? ActiveViewport { get; set; }
|
||||
public IReadOnlyList<string> SelectedIds { get; set; } = Array.Empty<string>();
|
||||
public int ObjectCount { get; set; }
|
||||
public string? DocumentPath { get; set; }
|
||||
public (double[] Eye, double[] Target, double[] Up, double Fov)? Camera { get; set; }
|
||||
public bool ThrowOnSelection { get; set; }
|
||||
public bool ThrowOnCamera { get; set; }
|
||||
public bool ThrowOnScene { get; set; }
|
||||
|
||||
public object? GetAppManager() => AppManager;
|
||||
public object? GetActiveDocument() => ActiveDocument;
|
||||
public object? GetActiveViewport() => ActiveViewport;
|
||||
public IReadOnlyList<string> GetSelectedIds()
|
||||
{
|
||||
if (ThrowOnSelection) throw new InvalidOperationException("boom");
|
||||
return SelectedIds;
|
||||
}
|
||||
public int GetObjectCount()
|
||||
{
|
||||
if (ThrowOnScene) throw new InvalidOperationException("boom");
|
||||
return ObjectCount;
|
||||
}
|
||||
public string? GetDocumentPath()
|
||||
{
|
||||
if (ThrowOnScene) throw new InvalidOperationException("boom");
|
||||
return DocumentPath;
|
||||
}
|
||||
public (double[] Eye, double[] Target, double[] Up, double Fov)? GetCameraTuple()
|
||||
{
|
||||
if (ThrowOnCamera) throw new InvalidOperationException("boom");
|
||||
return Camera;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelectedIds_Returns_Accessor_Values()
|
||||
{
|
||||
var fa = new FakeAccessor { SelectedIds = new[] { "a", "b" } };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var ids = p.GetSelectedIds();
|
||||
Assert.Equal(new[] { "a", "b" }, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSelectedIds_Swallows_Exception_Returns_Empty()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnSelection = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Returns_Accessor_Tuple()
|
||||
{
|
||||
var fa = new FakeAccessor
|
||||
{
|
||||
Camera = (
|
||||
Eye: new double[] { 1, 2, 3 },
|
||||
Target: new double[] { 4, 5, 6 },
|
||||
Up: new double[] { 0, 0, 1 },
|
||||
Fov: 60.0)
|
||||
};
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(new double[] { 1, 2, 3 }, c.Eye);
|
||||
Assert.Equal(new double[] { 4, 5, 6 }, c.Target);
|
||||
Assert.Equal(60.0, c.Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Null_Tuple_Returns_Default()
|
||||
{
|
||||
var fa = new FakeAccessor { Camera = null };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
Assert.Equal(new double[] { 0, 0, 1 }, c.Up);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCamera_Throwing_Accessor_Returns_Default_Not_Throw()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnCamera = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScene_Returns_Accessor_Values()
|
||||
{
|
||||
var fa = new FakeAccessor { ObjectCount = 42, DocumentPath = "C:/x.hmeg" };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(42, s.ObjectCount);
|
||||
Assert.Equal("C:/x.hmeg", s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetScene_Throwing_Accessor_Returns_Empty_Default()
|
||||
{
|
||||
var fa = new FakeAccessor { ThrowOnScene = true };
|
||||
var p = new ReflectionEngineStateProvider(fa);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
Assert.Null(s.DocumentPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_Reflection_Accessor_Without_Hmeg_Returns_Null_Safely()
|
||||
{
|
||||
// CI process has no Editor.AppManager.AppManager type loaded; the
|
||||
// accessor must return null/empty/default without throwing.
|
||||
var acc = new ReflectionAppManagerAccessor(seedRoot: new object());
|
||||
Assert.Null(acc.GetAppManager());
|
||||
Assert.Null(acc.GetActiveDocument());
|
||||
Assert.Null(acc.GetActiveViewport());
|
||||
Assert.Empty(acc.GetSelectedIds());
|
||||
Assert.Equal(0, acc.GetObjectCount());
|
||||
Assert.Null(acc.GetDocumentPath());
|
||||
Assert.Null(acc.GetCameraTuple());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Provider_With_Default_Accessor_Without_Hmeg_Returns_Defaults()
|
||||
{
|
||||
// End-to-end fallback: provider built from a non-HmEG seed must
|
||||
// produce the v2 safe defaults so /state continues to respond.
|
||||
var p = new ReflectionEngineStateProvider(seedRoot: new object());
|
||||
Assert.Empty(p.GetSelectedIds());
|
||||
var c = p.GetCamera();
|
||||
Assert.Equal(45.0, c.Fov);
|
||||
var s = p.GetScene();
|
||||
Assert.Equal(0, s.ObjectCount);
|
||||
Assert.True(p.GetRenderComplete());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Net;
|
||||
using Recordingtest.EgPlugin;
|
||||
using Recordingtest.Bridge;
|
||||
using Recordingtest.Sut.EgBim.PluginHost;
|
||||
using Xunit;
|
||||
|
||||
namespace Recordingtest.EgPlugin.Tests;
|
||||
namespace Recordingtest.Sut.EgBim.PluginHost.Tests;
|
||||
|
||||
public class StateRouterTests
|
||||
{
|
||||
Reference in New Issue
Block a user