Gate inspector transforms to selected source

This commit is contained in:
Meta Vens 2026-06-01 13:37:52 +01:00
commit 4d30d1e994
8 changed files with 54 additions and 35 deletions

View file

@ -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

View file

@ -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]].

View file

@ -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`.

View file

@ -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;
}

View file

@ -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);
});
});

View file

@ -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<MimirSceneEditorNode> 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
{

View file

@ -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."}

View file

@ -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