Bind Eve UI fields to CultCache paths

This commit is contained in:
Meta Vens 2026-06-01 17:43:00 +01:00
commit f6b964a440
9 changed files with 134 additions and 45 deletions

View file

@ -155,11 +155,16 @@ closer to CultUI than to the old flat dashboard node list:
- `bindNodeId`: optional bridge back to the compatibility `nodes` projection.
- `commandId`: provider-owned intent id. Eve clients invoke commands; providers
accept or reject and publish the next snapshot.
- `binding`: optional typed CultCache/CultMesh field binding. It names
`documentSchema`, `documentId`, field `path`, `valueKind`, `access`,
`authority`, and optional `commandId`. This is the Verse-backed field
contract; native callbacks and DOM handlers are only lowering actuators.
The old `nodes` array is no longer expected to carry complete UI composition.
It is a compatibility/debug/data surface. Clients that understand `surface`
should render the composed tree first and use `nodes` only for selection,
state-path binding, and fallback.
legacy node binding, and fallback. `bindNodeId` is compatibility glue, not the
portable state owner.
The binary CultMesh state document mirrors this ownership: field 7 carries the
optional `surface` tree. Older clients can stop at the node projection; current

View file

@ -309,7 +309,11 @@ a named invariant that the native runtime cannot protect yet.
overlays, and the D3D12 hit-test path lowers the same tree into preview
gestures. Eve is the portable dashboard authority; CultUI is only the original
C# API shape used here so the same document can be lowered by native, web,
Android, or iOS backends without making Direct2D the owner. Placement commits
Android, or iOS backends without making Direct2D the owner. Elements can now
carry typed CultCache/CultMesh bindings with document schema/id, field path,
value kind, access, authority, and optional command id. Those bindings are
the Verse-backed state contract; callbacks remain native lowering actuators.
Placement commits
keep video layers inside the program frame, snap near edges/center, and
preserve the source aspect ratio while resizing through direct preview
handles, sliders, or the scale gizmo. Explicit transform/source controls are

View file

@ -559,7 +559,11 @@ snap guides come from the shared Eve dashboard document state that Fensalir's
Direct2D and D3D12 paths lower locally; arrows nudge the selected layer,
PageUp/PageDown reorder layers, and the inspector owns the selected source
contain/cover/stretch fit mode. Eve is the portable dashboard authority, while
CultUI is only the C# reference API shape for the document. World SDF glyph rendering, ASSIMP-style
CultUI is only the C# reference API shape for the document. The shared element
contract now includes typed CultCache/CultMesh field bindings: document
schema/id, field path, value kind, access, authority, and optional command id.
Callbacks are native lowering actuators; the binding is the portable
Verse-backed state contract. World SDF glyph rendering, ASSIMP-style
mesh decoding/upload, and pixel-accurate gizmo hit-testing are explicit
Fensalir renderer cuts, mapped in
[[docs/scene-editor-control-surface|Mimir Scene Editor Control Surface]].
@ -579,9 +583,11 @@ selected Face/status pane, state tree, and detail panel.
Dashboard manifests and states have typed CultMesh-compatible document shapes
`mimir.eve_dashboard_manifest.v1` and `mimir.eve_dashboard_state.v1`, while the
live iPad lane remains WebSocket/TCP because it is the proven tunnelable
transport. The Mimir stream-layout provider still uses fixture state; the next
cut is binding it to live `MimirPresentationControlState` and
`MimirSceneEditorState`.
transport. `mimir.eve_dashboard_state.v1` now mirrors each retained element's
optional binding object so clients can move fields into CultCache without
turning `bindNodeId` into a second authority. The Mimir stream-layout provider
still uses fixture state; the next cut is binding it to live
`MimirPresentationControlState` and `MimirSceneEditorState`.
## Visual Fusion

View file

@ -180,6 +180,9 @@ Get-Content .\state\evidence.jsonl -Tail 8
fixed-height OBS-like control block below it. Eve is the portable dashboard
authority; the C# CultUI-shaped Aquarium contract is only the reference API
shape, and Fensalir Direct2D/D3D12 are native lowerings of the same document.
Elements can now carry typed CultCache/CultMesh bindings with document
schema/id, field path, value kind, access, authority, and optional command id;
native callbacks remain local lowering actuators, not portable state owners.
`MimirProgramRecorder` owns `Record` / `Stop`, status text, and timestamped
receipts under `C:\Users\Meta\Videos\Mimir` by default
(`MIMIR_RECORD_OUTPUT_DIR` overrides). It delegates to

