PanelFit.cs
namespace Bimp;
/// <summary>
/// Keeps a world panel's UI laid out at a fixed reference resolution, scaled to whatever size the panel is.
/// <para>
/// Our screen UI is styled in pixels for a 1920x1080 panel. On a smaller <see cref="WorldPanel.PanelSize"/>
/// there are fewer pixels, so text and controls would get cut off. <see cref="WorldPanel.RenderScale"/>
/// changes how many layout pixels a panel has without changing its size in the world
/// (layout bounds = PanelSize / RenderScale, drawn scaled by RenderScale), so setting it to
/// PanelSize / reference / (the world panel's UI scale, 2) makes the UI lay out exactly as designed,
/// just smaller or bigger.
/// </para>
/// </summary>
public static class PanelFit
{
/// <summary>
/// Scale for a panel of <paramref name="panelSize"/> to fit a UI designed for <paramref name="reference"/>.
/// Uses the tighter axis, so the whole design always fits - an unusual aspect ratio just gets more room on the other axis.
/// </summary>
public static float ScaleFor( Vector2 panelSize, Vector2 reference )
{
if ( panelSize.x <= 0 || panelSize.y <= 0 || reference.x <= 0 || reference.y <= 0 ) return 1.0f;
return MathF.Min( panelSize.x / reference.x, panelSize.y / reference.y );
}
/// <summary>
/// The bounds of <paramref name="model"/> in <paramref name="space"/>'s local space (unscaled by its world scale).
/// </summary>
public static BBox LocalBounds( GameObject space, ModelRenderer model )
{
var corners = model.Model.Bounds.Corners
.Select( c => space.WorldTransform.PointToLocal( model.WorldTransform.PointToWorld( c ) ) );
return BBox.FromPoints( corners );
}
/// <summary>
/// Size the WorldPanel on <paramref name="component"/>'s GameObject to the front face of <paramref name="model"/>
/// (its extent along the panel's local Y/Z plane), minus <paramref name="bezel"/> on each side.
/// Optionally fits a BoxCollider on the same object to the model too, so USE hits the whole screen.
/// Returns the model's local bounds, or null if nothing could be fitted.
/// </summary>
public static BBox? FitToModel( Component component, ModelRenderer model, float bezel, bool fitCollider )
{
var panel = component.Components.Get<Sandbox.WorldPanel>();
if ( panel is null || !model.IsValid() || model.Model is null ) return null;
if ( model.Model.Bounds.Size.Length <= 0.01f ) return null;
var bounds = LocalBounds( component.GameObject, model );
// The panel is drawn in our local Y/Z plane, PanelSize pixels * ScreenToWorldScale units, and scaled
// by our world scale - the same as these local bounds - so this matches the model at any object scale.
var w = bounds.Size.y - bezel * 2;
var h = bounds.Size.z - bezel * 2;
if ( w < 1 || h < 1 ) return null;
var size = new Vector2( w, h ) / Sandbox.UI.WorldPanel.ScreenToWorldScale;
if ( (panel.PanelSize - size).Length > 0.5f )
panel.PanelSize = size;
if ( fitCollider && component.Components.Get<BoxCollider>() is BoxCollider box )
{
// Cover the model's face, and in depth from the model to just in front of the panel
var minX = MathF.Min( bounds.Mins.x, -1 );
var maxX = MathF.Max( bounds.Maxs.x, 1 );
var center = new Vector3( (minX + maxX) * 0.5f, bounds.Center.y, bounds.Center.z );
var scale = new Vector3( maxX - minX, bounds.Size.y, bounds.Size.z );
if ( (box.Center - center).Length > 0.05f ) box.Center = center;
if ( (box.Scale - scale).Length > 0.05f ) box.Scale = scale;
}
return bounds;
}
/// <summary>
/// Set the WorldPanel on <paramref name="component"/>'s GameObject to fit <paramref name="reference"/>.
/// </summary>
public static void Apply( Component component, Vector2 reference )
{
var panel = component.Components.Get<Sandbox.WorldPanel>();
if ( panel is null ) return;
// World panels boost their UI scale (Sandbox.UI.WorldPanel sets RootPanel.Scale = 2), so every CSS
// pixel is 2 layout pixels. Divide it out, so DesignSize is in real CSS pixels and the UI is drawn
// at the size it's designed for instead of twice as big.
var uiScale = (panel.GetPanel() as Sandbox.UI.RootPanel)?.Scale ?? 2.0f;
if ( uiScale <= 0 ) uiScale = 1;
var scale = ScaleFor( panel.PanelSize, reference ) / uiScale;
if ( scale > 0 && MathF.Abs( panel.RenderScale - scale ) > 0.0001f )
panel.RenderScale = scale;
}
}