Merge branch 'imagexport'

This commit is contained in:
spherallic 2023-04-26 17:16:54 +02:00
commit ee37a21720
8 changed files with 1382 additions and 2 deletions

View File

@ -146,6 +146,12 @@
<Compile Include="Interface\FindReplaceForm.Designer.cs">
<DependentUpon>FindReplaceForm.cs</DependentUpon>
</Compile>
<Compile Include="Interface\ImageExportSettingsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Interface\ImageExportSettingsForm.Designer.cs">
<DependentUpon>ImageExportSettingsForm.cs</DependentUpon>
</Compile>
<Compile Include="Interface\MakeDoorForm.cs">
<SubType>Form</SubType>
</Compile>
@ -200,6 +206,7 @@
<Compile Include="Interface\VertexSlopeAssistTagForm.designer.cs">
<DependentUpon>VertexSlopeAssistTagForm.cs</DependentUpon>
</Compile>
<Compile Include="IO\ImageExporter.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
@ -239,6 +246,9 @@
</EmbeddedResource>
<EmbeddedResource Include="Interface\VertexSlopeAssistTagForm.resx">
<DependentUpon>VertexSlopeAssistTagForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Interface\ImageExportSettingsForm.resx">
<DependentUpon>ImageExportSettingsForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Actions.cfg" />
</ItemGroup>

View File

@ -875,6 +875,24 @@ namespace CodeImp.DoomBuilder.BuilderModes
}
}
[BeginAction("exporttoimage")]
private void ExportToImage()
{
// Convert geometry selection to sectors
General.Map.Map.ConvertSelection(SelectionType.Sectors);
// Get sectors
ICollection<Sector> sectors = General.Map.Map.SelectedSectorsCount == 0 ? General.Map.Map.Sectors : General.Map.Map.GetSelectedSectors(true);
if (sectors.Count == 0)
{
General.Interface.DisplayStatus(StatusType.Warning, "Image export failed. Map has no sectors!");
return;
}
ImageExportSettingsForm form = new ImageExportSettingsForm();
form.ShowDialog();
}
#endregion
}
}

View File

@ -0,0 +1,548 @@
#region ================== Copyright (c) 2020 Boris Iwanski
/*
* This program is free software: you can redistribute it and/or modify
*
* it under the terms of the GNU General Public License as published by
*
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
*
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
*
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.If not, see<http://www.gnu.org/licenses/>.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;
using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.Geometry;
using CodeImp.DoomBuilder.Map;
using System.Diagnostics;
namespace CodeImp.DoomBuilder.BuilderModes.IO
{
#region ================== Structs
internal struct ImageExportSettings
{
public string Path;
public string Name;
public string Extension;
public bool Floor;
public bool Fullbright;
public bool Brightmap;
public bool Tiles;
public PixelFormat PixelFormat;
public ImageFormat ImageFormat;
public float Scale;
public ImageExportSettings(string path, string name, string extension, bool floor, bool fullbright, bool brightmap, bool tiles, float scale, PixelFormat pformat, ImageFormat iformat)
{
Path = path;
Name = name;
Extension = extension;
Floor = floor;
Brightmap = brightmap;
Tiles = tiles;
Fullbright = fullbright;
PixelFormat = pformat;
ImageFormat = iformat;
Scale = scale;
}
}
#endregion
#region ================== Exceptions
[Serializable]
public class ImageExportCanceledException : Exception { }
[Serializable]
public class ImageExportImageTooBigException : Exception { }
#endregion
internal class ImageExporter
{
#region ================== Variables
private ICollection<Sector> sectors;
private ImageExportSettings settings;
private int numitems;
private int doneitems;
private int donepercent;
private Action addprogress;
private Action<string> showphase;
private Func<bool> checkcanelexport;
private bool cancelexport;
#endregion
#region ================== Constants
private const int TILE_SIZE = 64;
#endregion
#region ================== Constructors
public ImageExporter(ICollection<Sector> sectors, ImageExportSettings settings, Action addprogress, Action<string> showphase, Func<bool> checkcanelexport)
{
this.sectors = sectors;
this.settings = settings;
this.addprogress = addprogress;
this.showphase = showphase;
this.checkcanelexport = checkcanelexport;
cancelexport = false;
}
#endregion
#region ================== Methods
/// <summary>
/// Exports the sectors to images
/// </summary>
public void Export()
{
Vector2D size;
Vector2D offset;
GetSizeAndOffset(out size, out offset);
// Count the number of triangles for reporting progress
numitems = 0;
doneitems = 0;
donepercent = 0;
foreach (Sector s in sectors)
numitems += s.Triangles.Vertices.Count / 3;
if (settings.Tiles)
numitems += GetNumTiles();
// If exporting a brightmap everything has to be done twice
if (settings.Brightmap)
numitems *= 2;
// Use the same image for the normal texture and the brightmap because of memory concerns
showphase("Preparing");
using (Bitmap image = new Bitmap((int)(size.x * settings.Scale), (int)(size.y * settings.Scale), settings.PixelFormat))
{
showphase("Creating normal image");
// Normal texture image
CreateImage(image, offset, settings.Scale, false);
if (settings.Tiles)
{
showphase("Saving 64x64 tile images (" + GetNumTiles() + ")");
SaveImageAsTiles(image);
}
else
{
showphase("Saving normal image");
try
{
image.Save(Path.Combine(settings.Path, settings.Name) + settings.Extension, settings.ImageFormat);
}
catch(ExternalException)
{
throw new ImageExportImageTooBigException();
}
}
// The brightmap
if (settings.Brightmap)
{
showphase("Creating brightmap image");
CreateImage(image, offset, settings.Scale, true);
showphase("Saving brightmap image");
if (settings.Tiles)
{
showphase("Saving 64x64 tile images (" + GetNumTiles() + ")");
SaveImageAsTiles(image, "_brightmap");
}
else
{
image.Save(Path.Combine(settings.Path, settings.Name) + "_brightmap" + settings.Extension, settings.ImageFormat);
showphase("Saving normal image");
}
}
}
}
/// <summary>
/// Create the image ready to be exported
/// </summary>
/// <param name="texturebitmap">The image the graphics will be drawn to</param>
/// <param name="offset">The offset of the selection in map space</param>
/// <param name="asbrightmap">True if the image should be a brightmap, false if normally textured</param>
/// <returns>The image to be exported</returns>
private void CreateImage(Bitmap texturebitmap, Vector2D offset, float scale, bool asbrightmap)
{
// The texture
using (Graphics gtexture = Graphics.FromImage(texturebitmap))
{
gtexture.Clear(Color.Black); // If we don't clear to black we'll see seams where the sectors touch, due to the AA
gtexture.InterpolationMode = InterpolationMode.HighQualityBilinear;
gtexture.CompositingQuality = CompositingQuality.HighQuality;
gtexture.PixelOffsetMode = PixelOffsetMode.HighQuality;
gtexture.SmoothingMode = SmoothingMode.AntiAlias; // Without AA the sector edges will be quite rough
using (GraphicsPath gpath = new GraphicsPath())
{
foreach (Sector s in sectors)
{
float rotation = (float)s.Fields.GetValue("rotationfloor", 0.0);
// If a sector is rotated any offset is on the rotated axes. But we need to offset by
// map coordinates. We'll use this vector to compute that offset
Vector2D rotationvector = Vector2D.FromAngle(Angle2D.DegToRad(rotation) + Angle2D.PIHALF);
// Sectors are triangulated, so draw every triangle
for (int i = 0; i < s.Triangles.Vertices.Count / 3; i++)
{
// The GDI image has the 0/0 coordinate in the top left, so invert the y component
Vector2D v1 = (s.Triangles.Vertices[i * 3] - offset) * scale; v1.y *= -1.0f;
Vector2D v2 = (s.Triangles.Vertices[i * 3 + 1] - offset) * scale; v2.y *= -1.0f;
Vector2D v3 = (s.Triangles.Vertices[i * 3 + 2] - offset) * scale; v3.y *= -1.0f;
gpath.AddLine((float)v1.x, (float)v1.y, (float)v2.x, (float)v2.y);
gpath.AddLine((float)v2.x, (float)v2.y, (float)v3.x, (float)v3.y);
gpath.CloseFigure();
doneitems++;
int newpercent = (int)(((double)doneitems / numitems) * 100);
if (newpercent > donepercent)
{
donepercent = newpercent;
addprogress();
if (checkcanelexport())
throw new ImageExportCanceledException();
}
}
if (asbrightmap)
{
// Create the brightmap based on the sector brightness
int brightness = General.Clamp(s.Brightness, 0, 255);
using (SolidBrush sbrush = new SolidBrush(Color.FromArgb(255, brightness, brightness, brightness)))
gtexture.FillPath(sbrush, gpath);
}
else
{
Bitmap brushtexture;
Vector2D textureoffset = new Vector2D();
Vector2D texturescale = new Vector2D();
if (settings.Floor)
{
// The image might have a color correction applied, but we need it without. So we use LocalGetBitmap, because it reloads the image,
// but doesn't applie the color correction if we set UseColorCorrection to false first
ImageData imagedata = General.Map.Data.GetFlatImage(s.FloorTexture);
imagedata.UseColorCorrection = false;
brushtexture = new Bitmap(imagedata.GetBitmap());
imagedata.UseColorCorrection = true;
textureoffset.x = s.Fields.GetValue("xpanningfloor", 0.0f) * scale;
textureoffset.y = s.Fields.GetValue("ypanningfloor", 0.0f) * scale;
// GZDoom uses bigger numbers for smaller scales (i.e. a scale of 2 will halve the size), so we need to change the scale
texturescale.x = 1.0f / s.Fields.GetValue("xscalefloor", 1.0f);
texturescale.y = 1.0f / s.Fields.GetValue("yscalefloor", 1.0f);
}
else
{
// The image might have a color correction applied, but we need it without. So we use LocalGetBitmap, because it reloads the image,
// but doesn't applie the color correction if we set UseColorCorrection to false first
ImageData imagedata = General.Map.Data.GetFlatImage(s.CeilTexture);
imagedata.UseColorCorrection = false;
brushtexture = new Bitmap(imagedata.GetBitmap());
imagedata.UseColorCorrection = true;
textureoffset.x = s.Fields.GetValue("xpanningceiling", 0.0f) * scale;
textureoffset.y = s.Fields.GetValue("ypanningceiling", 0.0f) * scale;
// GZDoom uses bigger numbers for smaller scales (i.e. a scale of 2 will halve the size), so we need to change the scale
texturescale.x = 1.0f / s.Fields.GetValue("xscaleceiling", 1.0f);
texturescale.y = 1.0f / s.Fields.GetValue("yscaleceiling", 1.0f);
}
// Create the transformation matrix
Matrix matrix = new Matrix();
matrix.Rotate(rotation);
matrix.Translate((float)(-offset.x * scale * rotationvector.x), (float)(offset.x * scale * rotationvector.y)); // Left/right offset from the map origin
matrix.Translate((float)(offset.y * scale * rotationvector.y), (float)(offset.y * scale * rotationvector.x)); // Up/down offset from the map origin
matrix.Translate(-(float)textureoffset.x, -(float)textureoffset.y); // Texture offset
matrix.Scale((float)texturescale.x, (float)texturescale.y);
if (!settings.Fullbright)
{
int brightness = General.Clamp(s.Brightness, 0, 255);
AdjustBrightness(ref brushtexture, brightness > 0 ? brightness / 255.0f : 0.0f);
}
if (scale > 1.0f)
ResizeImage(ref brushtexture, brushtexture.Width * (int)scale, brushtexture.Height * (int)scale);
// Create the texture brush and apply the matrix
TextureBrush tbrush = new TextureBrush(brushtexture);
tbrush.Transform = matrix;
// Draw the islands of the sector
gtexture.FillPath(tbrush, gpath);
// Dispose unneeded objects
brushtexture.Dispose();
tbrush.Dispose();
matrix.Dispose();
}
// Reset the graphics path
gpath.Reset();
}
}
}
}
/// <summary>
/// Saves the image in several uniform sized tiles. It will add numbers starting from 1, going from top left to bottom right, to the filename
/// </summary>
/// <param name="image">the image to split into tiles</param>
/// <param name="suffix">additional suffix for filenames</param>
private void SaveImageAsTiles(Bitmap image, string suffix="")
{
int xnum = (int)Math.Ceiling(image.Size.Width / (double)TILE_SIZE);
int ynum = (int)Math.Ceiling(image.Size.Height / (double)TILE_SIZE);
int imagenum = 1;
for (int y = 0; y < ynum; y++)
{
for (int x = 0; x < xnum; x++)
{
int width = TILE_SIZE;
int height = TILE_SIZE;
// If the width and height are not divisible without remainder make sure only part of the source image is copied
if (x * TILE_SIZE + TILE_SIZE > image.Size.Width)
width = image.Size.Width - (x * TILE_SIZE);
if(y * TILE_SIZE + TILE_SIZE > image.Size.Height)
height = image.Size.Height - (y * TILE_SIZE);
using (Bitmap bitmap = new Bitmap(TILE_SIZE, TILE_SIZE))
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Black);
g.DrawImage(image, new Rectangle(0, 0, width, height), new Rectangle(x * TILE_SIZE, y * TILE_SIZE, width, height), GraphicsUnit.Pixel);
bitmap.Save(string.Format("{0}{1}{2}{3}", Path.Combine(settings.Path, settings.Name), suffix, imagenum, settings.Extension));
}
imagenum++;
doneitems++;
int newpercent = (int)(((double)doneitems / numitems) * 100);
if (newpercent > donepercent)
{
donepercent = newpercent;
addprogress();
if (checkcanelexport())
throw new ImageExportCanceledException();
}
if (checkcanelexport())
throw new ImageExportCanceledException();
}
}
}
/// <summary>
/// Generates a list of images file names that will be creates
/// </summary>
/// <returns>List of image file names</returns>
public List<string> GetImageNames()
{
List<string> imagenames = new List<string>();
Vector2D offset;
Vector2D size;
GetSizeAndOffset(out size, out offset);
if(settings.Tiles)
{
int x = (int)Math.Ceiling(size.x / TILE_SIZE);
int y = (int)Math.Ceiling(size.y / TILE_SIZE);
for (int i = 1; i <= x * y; i++)
imagenames.Add(string.Format("{0}{1}{2}", Path.Combine(settings.Path, settings.Name), i, settings.Extension));
if(settings.Brightmap)
for (int i = 1; i <= x * y; i++)
imagenames.Add(string.Format("{0}{1}_brightmap{2}", Path.Combine(settings.Path, settings.Name), i, settings.Extension));
}
else
{
imagenames.Add(string.Format("{0}{1}", Path.Combine(settings.Path, settings.Name), settings.Extension));
if(settings.Brightmap)
imagenames.Add(string.Format("{0}_brightmap{1}", Path.Combine(settings.Path, settings.Name), settings.Extension));
}
return imagenames;
}
/// <summary>
/// Gets the total number of tiles
/// </summary>
/// <returns>Number of tiles</returns>
public int GetNumTiles()
{
Vector2D size;
Vector2D offset;
GetSizeAndOffset(out size, out offset);
int xnum = (int)Math.Ceiling(size.x * settings.Scale / (double)TILE_SIZE);
int ynum = (int)Math.Ceiling(size.y * settings.Scale / (double)TILE_SIZE);
return xnum * ynum;
}
/// <summary>
/// Gets the size of the image, based on the sectors, and the offset from the map origin
/// </summary>
/// <param name="size">stores the size of the size of the image</param>
/// <param name="offset">stores the offset from the map origin</param>
private void GetSizeAndOffset(out Vector2D size, out Vector2D offset)
{
offset = new Vector2D(float.MaxValue, float.MinValue);
size = new Vector2D(float.MinValue, float.MaxValue);
// Find the top left and bottom right corners of the selection
foreach (Sector s in sectors)
{
foreach (Sidedef sd in s.Sidedefs)
{
foreach (Vertex v in new Vertex[] { sd.Line.Start, sd.Line.End })
{
if (v.Position.x < offset.x)
offset.x = v.Position.x;
if (v.Position.x > size.x)
size.x = v.Position.x;
if (v.Position.y > offset.y)
offset.y = v.Position.y;
if (v.Position.y < size.y)
size.y = v.Position.y;
}
}
}
// Right now "size" is the bottom right corener of the selection, so subtract the offset
// (top left corner of the selection). y will always be negative, so make it positive
size -= offset;
size.y *= -1.0f;
}
/// <summary>
/// Adjusts the brightness of an image. Code by Rod Stephens http://csharphelper.com/blog/2014/10/use-an-imageattributes-object-to-adjust-an-images-brightness-in-c/
/// </summary>
/// <param name="image">The image to adjust</param>
/// <param name="brightness">Brightness between 0.0f and 1.0f</param>
private void AdjustBrightness(ref Bitmap image, float brightness)
{
// Make the ColorMatrix.
float b = brightness;
ColorMatrix cm = new ColorMatrix(new float[][] {
new float[] {b, 0, 0, 0, 0},
new float[] {0, b, 0, 0, 0},
new float[] {0, 0, b, 0, 0},
new float[] {0, 0, 0, 1, 0},
new float[] {0, 0, 0, 0, 1},
});
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(cm);
// Draw the image onto the new bitmap while applying the new ColorMatrix.
Point[] points = {
new Point(0, 0),
new Point(image.Width, 0),
new Point(0, image.Height),
};
Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
// Make the result bitmap.
Bitmap bm = new Bitmap(image.Width, image.Height);
using (Graphics gr = Graphics.FromImage(bm))
{
gr.DrawImage(image, points, rect, GraphicsUnit.Pixel, attributes);
}
// Dispose the original...
image.Dispose();
// ... and set it as the adjusted image
image = bm;
}
/// <summary>
/// Resize the image to the specified width and height. Taken from https://stackoverflow.com/a/24199315 (with some modifications)
/// </summary>
/// <param name="image">The image to resize.</param>
/// <param name="width">The width to resize to.</param>
/// <param name="height">The height to resize to.</param>
/// <returns>The resized image.</returns>
private void ResizeImage(ref Bitmap image, int width, int height)
{
var destRect = new Rectangle(0, 0, width, height);
var destImage = new Bitmap(width, height);
destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
using (var graphics = Graphics.FromImage(destImage))
{
graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.NearestNeighbor;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
using (var wrapMode = new ImageAttributes())
{
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
}
}
image.Dispose();
image = destImage;
}
#endregion
}
}

View File

@ -0,0 +1,301 @@
namespace CodeImp.DoomBuilder.BuilderModes.Interface
{
partial class ImageExportSettingsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.tbExportPath = new System.Windows.Forms.TextBox();
this.browse = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.close = new System.Windows.Forms.Button();
this.export = new System.Windows.Forms.Button();
this.saveFileDialog = new System.Windows.Forms.SaveFileDialog();
this.cbImageFormat = new System.Windows.Forms.ComboBox();
this.cbPixelFormat = new System.Windows.Forms.ComboBox();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.rbFloor = new System.Windows.Forms.RadioButton();
this.rbCeiling = new System.Windows.Forms.RadioButton();
this.cbFullbright = new System.Windows.Forms.CheckBox();
this.cbBrightmap = new System.Windows.Forms.CheckBox();
this.cbTiles = new System.Windows.Forms.CheckBox();
this.cbScale = new System.Windows.Forms.ComboBox();
this.label4 = new System.Windows.Forms.Label();
this.progress = new System.Windows.Forms.ProgressBar();
this.lbPhase = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// tbExportPath
//
this.tbExportPath.Location = new System.Drawing.Point(50, 9);
this.tbExportPath.Name = "tbExportPath";
this.tbExportPath.Size = new System.Drawing.Size(344, 20);
this.tbExportPath.TabIndex = 2;
//
// browse
//
this.browse.Image = global::CodeImp.DoomBuilder.BuilderModes.Properties.Resources.Folder;
this.browse.Location = new System.Drawing.Point(400, 7);
this.browse.Name = "browse";
this.browse.Size = new System.Drawing.Size(30, 24);
this.browse.TabIndex = 3;
this.browse.UseVisualStyleBackColor = true;
this.browse.Click += new System.EventHandler(this.browse_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 12);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(32, 13);
this.label1.TabIndex = 4;
this.label1.Text = "Path:";
//
// close
//
this.close.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.close.Location = new System.Drawing.Point(360, 153);
this.close.Name = "close";
this.close.Size = new System.Drawing.Size(75, 23);
this.close.TabIndex = 7;
this.close.Text = "Close";
this.close.UseVisualStyleBackColor = true;
this.close.Click += new System.EventHandler(this.close_Click);
//
// export
//
this.export.Location = new System.Drawing.Point(279, 153);
this.export.Name = "export";
this.export.Size = new System.Drawing.Size(75, 23);
this.export.TabIndex = 6;
this.export.Text = "Export";
this.export.UseVisualStyleBackColor = true;
this.export.Click += new System.EventHandler(this.export_Click);
//
// saveFileDialog
//
this.saveFileDialog.Filter = "PNG (*.png)|*.png|JPEG (*.jpg)|*.jpg|All files (*.*)|*.*";
//
// cbImageFormat
//
this.cbImageFormat.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbImageFormat.FormattingEnabled = true;
this.cbImageFormat.Items.AddRange(new object[] {
"PNG",
"JPG"});
this.cbImageFormat.Location = new System.Drawing.Point(102, 35);
this.cbImageFormat.Name = "cbImageFormat";
this.cbImageFormat.Size = new System.Drawing.Size(71, 21);
this.cbImageFormat.TabIndex = 8;
this.cbImageFormat.SelectedIndexChanged += new System.EventHandler(this.cbImageFormat_SelectedIndexChanged);
//
// cbPixelFormat
//
this.cbPixelFormat.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbPixelFormat.FormattingEnabled = true;
this.cbPixelFormat.Items.AddRange(new object[] {
"32 bit",
"24 bit",
"16 bit"});
this.cbPixelFormat.Location = new System.Drawing.Point(102, 62);
this.cbPixelFormat.Name = "cbPixelFormat";
this.cbPixelFormat.Size = new System.Drawing.Size(71, 21);
this.cbPixelFormat.TabIndex = 9;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(12, 38);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(71, 13);
this.label2.TabIndex = 10;
this.label2.Text = "Image format:";
//
// label3
//
this.label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(12, 65);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(64, 13);
this.label3.TabIndex = 11;
this.label3.Text = "Color depth:";
//
// rbFloor
//
this.rbFloor.AutoSize = true;
this.rbFloor.Checked = true;
this.rbFloor.Location = new System.Drawing.Point(197, 39);
this.rbFloor.Name = "rbFloor";
this.rbFloor.Size = new System.Drawing.Size(48, 17);
this.rbFloor.TabIndex = 12;
this.rbFloor.TabStop = true;
this.rbFloor.Text = "Floor";
this.rbFloor.UseVisualStyleBackColor = true;
//
// rbCeiling
//
this.rbCeiling.AutoSize = true;
this.rbCeiling.Location = new System.Drawing.Point(197, 61);
this.rbCeiling.Name = "rbCeiling";
this.rbCeiling.Size = new System.Drawing.Size(56, 17);
this.rbCeiling.TabIndex = 13;
this.rbCeiling.Text = "Ceiling";
this.rbCeiling.UseVisualStyleBackColor = true;
//
// cbFullbright
//
this.cbFullbright.AutoSize = true;
this.cbFullbright.Checked = true;
this.cbFullbright.CheckState = System.Windows.Forms.CheckState.Checked;
this.cbFullbright.Location = new System.Drawing.Point(279, 40);
this.cbFullbright.Name = "cbFullbright";
this.cbFullbright.Size = new System.Drawing.Size(87, 17);
this.cbFullbright.TabIndex = 14;
this.cbFullbright.Text = "Use fullbright";
this.cbFullbright.UseVisualStyleBackColor = true;
//
// cbBrightmap
//
this.cbBrightmap.AutoSize = true;
this.cbBrightmap.Location = new System.Drawing.Point(279, 64);
this.cbBrightmap.Name = "cbBrightmap";
this.cbBrightmap.Size = new System.Drawing.Size(106, 17);
this.cbBrightmap.TabIndex = 15;
this.cbBrightmap.Text = "Create brightmap";
this.cbBrightmap.UseVisualStyleBackColor = true;
//
// cbTiles
//
this.cbTiles.AutoSize = true;
this.cbTiles.Location = new System.Drawing.Point(279, 88);
this.cbTiles.Name = "cbTiles";
this.cbTiles.Size = new System.Drawing.Size(110, 17);
this.cbTiles.TabIndex = 16;
this.cbTiles.Text = "Create 64x64 tiles";
this.cbTiles.UseVisualStyleBackColor = true;
//
// cbScale
//
this.cbScale.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbScale.FormattingEnabled = true;
this.cbScale.Items.AddRange(new object[] {
"100%",
"200%",
"400%",
"800%"});
this.cbScale.Location = new System.Drawing.Point(102, 89);
this.cbScale.Name = "cbScale";
this.cbScale.Size = new System.Drawing.Size(71, 21);
this.cbScale.TabIndex = 17;
//
// label4
//
this.label4.AutoSize = true;
this.label4.Location = new System.Drawing.Point(12, 92);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(37, 13);
this.label4.TabIndex = 18;
this.label4.Text = "Scale:";
//
// progress
//
this.progress.Location = new System.Drawing.Point(12, 153);
this.progress.Name = "progress";
this.progress.Size = new System.Drawing.Size(261, 23);
this.progress.Step = 1;
this.progress.TabIndex = 19;
this.progress.Visible = false;
//
// lbPhase
//
this.lbPhase.AutoSize = true;
this.lbPhase.Location = new System.Drawing.Point(14, 127);
this.lbPhase.Name = "lbPhase";
this.lbPhase.Size = new System.Drawing.Size(45, 13);
this.lbPhase.TabIndex = 20;
this.lbPhase.Text = "lbPhase";
this.lbPhase.Visible = false;
//
// ImageExportSettingsForm
//
this.AcceptButton = this.export;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.close;
this.ClientSize = new System.Drawing.Size(447, 188);
this.Controls.Add(this.lbPhase);
this.Controls.Add(this.progress);
this.Controls.Add(this.label4);
this.Controls.Add(this.cbScale);
this.Controls.Add(this.cbTiles);
this.Controls.Add(this.cbBrightmap);
this.Controls.Add(this.cbFullbright);
this.Controls.Add(this.rbCeiling);
this.Controls.Add(this.rbFloor);
this.Controls.Add(this.label3);
this.Controls.Add(this.label2);
this.Controls.Add(this.cbPixelFormat);
this.Controls.Add(this.cbImageFormat);
this.Controls.Add(this.close);
this.Controls.Add(this.export);
this.Controls.Add(this.label1);
this.Controls.Add(this.browse);
this.Controls.Add(this.tbExportPath);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ImageExportSettingsForm";
this.Text = "Image export settings";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ImageExportSettingsForm_FormClosing);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button browse;
private System.Windows.Forms.TextBox tbExportPath;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Button close;
private System.Windows.Forms.Button export;
private System.Windows.Forms.SaveFileDialog saveFileDialog;
private System.Windows.Forms.ComboBox cbImageFormat;
private System.Windows.Forms.ComboBox cbPixelFormat;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.RadioButton rbFloor;
private System.Windows.Forms.RadioButton rbCeiling;
private System.Windows.Forms.CheckBox cbFullbright;
private System.Windows.Forms.CheckBox cbBrightmap;
private System.Windows.Forms.CheckBox cbTiles;
private System.Windows.Forms.ComboBox cbScale;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.ProgressBar progress;
private System.Windows.Forms.Label lbPhase;
}
}

View File

@ -0,0 +1,358 @@
#region ================== Copyright (c) 2020 Boris Iwanski
/*
* This program is free software: you can redistribute it and/or modify
*
* it under the terms of the GNU General Public License as published by
*
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
*
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
*
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.If not, see<http://www.gnu.org/licenses/>.
*/
#endregion
using System;
using System.Drawing.Imaging;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.BuilderModes.IO;
namespace CodeImp.DoomBuilder.BuilderModes.Interface
{
enum ImageExportResult
{
OK,
Canceled,
OutOfMemory,
ImageTooBig
}
public partial class ImageExportSettingsForm : Form
{
#region ================== Properties
public string FilePath { get { return tbExportPath.Text.Trim(); } }
public bool Floor { get { return rbFloor.Checked; } }
public bool Fullbright { get { return cbFullbright.Checked; } }
public bool Brightmap { get { return cbBrightmap.Checked; } }
public bool Tiles { get { return cbTiles.Checked; } }
public float ImageScale { get { return (float)Math.Pow(2, cbScale.SelectedIndex); } }
#endregion
#region ================== Delegates
private delegate void CallVoidMethodDeletage();
private delegate void CallStringMethodDeletage(string s);
private delegate void CallImageExportResultMethodDeletage(ImageExportResult ier);
#endregion
#region ================== Variables
bool exporting;
bool cancelexport;
#endregion
#region ================== Constructor
public ImageExportSettingsForm()
{
InitializeComponent();
cbImageFormat.SelectedIndex = 0;
cbPixelFormat.SelectedIndex = 0;
exporting = false;
cancelexport = false;
string name = Path.GetFileNameWithoutExtension(General.Map.FileTitle) + "_" + General.Map.Options.LevelName + "_" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName());
if (string.IsNullOrEmpty(General.Map.FilePathName))
{
saveFileDialog.FileName = name;
}
else
{
saveFileDialog.InitialDirectory = Path.GetDirectoryName(General.Map.FilePathName);
saveFileDialog.FileName = Path.GetDirectoryName(General.Map.FilePathName) + Path.DirectorySeparatorChar + name + ".png";
tbExportPath.Text = saveFileDialog.FileName;
}
cbFullbright.Checked = General.Settings.ReadPluginSetting("imageexportfullbright", true);
cbBrightmap.Checked = General.Settings.ReadPluginSetting("imageexportbrightmap", false);
cbTiles.Checked = General.Settings.ReadPluginSetting("imageexporttiles", false);
cbScale.SelectedIndex = General.Settings.ReadPluginSetting("imageexportscale", 0);
}
#endregion
#region ================== Methods
public ImageFormat GetImageFormat()
{
switch(cbImageFormat.SelectedIndex)
{
case 1: // JPG
return ImageFormat.Jpeg;
default: // PNG
return ImageFormat.Png;
}
}
public PixelFormat GetPixelFormat()
{
switch(cbPixelFormat.SelectedIndex)
{
case 1: // 24 bit
return PixelFormat.Format24bppRgb;
case 2: // 16 bit
return PixelFormat.Format16bppRgb555;
default: // 32 bit
return PixelFormat.Format32bppArgb;
}
}
/// <summary>
/// Starts exporting the image(s). Disables all controls and starts the thread that does the actual exporting.
/// </summary>
private void StartExport()
{
ICollection<Sector> sectors = General.Map.Map.SelectedSectorsCount == 0 ? General.Map.Map.Sectors : General.Map.Map.GetSelectedSectors(true);
exporting = true;
cancelexport = false;
progress.Maximum = 100; //sectors.Count * (Brightmap ? 2 : 1);
progress.Value = 0;
progress.Visible = true;
lbPhase.Text = "";
lbPhase.Visible = true;
foreach (Control c in Controls)
{
if (!(c is ProgressBar || c is Label))
c.Enabled = false;
}
export.Enabled = true;
export.Text = "Cancel";
ImageExportSettings settings = new ImageExportSettings(Path.GetDirectoryName(FilePath), Path.GetFileNameWithoutExtension(FilePath), Path.GetExtension(FilePath), Floor, Fullbright, Brightmap, Tiles, ImageScale, GetPixelFormat(), GetImageFormat());
RunExport(settings);
}
/// <summary>
/// Enables all controls. This has to be called when the export is finished (either successfully or unsuccessfully)
/// </summary>
/// <param name="ier">Image export result</param>
private void StopExport(ImageExportResult ier)
{
if (this.InvokeRequired)
{
CallImageExportResultMethodDeletage d = StopExport;
this.Invoke(d, ier);
}
else
{
progress.Visible = false;
lbPhase.Visible = false;
foreach (Control c in Controls)
{
if (!(c is ProgressBar || c is Label))
c.Enabled = true;
}
export.Text = "Export";
if (ier == ImageExportResult.OK)
MessageBox.Show("Export successful.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information);
else if(ier == ImageExportResult.Canceled)
MessageBox.Show("Export canceled.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (ier == ImageExportResult.OutOfMemory)
MessageBox.Show("Exporting failed. There's likely not enough consecutive free memory to create the image. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
else if(ier == ImageExportResult.ImageTooBig)
MessageBox.Show("Exporting failed. The image is likely too big for the current settings. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
exporting = false;
}
private bool CheckCancelExport()
{
return cancelexport;
}
/// <summary>
/// Shows the current phase in textual form. Is called by the exporter
/// </summary>
/// <param name="text"></param>
private void ShowPhase(string text)
{
if (this.InvokeRequired)
{
CallStringMethodDeletage d = ShowPhase;
this.Invoke(d, text);
}
else
{
lbPhase.Text = text;
}
}
/// <summary>
/// Adds progress to the progress bar. Is called by the exporter
/// </summary>
private void AddProgress()
{
if (progress.InvokeRequired)
{
CallVoidMethodDeletage d = AddProgress;
progress.Invoke(d);
}
else
{
// Just winforms things to make the progress bar animation not lag behind
int value = progress.Value + 1;
progress.Value = value;
progress.Value = value - 1;
progress.Value = value;
}
}
/// <summary>
/// Runs the actual exporter
/// </summary>
/// <param name="settings">Export settings</param>
private void RunExport(ImageExportSettings settings)
{
ICollection<Sector> sectors = General.Map.Map.SelectedSectorsCount == 0 ? General.Map.Map.Sectors : General.Map.Map.GetSelectedSectors(true);
ImageExporter exporter = new ImageExporter(sectors, settings, AddProgress, ShowPhase, CheckCancelExport);
try
{
exporter.Export();
}
catch (ArgumentException) // Happens if there's not enough consecutive memory to create the file
{
StopExport(ImageExportResult.OutOfMemory);
return;
}
catch(ImageExportCanceledException)
{
StopExport(ImageExportResult.Canceled);
return;
}
catch(ImageExportImageTooBigException)
{
StopExport(ImageExportResult.ImageTooBig);
return;
}
StopExport(ImageExportResult.OK);
}
#endregion
#region ================== Events
private void browse_Click(object sender, EventArgs e)
{
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
tbExportPath.Text = saveFileDialog.FileName;
string extension = Path.GetExtension(saveFileDialog.FileName);
switch(extension)
{
case ".jpg":
cbImageFormat.SelectedIndex = 1;
break;
default:
cbImageFormat.SelectedIndex = 0;
break;
}
}
}
private void close_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
private void export_Click(object sender, EventArgs e)
{
if (exporting)
{
cancelexport = true;
export.Enabled = false;
}
else
{
General.Settings.WritePluginSetting("imageexportfullbright", cbFullbright.Checked);
General.Settings.WritePluginSetting("imageexportbrightmap", cbBrightmap.Checked);
General.Settings.WritePluginSetting("imageexporttiles", cbTiles.Checked);
General.Settings.WritePluginSetting("imageexportscale", cbScale.SelectedIndex);
// Exporting works like this:
// In here StartExport() is called
// StartExport() disables all controls and creates a thread that runs RunExport() in the background; then the StartExport method ends
// RunExport() creates an instance of ImageExporter and starts the actual export
// When ImageExporter finishes its job it runs StopExport()
// StopExport() enables all controls again
StartExport();
}
}
private void cbImageFormat_SelectedIndexChanged(object sender, EventArgs e)
{
string newextension = "";
switch (cbImageFormat.SelectedIndex)
{
case 1: // JPG
newextension = ".jpg";
break;
default: // PNG
newextension = ".png";
break;
}
tbExportPath.Text = Path.ChangeExtension(tbExportPath.Text, newextension);
}
private void ImageExportSettingsForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Do not allow closing the form while the export is running
if (exporting)
e.Cancel = true;
}
#endregion
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="saveFileDialog.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@ -129,6 +129,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
this.itempastepropsoptions = new System.Windows.Forms.ToolStripMenuItem();
this.alignsectorlinedefsitem = new System.Windows.Forms.ToolStripMenuItem();
this.alignlinedefsitem = new System.Windows.Forms.ToolStripMenuItem();
this.selectionToImageToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menustrip.SuspendLayout();
this.manualstrip.SuspendLayout();
this.fileMenuStrip.SuspendLayout();
@ -583,6 +584,15 @@ namespace CodeImp.DoomBuilder.BuilderModes
this.selectSimilarThingsItem.Text = "Select Similar...";
this.selectSimilarThingsItem.Click += new System.EventHandler(this.InvokeTaggedAction);
//
// selectionToImageToolStripMenuItem
//
this.selectionToImageToolStripMenuItem.Name = "selectionToImageToolStripMenuItem";
this.selectionToImageToolStripMenuItem.Size = new System.Drawing.Size(226, 22);
this.selectionToImageToolStripMenuItem.Tag = "exporttoimage";
this.selectionToImageToolStripMenuItem.Text = "Selection to image";
this.selectionToImageToolStripMenuItem.Click += new System.EventHandler(this.InvokeTaggedAction);
//
//
// vertsmenu
//
this.vertsmenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
@ -1026,9 +1036,10 @@ namespace CodeImp.DoomBuilder.BuilderModes
// exportStripMenuItem
//
this.exportStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripMenuItem5});
this.toolStripMenuItem5,
this.selectionToImageToolStripMenuItem});
this.exportStripMenuItem.Name = "exportStripMenuItem";
this.exportStripMenuItem.Size = new System.Drawing.Size(52, 20);
this.exportStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportStripMenuItem.Text = "Export";
//
// toolStripMenuItem5
@ -1229,5 +1240,6 @@ namespace CodeImp.DoomBuilder.BuilderModes
private System.Windows.Forms.ToolStripMenuItem vertexSlopeAssistT;
private System.Windows.Forms.ToolStripMenuItem alignsectorlinedefsitem;
private System.Windows.Forms.ToolStripMenuItem alignlinedefsitem;
private System.Windows.Forms.ToolStripMenuItem selectionToImageToolStripMenuItem;
}
}

View File

@ -1683,6 +1683,16 @@ exporttoobj
allowscroll = false;
}
exporttoimage
{
title = "Export to image";
category = "tools";
description = "Exports selected sectors (or the whole map if no sectors selected) to an image";
allowkeys = true;
allowmouse = false;
allowscroll = false;
}
flooralignmode
{
title = "Floor Align Mode";