View file

@ -382,7 +382,17 @@ internal sealed class EveDashboardServer(int port, EveDashboardProviderCatalog p
element.Metric == null
? null
: new MimirEveDashboardUiMetricSnapshot(element.Metric.Label, element.Metric.Value, element.Metric.Tone),
element.Children.Select(ToCultMeshElement).ToArray());
element.Children.Select(ToCultMeshElement).ToArray(),
element.Binding == null
? null
: new MimirEveDashboardUiBindingSnapshot(
element.Binding.DocumentSchema,
element.Binding.DocumentId,
element.Binding.Path,
element.Binding.ValueKind,
element.Binding.Access,
element.Binding.Authority,
element.Binding.CommandId));
}
private async Task BroadcastStateAsync()
@ -2206,6 +2216,23 @@ internal sealed class DashboardSurface
internal sealed record DashboardSurfaceAsset(string Id, string Kind, string Uri);
internal sealed class DashboardUiBinding
{
public string DocumentSchema { get; set; } = "";
public string DocumentId { get; set; } = "";
public string Path { get; set; } = "";
public string ValueKind { get; set; } = "";
public string Access { get; set; } = "read";
public string Authority { get; set; } = "";
public string? CommandId { get; set; }
}
internal sealed class DashboardUiElement
{
public string Id { get; set; } = "";
@ -2224,6 +2251,8 @@ internal sealed class DashboardUiElement
public string? CommandId { get; set; }
public DashboardUiBinding? Binding { get; set; }
public DashboardUiLayout? Layout { get; set; }
public DashboardUiStyle? Style { get; set; }

View file

@ -35,6 +35,10 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
private const int DefaultSpectrumSourceLaneCapacity = 8;
private const int VideoSourceListSlotCount = 12;
private const int AudioMixerChannelSlotCount = 6;
private const string ProgramSurfaceConfigSchema = "mimir.program_surface_config.v1";
private const string RuntimeStateSchema = "mimir.runtime_state.v1";
private const string RuntimeCommandSchema = "mimir.runtime_command.v1";
private const string DashboardAuthority = "mimir.runtime.dashboard";
private const string SpectrumFieldResourceKey = "mimir:resource:spectrum:field-upload";
private const string SpectrumRampResourceKey = AquariumBuiltInFieldResources.BlackbodyRampResourceKey;
private static readonly string SpectrumRampTexturePath = AquariumBuiltInFieldResources.ResolveBlackbodyRampPath();
@ -864,6 +868,26 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
synchronization.Dispose();
}
private AquariumUiBinding ProgramSurfaceBinding(string path, string valueKind)
{
return AquariumUiBinding.ReadWrite(ProgramSurfaceConfigSchema, programSurfaceConfig.SurfaceId, path, valueKind, DashboardAuthority);
}
private static AquariumUiBinding RuntimeRead(string path, string valueKind)
{
return AquariumUiBinding.Read(RuntimeStateSchema, "mimir.local", path, valueKind, DashboardAuthority);
}
private static AquariumUiBinding RuntimeReadWrite(string path, string valueKind)
{
return AquariumUiBinding.ReadWrite(RuntimeStateSchema, "mimir.local", path, valueKind, DashboardAuthority);
}
private static AquariumUiBinding RuntimeCommand(string commandId, string path)
{
return AquariumUiBinding.Command(RuntimeCommandSchema, "mimir.local", path, DashboardAuthority, commandId);
}
private AquariumUiDocument CreateUi()
{
return new AquariumUiDocument()
@ -873,19 +897,19 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
{
shell.Pane("mimir.video-sources", "", graph =>
{
graph.Text("video.program-summary", DescribeProgramVideo, "caption", weight: 0.75f);
graph.Text("video.program-summary", DescribeProgramVideo, "caption", weight: 0.75f, binding: RuntimeRead("/program/video/summary", "string"));
graph.Text("video.source-list-title", "Program Sources", "strong", weight: 0.55f);
for (var slot = 0; slot < VideoSourceListSlotCount; slot++)
{
var sourceIndex = slot;
graph.Row($"video.source-row.{slot}", row =>
{
row.Button($"video.source-row.{slot}.select", "Select", () => SelectVideoSlot(sourceIndex), weight: 0.55f);
row.Text($"video.source-row.{slot}.label", () => DescribeVideoSourceSlot(sourceIndex), "caption", weight: 1.45f);
row.Button($"video.source-row.{slot}.select", "Select", () => SelectVideoSlot(sourceIndex), weight: 0.55f, binding: RuntimeCommand($"program.video.select.{slot}", $"/commands/program/video/select/{slot}"));
row.Text($"video.source-row.{slot}.label", () => DescribeVideoSourceSlot(sourceIndex), "caption", weight: 1.45f, binding: RuntimeRead($"/program/video/sources/{slot}/summary", "string"));
}, weight: 0.75f, isVisible: () => VideoSourceSlotVisible(sourceIndex));
}
graph.Text("video.empty", () => presentationControls.VideoFeeds.Count == 0 ? "No synced video streams yet." : "", "caption", weight: 0.65f);
graph.Text("video.selected", sceneEditor.DescribeSelection, "caption", weight: 1.2f);
graph.Text("video.selected", sceneEditor.DescribeSelection, "caption", weight: 1.2f, binding: RuntimeRead("/program/video/selection/summary", "string"));
}, weight: 0.85f);
shell.Vertical("mimir.editor-center", center =>
@ -898,7 +922,8 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
blitOutputBuffer: true,
handleInteraction: sceneEditor.HandlePreviewInteraction,
readState: () => sceneEditor.PreviewState,
readGuides: () => sceneEditor.PreviewGuides);
readGuides: () => sceneEditor.PreviewGuides,
binding: RuntimeReadWrite("/program/video/placement", "preview-items"));
center.Pane("mimir.audio-mixer", "", mixer =>
{
mixer.Text("audio.mixer-title", "Audio Mixer", "strong", weight: 0.55f);
@ -909,18 +934,18 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
var channelIndex = slot;
bank.Card($"audio.channel.{slot}", strip =>
{
strip.Text($"audio.channel.{slot}.name", () => AudioChannelName(channelIndex), "strong", weight: 0.55f);
strip.Text($"audio.channel.{slot}.name", () => AudioChannelName(channelIndex), "strong", weight: 0.55f, binding: RuntimeRead($"/program/audio/channels/{slot}/name", "string"));
strip.Row($"audio.channel.{slot}.toggles", toggles =>
{
toggles.Toggle($"audio.channel.{slot}.mute", "M", () => AudioMuted(channelIndex), value => SetAudioMuted(channelIndex, value));
toggles.Toggle($"audio.channel.{slot}.solo", "S", () => AudioSolo(channelIndex), value => SetAudioSolo(channelIndex, value));
toggles.Toggle($"audio.channel.{slot}.mute", "M", () => AudioMuted(channelIndex), value => SetAudioMuted(channelIndex, value), binding: RuntimeReadWrite($"/program/audio/channels/{slot}/mute", "bool"));
toggles.Toggle($"audio.channel.{slot}.solo", "S", () => AudioSolo(channelIndex), value => SetAudioSolo(channelIndex, value), binding: RuntimeReadWrite($"/program/audio/channels/{slot}/solo", "bool"));
}, weight: 0.55f);
strip.Slider($"audio.channel.{slot}.gain", "Gain", () => AudioGain(channelIndex), value => SetAudioGain(channelIndex, value), 0.0f, 2.0f, "0.00", weight: 0.65f);
strip.Text($"audio.channel.{slot}.status", () => DescribeAudioChannelStatus(channelIndex), "caption", weight: 0.95f);
strip.Slider($"audio.channel.{slot}.gain", "Gain", () => AudioGain(channelIndex), value => SetAudioGain(channelIndex, value), 0.0f, 2.0f, "0.00", weight: 0.65f, binding: RuntimeReadWrite($"/program/audio/channels/{slot}/gain", "float"));
strip.Text($"audio.channel.{slot}.status", () => DescribeAudioChannelStatus(channelIndex), "caption", weight: 0.95f, binding: RuntimeRead($"/program/audio/channels/{slot}/status", "string"));
});
}
}, weight: 1.0f, gap: 6.0f);
mixer.Text("audio.program", DescribeProgramAudio, "caption", weight: 0.35f);
mixer.Text("audio.program", DescribeProgramAudio, "caption", weight: 0.35f, binding: RuntimeRead("/program/audio/summary", "string"));
}, weight: 0.95f);
}, weight: 1.75f);
@ -928,30 +953,30 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
{
right.Pane("mimir.inspector", "", inspector =>
{
inspector.Text("inspector.selection", sceneEditor.DescribeSelection, "strong");
inspector.Options("inspector.video-feed", "Feed", () => presentationControls.SelectedVideoIndex, SelectVideoLayer, VideoFeedOptions());
inspector.Text("inspector.selection", sceneEditor.DescribeSelection, "strong", binding: RuntimeRead("/program/selection/summary", "string"));
inspector.Options("inspector.video-feed", "Feed", () => presentationControls.SelectedVideoIndex, SelectVideoLayer, VideoFeedOptions(), binding: RuntimeReadWrite("/program/selection/videoFeedIndex", "int"));
inspector.Vertical("inspector.source-controls", source =>
{
source.Toggle("video.visible", "Visible", () => sceneEditor.SelectedNode?.Visible ?? false, SetSelectedVideoVisible);
source.Toggle("video.solo", "Solo", () => presentationControls.SelectedVideo?.Solo ?? false, SetSelectedVideoSolo);
source.Slider("video.opacity", "Opacity", () => presentationControls.SelectedVideo?.Opacity ?? 1.0f, SetSelectedVideoOpacity, 0.0f, 1.0f, "0.00");
source.Toggle("video.visible", "Visible", () => sceneEditor.SelectedNode?.Visible ?? false, SetSelectedVideoVisible, binding: RuntimeReadWrite("/program/selection/video/visible", "bool"));
source.Toggle("video.solo", "Solo", () => presentationControls.SelectedVideo?.Solo ?? false, SetSelectedVideoSolo, binding: RuntimeReadWrite("/program/selection/video/solo", "bool"));
source.Slider("video.opacity", "Opacity", () => presentationControls.SelectedVideo?.Opacity ?? 1.0f, SetSelectedVideoOpacity, 0.0f, 1.0f, "0.00", binding: RuntimeReadWrite("/program/selection/video/opacity", "float"));
source.Text("inspector.program-placement", "Program Frame", "strong");
source.Slider("program.x", "X", () => sceneEditor.SelectedProgramX, sceneEditor.SetSelectedProgramX, 0.0f, 1.0f, "0.000");
source.Slider("program.y", "Y", () => sceneEditor.SelectedProgramY, sceneEditor.SetSelectedProgramY, 0.0f, 1.0f, "0.000");
source.Slider("program.w", "W", () => sceneEditor.SelectedProgramWidth, sceneEditor.SetSelectedProgramWidth, 0.01f, 1.0f, "0.000");
source.Slider("program.h", "H", () => sceneEditor.SelectedProgramHeight, sceneEditor.SetSelectedProgramHeight, 0.01f, 1.0f, "0.000");
source.Slider("program.rotation", "Rotation", () => sceneEditor.SelectedNode?.Transform.RotationRadians ?? 0.0f, sceneEditor.SetSelectedRotation, -MathF.PI, MathF.PI, "0.00");
source.Options("program.fit-mode", "Fit", () => sceneEditor.SelectedFitModeIndex, sceneEditor.SetSelectedFitModeIndex, sceneEditor.FitModeOptions);
source.Slider("program.x", "X", () => sceneEditor.SelectedProgramX, sceneEditor.SetSelectedProgramX, 0.0f, 1.0f, "0.000", binding: ProgramSurfaceBinding("/selection/programLayer/placement/x", "float"));
source.Slider("program.y", "Y", () => sceneEditor.SelectedProgramY, sceneEditor.SetSelectedProgramY, 0.0f, 1.0f, "0.000", binding: ProgramSurfaceBinding("/selection/programLayer/placement/y", "float"));
source.Slider("program.w", "W", () => sceneEditor.SelectedProgramWidth, sceneEditor.SetSelectedProgramWidth, 0.01f, 1.0f, "0.000", binding: ProgramSurfaceBinding("/selection/programLayer/placement/width", "float"));
source.Slider("program.h", "H", () => sceneEditor.SelectedProgramHeight, sceneEditor.SetSelectedProgramHeight, 0.01f, 1.0f, "0.000", binding: ProgramSurfaceBinding("/selection/programLayer/placement/height", "float"));
source.Slider("program.rotation", "Rotation", () => sceneEditor.SelectedNode?.Transform.RotationRadians ?? 0.0f, sceneEditor.SetSelectedRotation, -MathF.PI, MathF.PI, "0.00", binding: ProgramSurfaceBinding("/selection/programLayer/placement/rotationRadians", "float"));
source.Options("program.fit-mode", "Fit", () => sceneEditor.SelectedFitModeIndex, sceneEditor.SetSelectedFitModeIndex, sceneEditor.FitModeOptions, binding: ProgramSurfaceBinding("/selection/programLayer/fitMode", "enum"));
source.Row("program.fit", fit =>
{
fit.Button("program.center", "Center", sceneEditor.CenterSelectedProgramLayer);
fit.Button("program.fit-selected", "Fit", sceneEditor.FitSelectedProgramLayer);
fit.Button("program.fill-selected", "Fill", sceneEditor.FillSelectedProgramLayer);
fit.Button("program.center", "Center", sceneEditor.CenterSelectedProgramLayer, binding: RuntimeCommand("program.layer.center", "/commands/program/layer/center"));
fit.Button("program.fit-selected", "Fit", sceneEditor.FitSelectedProgramLayer, binding: RuntimeCommand("program.layer.fit", "/commands/program/layer/fit"));
fit.Button("program.fill-selected", "Fill", sceneEditor.FillSelectedProgramLayer, binding: RuntimeCommand("program.layer.fill", "/commands/program/layer/fill"));
});
source.Row("program.order", order =>
{
order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1));
order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1));
order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1), binding: RuntimeCommand("program.layer.up", "/commands/program/layer/up"));
order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1), binding: RuntimeCommand("program.layer.down", "/commands/program/layer/down"));
});
}, isVisible: () => sceneEditor.HasSelectedProgramSource);
}, weight: 1.0f);
@ -959,21 +984,21 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
right.Pane("mimir.output", "", output =>
{
output.Text("output.title", "Output", "strong", weight: 0.55f);
output.Text("output.program-summary", DescribeProgramOutput, "caption", weight: 0.6f);
output.Text("output.program-summary", DescribeProgramOutput, "caption", weight: 0.6f, binding: RuntimeRead("/program/output/summary", "string"));
output.Row("output.recording", row =>
{
row.Button("program.record", "Record", StartProgramRecording, weight: 0.7f);
row.Button("program.record-stop", "Stop", programRecorder.Stop, weight: 0.55f);
row.Text("program.record-status", programRecorder.Describe, "caption", weight: 1.75f);
row.Button("program.record", "Record", StartProgramRecording, weight: 0.7f, binding: RuntimeCommand("program.record.start", "/commands/program/record/start"));
row.Button("program.record-stop", "Stop", programRecorder.Stop, weight: 0.55f, binding: RuntimeCommand("program.record.stop", "/commands/program/record/stop"));
row.Text("program.record-status", programRecorder.Describe, "caption", weight: 1.75f, binding: RuntimeRead("/program/output/recording/status", "string"));
}, weight: 0.7f);
output.Text("output.record-path", () => $"folder {programRecorder.OutputDirectory}", "caption", weight: 0.7f);
output.Text("output.record-path", () => $"folder {programRecorder.OutputDirectory}", "caption", weight: 0.7f, binding: RuntimeRead("/program/output/recording/folder", "path"));
output.Row("output.streaming", row =>
{
row.Button("program.stream", "Live", StartProgramStreaming, weight: 0.7f);
row.Button("program.stream-stop", "Stop", programStreamer.Stop, weight: 0.55f);
row.Text("program.stream-status", programStreamer.Describe, "caption", weight: 1.75f);
row.Button("program.stream", "Live", StartProgramStreaming, weight: 0.7f, binding: RuntimeCommand("program.stream.start", "/commands/program/stream/start"));
row.Button("program.stream-stop", "Stop", programStreamer.Stop, weight: 0.55f, binding: RuntimeCommand("program.stream.stop", "/commands/program/stream/stop"));
row.Text("program.stream-status", programStreamer.Describe, "caption", weight: 1.75f, binding: RuntimeRead("/program/output/streaming/status", "string"));
}, weight: 0.7f);
output.Text("output.stream-target", () => $"target {programStreamer.TargetUrl}", "caption", weight: 0.7f);
output.Text("output.stream-target", () => $"target {programStreamer.TargetUrl}", "caption", weight: 0.7f, binding: RuntimeRead("/program/output/streaming/target", "uri"));
}, fixedExtent: 172.0f);
}, weight: 0.95f);
});

