mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-12 13:00:32 +00:00
5a5c576a33
* DDS will always save the format entirely literally, and the float formats do not support alpha processing currently. * TGA and PNG will either discard the alpha and write entirely opaque, or they'll include it in the file. Blending to colour or checkerboard is not supported.
645 lines
22 KiB
C#
645 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Data;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using renderdoc;
|
|
using renderdocui.Code;
|
|
|
|
namespace renderdocui.Windows.Dialogs
|
|
{
|
|
public partial class TextureSaveDialog : Form
|
|
{
|
|
struct AlphaMappingString
|
|
{
|
|
public AlphaMappingString(AlphaMapping v)
|
|
{
|
|
val = v;
|
|
}
|
|
|
|
public AlphaMapping val;
|
|
|
|
public override string ToString()
|
|
{
|
|
switch (val)
|
|
{
|
|
case AlphaMapping.Discard:
|
|
return "Discard";
|
|
case AlphaMapping.BlendToColour:
|
|
return "Blend to Colour";
|
|
case AlphaMapping.BlendToCheckerboard:
|
|
return "Blend To Checkerboard";
|
|
case AlphaMapping.Preserve:
|
|
return "Preserve";
|
|
}
|
|
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public TextureSaveDialog(Core core)
|
|
{
|
|
InitializeComponent();
|
|
|
|
filename.Font =
|
|
fileFormat.Font =
|
|
jpegCompression.Font =
|
|
mipSelect.Font =
|
|
sampleSelect.Font =
|
|
sliceSelect.Font =
|
|
blackPoint.Font =
|
|
whitePoint.Font =
|
|
core.Config.PreferredFont;
|
|
|
|
fileFormat.Items.Clear();
|
|
|
|
string filter = "";
|
|
|
|
foreach (var ft in (FileType[])Enum.GetValues(typeof(FileType)))
|
|
{
|
|
fileFormat.Items.Add(ft.ToString());
|
|
|
|
if (filter.Length > 0) filter += "|";
|
|
filter += String.Format("{0} Files (*.{1})|*.{1}", ft.ToString(), ft.ToString().ToLower(Application.CurrentCulture));
|
|
}
|
|
|
|
saveTexDialog.Filter = filter;
|
|
|
|
//if (tex.format == null)
|
|
{
|
|
tex.format = new ResourceFormat();
|
|
tex.format.compCount = 4;
|
|
tex.width = tex.height = 128;
|
|
tex.depth = 1;
|
|
tex.arraysize = 6;
|
|
tex.cubemap = true;
|
|
tex.msSamp = 2;
|
|
tex.mips = 5;
|
|
}
|
|
}
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
if (keyData == Keys.Escape)
|
|
{
|
|
DialogResult = DialogResult.Cancel;
|
|
this.Close();
|
|
return true;
|
|
}
|
|
if (keyData == Keys.Enter)
|
|
{
|
|
ok_Click(this, null);
|
|
return true;
|
|
}
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
public FetchTexture tex = new FetchTexture();
|
|
public TextureSave saveData = new TextureSave();
|
|
|
|
public string Filename
|
|
{
|
|
get
|
|
{
|
|
return filename.Text;
|
|
}
|
|
}
|
|
|
|
private void TextureSaveDialog_Shown(object sender, EventArgs e)
|
|
{
|
|
jpegCompression.Value = saveData.jpegQuality;
|
|
|
|
blackPoint.Text = Formatter.Format(saveData.comp.blackPoint);
|
|
whitePoint.Text = Formatter.Format(saveData.comp.whitePoint);
|
|
|
|
mipSelect.Items.Clear();
|
|
for (int i = 0; i < tex.mips; i++)
|
|
mipSelect.Items.Add(i + " - " + Math.Max(1, tex.width >> i) + "x" + Math.Max(1, tex.height >> i));
|
|
|
|
mipSelect.SelectedIndex = (saveData.mip >= 0 ? saveData.mip : 0);
|
|
|
|
sampleSelect.Items.Clear();
|
|
for (int i = 0; i < tex.msSamp; i++)
|
|
sampleSelect.Items.Add(String.Format("Sample {0}", i));
|
|
|
|
sampleSelect.SelectedIndex = Math.Min((int)tex.msSamp, (saveData.sample.sampleIndex == ~0U ? 0 : (int)saveData.sample.sampleIndex));
|
|
|
|
if (saveData.sample.sampleIndex == ~0U)
|
|
{
|
|
resolveSamples.Checked = true;
|
|
}
|
|
else
|
|
{
|
|
oneSample.Checked = true;
|
|
}
|
|
|
|
String[] cubeFaces = { "X+", "X-", "Y+", "Y-", "Z+", "Z-" };
|
|
|
|
UInt32 numSlices = Math.Max(tex.arraysize, tex.depth);
|
|
|
|
sliceSelect.Items.Clear();
|
|
|
|
for (UInt32 i = 0; i < numSlices; i++)
|
|
{
|
|
if (tex.cubemap)
|
|
{
|
|
String name = cubeFaces[i % 6];
|
|
if (numSlices > 6)
|
|
name = string.Format("[{0}] {1}", (i / 6), cubeFaces[i % 6]); // Front 1, Back 2, 3, 4 etc for cube arrays
|
|
sliceSelect.Items.Add(name);
|
|
}
|
|
else
|
|
{
|
|
sliceSelect.Items.Add("Slice " + i);
|
|
}
|
|
}
|
|
|
|
sliceSelect.SelectedIndex = (saveData.slice.sliceIndex >= 0 ? saveData.slice.sliceIndex : 0);
|
|
|
|
gridWidth.Maximum = tex.depth * tex.arraysize * tex.msSamp;
|
|
|
|
mipGroup.Visible = (tex.mips > 1);
|
|
|
|
sampleGroup.Visible = (tex.msSamp > 1);
|
|
|
|
sliceGroup.Visible = (tex.depth > 1 || tex.arraysize > 1 || tex.msSamp > 1);
|
|
|
|
if (saveData.destType != FileType.DDS)
|
|
{
|
|
cubeCruciform.Enabled = (tex.cubemap && tex.arraysize == 6);
|
|
|
|
if (!oneSlice.Checked && !cubeCruciform.Enabled)
|
|
mapSlicesToGrid.Checked = true;
|
|
}
|
|
|
|
fileFormat.SelectedIndex = 0;
|
|
fileFormat.SelectedIndex = 1;
|
|
fileFormat.SelectedIndex = (int)saveData.destType;
|
|
|
|
if(saveData.alpha == AlphaMapping.Discard)
|
|
alphaMap.SelectedIndex = 0;
|
|
else
|
|
alphaMap.SelectedIndex = alphaMap.Items.Count - 1;
|
|
}
|
|
|
|
private void fileFormat_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.destType = (FileType)fileFormat.SelectedIndex;
|
|
|
|
jpegCompression.Enabled = (saveData.destType == FileType.JPG);
|
|
|
|
alphaLDRGroup.Visible = (saveData.destType != FileType.HDR &&
|
|
saveData.destType != FileType.EXR &&
|
|
saveData.destType != FileType.DDS);
|
|
|
|
bool noAlphaFormat = (saveData.destType == FileType.BMP || saveData.destType == FileType.JPG);
|
|
|
|
// any filetype, PNG supporting or not, can choose to preserve or discard the alpha
|
|
alphaMap.Enabled = tex.format.compCount == 4;
|
|
|
|
if (alphaMap.Enabled)
|
|
{
|
|
if (noAlphaFormat && alphaMap.Items.Count != 3)
|
|
{
|
|
int idx = (int)alphaMap.SelectedIndex;
|
|
|
|
alphaMap.Items.Clear();
|
|
alphaMap.Items.AddRange(new object[] {
|
|
new AlphaMappingString(AlphaMapping.Discard),
|
|
new AlphaMappingString(AlphaMapping.BlendToColour),
|
|
new AlphaMappingString(AlphaMapping.BlendToCheckerboard)
|
|
});
|
|
|
|
// if we were discard before, still discard, otherwise blend to checkerboard
|
|
if (idx <= 0)
|
|
alphaMap.SelectedIndex = 0;
|
|
else
|
|
alphaMap.SelectedIndex = alphaMap.Items.Count - 1;
|
|
}
|
|
else if (alphaMap.Items.Count != 2)
|
|
{
|
|
int idx = (int)alphaMap.SelectedIndex;
|
|
|
|
alphaMap.Items.Clear();
|
|
alphaMap.Items.AddRange(new object[] {
|
|
new AlphaMappingString(AlphaMapping.Discard),
|
|
new AlphaMappingString(AlphaMapping.Preserve)
|
|
});
|
|
|
|
// allow the previous selection to clamp, to either discard or preserve
|
|
alphaMap.SelectedIndex = Helpers.Clamp(idx, 0, alphaMap.Items.Count-1);
|
|
}
|
|
}
|
|
|
|
if (alphaMap.Items.Count == 0)
|
|
{
|
|
alphaMap.Items.Clear();
|
|
alphaMap.Items.AddRange(new object[] {
|
|
new AlphaMappingString(AlphaMapping.Discard),
|
|
new AlphaMappingString(AlphaMapping.Preserve)
|
|
});
|
|
}
|
|
|
|
alphaCol.Enabled = (saveData.alpha == AlphaMapping.BlendToColour && tex.format.compCount == 4 && noAlphaFormat);
|
|
|
|
if (saveData.destType == FileType.DDS)
|
|
{
|
|
exportAllMips.Enabled = exportAllMips.Checked = true;
|
|
exportAllSlices.Enabled = exportAllSlices.Checked = true;
|
|
|
|
cubeCruciform.Enabled = cubeCruciform.Checked = false;
|
|
gridWidth.Enabled = mapSlicesToGrid.Enabled = mapSlicesToGrid.Checked = false;
|
|
}
|
|
else
|
|
{
|
|
exportAllMips.Enabled = false;
|
|
oneMip.Checked = oneSlice.Checked = true;
|
|
}
|
|
SetFilenameFromFiletype();
|
|
}
|
|
|
|
private void jpegCompression_ValueChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.jpegQuality = (int)jpegCompression.Value;
|
|
}
|
|
|
|
private void gridWidth_ValueChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.slice.sliceGridWidth = (int)gridWidth.Value;
|
|
}
|
|
|
|
private void alphaMap_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.alpha = ((AlphaMappingString)alphaMap.SelectedItem).val;
|
|
|
|
alphaCol.Enabled = (saveData.alpha == AlphaMapping.BlendToColour);
|
|
}
|
|
|
|
private void mipSelect_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.mip = (int)mipSelect.SelectedIndex;
|
|
}
|
|
|
|
private void sampleSelect_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.sample.sampleIndex = (uint)sampleSelect.SelectedIndex;
|
|
}
|
|
|
|
private void sliceSelect_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
saveData.slice.sliceIndex = (int)sliceSelect.SelectedIndex;
|
|
}
|
|
|
|
private void alphaCol_Click(object sender, EventArgs e)
|
|
{
|
|
var res = colorDialog.ShowDialog();
|
|
|
|
if (res == DialogResult.OK || res == DialogResult.Yes)
|
|
{
|
|
saveData.alphaCol = new FloatVector(
|
|
((float)colorDialog.Color.R) / 255.0f,
|
|
((float)colorDialog.Color.G) / 255.0f,
|
|
((float)colorDialog.Color.B) / 255.0f);
|
|
}
|
|
}
|
|
|
|
private void SetFiletypeFromFilename()
|
|
{
|
|
try
|
|
{
|
|
string ext = Path.GetExtension(filename.Text).ToUpperInvariant().Substring(1); // trim . from extension
|
|
|
|
foreach (var ft in (FileType[])Enum.GetValues(typeof(FileType)))
|
|
{
|
|
if (ft.ToString().ToUpperInvariant() == ext)
|
|
{
|
|
fileFormat.SelectedIndex = (int)ft;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// invalid path or similar
|
|
}
|
|
}
|
|
|
|
private void SetFilenameFromFiletype()
|
|
{
|
|
try
|
|
{
|
|
string filenameExt = Path.GetExtension(filename.Text).ToLowerInvariant().Substring(1); // trim . from extension
|
|
|
|
FileType[] types = (FileType[])Enum.GetValues(typeof(FileType));
|
|
|
|
string selectedExt = types[fileFormat.SelectedIndex].ToString().ToLowerInvariant();
|
|
|
|
if (selectedExt != filenameExt)
|
|
{
|
|
filename.Text = filename.Text.Substring(0, filename.Text.Length - filenameExt.Length);
|
|
filename.Text += selectedExt;
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// invalid path or similar
|
|
}
|
|
}
|
|
|
|
private void browse_Click(object sender, EventArgs e)
|
|
{
|
|
saveTexDialog.FilterIndex = fileFormat.SelectedIndex + 1;
|
|
var res = saveTexDialog.ShowDialog();
|
|
if (res == DialogResult.OK || res == DialogResult.Yes)
|
|
{
|
|
filename.Text = saveTexDialog.FileName;
|
|
SetFiletypeFromFilename();
|
|
}
|
|
}
|
|
|
|
private void ok_Click(object sender, EventArgs e)
|
|
{
|
|
saveData.alpha = ((AlphaMappingString)alphaMap.SelectedItem).val;
|
|
|
|
if (saveData.alpha == AlphaMapping.BlendToCheckerboard)
|
|
{
|
|
saveData.alphaCol = new FloatVector(0.666f, 0.666f, 0.666f);
|
|
}
|
|
|
|
if (exportAllMips.Checked)
|
|
saveData.mip = -1;
|
|
else
|
|
saveData.mip = (int)mipSelect.SelectedIndex;
|
|
|
|
if (resolveSamples.Checked)
|
|
{
|
|
saveData.sample.sampleIndex = ~0U;
|
|
saveData.sample.mapToArray = false;
|
|
}
|
|
else if (mapSampleArray.Checked)
|
|
{
|
|
saveData.sample.sampleIndex = 0;
|
|
saveData.sample.mapToArray = true;
|
|
}
|
|
else
|
|
{
|
|
saveData.sample.sampleIndex = (uint)sampleSelect.SelectedIndex;
|
|
saveData.sample.mapToArray = false;
|
|
}
|
|
|
|
if (!exportAllSlices.Checked)
|
|
{
|
|
saveData.slice.cubeCruciform = saveData.slice.slicesAsGrid = false;
|
|
saveData.slice.sliceGridWidth = 1;
|
|
saveData.slice.sliceIndex = (int)sliceSelect.SelectedIndex;
|
|
}
|
|
else
|
|
{
|
|
saveData.slice.sliceIndex = -1;
|
|
if (cubeCruciform.Checked)
|
|
{
|
|
saveData.slice.cubeCruciform = true;
|
|
saveData.slice.slicesAsGrid = false;
|
|
saveData.slice.sliceGridWidth = 1;
|
|
}
|
|
else
|
|
{
|
|
saveData.slice.cubeCruciform = false;
|
|
saveData.slice.slicesAsGrid = true;
|
|
saveData.slice.sliceGridWidth = (int)gridWidth.Value;
|
|
}
|
|
}
|
|
|
|
saveData.destType = (FileType)fileFormat.SelectedIndex;
|
|
saveData.jpegQuality = (int)jpegCompression.Value;
|
|
|
|
float.TryParse(blackPoint.Text, out saveData.comp.blackPoint);
|
|
float.TryParse(whitePoint.Text, out saveData.comp.whitePoint);
|
|
|
|
try
|
|
{
|
|
// use same path for non-existing path as invalid path
|
|
if (!Directory.Exists(Path.GetDirectoryName(Filename)))
|
|
throw new ArgumentException();
|
|
|
|
if (File.Exists(Filename))
|
|
{
|
|
var res = MessageBox.Show(String.Format("{0} already exists.\nDo you want to replace it?", Path.GetFileName(Filename)), "Confirm Save Texture",
|
|
MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2);
|
|
|
|
if (res != DialogResult.Yes)
|
|
return;
|
|
}
|
|
}
|
|
catch(ArgumentException)
|
|
{
|
|
// invalid path or similar
|
|
|
|
MessageBox.Show(String.Format("{0}\nPath does not exist.\nCheck the path and try again.", Filename), "Save Texture",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);
|
|
|
|
return;
|
|
}
|
|
|
|
// path is valid and either doesn't exist or user confirmed replacement
|
|
DialogResult = DialogResult.OK;
|
|
Close();
|
|
}
|
|
|
|
private void blackPoint_TextChanged(object sender, EventArgs e)
|
|
{
|
|
float.TryParse(blackPoint.Text, out saveData.comp.blackPoint);
|
|
}
|
|
|
|
private void whitePoint_TextChanged(object sender, EventArgs e)
|
|
{
|
|
float.TryParse(whitePoint.Text, out saveData.comp.whitePoint);
|
|
}
|
|
|
|
private bool recurse = false;
|
|
|
|
// a horrible mess of functions to try and maintain valid combinations of options for different
|
|
// filetypes etc. There might be a better way of doing this...
|
|
|
|
private void exportAllMips_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
oneMip.Checked = !exportAllMips.Checked;
|
|
mipSelect.Enabled = oneMip.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void oneMip_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
exportAllMips.Checked = !oneMip.Checked;
|
|
mipSelect.Enabled = oneMip.Checked;
|
|
|
|
if (saveData.destType != FileType.DDS)
|
|
{
|
|
oneMip.Checked = true;
|
|
exportAllMips.Checked = false;
|
|
mipSelect.Enabled = true;
|
|
}
|
|
|
|
recurse = false;
|
|
}
|
|
|
|
private void mapSampleArray_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
if (mapSampleArray.Checked)
|
|
{
|
|
resolveSamples.Checked = oneSample.Checked = false;
|
|
}
|
|
else
|
|
{
|
|
resolveSamples.Checked = false;
|
|
oneSample.Checked = true;
|
|
}
|
|
sampleSelect.Enabled = oneSample.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void resolveSamples_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
if (resolveSamples.Checked)
|
|
{
|
|
mapSampleArray.Checked = oneSample.Checked = false;
|
|
}
|
|
else
|
|
{
|
|
mapSampleArray.Checked = false;
|
|
oneSample.Checked = true;
|
|
}
|
|
sampleSelect.Enabled = oneSample.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void oneSample_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
if (oneSample.Checked)
|
|
{
|
|
mapSampleArray.Checked = resolveSamples.Checked = false;
|
|
}
|
|
else
|
|
{
|
|
mapSampleArray.Checked = false;
|
|
resolveSamples.Checked = true;
|
|
}
|
|
sampleSelect.Enabled = oneSample.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void mapSlicesToGrid_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
if (mapSlicesToGrid.Checked)
|
|
{
|
|
cubeCruciform.Checked = false;
|
|
}
|
|
else if (saveData.destType != FileType.DDS)
|
|
{
|
|
oneSlice.Checked = true;
|
|
exportAllSlices.Checked = false;
|
|
cubeCruciform.Enabled = mapSlicesToGrid.Enabled = gridWidth.Enabled = false;
|
|
sliceSelect.Enabled = true;
|
|
}
|
|
recurse = false;
|
|
if (saveData.destType == FileType.DDS)
|
|
gridWidth.Enabled = false;
|
|
else
|
|
gridWidth.Enabled = mapSlicesToGrid.Checked;
|
|
}
|
|
|
|
private void cubeCruciform_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
if (cubeCruciform.Checked)
|
|
{
|
|
mapSlicesToGrid.Checked = false;
|
|
}
|
|
else if (saveData.destType != FileType.DDS)
|
|
{
|
|
oneSlice.Checked = true;
|
|
exportAllSlices.Checked = false;
|
|
cubeCruciform.Enabled = mapSlicesToGrid.Enabled = gridWidth.Enabled = false;
|
|
sliceSelect.Enabled = true;
|
|
}
|
|
recurse = false;
|
|
}
|
|
|
|
private void oneSlice_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
exportAllSlices.Checked = !oneSlice.Checked;
|
|
if (saveData.destType == FileType.DDS)
|
|
{
|
|
mapSlicesToGrid.Enabled = gridWidth.Enabled = cubeCruciform.Enabled = false;
|
|
}
|
|
else
|
|
{
|
|
mapSlicesToGrid.Enabled = gridWidth.Enabled = !oneSlice.Checked;
|
|
|
|
if (!oneSlice.Checked && !cubeCruciform.Checked)
|
|
mapSlicesToGrid.Checked = true;
|
|
|
|
if (tex.cubemap && tex.arraysize == 6) cubeCruciform.Enabled = !oneSlice.Checked;
|
|
else cubeCruciform.Enabled = false;
|
|
}
|
|
sliceSelect.Enabled = oneSlice.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void exportAllSlices_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (recurse) return;
|
|
recurse = true;
|
|
oneSlice.Checked = !exportAllSlices.Checked;
|
|
if (saveData.destType == FileType.DDS)
|
|
{
|
|
mapSlicesToGrid.Enabled = gridWidth.Enabled = cubeCruciform.Enabled = false;
|
|
}
|
|
else
|
|
{
|
|
mapSlicesToGrid.Enabled = gridWidth.Enabled = !oneSlice.Checked;
|
|
|
|
if (!oneSlice.Checked && !cubeCruciform.Checked)
|
|
mapSlicesToGrid.Checked = true;
|
|
|
|
if (tex.cubemap && tex.arraysize == 6) cubeCruciform.Enabled = !oneSlice.Checked;
|
|
else cubeCruciform.Enabled = false;
|
|
}
|
|
sliceSelect.Enabled = oneSlice.Checked;
|
|
recurse = false;
|
|
}
|
|
|
|
private void filename_KeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
typingTimer.Enabled = true;
|
|
typingTimer.Stop();
|
|
typingTimer.Start();
|
|
}
|
|
|
|
private void typingTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
SetFiletypeFromFilename();
|
|
typingTimer.Enabled = false;
|
|
}
|
|
}
|
|
}
|