Editor UI dialog that browses architecture map palettes. It lists available palette documents and a blockout option, shows previews and details, allows editing materials, rebuilding thumbnails, and applying a palette to an ArchPalette target.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Editor;
using Sandbox;
using Margin = Sandbox.UI.Margin;
namespace Sunless.Architecture;
public sealed class ArchPaletteBrowser : Dialog
{
readonly ArchPalette target;
readonly Action applied;
readonly List<ArchMapPalette.Document> documents = new();
Layout list;
Label heading;
Label blurb;
ArchTileGrid grid;
Button apply;
Button edit;
ArchMapPalette.Document chosen;
bool blockout;
public ArchPaletteBrowser( Widget parent, ArchPalette target, Action applied ) : base( parent )
{
this.target = target;
this.applied = applied;
Window.Title = "Map Palettes";
Window.SetWindowIcon( "palette" );
Window.Size = Proportional( parent, 0.6f );
foreach ( var file in ArchMapPalette.Available() )
{
var document = ArchMapPalette.ReadDocument( file );
if ( document is not null )
{
documents.Add( document );
}
}
Build();
chosen = documents.FirstOrDefault();
blockout = chosen is null;
ShowChosen();
}
static Vector2 Proportional( Widget parent, float fraction )
{
var screen = parent?.ScreenGeometry ?? new Rect( 0, 0, 1600, 900 );
var width = Math.Max( 900f, screen.Width * fraction );
var height = Math.Max( 600f, screen.Height * fraction );
return new Vector2( width, height );
}
void Build()
{
Layout = Layout.Column();
Layout.Margin = 0;
var split = Layout.AddRow( 1 );
var sidebar = new Widget( this ) { Layout = Layout.Column() };
sidebar.MinimumWidth = 250;
sidebar.MaximumWidth = 250;
sidebar.Layout.Margin = 8;
sidebar.Layout.Spacing = 6;
var sidebarScroll = new ScrollArea( sidebar );
sidebarScroll.HorizontalScrollbarMode = ScrollbarMode.Off;
var sidebarCanvas = new Widget( sidebarScroll ) { Layout = Layout.Column() };
sidebarCanvas.Layout.Spacing = 4;
sidebarScroll.Canvas = sidebarCanvas;
list = sidebarCanvas.Layout;
sidebar.Layout.Add( sidebarScroll, 1 );
split.Add( sidebar );
split.AddSeparator();
var detail = new Widget( this ) { Layout = Layout.Column() };
detail.Layout.Margin = 0;
detail.Layout.Spacing = 0;
var head = detail.Layout.AddColumn();
head.Margin = new Margin( 12, 10, 12, 8 );
head.Spacing = 3;
heading = new Label( "" );
heading.SetStyles( "font-size: 15px; font-weight: 700;" );
head.Add( heading );
blurb = new Label( "" );
blurb.SetStyles( "color: rgba(255,255,255,0.55); font-size: 11px;" );
blurb.WordWrap = true;
head.Add( blurb );
detail.Layout.AddSeparator();
grid = new ArchTileGrid( detail, ArchTileSize.Preview );
detail.Layout.Add( grid, 1 );
detail.Layout.AddSeparator();
var footer = detail.Layout.AddRow();
footer.Margin = new Margin( 12, 8, 12, 10 );
footer.Spacing = 6;
edit = new Button( "Edit Materials", "tune" ) { Clicked = EditChosen };
footer.Add( edit );
footer.Add( new Button( "Rebuild Previews", "refresh" )
{
ToolTip = "Wipe the cached thumbnails and 3D slot previews, then re-render them",
Clicked = () =>
{
ArchThumb.Forget();
ArchSlotPreview.Invalidate();
RebuildList();
ShowChosen();
}
} );
footer.AddStretchCell();
footer.Add( new Button( "Close", "close" ) { Clicked = Close } );
apply = new Button.Primary( "Apply Palette", "check" ) { Clicked = ApplyChosen };
footer.Add( apply );
split.Add( detail, 1 );
RebuildList();
}
void RebuildList()
{
list.Clear( true );
// The blockout skin sits with the scan palettes: picked the same way, materials from a colour table not harvests.
list.Add( new ArchTile( null, ArchBlockout.Grid(), "blockout", $"{ArchBlockout.Roles.Count} roles, one grid", false, new Vector2( 230, 150 ) )
{
Clicked = () => { blockout = true; ShowChosen(); RefreshSelection(); },
Current = () => blockout
} );
foreach ( var document in documents )
{
var captured = document;
var card = new ArchTile( null, Mosaic( captured ), captured.Map, $"{captured.Surfaces.Count} surfaces", false, new Vector2( 230, 150 ) )
{
Clicked = () => { chosen = captured; blockout = false; ShowChosen(); RefreshSelection(); },
Current = () => !blockout && ReferenceEquals( chosen, captured )
};
list.Add( card );
}
list.AddStretchCell();
}
void RefreshSelection()
{
Update();
}
static Pixmap Mosaic( ArchMapPalette.Document document )
{
foreach ( var surface in document.Surfaces )
{
var thumb = ArchThumb.For( surface.Category, surface.Name, surface.Id );
if ( thumb is not null )
{
return thumb;
}
}
return null;
}
void ShowChosen()
{
if ( blockout )
{
ShowBlockout();
return;
}
edit.Enabled = true;
if ( chosen is null )
{
heading.Text = "No palettes found";
blurb.Text = "Nothing in tools/megascans/palettes.";
grid.Set( Enumerable.Empty<ArchTileSection>() );
return;
}
heading.Text = string.IsNullOrWhiteSpace( chosen.Title ) ? chosen.Map : chosen.Title;
var bindings = ArchMapPalette.Resolve( chosen );
var bound = new HashSet<ArchMapPalette.Surface>( bindings.Values );
var ready = bindings.Values.Distinct().Count( ArchMapPalette.IsImported );
blurb.Text = $"The builder skins {bindings.Count} surface slots from {bound.Count} of this palette's {chosen.Surfaces.Count} materials"
+ $" - {ready} imported and ready. Everything else is here for hand-painting faces in the Mapping tool.";
var builder = new ArchTileSection
{
Title = $"WHAT THE BUILDER USES - {bindings.Count} slots",
Note = "Rendered in place on a test building, with the affected mesh outlined in white. The badge is the surface slot it drives."
};
foreach ( var slot in ArchMapPalette.SlotOrder )
{
if ( !bindings.TryGetValue( slot, out var surface ) )
{
continue;
}
var captured = surface;
var captureSlot = slot;
builder.Tiles.Add( parent => SlotTile( parent, captured, captureSlot, bindings ) );
}
var library = new ArchTileSection
{
Title = $"ALSO IN THE PALETTE - {chosen.Surfaces.Count - bound.Count}",
Note = "Not auto-assigned. Pick these per-face in the Mapping tool at 0.3076 units/texel."
};
foreach ( var surface in chosen.Surfaces.Where( s => !bound.Contains( s ) ) )
{
var captured = surface;
library.Tiles.Add( parent => Tile( parent, captured, null, 0.6f ) );
}
grid.Set( new[] { builder, library } );
}
void ShowBlockout()
{
heading.Text = "Blockout grid";
blurb.Text = ArchBlockout.Describe();
// Materials come from the colour table in code; nothing to reassign by hand.
edit.Enabled = false;
var section = new ArchTileSection
{
Title = $"WHAT THE BUILDER USES - {ArchBlockout.Roles.Count} slots",
Note = "One material per role, the same grid under all of them. Change a hue in ArchBlockout.Roles and delete its vmat to re-generate."
};
foreach ( var pair in ArchBlockout.Roles )
{
var role = pair.Role;
section.Tiles.Add( parent => new ArchTile( parent, ArchBlockout.Grid(), $"#{pair.Hex}", ArchBlockout.PathFor( role ), false, grid.TileSize )
{
Fill = ArchBlockout.ColourOf( role ),
Badge = role.ToString(),
Glyph = box => ArchSlotIcon.Draw( box, role, Color.White )
} );
}
grid.Set( new[] { section } );
}
ArchTile Tile( Widget parent, ArchMapPalette.Surface surface, string badge, float dim )
{
var thumb = ArchThumb.For( surface.Category, surface.Name, surface.Id );
var missing = !ArchMapPalette.IsImported( surface );
return new ArchTile( parent, thumb, surface.Role, missing ? "NOT IMPORTED" : surface.Category, missing, grid.TileSize )
{
Badge = badge,
Dim = dim,
ToolTip = surface.Name
};
}
ArchTile SlotTile( Widget parent, ArchMapPalette.Surface surface, ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings )
{
var missing = !ArchMapPalette.IsImported( surface );
var thumb = missing
? ArchThumb.For( surface.Category, surface.Name, surface.Id )
: ArchSlotPreview.For( chosen.Map, slot, bindings, 512 ) ?? ArchThumb.For( surface.Category, surface.Name, surface.Id );
return new ArchTile( parent, thumb, surface.Role, missing ? "NOT IMPORTED" : surface.Category, missing, grid.TileSize )
{
Badge = slot.ToString(),
Glyph = box => ArchSlotIcon.Draw( box, slot, Color.White ),
ToolTip = surface.Name
};
}
void ApplyChosen()
{
if ( blockout )
{
if ( ArchBlockout.Apply( target ) > 0 )
{
applied?.Invoke();
}
Close();
return;
}
if ( chosen is null )
{
return;
}
if ( ArchMapPalette.Apply( chosen.File, target ) > 0 )
{
applied?.Invoke();
}
Close();
}
void EditChosen()
{
if ( blockout || chosen is null )
{
return;
}
var browser = new ArchMaterialBrowser( this, chosen, () =>
{
var reloaded = ArchMapPalette.ReadDocument( chosen.File );
if ( reloaded is null )
{
return;
}
var index = documents.FindIndex( d => d.File == chosen.File );
if ( index >= 0 )
{
documents[index] = reloaded;
}
chosen = reloaded;
ArchThumb.Forget();
ArchSlotPreview.Invalidate();
RebuildList();
ShowChosen();
} );
browser.Show();
}
}