View file

@ -177,7 +177,18 @@ public sealed record MimirEveDashboardUiElementSnapshot(
[property: Key(8)] MimirEveDashboardUiLayoutSnapshot? Layout,
[property: Key(9)] MimirEveDashboardUiStyleSnapshot? Style,
[property: Key(10)] MimirEveDashboardUiMetricSnapshot? Metric,
[property: Key(11)] MimirEveDashboardUiElementSnapshot[] Children);
[property: Key(11)] MimirEveDashboardUiElementSnapshot[] Children,
[property: Key(12)] MimirEveDashboardUiBindingSnapshot? Binding = null);
[MessagePackObject]
public sealed record MimirEveDashboardUiBindingSnapshot(
[property: Key(0)] string DocumentSchema,
[property: Key(1)] string DocumentId,
[property: Key(2)] string Path,
[property: Key(3)] string ValueKind,
[property: Key(4)] string Access,
[property: Key(5)] string Authority,
[property: Key(6)] string? CommandId);
[MessagePackObject]
public sealed record MimirEveDashboardUiLayoutSnapshot(

View file

@ -333,3 +333,5 @@
{"ts": "2026-06-01T14:26:12+01:00", "type": "implementation", "status": "ok", "note": "Picked the compositor UX low-hanging fruit: hover/active preview state, snap guides, empty-canvas deselect, selected-source contain/cover/stretch fit mode, arrow-key nudging that bypasses snap for precision, and PageUp/PageDown layer ordering. Scene-editor smoke now asserts hover, guide, fit, nudge, empty-deselect, direct move/resize handles, aspect lock, snap, and non-source inspector gating."}
{"ts": "2026-06-01T14:39:51+01:00", "type": "decision", "status": "ok", "note": "2026-06-01: Mimir editor output authority moved to a right-rail Output panel below Inspector. MimirProgramRecorder now writes timestamped receipts to the user Videos\\\\Mimir folder by default, while MimirProgramStreamer owns Live/Stop for the configured Broadcast RTMP path and refuses to publish without MIMIR_STREAM_INPUT unless a synthetic smoke is explicitly enabled."}
{"ts": "2026-06-01T14:51:58+01:00", "type": "decision", "status": "ok", "note": "2026-06-01: Eve is the portable dashboard authority. The C# CultUI-shaped Aquarium UI contract is a reference API for retained dashboard documents; Fensalir Direct2D/D3D12 are native lowerings of that document, not the owner. The Mimir editor now uses edge-to-edge headerless panels with fixed output controls and an expanding inspector through that shared document model."}
{"ts": "2026-06-01T17:38:44+01:00", "type": "implementation", "status": "ok", "note": "2026-06-01: Eve/CultUI retained elements now carry typed CultCache/CultMesh binding descriptors (document schema/id, field path, value kind, access, authority, optional command id). Mimir binds editor output controls, selected-layer inspector fields, audio strips, and preview placement to those descriptors while native callbacks remain local lowering actuators."}
{"ts": "2026-06-01T17:42:27+01:00", "type": "verification", "status": "ok", "note": "2026-06-01: Release builds passed for Fensalir Aquarium.Engine, Mimir.EveDashboard, and Mimir.App after adding typed Eve/CultUI binding descriptors. Mimir.BufferSmoke --scene-editor-smoke passed with preview/source placement interactions intact."}

View file

@ -70,7 +70,11 @@ invariants:
hover/active handle state and snap guides are supplied through the shared
document state lowered by Fensalir's Direct2D/D3D12 paths; Eve is the
portable dashboard authority and CultUI is only the original C# reference
API shape. Keyboard arrows nudge selected layers without
API shape. Dashboard fields now carry explicit typed CultCache/CultMesh
bindings with document schema/id, field path, value kind, access,
authority, and optional command id; local callbacks are native lowering
actuators while Verse-backed CultCache fields are the portable state
contract. Keyboard arrows nudge selected layers without
snapping, PageUp/PageDown reorder layers, and the inspector owns the
selected source contain/cover/stretch fit mode.
- The right rail `Output` panel owns OBS-like program actions below the