BREAKING: 3-tier split step 2 + engine-bridge v3 EgBim lambdas wired

Completes the Generic / HmEG-aware / App-specific separation started in
f6b6e74. The legacy EgPlugin / EngineBridge / EngineBridge.Client /
EngineBridge.Probe modules are moved into their proper tiers, namespaces
and csproj/sln entries are renamed, and the HmegDirectStateProvider
lambdas are finally populated with real handles from the EgBim plugin
host. A new Recordingtest.Architecture.Tests project enforces the tier
rule at build time.

Moves (git mv + csproj/RootNamespace/AssemblyName rename + sln):

  src/Recordingtest.EgPlugin
    -> src/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost
  src/Recordingtest.EngineBridge
    -> src/Hmeg/Recordingtest.Hmeg.Catalog
  src/Recordingtest.EngineBridge.Client
    -> src/Hmeg/Recordingtest.Hmeg.Bridge.Client
  src/Recordingtest.EngineBridge.Probe
    -> src/Hmeg/Recordingtest.Hmeg.Catalog.Probe

  tests/Recordingtest.EgPlugin.Tests
    -> tests/Sut/EgBim/Recordingtest.Sut.EgBim.PluginHost.Tests
  tests/Recordingtest.EngineBridge.Tests
    -> tests/Hmeg/Recordingtest.Hmeg.Catalog.Tests
  tests/Recordingtest.EngineBridge.IntegrationTests
    -> tests/Hmeg/Recordingtest.Hmeg.Catalog.IntegrationTests

Namespace rename applied across all .cs files and csproj RootNamespace:

  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

New: tests/Recordingtest.Architecture.Tests/

  DependencyGraphTests walks Assembly.GetReferencedAssemblies() for each
  tier and fails if a forbidden reference leaks in:
    - Generic modules must not reference HmEG or any app-specific DLL
    - HmEG-aware modules must not reference app-specific DLLs
    - Recordingtest.Hmeg.Bridge must reference HmEG (positive check)
  11 tests, all passing. Prevents future drift from CLAUDE.md §8.1.

Engine-bridge v3 wire-up (HmEgBridgePlugin.BuildProvider):

  Previously the HmegDirectStateProvider lambdas returned null and the
  chain fell through to reflection. They now call directly into the
  EditorPlugin base class that HmEgBridgePlugin inherits:

    spaceProvider    = () => RootSpace
                           // AppManager.ViewportManager.RootSpace
    viewportProvider = () => View
                           // EGViewport : Control, HmEGViewport
    documentPathProvider = () => AppManager?.FileManager?.CurrentFile

  Every lambda is wrapped in try/catch so plugin construction still
  cannot throw back into the SUT. Editor02.HmEGAppManager.dll added as
  a reference on Recordingtest.Sut.EgBim.PluginHost.csproj — app-
  specific tier, which is allowed by the architecture tests.

Entry points were confirmed from read-only review of the SUT sources at
  D:\GiteaAll\EG-BIM_Modeler\EditorPluginInterface\EditorPlugin.cs
  D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\HmEGAppManager.cs
  D:\GiteaAll\EG-BIM_Modeler\HmEGApplicationManagementLibrary\SubManager\FileManager.cs

closing out Q1/Q2/Q6/Q7 from docs/hmeg-api-survey.md.

Tests: 115 -> 126 (+11 Architecture), 0 failures.

Next step: live verification of /scene /camera /selection with a real
SUT session; any discrepancy in HmegDirectStateProvider reflection will
be tightened after observing real HmEG camera field names.

Ref: #10 follow-up, #14 follow-up, docs/contracts/generic-sut-split.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-09 10:39:13 +09:00
parent f6b6e7449e
commit 03fb504eea
36 changed files with 542 additions and 206 deletions

View File

@@ -0,0 +1,11 @@
namespace Recordingtest.Hmeg.Bridge.Client;
public sealed class EngineBridgeException : Exception
{
public string Endpoint { get; }
public EngineBridgeException(string endpoint, string message, Exception? inner = null)
: base($"engine-bridge {endpoint}: {message}", inner)
{
Endpoint = endpoint;
}
}

View File

@@ -0,0 +1,122 @@
using System.Net.Http;
using System.Text.Json;
using Recordingtest.Hmeg.Catalog;
namespace Recordingtest.Hmeg.Bridge.Client;
public sealed class HmEgHttpSnapshot : IEngineSnapshot, IDisposable
{
public const string DefaultBaseUrl = "http://localhost:38080";
private readonly HttpClient _http;
private readonly bool _ownsClient;
private readonly string _baseUrl;
public HmEgHttpSnapshot(string baseUrl = DefaultBaseUrl, HttpClient? httpClient = null, TimeSpan? timeout = null)
{
_baseUrl = baseUrl.TrimEnd('/');
if (httpClient is null)
{
_http = new HttpClient { Timeout = timeout ?? TimeSpan.FromSeconds(2) };
_ownsClient = true;
}
else
{
_http = httpClient;
if (timeout.HasValue) _http.Timeout = timeout.Value;
_ownsClient = false;
}
}
public IReadOnlyList<string> SelectedObjectIds
{
get
{
using var doc = Get("/selection");
var arr = doc.RootElement.GetProperty("selected_ids");
var list = new List<string>(arr.GetArrayLength());
foreach (var e in arr.EnumerateArray()) list.Add(e.GetString() ?? string.Empty);
return list;
}
}
public CameraState Camera
{
get
{
using var doc = Get("/camera");
var r = doc.RootElement;
return new CameraState(
ToArray(r.GetProperty("eye")),
ToArray(r.GetProperty("target")),
ToArray(r.GetProperty("up")),
r.GetProperty("fov").GetDouble());
}
}
public SceneSummary Scene
{
get
{
using var doc = Get("/scene");
var r = doc.RootElement;
string? path = r.TryGetProperty("document_path", out var dp) && dp.ValueKind == JsonValueKind.String ? dp.GetString() : null;
return new SceneSummary(r.GetProperty("object_count").GetInt32(), path);
}
}
public bool IsRenderComplete
{
get
{
using var doc = Get("/render");
return doc.RootElement.GetProperty("complete").GetBoolean();
}
}
public bool IsHealthy
{
get
{
try
{
using var doc = Get("/health");
return doc.RootElement.TryGetProperty("status", out var s) && s.GetString() == "ok";
}
catch (EngineBridgeException) { return false; }
}
}
private JsonDocument Get(string endpoint)
{
try
{
using var resp = _http.GetAsync(_baseUrl + endpoint).GetAwaiter().GetResult();
if (!resp.IsSuccessStatusCode)
throw new EngineBridgeException(endpoint, $"HTTP {(int)resp.StatusCode}");
var body = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return JsonDocument.Parse(body);
}
catch (EngineBridgeException) { throw; }
catch (TaskCanceledException ex)
{
throw new EngineBridgeException(endpoint, "timeout", ex);
}
catch (Exception ex)
{
throw new EngineBridgeException(endpoint, ex.Message, ex);
}
}
private static double[] ToArray(JsonElement e)
{
var arr = new double[e.GetArrayLength()];
int i = 0;
foreach (var item in e.EnumerateArray()) arr[i++] = item.GetDouble();
return arr;
}
public void Dispose()
{
if (_ownsClient) _http.Dispose();
}
}

View File

@@ -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>