Bind Eve UI fields to CultCache paths
This commit is contained in:
parent
3181bb3a28
commit
f6b964a440
9 changed files with 134 additions and 45 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue