Editor/SeamlessImageToolWindow.cs
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;
using Button = Editor.Button;
using Checkbox = Editor.Checkbox;
using ComboBox = Editor.ComboBox;
using Label = Editor.Label;
using LineEdit = Editor.LineEdit;
using TextEdit = Editor.TextEdit;
using Widget = Editor.Widget;

[Dock( "Editor", "Seam-Less™", "texture" )]
public class SeamlessImageToolWindow : Widget
{
	public static SeamlessImageToolWindow Current { get; private set; }

	private readonly SeamlessToolSettings settings = new();
	private readonly List<string> supportedExtensions = new()
	{
		".png",
		".jpg",
		".jpeg",
		".webp",
		".bmp",
		".tga",
		".tif",
		".tiff",
		".psd",
		".svg"
	};

	private SeamlessPreviewWidget previewWidget;
	private Splitter mainSplitter;
	private Splitter contentSplitter;
	private TextEdit consoleOutput;
	private Label currentFileLabel;
	private Label imageInfoLabel;
	private LineEdit outputFolderEdit;
	private LineEdit suffixEdit;
	private LineEdit seedEdit;
	private Widget tileCountRow;
	private Widget seedRow;
	private Widget randomizeRow;
	private Widget variationPresetRow;
	private Widget patchShapeRow;
	private Widget variationStrengthRow;
	private Widget seamGravityRow;
	private Widget patchCountRow;
	private Widget patchSizeRow;
	private Widget maskSoftnessRow;
	private Widget bombDensityRow;
	private Widget brickTilePatternRow;
	private Widget brickTileCellWidthRow;
	private Widget brickTileCellHeightRow;
	private Widget brickTileGroutWidthRow;
	private Widget brickTileRowOffsetRow;
	private Widget brickTileStructureStrengthRow;
	private string sourceImagePath;
	private string lastExportPath;
	private Bitmap originalBitmap;
	private Bitmap processedBitmap;
	private bool outputFolderFollowsSource = true;
	private bool processQueued;
	private DateTime queuedProcessTime;
	private const int InspectorLabelWidth = 132;
	private const string MainSplitterCookie = "SeamLessImageCreator.MainSplitter";
	private const string ContentSplitterCookie = "SeamLessImageCreator.ContentSplitter";

	public SeamlessImageToolWindow( Widget parent ) : base( parent )
	{
		Current = this;

		Name = "Seam-Less™";
		WindowTitle = "Seam-Less™";
		MinimumSize = new Vector2( 1040, 680 );
		AcceptDrops = true;

		SetWindowIcon( "texture" );
		BuildLayout();

		LogMessage( "Ready. Open an image or drag one into the preview." );
		ShowStatus( "Ready", 4f );
	}

	public override void OnDestroyed()
	{
		SaveSplitterState();
		DisposeBitmaps();

		if ( Current == this )
			Current = null;

		base.OnDestroyed();
	}

	public override void OnDragHover( Widget.DragEvent ev )
	{
		var files = GetFilesFromDragData( ev.Data );
		ev.Action = FindFirstSupportedFile( files ) != null ? DropAction.Copy : DropAction.Ignore;
	}

	public override void OnDragDrop( Widget.DragEvent ev )
	{
		var files = GetFilesFromDragData( ev.Data );

		if ( files.Count == 0 )
		{
			ev.Action = DropAction.Ignore;
			return;
		}

		ev.Action = DropAction.Copy;
		LoadDroppedFiles( files.ToArray() );
	}

	[EditorEvent.Frame]
	public void Frame()
	{
		if ( !processQueued || DateTime.Now < queuedProcessTime )
			return;

		processQueued = false;
		ProcessImage( false );
	}

	private void BuildLayout()
	{
		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 8;

		Layout.Add( BuildToolbar() );

		mainSplitter = new Splitter( this );
		mainSplitter.IsVertical = true;
		mainSplitter.OpaqueResize = true;
		mainSplitter.HandleWidth = 5;
		Layout.Add( mainSplitter, 1 );

		contentSplitter = new Splitter( mainSplitter );
		contentSplitter.IsHorizontal = true;
		contentSplitter.OpaqueResize = true;
		contentSplitter.HandleWidth = 5;

		previewWidget = new SeamlessPreviewWidget( contentSplitter );
		previewWidget.FilesDropped = LoadDroppedFiles;

		var inspectorScroll = new ScrollArea( contentSplitter );
		inspectorScroll.MinimumWidth = 360;
		inspectorScroll.Canvas = BuildInspector( inspectorScroll );
		inspectorScroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
		inspectorScroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;

		contentSplitter.AddWidget( previewWidget );
		contentSplitter.AddWidget( inspectorScroll );
		contentSplitter.SetStretch( 0, 5 );
		contentSplitter.SetStretch( 1, 2 );
		contentSplitter.SetCollapsible( 0, false );
		contentSplitter.SetCollapsible( 1, false );

		var console = BuildConsole( mainSplitter );

		mainSplitter.AddWidget( contentSplitter );
		mainSplitter.AddWidget( console );
		mainSplitter.SetStretch( 0, 5 );
		mainSplitter.SetStretch( 1, 1 );
		mainSplitter.SetCollapsible( 0, false );
		mainSplitter.SetCollapsible( 1, false );

		RestoreSplitterState();
	}

	private ToolBar BuildToolbar()
	{
		var toolbar = new ToolBar( this, "SeamLessImageCreatorToolbar" );
		toolbar.Movable = false;
		toolbar.Floatable = false;
		toolbar.SetIconSize( new Vector2( 18, 18 ) );

		toolbar.AddOption( "Open", "folder_open", OpenImageDialog );
		toolbar.AddOption( "Process", "auto_fix_high", () => ProcessImage( true ) );
		toolbar.AddOption( "Export", "save", ExportImage );
		toolbar.AddOption( "Reveal Output", "folder", RevealOutput );
		toolbar.AddSeparator();
		toolbar.AddOption( "Clear Image and Console", "delete", ClearLog );

		return toolbar;
	}

	private Widget BuildInspector( Widget parent )
	{
		var inspector = new Widget( parent );
		inspector.MinimumWidth = 340;
		inspector.Layout = Layout.Column();
		inspector.Layout.Margin = 10;
		inspector.Layout.Spacing = 3;
		inspector.SetStyles( "background-color: #303030; border-left: 1px solid #202020;" );

		AddSectionLabel( inspector, "Image" );

		currentFileLabel = AddReadOnlyValueRow( inspector, "File", "No image loaded", "The image currently loaded into the tool." );
		imageInfoLabel = AddReadOnlyValueRow( inspector, "Info", "Drop an image into the preview, or use Open.", "Shows the loaded image size and file type." );

		AddSectionLabel( inspector, "Preview" );
		AddPreviewModeControl( inspector );
		tileCountRow = AddSliderControl( inspector, "Tile Count", 2f, 8f, 1f, settings.PreviewTileCount, value =>
		{
			settings.PreviewTileCount = Math.Clamp( (int)MathF.Round( value ), 2, 8 );
			UpdatePreviewSettings();
		}, "Changes how many repeats are shown in tiled preview. This does not change export size." );
		AddPreviewButtonRow( inspector );
		UpdateTileCountVisibility();

		AddSectionLabel( inspector, "Seam Processing" );
		AddProcessingModeControl( inspector );
		AddSeamShapeControl( inspector );
		seedRow = AddSeedRow( inspector, "Changes the random pattern used for patches, noise, and texture bombing." );
		randomizeRow = AddRandomizeRow( inspector, "Creates a new random pattern for the current stochastic or noise settings." );
		variationPresetRow = AddVariationPresetControl( inspector );
		patchShapeRow = AddPatchShapeControl( inspector );
		variationStrengthRow = AddSliderControl( inspector, "Variation Strength", 0f, 1f, 0.01f, settings.VariationStrength, value =>
		{
			settings.VariationStrength = value;
			QueueAutoProcess();
		}, "Blends between the safer base result and the randomized texture result." );
		seamGravityRow = AddSliderControl( inspector, "Seam Gravity", 0f, 1f, 0.01f, settings.SeamGravity, value =>
		{
			settings.SeamGravity = value;
			QueueAutoProcess();
		}, "Pulls randomized patches and texture bombs toward the center seam cross. 0 keeps the normal spread." );
		patchCountRow = AddSliderControl( inspector, "Patch Count", 2f, 512f, 1f, settings.PatchCount, value =>
		{
			settings.PatchCount = Math.Clamp( (int)MathF.Round( value ), 2, 512 );
			QueueAutoProcess();
		}, "Changes how many patch regions are used for irregular patch and bombing modes." );
		patchSizeRow = AddSliderControl( inspector, "Patch Size", 8f, 512f, 1f, settings.PatchSize, value =>
		{
			settings.PatchSize = value;
			QueueAutoProcess();
		}, "Changes the average size of each randomized texture patch." );
		maskSoftnessRow = AddSliderControl( inspector, "Mask Softness", 0.05f, 1f, 0.01f, settings.MaskSoftness, value =>
		{
			settings.MaskSoftness = value;
			QueueAutoProcess();
		}, "Controls how softly patch edges blend into the texture." );
		bombDensityRow = AddSliderControl( inspector, "Bomb Density", 0.1f, 4f, 0.1f, settings.TextureBombDensity, value =>
		{
			settings.TextureBombDensity = value;
			QueueAutoProcess();
		}, "Adds more or fewer texture-bomb stamps across the image." );
		brickTilePatternRow = AddBrickTilePatternControl( inspector );
		brickTileCellWidthRow = AddSliderControl( inspector, "Cell Width", 4f, 4096f, 1f, settings.BrickTileCellWidth, value =>
		{
			settings.BrickTileCellWidth = value;
			QueueAutoProcess();
		}, "Matches the width of one tile, brick, plank, or panel repeat." );
		brickTileCellHeightRow = AddSliderControl( inspector, "Cell Height", 4f, 4096f, 1f, settings.BrickTileCellHeight, value =>
		{
			settings.BrickTileCellHeight = value;
			QueueAutoProcess();
		}, "Matches the height of one tile, brick row, plank row, or panel repeat." );
		brickTileGroutWidthRow = AddSliderControl( inspector, "Grout Width", 0f, 256f, 1f, settings.BrickTileGroutWidth, value =>
		{
			settings.BrickTileGroutWidth = value;
			QueueAutoProcess();
		}, "Controls how wide the structured grout or edge lines are." );
		brickTileRowOffsetRow = AddSliderControl( inspector, "Row Offset", 0f, 1f, 0.01f, settings.BrickTileRowOffset, value =>
		{
			settings.BrickTileRowOffset = value;
			QueueAutoProcess();
		}, "Offsets alternating rows for running brick, plank, and panel layouts." );
		brickTileStructureStrengthRow = AddSliderControl( inspector, "Grid Strength", 0f, 1f, 0.01f, settings.BrickTileStructureStrength, value =>
		{
			settings.BrickTileStructureStrength = value;
			QueueAutoProcess();
		}, "Makes seam blending respect tile, brick, grout, and plank edges more strongly." );
		AddSliderControl( inspector, "Seam Width", 1f, 256f, 1f, settings.SeamWidth, value =>
		{
			settings.SeamWidth = value;
			QueueAutoProcess();
		}, "Controls how wide the seam-hiding blend area is." );
		AddSliderControl( inspector, "Blend Strength", 0f, 1f, 0.01f, settings.BlendStrength, value =>
		{
			settings.BlendStrength = value;
			QueueAutoProcess();
		}, "Controls how strongly the seam area blends with its mirrored side." );
		AddSliderControl( inspector, "Blur", 0f, 16f, 0.1f, settings.BlurRadius, value =>
		{
			settings.BlurRadius = value;
			QueueAutoProcess();
		}, "Softens the processed texture after the seamless pass." );
		AddSliderControl( inspector, "Sharpen", 0f, 2f, 0.05f, settings.SharpenAmount, value =>
		{
			settings.SharpenAmount = value;
			QueueAutoProcess();
		}, "Adds crispness back after processing or blur." );

		AddCheckboxRow( inspector, "Auto Process", settings.AutoProcess, state =>
		{
			settings.AutoProcess = state == CheckState.On;

			if ( settings.AutoProcess )
				QueueAutoProcess();
		}, "Updates the preview automatically when settings change." );
		UpdateProcessingControlVisibility();

		AddSectionLabel( inspector, "Export" );
		AddExportFormatControl( inspector );
		AddSliderControl( inspector, "Quality", 1f, 100f, 1f, settings.ExportQuality, value =>
		{
			settings.ExportQuality = Math.Clamp( (int)MathF.Round( value ), 1, 100 );
		}, "Controls JPG and WebP compression quality. PNG and BMP ignore this." );

		suffixEdit = AddTextRow( inspector, "Suffix", settings.OutputSuffix, "Text added to the exported file name." );
		suffixEdit.TextEdited += text =>
		{
			settings.OutputSuffix = text;
		};

		AddOutputFolderControl( inspector, "Folder where exported seamless textures will be written." );

		AddCheckboxRow( inspector, "Overwrite Existing", settings.OverwriteExisting, state =>
		{
			settings.OverwriteExisting = state == CheckState.On;
		}, "Allows export to replace an existing file with the same name." );

		AddCheckboxRow( inspector, "Auto Create .VMAT?", settings.ExportVMAT, state =>
		{
			settings.ExportVMAT = state == CheckState.On;
		}, "Auto creates a .vmat using the exported texture." );

		AddLargeExportButton( inspector );

		inspector.Layout.AddStretchCell();

		return inspector;
	}

	private Widget BuildConsole( Widget parent )
	{
		var pane = new Widget( parent );
		pane.Name = "Console";
		pane.WindowTitle = "Console";
		pane.SetWindowIcon( "terminal" );
		pane.MinimumSize = new Vector2( 520, 140 );
		pane.Layout = Layout.Column();
		pane.Layout.Margin = 8;
		pane.Layout.Spacing = 6;
		pane.SetStyles( "background-color: #101215; border: 1px solid #2a3038; border-radius: 6px;" );

		var title = pane.Layout.Add( new Label( "Console", pane ) );
		title.SetStyles( "font-weight: bold; color: #d9dee7;" );

		consoleOutput = pane.Layout.Add( new TextEdit( pane ), 1 );
		consoleOutput.ReadOnly = true;
		consoleOutput.TextSelectable = true;
		consoleOutput.BackgroundVisible = false;
		consoleOutput.MaximumBlockCount = 200;
		consoleOutput.SetStyles( "font-family: monospace; font-size: 11px; color: #b8c0cc;" );

		return pane;
	}

	private void AddSectionLabel( Widget inspector, string text )
	{
		var row = new Widget( inspector );
		row.MinimumHeight = 24;
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.SetStyles( "background-color: #2b2b2b; border-top: 1px solid #202020; border-bottom: 1px solid #1f1f1f; margin-top: 6px;" );

		var marker = row.Layout.Add( new Label( "-", row ) );
		marker.FixedWidth = 20;
		marker.SetStyles( "color: #8b8b8b; font-weight: bold; font-size: 12px;" );

		var label = row.Layout.Add( new Label( text, row ), 1 );
		label.SetStyles( "font-size: 11px; font-weight: bold; color: #ffffff; padding-left: 2px;" );

		inspector.Layout.Add( row );
	}

	private void AddPreviewModeControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "View Mode", "Changes what the preview shows. This does not change the exported image." );
		combo.AddItem( "Original" );
		combo.AddItem( "Processed" );
		combo.AddItem( "Tiled" );
		combo.CurrentIndex = (int)settings.PreviewMode;
		combo.ItemChanged += () =>
		{
			settings.PreviewMode = (SeamlessPreviewMode)Math.Clamp( combo.CurrentIndex, 0, 2 );
			UpdatePreviewSettings();
		};
	}

	private void AddPreviewButtonRow( Widget inspector )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 10;
		row.MinimumHeight = 30;
		row.SetStyles( "padding-top: 4px; padding-bottom: 2px;" );

		var spacer = row.Layout.Add( new Label( "", row ) );
		spacer.FixedWidth = InspectorLabelWidth;

		var fitButton = row.Layout.Add( new Button( "Fit", "fit_screen", row ) );
		fitButton.MinimumWidth = 86;
		fitButton.Clicked = () => previewWidget.FitToView();
		StyleInspectorButton( fitButton );
		SetInspectorToolTip( "Fit the current preview into the preview panel.", fitButton );

		var oneToOneButton = row.Layout.Add( new Button( "1:1", "center_focus_strong", row ) );
		oneToOneButton.MinimumWidth = 86;
		oneToOneButton.Clicked = () => previewWidget.SetOneToOne();
		StyleInspectorButton( oneToOneButton );
		SetInspectorToolTip( "Show the current preview at original pixel size.", oneToOneButton );

		row.Layout.AddStretchCell();
		inspector.Layout.Add( row );
	}

	private Widget AddProcessingModeControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Processing Mode", "Chooses the method used to make the texture seamless." );
		combo.AddItem( "Offset Blend" );
		combo.AddItem( "Irregular Patch" );
		combo.AddItem( "Texture Bombing" );
		combo.AddItem( "Brick, Tile & Planks" );
		combo.CurrentIndex = (int)settings.ProcessingMode;
		combo.ItemChanged += () =>
		{
			settings.ProcessingMode = (SeamlessProcessingMode)Math.Clamp( combo.CurrentIndex, 0, 3 );
			UpdateProcessingControlVisibility();
			QueueAutoProcess();
		};

		return combo.Parent;
	}

	private Widget AddSeamShapeControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Seam Shape", "Changes the shape of the center blend used to hide seams." );
		combo.AddItem( "Smooth" );
		combo.AddItem( "Linear" );
		combo.AddItem( "Wavy" );
		combo.AddItem( "Noise" );
		combo.AddItem( "Irregular" );
		combo.CurrentIndex = (int)settings.SeamShape;
		combo.ItemChanged += () =>
		{
			settings.SeamShape = (SeamlessSeamShape)Math.Clamp( combo.CurrentIndex, 0, 4 );
			UpdateProcessingControlVisibility();
			QueueAutoProcess();
		};

		return combo.Parent;
	}

	private Widget AddVariationPresetControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Variation", "Controls how subtle or strong the random texture variation feels." );
		combo.AddItem( "Gentle" );
		combo.AddItem( "Balanced" );
		combo.AddItem( "Strong" );
		combo.CurrentIndex = (int)settings.VariationPreset;
		combo.ItemChanged += () =>
		{
			settings.VariationPreset = (SeamlessVariationPreset)Math.Clamp( combo.CurrentIndex, 0, 2 );
			QueueAutoProcess();
		};

		return combo.Parent;
	}

	private Widget AddPatchShapeControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Shape", "Changes the mask shape used by irregular patches and texture bombs." );
		combo.AddItem( "Circle" );
		combo.AddItem( "Square" );
		combo.AddItem( "Diamond" );
		combo.AddItem( "Hexagon" );
		combo.CurrentIndex = (int)settings.PatchShape;
		combo.ItemChanged += () =>
		{
			settings.PatchShape = (SeamlessPatchShape)Math.Clamp( combo.CurrentIndex, 0, 3 );
			QueueAutoProcess();
		};

		return combo.Parent;
	}

	private Widget AddBrickTilePatternControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Brick & Tile Type", "Chooses the structured layout used for non-organic textures." );
		combo.AddItem( "Square Tile" );
		combo.AddItem( "Running Brick" );
		combo.AddItem( "Stacked Brick" );
		combo.AddItem( "Planks" );
		combo.AddItem( "Panels" );
		combo.CurrentIndex = (int)settings.BrickTilePattern;
		combo.ItemChanged += () =>
		{
			settings.BrickTilePattern = (SeamlessBrickTilePattern)Math.Clamp( combo.CurrentIndex, 0, 4 );
			UpdateProcessingControlVisibility();
			QueueAutoProcess();
		};

		return combo.Parent;
	}

	private void AddExportFormatControl( Widget inspector )
	{
		var combo = AddComboRow( inspector, "Format", "Chooses the image format used when exporting." );
		combo.AddItem( "PNG" );
		combo.AddItem( "JPG" );
		combo.AddItem( "WebP" );
		combo.AddItem( "BMP" );
		combo.CurrentIndex = (int)settings.ExportFormat;
		combo.ItemChanged += () =>
		{
			settings.ExportFormat = (SeamlessExportFormat)Math.Clamp( combo.CurrentIndex, 0, 3 );
		};
	}

	private ComboBox AddComboRow( Widget inspector, string labelText, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( labelText, row ) );
		StylePropertyLabel( label );

		var combo = row.Layout.Add( new ComboBox( row ), 1 );
		StyleInputWidget( combo );
		SetInspectorToolTip( description, label );
		inspector.Layout.Add( row );

		return combo;
	}

	private LineEdit AddTextRow( Widget inspector, string labelText, string text, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( labelText, row ) );
		StylePropertyLabel( label );

		var lineEdit = row.Layout.Add( new LineEdit( row ), 1 );
		lineEdit.Text = text ?? "";
		lineEdit.ClearButtonEnabled = true;
		StyleInputWidget( lineEdit );
		SetInspectorToolTip( description, label );

		inspector.Layout.Add( row );
		return lineEdit;
	}

	private Widget AddSliderControl( Widget inspector, string labelText, float min, float max, float step, float initialValue, Action<float> changed, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( labelText, row ) );
		StylePropertyLabel( label );

		var valueBox = row.Layout.Add( new Widget( row ), 1 );
		valueBox.Layout = Layout.Row();
		valueBox.Layout.Spacing = 5;
		valueBox.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px;" );

		var typeBadge = valueBox.Layout.Add( new Label( "f", valueBox ) );
		typeBadge.FixedWidth = 20;
		typeBadge.SetStyles( "background-color: #26361d; color: #91c65c; font-weight: bold; font-size: 11px; padding-left: 6px;" );

		var slider = valueBox.Layout.Add( new FloatSlider( valueBox ), 1 );
		slider.Minimum = min;
		slider.Maximum = max;
		slider.Step = step;
		slider.Value = initialValue;
		slider.HighlightColor = new Color( 0.55f, 0.78f, 0.28f, 1f );
		slider.MinimumHeight = 22;

		var valueLabel = valueBox.Layout.Add( new Label( FormatSliderValue( initialValue, step ), valueBox ) );
		valueLabel.FixedWidth = 48;
		valueLabel.SetStyles( "color: #f0f0f0; font-size: 11px; padding-left: 4px;" );

		slider.OnValueEdited = () =>
		{
			var snapped = SnapSliderValue( slider.Value, step, min, max );
			slider.Value = snapped;
			valueLabel.Text = FormatSliderValue( snapped, step );
			changed?.Invoke( snapped );
		};

		SetInspectorToolTip( description, label );
		inspector.Layout.Add( row );
		return row;
	}

	private Widget AddSeedRow( Widget inspector, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( "Seed", row ) );
		StylePropertyLabel( label );

		seedEdit = row.Layout.Add( new LineEdit( row ), 1 );
		seedEdit.Text = settings.RandomSeed.ToString();
		StyleInputWidget( seedEdit );
		seedEdit.TextEdited += text =>
		{
			if ( int.TryParse( text, out var seed ) )
			{
				settings.RandomSeed = seed;
				QueueAutoProcess();
			}
		};

		SetInspectorToolTip( description, label );
		inspector.Layout.Add( row );
		return row;
	}

	private Widget AddRandomizeRow( Widget inspector, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var spacer = row.Layout.Add( new Label( "", row ) );
		spacer.FixedWidth = InspectorLabelWidth;

		var button = row.Layout.Add( new Button( "Randomize", "casino", row ) );
		StyleInspectorButton( button );
		button.Clicked = RandomizeSeed;

		row.Layout.AddStretchCell();
		inspector.Layout.Add( row );
		return row;
	}

	private void AddOutputFolderControl( Widget inspector, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( "Output Folder", row ) );
		StylePropertyLabel( label );

		outputFolderEdit = row.Layout.Add( new LineEdit( row ), 1 );
		outputFolderEdit.Text = settings.OutputFolder;
		outputFolderEdit.PlaceholderText = "Uses the source image folder";
		outputFolderEdit.ClearButtonEnabled = true;
		StyleInputWidget( outputFolderEdit );
		outputFolderEdit.TextEdited += text =>
		{
			outputFolderFollowsSource = false;
			settings.OutputFolder = text;
		};

		SetInspectorToolTip( description, label );
		inspector.Layout.Add( row );

		var buttonRow = new Widget( inspector );
		buttonRow.Layout = Layout.Row();
		buttonRow.Layout.Spacing = 10;
		buttonRow.MinimumHeight = 30;
		buttonRow.SetStyles( "padding-top: 4px; padding-bottom: 2px;" );

		var spacer = buttonRow.Layout.Add( new Label( "", buttonRow ) );
		spacer.FixedWidth = InspectorLabelWidth;

		var sourceButton = buttonRow.Layout.Add( new Button( "Use Source", "source", buttonRow ) );
		sourceButton.MinimumWidth = 104;
		sourceButton.Clicked = UseSourceFolder;
		StyleInspectorButton( sourceButton );
		SetInspectorToolTip( "Use the loaded image's folder as the export folder.", sourceButton );

		var browseButton = buttonRow.Layout.Add( new Button( "Browse", "folder_open", buttonRow ) );
		browseButton.MinimumWidth = 88;
		browseButton.Clicked = BrowseOutputFolder;
		StyleInspectorButton( browseButton );
		SetInspectorToolTip( "Choose a custom export folder.", browseButton );

		buttonRow.Layout.AddStretchCell();
		inspector.Layout.Add( buttonRow );
	}

	private Label AddReadOnlyValueRow( Widget inspector, string labelText, string value, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( labelText, row ) );
		StylePropertyLabel( label );

		var valueLabel = row.Layout.Add( new Label( value, row ), 1 );
		valueLabel.WordWrap = true;
		valueLabel.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px; color: #f0f0f0; font-size: 11px; padding-left: 8px; padding-top: 3px;" );

		SetInspectorToolTip( description, label );
		inspector.Layout.Add( row );
		return valueLabel;
	}

	private void AddCheckboxRow( Widget inspector, string labelText, bool initialValue, Action<CheckState> changed, string description = null )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 6;
		row.MinimumHeight = 24;

		var label = row.Layout.Add( new Label( labelText, row ) );
		StylePropertyLabel( label );

		var checkbox = row.Layout.Add( new Checkbox( row ) );
		checkbox.Value = initialValue;
		checkbox.MinimumWidth = 24;
		checkbox.StateChanged += changed;

		SetInspectorToolTip( description, label );
		row.Layout.AddStretchCell();
		inspector.Layout.Add( row );
	}

	private void AddLargeExportButton( Widget inspector )
	{
		var row = new Widget( inspector );
		row.Layout = Layout.Row();
		row.Layout.Spacing = 8;
		row.MinimumHeight = 58;
		row.SetStyles( "padding-top: 10px;" );

		row.Layout.AddStretchCell();

		var button = row.Layout.Add( new Button( "Export Seamless Image", "save", row ) );
		button.MinimumWidth = 250;
		button.MinimumHeight = 40;
		button.Clicked = ExportImage;
		button.SetStyles( "background-color: #467022; border: 1px solid #6d9d36; border-radius: 4px; color: #ffffff; font-size: 13px; font-weight: bold;" );

		row.Layout.AddStretchCell();
		inspector.Layout.Add( row );
	}

	private void SetInspectorToolTip( string description, params Widget[] widgets )
	{
		if ( string.IsNullOrWhiteSpace( description ) )
			return;

		foreach ( var widget in widgets )
		{
			if ( widget != null )
				widget.ToolTip = description;
		}
	}

	private void StylePropertyLabel( Label label )
	{
		label.FixedWidth = InspectorLabelWidth;
		label.SetStyles( "color: #d6d6d6; font-size: 11px; padding-top: 4px; padding-left: 10px;" );
	}

	private void StyleInputWidget( Widget widget )
	{
		widget.MinimumHeight = 22;
		widget.SetStyles( "background-color: #171717; border: 1px solid #252525; border-radius: 3px; color: #f0f0f0; font-size: 11px;" );
	}

	private void StyleInspectorButton( Button button )
	{
		button.MinimumHeight = 22;
		button.SetStyles( "background-color: #1d1d1d; border: 1px solid #292929; border-radius: 3px; color: #e6e6e6; font-size: 11px;" );
	}

	private void RandomizeSeed()
	{
		settings.RandomSeed = new Random().Next( 1, int.MaxValue );

		if ( seedEdit != null )
			seedEdit.Text = settings.RandomSeed.ToString();

		QueueAutoProcess();
		LogMessage( $"Random seed set to {settings.RandomSeed}" );
	}

	private void UpdateProcessingControlVisibility()
	{
		var stochastic = IsStochasticMode();
		var seededSeamShape = settings.SeamShape == SeamlessSeamShape.Wavy
			|| settings.SeamShape == SeamlessSeamShape.Noise
			|| settings.SeamShape == SeamlessSeamShape.Irregular;
		var bombing = settings.ProcessingMode == SeamlessProcessingMode.TextureBombing;
		var brickTile = settings.ProcessingMode == SeamlessProcessingMode.BrickAndTile;
		var brickTileUsesRowOffset = settings.BrickTilePattern == SeamlessBrickTilePattern.RunningBrick
			|| settings.BrickTilePattern == SeamlessBrickTilePattern.Planks
			|| settings.BrickTilePattern == SeamlessBrickTilePattern.Panels;

		SetRowHidden( seedRow, !stochastic && !seededSeamShape );
		SetRowHidden( randomizeRow, !stochastic && !seededSeamShape );
		SetRowHidden( variationPresetRow, !stochastic );
		SetRowHidden( patchShapeRow, !stochastic );
		SetRowHidden( variationStrengthRow, !stochastic );
		SetRowHidden( seamGravityRow, !stochastic );
		SetRowHidden( patchCountRow, !stochastic );
		SetRowHidden( patchSizeRow, !stochastic );
		SetRowHidden( maskSoftnessRow, !stochastic );
		SetRowHidden( bombDensityRow, !bombing );
		SetRowHidden( brickTilePatternRow, !brickTile );
		SetRowHidden( brickTileCellWidthRow, !brickTile );
		SetRowHidden( brickTileCellHeightRow, !brickTile );
		SetRowHidden( brickTileGroutWidthRow, !brickTile );
		SetRowHidden( brickTileRowOffsetRow, !brickTile || !brickTileUsesRowOffset );
		SetRowHidden( brickTileStructureStrengthRow, !brickTile );
	}

	private bool IsStochasticMode()
	{
		return settings.ProcessingMode == SeamlessProcessingMode.IrregularPatch
			|| settings.ProcessingMode == SeamlessProcessingMode.TextureBombing;
	}

	private void SetRowHidden( Widget row, bool hidden )
	{
		if ( row != null )
			row.Hidden = hidden;
	}

	private void RestoreSplitterState()
	{
		try
		{
			if ( EditorCookie.TryGetString( ContentSplitterCookie, out var contentState ) && !string.IsNullOrWhiteSpace( contentState ) )
				contentSplitter?.RestoreState( contentState );

			if ( EditorCookie.TryGetString( MainSplitterCookie, out var mainState ) && !string.IsNullOrWhiteSpace( mainState ) )
				mainSplitter?.RestoreState( mainState );
		}
		catch ( Exception ex )
		{
			LogMessage( $"Could not restore panel sizes, using defaults: {ex.Message}" );
		}
	}

	private void SaveSplitterState()
	{
		try
		{
			if ( contentSplitter != null )
				EditorCookie.SetString( ContentSplitterCookie, contentSplitter.SaveState() );

			if ( mainSplitter != null )
				EditorCookie.SetString( MainSplitterCookie, mainSplitter.SaveState() );
		}
		catch ( Exception ex )
		{
			Log.Info( $"Seam-Less: Could not save panel sizes: {ex.Message}" );
		}
	}

	private void OpenImageDialog()
	{
		var dialog = new FileDialog( this )
		{
			Title = "Open Image",
			DefaultSuffix = ".png"
		};

		dialog.SetModeOpen();
		dialog.SetFindExistingFile();
		dialog.SetNameFilter( "Images (*.png *.jpg *.jpeg *.webp *.bmp *.tga *.tif *.tiff *.psd *.svg);;All Files (*.*)" );

		if ( !dialog.Execute() )
			return;

		LoadImage( dialog.SelectedFile );
	}

	private void LoadDroppedFiles( string[] droppedFiles )
	{
		var files = new List<string>();

		foreach ( var file in droppedFiles )
		{
			files.Add( file );
		}

		var supportedFile = FindFirstSupportedFile( files );

		if ( supportedFile == null )
		{
			LogMessage( "Drop ignored. No supported image file was found." );
			ShowStatus( "No supported image file found", 4f );
			return;
		}

		LoadImage( supportedFile );

		var skippedCount = Math.Max( 0, files.Count - 1 );
		if ( skippedCount > 0 )
			LogMessage( $"One-image mode loaded the first supported image and skipped {skippedCount} other file(s)." );
	}

	private void LoadImage( string path )
	{
		var resolvedPath = ResolveFilePath( path );

		if ( string.IsNullOrWhiteSpace( resolvedPath ) || !File.Exists( resolvedPath ) )
		{
			LogMessage( $"Could not find image: {path}" );
			ShowStatus( "Image file not found", 4f );
			return;
		}

		if ( !IsSupportedImageFile( resolvedPath ) )
		{
			LogMessage( $"Unsupported image type: {Path.GetExtension( resolvedPath )}" );
			ShowStatus( "Unsupported image type", 4f );
			return;
		}

		try
		{
			var bitmap = LoadBitmapFromFile( resolvedPath );

			if ( bitmap == null || !bitmap.IsValid )
			{
				bitmap?.Dispose();
				LogMessage( $"S&box could not decode image: {Path.GetFileName( resolvedPath )}" );
				ShowStatus( "Image decode failed", 4f );
				return;
			}

			originalBitmap?.Dispose();
			processedBitmap?.Dispose();
			originalBitmap = bitmap;
			processedBitmap = null;
			sourceImagePath = resolvedPath;

			if ( outputFolderFollowsSource || string.IsNullOrWhiteSpace( settings.OutputFolder ) )
				SetOutputFolderToSource();

			currentFileLabel.Text = Path.GetFileName( sourceImagePath );
			imageInfoLabel.Text = $"{originalBitmap.Width} x {originalBitmap.Height}  |  {Path.GetExtension( sourceImagePath ).TrimStart( '.' ).ToUpperInvariant()}";
			previewWidget.SetCaption( $"{Path.GetFileName( sourceImagePath )}  |  {originalBitmap.Width} x {originalBitmap.Height}" );

			LogMessage( $"Loaded {sourceImagePath}" );
			ShowStatus( "Image loaded", 4f );
			ProcessImage( false );
		}
		catch ( Exception ex )
		{
			LogMessage( $"Load failed: {ex.Message}" );
			ShowStatus( "Load failed", 4f );
		}
	}

	private Bitmap LoadBitmapFromFile( string path )
	{
		var extension = Path.GetExtension( path ).ToLowerInvariant();
		var bytes = File.ReadAllBytes( path );

		return extension switch
		{
			".tga" => Bitmap.CreateFromTgaBytes( bytes ),
			".tif" => Bitmap.CreateFromTifBytes( bytes ),
			".tiff" => Bitmap.CreateFromTifBytes( bytes ),
			".psd" => Bitmap.CreateFromPsdBytes( bytes ),
			".svg" => Bitmap.CreateFromSvgString( File.ReadAllText( path ), null, null, null, null, null ),
			_ => Bitmap.CreateFromBytes( bytes )
		};
	}

	private void ProcessImage( bool manual )
	{
		if ( originalBitmap == null || !originalBitmap.IsValid )
		{
			if ( manual )
				LogMessage( "No image loaded. Open or drop an image first." );

			ShowStatus( "No image loaded", 4f );
			return;
		}

		try
		{
			var processStarted = DateTime.Now;
			processedBitmap?.Dispose();
			processedBitmap = SeamlessImageProcessor.Process( originalBitmap, settings );

			if ( processedBitmap == null || !processedBitmap.IsValid )
			{
				LogMessage( "Process failed. The bitmap result was invalid." );
				ShowStatus( "Process failed", 4f );
				return;
			}

			SeamlessSuiteActiveTexture.Set( processedBitmap, sourceImagePath, Path.GetFileNameWithoutExtension( sourceImagePath ) );
			UpdatePreviewSettings();

			if ( manual )
				LogMessage( "Processed image with current seam settings." );

			var elapsed = DateTime.Now - processStarted;
			if ( elapsed.TotalMilliseconds > 1500 )
				LogMessage( $"Processing took {elapsed.TotalSeconds:0.0}s. Large stochastic textures can take a moment." );

			ShowStatus( "Preview updated", 2f );
		}
		catch ( Exception ex )
		{
			LogMessage( $"Process failed: {ex.Message}" );
			ShowStatus( "Process failed", 4f );
		}
	}

	private void ExportImage()
	{
		if ( processedBitmap == null || !processedBitmap.IsValid )
			ProcessImage( true );

		if ( processedBitmap == null || !processedBitmap.IsValid )
			return;

		try
		{
			var outputDirectory = GetOutputDirectory();
			Directory.CreateDirectory( outputDirectory );

			var outputPath = BuildOutputPath( outputDirectory );
			var bytes = GetExportBytes();

			File.WriteAllBytes( outputPath, bytes );
			var revealPath = outputPath;
			var vmatCreated = false;

			try
			{
				AssetSystem.RegisterFile( outputPath );
			}
			catch ( Exception registerException )
			{
				LogMessage( $"Export wrote the file, but S&box asset registration skipped it: {registerException.Message}" );
			}

			LogMessage( $"Exported {outputPath}" );

			if ( settings.ExportVMAT )
			{
				var materialResult = SeamlessMaterialExporter.Export( outputPath, settings.OverwriteExisting );

				if ( !string.IsNullOrWhiteSpace( materialResult.Warning ) )
					LogMessage( $"VMAT warning: {materialResult.Warning}" );

				if ( materialResult.Success )
				{
					revealPath = materialResult.VmatPath;
					vmatCreated = true;
					LogMessage( $"Exported VMAT {materialResult.VmatPath}" );
				}
				else
				{
					var error = string.IsNullOrWhiteSpace( materialResult.Error )
						? "Unknown error."
						: materialResult.Error;
					LogMessage( $"VMAT export failed: {error}" );
				}
			}

			lastExportPath = revealPath;

			if ( vmatCreated )
				ShowStatus( "Texture and VMAT export complete", 5f );
			else if ( settings.ExportVMAT )
				ShowStatus( "Texture exported; VMAT failed", 5f );
			else
				ShowStatus( "Export complete", 5f );
		}
		catch ( Exception ex )
		{
			LogMessage( $"Export failed: {ex.Message}" );
			ShowStatus( "Export failed", 5f );
		}
	}

	private byte[] GetExportBytes()
	{
		var quality = Math.Clamp( settings.ExportQuality, 1, 100 );

		return settings.ExportFormat switch
		{
			SeamlessExportFormat.Jpg => processedBitmap.ToJpg( quality ),
			SeamlessExportFormat.WebP => processedBitmap.ToWebP( quality ),
			SeamlessExportFormat.Bmp => processedBitmap.ToBmp(),
			_ => processedBitmap.ToPng()
		};
	}

	private string BuildOutputPath( string outputDirectory )
	{
		var sourceName = string.IsNullOrWhiteSpace( sourceImagePath )
			? "seamless_texture"
			: Path.GetFileNameWithoutExtension( sourceImagePath );

		var suffix = CleanFileNamePart( settings.OutputSuffix );
		var extension = GetExportExtension();
		var basePath = Path.Combine( outputDirectory, $"{sourceName}{suffix}{extension}" );

		if ( settings.OverwriteExisting || !File.Exists( basePath ) )
			return basePath;

		for ( var i = 1; i < 1000; i++ )
		{
			var numberedPath = Path.Combine( outputDirectory, $"{sourceName}{suffix}_{i:000}{extension}" );

			if ( !File.Exists( numberedPath ) )
				return numberedPath;
		}

		return basePath;
	}

	private string GetExportExtension()
	{
		return settings.ExportFormat switch
		{
			SeamlessExportFormat.Jpg => ".jpg",
			SeamlessExportFormat.WebP => ".webp",
			SeamlessExportFormat.Bmp => ".bmp",
			_ => ".png"
		};
	}

	private string GetOutputDirectory()
	{
		if ( !string.IsNullOrWhiteSpace( settings.OutputFolder ) )
			return settings.OutputFolder;

		if ( !string.IsNullOrWhiteSpace( sourceImagePath ) )
			return Path.GetDirectoryName( sourceImagePath ) ?? Directory.GetCurrentDirectory();

		return Directory.GetCurrentDirectory();
	}

	private void RevealOutput()
	{
		if ( !string.IsNullOrWhiteSpace( lastExportPath ) && File.Exists( lastExportPath ) )
		{
			EditorUtility.OpenFileFolder( lastExportPath );
			return;
		}

		var outputDirectory = GetOutputDirectory();
		if ( Directory.Exists( outputDirectory ) )
		{
			EditorUtility.OpenFolder( outputDirectory );
			return;
		}

		LogMessage( "No output folder exists yet." );
		ShowStatus( "No output folder exists yet", 4f );
	}

	private void BrowseOutputFolder()
	{
		var dialog = new FileDialog( this )
		{
			Title = "Choose Output Folder"
		};

		dialog.SetModeOpen();
		dialog.SetFindDirectory();

		if ( !string.IsNullOrWhiteSpace( settings.OutputFolder ) )
			dialog.Directory = settings.OutputFolder;

		if ( !dialog.Execute() )
			return;

		var folder = !string.IsNullOrWhiteSpace( dialog.SelectedFile )
			? dialog.SelectedFile
			: dialog.Directory;

		if ( string.IsNullOrWhiteSpace( folder ) )
			return;

		outputFolderFollowsSource = false;
		settings.OutputFolder = folder;
		outputFolderEdit.Text = folder;
		LogMessage( $"Output folder set to {folder}" );
	}

	private void UseSourceFolder()
	{
		outputFolderFollowsSource = true;
		SetOutputFolderToSource();
		LogMessage( "Output folder now follows the source image folder." );
	}

	private void SetOutputFolderToSource()
	{
		var sourceFolder = !string.IsNullOrWhiteSpace( sourceImagePath )
			? Path.GetDirectoryName( sourceImagePath )
			: "";

		settings.OutputFolder = sourceFolder ?? "";

		if ( outputFolderEdit != null )
			outputFolderEdit.Text = settings.OutputFolder;
	}

	private void QueueAutoProcess()
	{
		if ( !settings.AutoProcess || originalBitmap == null || !originalBitmap.IsValid )
			return;

		processQueued = true;
		queuedProcessTime = DateTime.Now.AddMilliseconds( 140 );
	}

	private void UpdatePreviewSettings()
	{
		previewWidget.SetPreviewMode( settings.PreviewMode );
		previewWidget.SetTileCount( settings.PreviewTileCount );
		previewWidget.SetBitmaps( originalBitmap, processedBitmap );
		UpdateTileCountVisibility();
	}

	private void UpdateTileCountVisibility()
	{
		if ( tileCountRow == null )
			return;

		tileCountRow.Hidden = settings.PreviewMode != SeamlessPreviewMode.Tiled;
	}

	private string ResolveFilePath( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
			return path;

		if ( File.Exists( path ) )
			return path;

		var asset = AssetSystem.FindByPath( path );

		if ( asset != null && File.Exists( asset.AbsolutePath ) )
			return asset.AbsolutePath;

		return path;
	}

	private List<string> GetFilesFromDragData( DragData data )
	{
		var files = new List<string>();

		if ( data?.Files != null )
		{
			foreach ( var file in data.Files )
			{
				files.Add( file );
			}
		}

		if ( data?.Assets != null )
		{
			foreach ( var dragAsset in data.Assets )
			{
				if ( !string.IsNullOrWhiteSpace( dragAsset.AssetPath ) )
					files.Add( dragAsset.AssetPath );
			}
		}

		if ( data != null )
		{
			foreach ( var asset in data.OfType<Editor.Asset>() )
			{
				AddAssetPathCandidates( files, asset );
			}
		}

		return files;
	}

	private void AddAssetPathCandidates( List<string> files, Editor.Asset asset )
	{
		if ( asset == null || asset.IsDeleted )
			return;

		if ( !string.IsNullOrWhiteSpace( asset.AbsolutePath ) )
			files.Add( asset.AbsolutePath );

		if ( !string.IsNullOrWhiteSpace( asset.RelativePath ) )
			files.Add( asset.RelativePath );

		if ( !string.IsNullOrWhiteSpace( asset.Path ) )
			files.Add( asset.Path );
	}

	private string FindFirstSupportedFile( List<string> files )
	{
		foreach ( var file in files )
		{
			var resolved = ResolveFilePath( file );

			if ( IsSupportedImageFile( resolved ) )
				return resolved;
		}

		return null;
	}

	private bool IsSupportedImageFile( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
			return false;

		var extension = Path.GetExtension( path ).ToLowerInvariant();
		return supportedExtensions.Contains( extension );
	}

	private string CleanFileNamePart( string value )
	{
		if ( string.IsNullOrEmpty( value ) )
			return "";

		var invalid = Path.GetInvalidFileNameChars();
		var cleaned = value;

		foreach ( var character in invalid )
		{
			cleaned = cleaned.Replace( character, '_' );
		}

		return cleaned;
	}

	private float SnapSliderValue( float value, float step, float min, float max )
	{
		var clamped = Math.Clamp( value, min, max );

		if ( step <= 0f )
			return clamped;

		return Math.Clamp( MathF.Round( clamped / step ) * step, min, max );
	}

	private string FormatSliderValue( float value, float step )
	{
		if ( step >= 1f )
			return MathF.Round( value ).ToString();

		if ( step >= 0.05f )
			return value.ToString( "0.0" );

		return value.ToString( "0.00" );
	}

	private void ShowStatus( string message, float seconds )
	{
		var line = $"[{DateTime.Now:HH:mm:ss}] Status: {message}";
		consoleOutput?.AppendPlainText( line );
		consoleOutput?.ScrollToBottom();
	}

	private void LogMessage( string message )
	{
		var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
		consoleOutput?.AppendPlainText( line );
		consoleOutput?.ScrollToBottom();
		Log.Info( $"Seam-Less: {message}" );
	}

	private void ClearLog()
	{
		DisposeBitmaps();
		sourceImagePath = null;
		lastExportPath = null;
		processQueued = false;
		SeamlessSuiteActiveTexture.Clear();

		if ( outputFolderFollowsSource )
			SetOutputFolderToSource();

		if ( currentFileLabel != null )
			currentFileLabel.Text = "No image loaded";

		if ( imageInfoLabel != null )
			imageInfoLabel.Text = "Drop an image into the preview, or use Open.";

		previewWidget?.SetCaption( "Drop an image here or use Open" );
		previewWidget?.SetBitmaps( null, null );
		consoleOutput?.Clear();
		Log.Info( "Seam-Less: Cleared image and log." );
	}

	private void DisposeBitmaps()
	{
		originalBitmap?.Dispose();
		processedBitmap?.Dispose();
		originalBitmap = null;
		processedBitmap = null;
	}
}