Live end-to-end verification against EG-BIM Modeler succeeded on the
second attempt:
/health -> {"status":"ok","port":38080}
/scene -> {"object_count":4,"document_path":"NewSpace0"}
/camera -> {"eye":[192.97,-328.52,170.72],
"target":[33.03,-72.61,10.78],
"up":[0,0,1],"fov":45}
/selection -> {"selected_ids":["ac0380a2-...","d9a287ee-..."]}
1st attempt returned default-zero camera. Root cause: the viewport lambda
used EditorPlugin.View, which is only populated when the plugin is
actually Run() by a user trigger; our bridge plugin just boots an HTTP
server from its constructor and never runs a command, so View stayed
null. Space access worked because RootSpace goes through AppManager,
which is populated for the whole app.
Fix (HmEgBridgePlugin.BuildProvider):
Before: viewportProvider = () => View;
After: viewportProvider = () => {
var vm = AppManager?.ViewportManager;
if (vm is null) return null;
return vm.FocusedViewport ?? vm.Viewports.FirstOrDefault();
};
Confirmed against read-only view of
HmEGApplicationManagementLibrary/SubManager/ViewportManager.cs
which exposes FocusedViewport and Viewports. EGViewport : HmEGViewport
so the lambda matches the Func<HmEGViewport?> contract directly.
Plus: scripts/deploy-egbim-plugin.bat for one-click deploy. Checks for
a running SUT, builds Debug, purges the legacy Recordingtest.EgPlugin
folder, cleans the destination, copies 3 DLLs (+ PDBs) into
EG-BIM Modeler/Plugins/Recordingtest.Sut.EgBim.PluginHost/
and prints the curl commands for verification. HmEG.dll and the
Editor*.dll assemblies are deliberately NOT copied — the SUT already
supplies them.
PROGRESS.md: engine-bridge v3 row finalized; the long-running "라이브
검증 대기" item is done. PLAN.md P1 advances to the Runner <-> sidecar
integration (snapshot /scene /camera /selection at scenario end and
include in the golden baseline).
Follow-up (noted in history): document_path returned "NewSpace0" for
an unsaved scratch document — need to retest with a saved .hmeg file
to confirm the real FileManager.CurrentFile round-trip.
Ref: #10 follow-up, engine-bridge-v3 contract DoD D7 satisfied.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
4.3 KiB
C#
120 lines
4.3 KiB
C#
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 = () =>
|
|
{
|
|
// EditorPlugin.View is only populated when the plugin is actually
|
|
// Run() by a user command; our bridge plugin lives as a long-
|
|
// running HTTP server and never runs a trigger, so View stays
|
|
// null. Instead pull the active viewport from the global
|
|
// ViewportManager, preferring FocusedViewport, then falling back
|
|
// to any registered viewport. EGViewport implements HmEGViewport.
|
|
try
|
|
{
|
|
var vm = AppManager?.ViewportManager;
|
|
if (vm is null) return null;
|
|
var focused = vm.FocusedViewport;
|
|
if (focused is not null) return focused;
|
|
var any = vm.Viewports;
|
|
if (any is null) return null;
|
|
foreach (var v in any)
|
|
{
|
|
if (v is not null) return v;
|
|
}
|
|
return null;
|
|
}
|
|
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;
|
|
}
|
|
}
|