Compare commits
1
Commits
09dbdfad79
...
44019a706e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44019a706e |
@@ -42,7 +42,7 @@ public readonly record struct PerfSnapshot(
|
|||||||
/// store's entities grouped by archetype, reflects a selected entity's components and fields,
|
/// store's entities grouped by archetype, reflects a selected entity's components and fields,
|
||||||
/// writes simple field edits back, and (when a renderer is supplied) picks entities under the
|
/// writes simple field edits back, and (when a renderer is supplied) picks entities under the
|
||||||
/// cursor and reports frame metrics. Holds no Myra state, so its logic is unit-testable without a
|
/// cursor and reports frame metrics. Holds no Myra state, so its logic is unit-testable without a
|
||||||
/// GPU. UI lazily rebuilds when <see cref="Revision"/> changes.
|
/// GPU. UI lazily rebuilds when <see cref="ListRevision"/> or <see cref="DetailRevision"/> change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EcsInspector
|
public sealed class EcsInspector
|
||||||
{
|
{
|
||||||
@@ -70,8 +70,14 @@ public sealed class EcsInspector
|
|||||||
/// <summary>True while the inspector panel is open.</summary>
|
/// <summary>True while the inspector panel is open.</summary>
|
||||||
public bool IsOpen { get; private set; }
|
public bool IsOpen { get; private set; }
|
||||||
|
|
||||||
/// <summary>Increments on every change the UI should rebuild for (open/select/edit/refresh).</summary>
|
/// <summary>Increments when the entity/archetype lists or tab state need a UI rebuild.</summary>
|
||||||
public int Revision { get; private set; }
|
public int ListRevision { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increments when the selected entity's field view needs a UI rebuild. Kept separate from
|
||||||
|
/// <see cref="ListRevision"/> so the periodic refresh never recreates a field editor mid-edit.
|
||||||
|
/// </summary>
|
||||||
|
public int DetailRevision { get; private set; }
|
||||||
|
|
||||||
/// <summary>The active tab.</summary>
|
/// <summary>The active tab.</summary>
|
||||||
public InspectorTab ActiveTab { get; private set; }
|
public InspectorTab ActiveTab { get; private set; }
|
||||||
@@ -93,21 +99,22 @@ public sealed class EcsInspector
|
|||||||
{
|
{
|
||||||
IsOpen = !IsOpen;
|
IsOpen = !IsOpen;
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
Bump();
|
BumpList();
|
||||||
|
BumpDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Switches the active tab.</summary>
|
/// <summary>Switches the active tab.</summary>
|
||||||
public void SetTab(InspectorTab tab)
|
public void SetTab(InspectorTab tab)
|
||||||
{
|
{
|
||||||
ActiveTab = tab;
|
ActiveTab = tab;
|
||||||
Bump();
|
BumpList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Arms (or disarms) world-pick mode.</summary>
|
/// <summary>Arms (or disarms) world-pick mode.</summary>
|
||||||
public void ArmPick(bool armed = true)
|
public void ArmPick(bool armed = true)
|
||||||
{
|
{
|
||||||
PickArmed = armed;
|
PickArmed = armed;
|
||||||
Bump();
|
BumpList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sets the archetype filter and refreshes the list.</summary>
|
/// <summary>Sets the archetype filter and refreshes the list.</summary>
|
||||||
@@ -115,7 +122,7 @@ public sealed class EcsInspector
|
|||||||
{
|
{
|
||||||
Search = search ?? "";
|
Search = search ?? "";
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
Bump();
|
BumpList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Selects an archetype group by index; clears the entity selection.</summary>
|
/// <summary>Selects an archetype group by index; clears the entity selection.</summary>
|
||||||
@@ -123,14 +130,16 @@ public sealed class EcsInspector
|
|||||||
{
|
{
|
||||||
SelectedArchetype = index;
|
SelectedArchetype = index;
|
||||||
SelectedEntityId = -1;
|
SelectedEntityId = -1;
|
||||||
Bump();
|
BumpList();
|
||||||
|
BumpDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Selects an entity by runtime id.</summary>
|
/// <summary>Selects an entity by runtime id.</summary>
|
||||||
public void SelectEntity(int entityId)
|
public void SelectEntity(int entityId)
|
||||||
{
|
{
|
||||||
SelectedEntityId = entityId;
|
SelectedEntityId = entityId;
|
||||||
Bump();
|
BumpList();
|
||||||
|
BumpDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -145,11 +154,24 @@ public sealed class EcsInspector
|
|||||||
}
|
}
|
||||||
|
|
||||||
_refreshTimer += deltaSeconds;
|
_refreshTimer += deltaSeconds;
|
||||||
if (_refreshTimer >= 0.5f)
|
if (_refreshTimer < 0.5f)
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_refreshTimer = 0f;
|
_refreshTimer = 0f;
|
||||||
|
// Only rebuild the lists when entities were actually added or removed — never on a plain
|
||||||
|
// tick, so a field editor in the details pane is not destroyed while it has focus.
|
||||||
|
var count = 0;
|
||||||
|
foreach (var _ in _store.Entities)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count != _entityCount)
|
||||||
|
{
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
Bump();
|
BumpList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +253,7 @@ public sealed class EcsInspector
|
|||||||
|
|
||||||
// Adding an existing component type replaces it with the edited value.
|
// Adding an existing component type replaces it with the edited value.
|
||||||
AddComponentMethod.MakeGenericMethod(componentType).Invoke(entity, [boxed]);
|
AddComponentMethod.MakeGenericMethod(componentType).Invoke(entity, [boxed]);
|
||||||
Bump();
|
BumpDetail();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +303,8 @@ public sealed class EcsInspector
|
|||||||
{
|
{
|
||||||
SelectedEntityId = best;
|
SelectedEntityId = best;
|
||||||
PickArmed = false;
|
PickArmed = false;
|
||||||
Bump();
|
BumpList();
|
||||||
|
BumpDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
return best;
|
return best;
|
||||||
@@ -345,7 +368,9 @@ public sealed class EcsInspector
|
|||||||
private static object BoxedValue(EntityComponent component) => component.Value;
|
private static object BoxedValue(EntityComponent component) => component.Value;
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
private void Bump() => Revision++;
|
private void BumpList() => ListRevision++;
|
||||||
|
|
||||||
|
private void BumpDetail() => DetailRevision++;
|
||||||
|
|
||||||
private void RebuildIfNeeded()
|
private void RebuildIfNeeded()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace MrGameEng.Inspector;
|
|||||||
/// Entities/Performance tabs and arms world-pick; the Entities tab shows archetypes → entities →
|
/// Entities/Performance tabs and arms world-pick; the Entities tab shows archetypes → entities →
|
||||||
/// the selected entity's components and editable fields; the Performance tab shows renderer
|
/// the selected entity's components and editable fields; the Performance tab shows renderer
|
||||||
/// timings. A thin border tracks the selected entity in the world each frame. Rebuilds lazily on
|
/// timings. A thin border tracks the selected entity in the world each frame. Rebuilds lazily on
|
||||||
/// <see cref="EcsInspector.Revision"/> changes.
|
/// <see cref="EcsInspector.ListRevision"/>/<see cref="EcsInspector.DetailRevision"/> changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class EcsInspectorUi
|
internal sealed class EcsInspectorUi
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,8 @@ internal sealed class EcsInspectorUi
|
|||||||
private readonly Panel _highlight;
|
private readonly Panel _highlight;
|
||||||
private readonly VerticalStackPanel _panel;
|
private readonly VerticalStackPanel _panel;
|
||||||
|
|
||||||
private int _lastRevision = -1;
|
private int _lastListRevision = -1;
|
||||||
|
private int _lastDetailRevision = -1;
|
||||||
|
|
||||||
public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer)
|
public EcsInspectorUi(EcsInspector inspector, Renderer2D renderer)
|
||||||
{
|
{
|
||||||
@@ -91,10 +92,24 @@ internal sealed class EcsInspectorUi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_inspector.Revision != _lastRevision)
|
// Stats and perf are cheap text updates — refresh every frame so they stay live.
|
||||||
|
var perf = _inspector.Performance();
|
||||||
|
UpdateStats(perf);
|
||||||
|
if (_inspector.ActiveTab == InspectorTab.Performance)
|
||||||
{
|
{
|
||||||
_lastRevision = _inspector.Revision;
|
UpdatePerf(perf);
|
||||||
Rebuild();
|
}
|
||||||
|
|
||||||
|
if (_inspector.ListRevision != _lastListRevision)
|
||||||
|
{
|
||||||
|
_lastListRevision = _inspector.ListRevision;
|
||||||
|
RebuildLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inspector.DetailRevision != _lastDetailRevision)
|
||||||
|
{
|
||||||
|
_lastDetailRevision = _inspector.DetailRevision;
|
||||||
|
RebuildDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateHighlight();
|
UpdateHighlight();
|
||||||
@@ -116,25 +131,28 @@ internal sealed class EcsInspectorUi
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Rebuild()
|
private void RebuildLists()
|
||||||
{
|
{
|
||||||
var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities;
|
var entitiesTab = _inspector.ActiveTab == InspectorTab.Entities;
|
||||||
_entitiesSection.Visible = entitiesTab;
|
_entitiesSection.Visible = entitiesTab;
|
||||||
_perf.Visible = !entitiesTab;
|
_perf.Visible = !entitiesTab;
|
||||||
_pickLabel.Text = _inspector.PickArmed ? "Pick*" : "Pick";
|
_pickLabel.Text = _inspector.PickArmed ? "Pick*" : "Pick";
|
||||||
|
|
||||||
var perf = _inspector.Performance();
|
|
||||||
_stats.Text =
|
|
||||||
$"entities {perf.Entities} | archetypes {perf.Archetypes} | {perf.Fps} FPS"
|
|
||||||
+ (_inspector.PickArmed ? " | click an entity…" : "");
|
|
||||||
|
|
||||||
if (entitiesTab)
|
if (entitiesTab)
|
||||||
{
|
{
|
||||||
RebuildArchetypes();
|
RebuildArchetypes();
|
||||||
RebuildEntities();
|
RebuildEntities();
|
||||||
RebuildDetails();
|
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
private void UpdateStats(in PerfSnapshot perf)
|
||||||
|
{
|
||||||
|
_stats.Text =
|
||||||
|
$"entities {perf.Entities} | archetypes {perf.Archetypes} | {perf.Fps} FPS"
|
||||||
|
+ (_inspector.PickArmed ? " | click an entity…" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePerf(in PerfSnapshot perf)
|
||||||
{
|
{
|
||||||
_perf.Text =
|
_perf.Text =
|
||||||
$"FPS {perf.Fps}\n"
|
$"FPS {perf.Fps}\n"
|
||||||
@@ -147,7 +165,6 @@ internal sealed class EcsInspectorUi
|
|||||||
+ $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n"
|
+ $"sprites {perf.SubmittedSprites} drawn, {perf.CulledSprites} culled\n"
|
||||||
+ $"entities {perf.Entities}";
|
+ $"entities {perf.Entities}";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void RebuildArchetypes()
|
private void RebuildArchetypes()
|
||||||
{
|
{
|
||||||
@@ -209,16 +226,31 @@ internal sealed class EcsInspectorUi
|
|||||||
|
|
||||||
private Widget BuildFieldRow(Type componentType, FieldRow field)
|
private Widget BuildFieldRow(Type componentType, FieldRow field)
|
||||||
{
|
{
|
||||||
var row = new HorizontalStackPanel { Spacing = 6 };
|
var row = new HorizontalStackPanel { Spacing = 8 };
|
||||||
row.Widgets.Add(new Label { Text = field.Name, Width = 150 });
|
row.Widgets.Add(
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = field.Name,
|
||||||
|
Width = 140,
|
||||||
|
TextColor = new Color(170, 178, 190),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!field.Editable)
|
if (!field.Editable)
|
||||||
{
|
{
|
||||||
row.Widgets.Add(new Label { Text = field.Value });
|
row.Widgets.Add(new Label { Text = field.Value, TextColor = new Color(150, 156, 168) });
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
var editor = new TextBox { Text = field.Value, Width = 220 };
|
var editor = new TextBox
|
||||||
|
{
|
||||||
|
Text = field.Value,
|
||||||
|
Width = 150,
|
||||||
|
Padding = new Thickness(4, 2),
|
||||||
|
Background = new SolidBrush(new Color(28, 33, 42)),
|
||||||
|
Border = new SolidBrush(new Color(70, 82, 100)),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
};
|
||||||
var entityId = _inspector.SelectedEntityId;
|
var entityId = _inspector.SelectedEntityId;
|
||||||
editor.KeyDown += (_, args) =>
|
editor.KeyDown += (_, args) =>
|
||||||
{
|
{
|
||||||
@@ -230,7 +262,13 @@ internal sealed class EcsInspectorUi
|
|||||||
row.Widgets.Add(editor);
|
row.Widgets.Add(editor);
|
||||||
if (field.Kind == FieldKind.Enum && field.EnumOptions is { Length: > 0 })
|
if (field.Kind == FieldKind.Enum && field.EnumOptions is { Length: > 0 })
|
||||||
{
|
{
|
||||||
row.Widgets.Add(new Label { Text = "(" + string.Join("/", field.EnumOptions) + ")" });
|
row.Widgets.Add(
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = string.Join(" / ", field.EnumOptions),
|
||||||
|
TextColor = new Color(120, 128, 140),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
|
|||||||
Reference in New Issue
Block a user