Editor/MetricRowInjector.cs

Editor utility that injects a read-only metric row into the Transform component editors in the editor UI. It scans the editor widget tree periodically, finds GameObjectInspector entries, locates the Transform component editor, builds a customized SerializedProperty for the LocalPosition, adds a MetersAttribute and display name, and appends a ControlSheet row to the ComponentEditorWidget layout once per widget.

Reflection
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Editor;
using Editor.Inspectors;
using Sandbox;

namespace Metrify;

public static class MetricRowInjector
{
	private static readonly ConditionalWeakTable<Widget, object> _injected = new();
	private static RealTimeSince _sinceLastScan;

	[EditorEvent.Frame]
	public static void OnFrame()
	{
		// The UI tree only changes on selection/edit; a 10Hz scan is plenty and keeps the
		// recursive traversal off the per-frame cost.
		if ( _sinceLastScan < 0.1f ) return;
		_sinceLastScan = 0;

		var root = EditorWindow;
		if ( !root.IsValid() ) return;

		foreach ( var inspector in FindDescendants<GameObjectInspector>( root ) )
		{
			var transformSheet = FindDescendants<ComponentSheet>( inspector )
				.FirstOrDefault( s => s.Header?.Title == "Transform" );
			if ( !transformSheet.IsValid() ) continue;

			var editorWidget = FindDescendants<ComponentEditorWidget>( transformSheet ).FirstOrDefault();
			if ( !editorWidget.IsValid() || editorWidget.Layout is null ) continue;

			if ( _injected.TryGetValue( editorWidget, out _ ) ) continue;

			var metric = BuildMetricProperty( inspector.SerializedObject );
			if ( metric is null ) continue;

			var sheet = new ControlSheet();
			sheet.IncludePropertyNames = true;
			sheet.AddRow( metric );

			editorWidget.Layout.Add( sheet );
			_injected.Add( editorWidget, null );
		}
	}

	private static SerializedProperty BuildMetricProperty( SerializedObject inspectorObject )
	{
		var transformProp = inspectorObject?.GetProperty( nameof( GameObject.Transform ) );
		if ( transformProp is null ) return null;
		if ( !transformProp.TryGetAsObject( out var transform ) ) return null;

		var localPosition = transform.GetProperty( "LocalPosition" );
		if ( localPosition is null ) return null;

		var customizable = localPosition.GetCustomizable();
		customizable.SetDisplayName( "Local Position (meters)" );
		customizable.AddAttribute( new MetersAttribute() );

		return customizable;
	}

	private static IEnumerable<T> FindDescendants<T>( Widget root ) where T : Widget
	{
		foreach ( var child in root.Children )
		{
			if ( !child.IsValid() ) continue;

			if ( child is T match )
				yield return match;

			foreach ( var descendant in FindDescendants<T>( child ) )
				yield return descendant;
		}
	}
}