diff --git a/docs/eve-dashboard.md b/docs/eve-dashboard.md index 371d462..dbe73d2 100644 --- a/docs/eve-dashboard.md +++ b/docs/eve-dashboard.md @@ -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 diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index c934644..f061bb6 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -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 diff --git a/notes/current-system-map.md b/notes/current-system-map.md index 9b3e32c..593a98b 100644 --- a/notes/current-system-map.md +++ b/notes/current-system-map.md @@ -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 diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index a15ea6e..9a477da 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/src/Mimir.EveDashboard/Program.cs b/src/Mimir.EveDashboard/Program.cs index 57ef69a..d6d6e79 100644 --- a/src/Mimir.EveDashboard/Program.cs +++ b/src/Mimir.EveDashboard/Program.cs @@ -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; } diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index f1dabae..339dce3 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -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); }); diff --git a/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs b/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs index 28789f1..983dc15 100644 --- a/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs +++ b/src/Mimir.Runtime/Synchronization/MimirCultMeshContracts.cs @@ -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( diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 2c2000d..2ba3d50 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."} diff --git a/state/map.yaml b/state/map.yaml index 0d2b65c..fe33c6d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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