Gate inspector transforms to selected source
This commit is contained in:
parent
867984dfbd
commit
4d30d1e994
8 changed files with 54 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]].
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue