From 4d30d1e9947fea8de4eacdbc398a1f0655f6fb0a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 1 Jun 2026 13:37:52 +0100 Subject: [PATCH] Gate inspector transforms to selected source --- docs/implementation-plan.md | 4 +- notes/current-system-map.md | 4 +- notes/fresh-workspace-handoff.md | 2 + src/Mimir.BufferSmoke/Program.cs | 9 +++- src/Mimir.Runtime/MimirRuntime.cs | 41 ++++++++++--------- .../Synchronization/MimirSceneEditorState.cs | 24 ++++++----- state/evidence.jsonl | 1 + state/map.yaml | 4 +- 8 files changed, 54 insertions(+), 35 deletions(-) diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index f5c936c..44533ce 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -303,7 +303,9 @@ a named invariant that the native runtime cannot protect yet. so the app preview is a live program-surface witness rather than a symbolic rectangle diagram. 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. + resizing through direct preview handles, sliders, or the scale gizmo. Explicit + transform/source controls are inspector-only and only appear while a program + source is selected; the compositor canvas handles are the primary edit path. See [[scene-editor-control-surface|Mimir Scene Editor Control Surface]]. `Mimir.BufferSmoke --scene-editor-smoke` proves the graph/control/gizmo owner path. World SDF text glyph rendering, ASSIMP-style mesh import/upload, and diff --git a/notes/current-system-map.md b/notes/current-system-map.md index da9ee11..92e33a9 100644 --- a/notes/current-system-map.md +++ b/notes/current-system-map.md @@ -549,7 +549,9 @@ placement overlays on top. This editor is not the OBS program output authority; it is a live program-surface witness. The placement commit primitive keeps layers inside the program frame, snaps near edges/center, and preserves source aspect while resizing through direct preview handles, sliders, or the scale -gizmo. World SDF glyph rendering, ASSIMP-style +gizmo. Explicit transform/source controls are inspector-only and gated to a +selected program source; the center canvas handles are the primary compositor +edit path. 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]]. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index c849263..ea4a440 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -52,6 +52,8 @@ Get-Content .\state\evidence.jsonl -Tail 8 as its background and overlays Mimir's normalized layer placements on top. Placement commits snap near frame edges/center and preserve source aspect while resizing through direct preview handles, sliders, or the scale gizmo. + Explicit source/transform controls are inspector-only and only visible when a + program source is selected. Camera/SDF/model editor affordances have no live UI authority, and editor camera/SDF/spline output no longer contributes to the runtime frame. Smoke with `--scene-editor-smoke`. diff --git a/src/Mimir.BufferSmoke/Program.cs b/src/Mimir.BufferSmoke/Program.cs index 9c37e9f..27efbe3 100644 --- a/src/Mimir.BufferSmoke/Program.cs +++ b/src/Mimir.BufferSmoke/Program.cs @@ -4259,11 +4259,15 @@ static int RunSceneEditorSmoke() var hasCameraPreview = previewItems.Any(item => string.Equals(item.Id, "editor-camera", StringComparison.Ordinal)); var hasOnlyVideoPreview = previewItems.Count == 2 && previewItems.All(item => item.Id.StartsWith("feed:", StringComparison.Ordinal)); var programCentered = placement is { CenterX: 0.5f, CenterY: 0.5f }; + editor.SelectNode("editor-camera"); + var cameraIsNotProgramSource = !editor.HasSelectedProgramSource && + Math.Abs(editor.SelectedProgramX - 0.5f) < 0.001f && + Math.Abs(editor.SelectedProgramY - 0.5f) < 0.001f; Console.WriteLine( $"scene-editor-smoke nodes={editor.Nodes.Count} feeds={editor.Nodes.Count(node => node.Kind == MimirSceneEditorNodeKind.SensorFeedPanel)} " + $"preview={previewItems.Count} selected={editor.SelectedNode?.DisplayName} hasDisplay={hasDisplay} cameraPreview={hasCameraPreview} " + - $"placement={placement?.CenterX:0.000},{placement?.CenterY:0.000} programCentered={programCentered} aspectLocked={aspectLocked} snapped={snappedToBounds} previewMove={previewMoveHandled} previewHandle={previewHandleAspectLocked}"); + $"placement={placement?.CenterX:0.000},{placement?.CenterY:0.000} programCentered={programCentered} aspectLocked={aspectLocked} snapped={snappedToBounds} previewMove={previewMoveHandled} previewHandle={previewHandleAspectLocked} cameraSource={editor.HasSelectedProgramSource}"); return editor.Nodes.Count == 3 && hasDisplay && @@ -4273,7 +4277,8 @@ static int RunSceneEditorSmoke() aspectLocked && snappedToBounds && previewMoveHandled && - previewHandleAspectLocked + previewHandleAspectLocked && + cameraIsNotProgramSource ? 0 : 1; } diff --git a/src/Mimir.Runtime/MimirRuntime.cs b/src/Mimir.Runtime/MimirRuntime.cs index 4d816d6..5405193 100644 --- a/src/Mimir.Runtime/MimirRuntime.cs +++ b/src/Mimir.Runtime/MimirRuntime.cs @@ -922,26 +922,29 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec { inspector.Text("inspector.selection", sceneEditor.DescribeSelection, "strong"); inspector.Options("inspector.video-feed", "Feed", () => presentationControls.SelectedVideoIndex, SelectVideoLayer, VideoFeedOptions()); - inspector.Toggle("video.visible", "Visible", () => sceneEditor.SelectedNode?.Visible ?? false, SetSelectedVideoVisible); - inspector.Toggle("video.solo", "Solo", () => presentationControls.SelectedVideo?.Solo ?? false, SetSelectedVideoSolo); - inspector.Slider("video.opacity", "Opacity", () => presentationControls.SelectedVideo?.Opacity ?? 1.0f, SetSelectedVideoOpacity, 0.0f, 1.0f, "0.00"); - inspector.Text("inspector.program-placement", "Program Frame", "strong"); - inspector.Slider("program.x", "X", () => sceneEditor.SelectedProgramX, sceneEditor.SetSelectedProgramX, 0.0f, 1.0f, "0.000"); - inspector.Slider("program.y", "Y", () => sceneEditor.SelectedProgramY, sceneEditor.SetSelectedProgramY, 0.0f, 1.0f, "0.000"); - inspector.Slider("program.w", "W", () => sceneEditor.SelectedProgramWidth, sceneEditor.SetSelectedProgramWidth, 0.01f, 1.0f, "0.000"); - inspector.Slider("program.h", "H", () => sceneEditor.SelectedProgramHeight, sceneEditor.SetSelectedProgramHeight, 0.01f, 1.0f, "0.000"); - inspector.Slider("program.rotation", "Rotation", () => sceneEditor.SelectedNode?.Transform.RotationRadians ?? 0.0f, sceneEditor.SetSelectedRotation, -MathF.PI, MathF.PI, "0.00"); - inspector.Row("program.fit", fit => + inspector.Vertical("inspector.source-controls", source => { - fit.Button("program.center", "Center", sceneEditor.CenterSelectedProgramLayer); - fit.Button("program.fit-selected", "Fit", sceneEditor.FitSelectedProgramLayer); - fit.Button("program.fill-selected", "Fill", sceneEditor.FillSelectedProgramLayer); - }); - inspector.Row("program.order", order => - { - order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1)); - order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1)); - }); + 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.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.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); + }); + source.Row("program.order", order => + { + order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1)); + order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1)); + }); + }, isVisible: () => sceneEditor.HasSelectedProgramSource); }, weight: 0.95f); }); }); diff --git a/src/Mimir.Runtime/Synchronization/MimirSceneEditorState.cs b/src/Mimir.Runtime/Synchronization/MimirSceneEditorState.cs index b423166..99b995a 100644 --- a/src/Mimir.Runtime/Synchronization/MimirSceneEditorState.cs +++ b/src/Mimir.Runtime/Synchronization/MimirSceneEditorState.cs @@ -40,6 +40,8 @@ public sealed class MimirSceneEditorState public MimirSceneEditorNode? SelectedNode => nodes.FirstOrDefault(node => string.Equals(node.Id, SelectedNodeId, StringComparison.Ordinal)); + public bool HasSelectedProgramSource => SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel }; + public IReadOnlyList VisibleNodes => nodes.Where(node => node.Visible).ToArray(); @@ -279,17 +281,17 @@ public sealed class MimirSceneEditorState } } - public float SelectedProgramX => SelectedNode is { } selected ? PlacementForNode(selected).CenterX : 0.5f; + public float SelectedProgramX => SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected ? PlacementForNode(selected).CenterX : 0.5f; - public float SelectedProgramY => SelectedNode is { } selected ? PlacementForNode(selected).CenterY : 0.5f; + public float SelectedProgramY => SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected ? PlacementForNode(selected).CenterY : 0.5f; - public float SelectedProgramWidth => SelectedNode is { } selected ? PlacementForNode(selected).Width : 0.1f; + public float SelectedProgramWidth => SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected ? PlacementForNode(selected).Width : 0.1f; - public float SelectedProgramHeight => SelectedNode is { } selected ? PlacementForNode(selected).Height : 0.1f; + public float SelectedProgramHeight => SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected ? PlacementForNode(selected).Height : 0.1f; public void SetSelectedProgramX(float value) { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, PlacementForNode(selected) with { CenterX = value }); } @@ -297,7 +299,7 @@ public sealed class MimirSceneEditorState public void SetSelectedProgramY(float value) { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, PlacementForNode(selected) with { CenterY = value }); } @@ -305,7 +307,7 @@ public sealed class MimirSceneEditorState public void SetSelectedProgramWidth(float value) { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, ResizeProgramPlacement(selected, PlacementForNode(selected), value, resizeWidth: true)); } @@ -313,7 +315,7 @@ public sealed class MimirSceneEditorState public void SetSelectedProgramHeight(float value) { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, ResizeProgramPlacement(selected, PlacementForNode(selected), value, resizeWidth: false)); } @@ -321,7 +323,7 @@ public sealed class MimirSceneEditorState public void CenterSelectedProgramLayer() { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, PlacementForNode(selected) with { CenterX = 0.5f, CenterY = 0.5f }); } @@ -329,7 +331,7 @@ public sealed class MimirSceneEditorState public void FitSelectedProgramLayer() { - if (SelectedNode is not { } selected) + if (SelectedNode is not { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { return; } @@ -358,7 +360,7 @@ public sealed class MimirSceneEditorState public void FillSelectedProgramLayer() { - if (SelectedNode is { } selected) + if (SelectedNode is { Kind: MimirSceneEditorNodeKind.SensorFeedPanel } selected) { SetNodeProgramPlacement(selected, PlacementForNode(selected) with { diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c56ab59..5873654 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -329,3 +329,4 @@ {"ts": "2026-06-01T12:46:47+01:00", "type": "decision", "status": "ok", "note": "Mimir center preview now requests Fensalir's UI preview output-buffer blit and overlays MimirSceneEditorState placement items on top; Fensalir owns the backbuffer snapshot/draw, Mimir owns only preview intent and layer geometry."} {"ts": "2026-06-01T13:22:54+01:00", "type": "implementation", "status": "ok", "note": "Added MimirSceneEditorState program-placement affordances: one placement commit path clamps layers inside the program frame, snaps centers/edges near boundaries, and preserves the source aspect ratio for width/height slider edits plus scale-gizmo resizing. --scene-editor-smoke now asserts aspect locking and boundary snapping."} {"ts": "2026-06-01T13:32:22+01:00", "type": "implementation", "status": "ok", "note": "Direct compositor handles are now wired: Fensalir CultUI preview hit-tests selected item corner handles/body drags and emits normalized AquariumUiPreviewInteraction gestures, while MimirSceneEditorState handles those gestures through the same snapping/aspect-preserving placement owner. Sliders remain fallback/precision controls."} +{"ts": "2026-06-01T13:37:34+01:00", "type": "implementation", "status": "ok", "note": "Gated explicit Mimir inspector source/transform controls behind HasSelectedProgramSource. Direct compositor preview handles remain the primary edit path; inspector sliders/buttons are selected-source precision/fallback controls only. Scene-editor smoke now proves the editor camera is not a program source."} diff --git a/state/map.yaml b/state/map.yaml index 5dbf818..ec5366a 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -63,7 +63,9 @@ invariants: edges/center, and preserve source aspect while resizing through sliders or the scale gizmo. The center preview now has direct Fensalir CultUI body/corner-handle gestures wired to the same placement owner; sliders are - fallback/precision controls, not the primary compositor surface. + fallback/precision controls, not the primary compositor surface. Explicit + source/transform controls are inspector-only and visible only while a + program source is selected. - `MimirProgramRecorder` owns the first app-level recording control. The Video Sources rail exposes `Record` / `Stop`, status text, latest output path, and process-tree shutdown. Its current actuator delegates to