Editor UI component that renders a preview staging scene for architectural plans. It creates an editor Scene with camera and lighting, composes a temporary ArchPlan via a provided delegate, generates preview geometry, manages camera orbit/zoom controls, and reports simple mesh summaries.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// The preview plan is throwaway - it never becomes the document.
public readonly record struct ArchStaged( ArchPlan Plan, Vector3 Focus, float Distance );
// Renders through the PRODUCTION generators, so what is previewed is what gets built.
public sealed class ArchDesignerStage : SceneRenderingWidget
{
readonly ArchKit kit;
readonly Func<ArchStaged> compose;
readonly Scene previewScene;
Vector2 lastMouse;
float yaw = 35f;
float pitch = 12f;
float distance = 360f;
float fitted = 360f;
Vector3 focus;
bool dirty = true;
bool orbiting;
bool framed;
public ArchDesignerStage( Widget parent, ArchKit kit, Func<ArchStaged> compose ) : base( parent )
{
this.kit = kit;
this.compose = compose;
previewScene = Scene.CreateEditorScene();
Scene = previewScene;
MinimumSize = 420;
using ( previewScene.Push() )
{
var camera = new GameObject( true, "camera" );
camera.GetOrAddComponent<CameraComponent>().BackgroundColor = new Color( 0.075f, 0.08f, 0.095f );
var sun = new GameObject( true, "sun" );
var light = sun.GetOrAddComponent<DirectionalLight>();
light.WorldRotation = Rotation.From( 42f, -145f, 0f );
light.LightColor = Color.White * 3.2f;
light.SkyColor = new Color( 0.38f, 0.43f, 0.52f ) * 2.2f;
light.Shadows = false;
}
}
public void Restage()
{
dirty = true;
}
// As numbers: 'too thin' is a measurement, and a screenshot of it is not.
public string Describe()
{
var lines = new List<string>();
foreach ( var node in previewScene.GetAllObjects( true ) )
{
if ( node.Components.Get<MeshComponent>() is not { Mesh: not null } renderer )
{
continue;
}
var bounds = node.GetBounds();
var roles = renderer.Mesh.FaceHandles
.Select( handle => renderer.Mesh.GetFaceMaterial( handle )?.ResourceName ?? "none" )
.Distinct()
.OrderBy( role => role );
lines.Add( $"{node.Name}: {renderer.Mesh.FaceHandles.Count()} faces, "
+ $"size {bounds.Size.x:0.##} x {bounds.Size.y:0.##} x {bounds.Size.z:0.##}, "
+ $"z {bounds.Mins.z:0.##} to {bounds.Maxs.z:0.##}, [{string.Join( " ", roles )}]" );
}
return lines.Count == 0 ? "nothing generated" : string.Join( "\n", lines );
}
public void Front()
{
yaw = 90f;
pitch = 0f;
Refit();
}
public void Inside()
{
yaw = -90f;
pitch = 0f;
Refit();
}
public void Perspective()
{
yaw = 35f;
pitch = 12f;
Refit();
}
void Refit()
{
distance = fitted;
framed = false;
}
[EditorEvent.Frame]
public void OnFrame()
{
if ( !Visible || Width < 1f || Height < 1f )
{
Scene = null;
return;
}
Scene = previewScene;
if ( dirty )
{
Rebuild();
}
using ( previewScene.Push() )
{
previewScene.EditorTick( RealTime.Now, RealTime.Delta );
var look = new Angles( pitch, yaw, 0f );
previewScene.Camera.WorldPosition = focus - look.Forward * distance;
previewScene.Camera.WorldRotation = look.ToRotation();
previewScene.Camera.FieldOfView = 38f;
previewScene.Camera.ZNear = 1f;
previewScene.Camera.ZFar = 5000f;
}
}
void Rebuild()
{
dirty = false;
var staged = compose();
if ( staged.Plan is null )
{
return;
}
using ( previewScene.Push() )
{
ArchScene.Generate( previewScene, staged.Plan, kit );
}
focus = staged.Focus;
fitted = staged.Distance;
// Restage runs every keystroke; refitting each time would snatch a user's zoom back.
if ( !framed )
{
distance = fitted;
}
}
// Anchor on the PRESS: tracking moves alone turns the first drag into a jump.
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
lastMouse = e.LocalPosition;
orbiting = (e.ButtonState & MouseButtons.Left) != 0;
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
orbiting = false;
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
var delta = e.LocalPosition - lastMouse;
lastMouse = e.LocalPosition;
if ( !orbiting || (e.ButtonState & MouseButtons.Left) == 0 )
{
orbiting = false;
return;
}
yaw -= delta.x * 0.35f;
pitch = Math.Clamp( pitch + delta.y * 0.25f, -75f, 75f );
}
protected override void OnMouseWheel( WheelEvent e )
{
distance = Math.Clamp( distance * (e.Delta > 0f ? 0.9f : 1.1f), 80f, 3000f );
framed = true;
}
public override void OnDestroyed()
{
base.OnDestroyed();
previewScene?.Destroy();
Scene = null;
}
}