#include "FractoriumPch.h"
#include "Fractorium.h"

/// <summary>
/// Initialize the parameters UI.
/// </summary>
void Fractorium::InitParamsUI()
{
	int row = 0;
	int spinHeight = 20;
	double dmax = numeric_limits<double>::max();
	vector<string> comboVals;
	QTableWidget* table = ui.ColorTable;
	//Because QTableWidget does not allow for a single title bar/header
	//at the top of a multi-column table, the workaround hack is to just
	//make another single column table with no rows, and use the single
	//column header as the title bar. Then positioning it right above the table
	//that holds the data. Disallow selecting and resizing of the title bar.
	SetFixedTableHeader(ui.ColorTableHeader->horizontalHeader());
	SetFixedTableHeader(ui.GeometryTableHeader->horizontalHeader());
	SetFixedTableHeader(ui.FilterTableHeader->horizontalHeader());
	SetFixedTableHeader(ui.IterationTableHeader->horizontalHeader());
	SetFixedTableHeader(ui.AnimationTableHeader->horizontalHeader());
	//Color.
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_BrightnessSpin,	   spinHeight, 0.01,  dmax,       1, SIGNAL(valueChanged(double)), SLOT(OnBrightnessChanged(double)),	  true,  4.0,    4.0,  4.0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_GammaSpin,		   spinHeight,    1,  dmax,     0.5, SIGNAL(valueChanged(double)), SLOT(OnGammaChanged(double)),          true,  4.0,    4.0,  4.0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_GammaThresholdSpin, spinHeight,    0,  dmax,    0.01, SIGNAL(valueChanged(double)), SLOT(OnGammaThresholdChanged(double)), true,  0.1,    0.1,  0.0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_VibrancySpin,	   spinHeight,    0,  dmax,    0.01, SIGNAL(valueChanged(double)), SLOT(OnVibrancyChanged(double)),       true,  1.0,    1.0,  0.0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_HighlightSpin,	   spinHeight,  -1.0,   10,     0.1, SIGNAL(valueChanged(double)), SLOT(OnHighlightPowerChanged(double)), true,  1.0,    1.0, -1.0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_K2Spin,	           spinHeight,     0, 99.0,  0.0001, SIGNAL(valueChanged(double)), SLOT(OnK2Changed(double)),             true,    0, 0.0001,    0);
	m_HighlightSpin->DoubleClickLowVal(-1.0);
	int dec = 6;
	m_BrightnessSpin->setDecimals(dec);
	m_GammaSpin->setDecimals(dec);
	m_GammaThresholdSpin->setDecimals(dec);
	m_VibrancySpin->setDecimals(dec);
	m_HighlightSpin->setDecimals(dec);
	m_K2Spin->setDecimals(dec);
	m_BgRow = row;
	m_BackgroundColorButton = new QPushButton("...", table);
	m_BackgroundColorButton->setMinimumWidth(21);
	m_BackgroundColorButton->setMaximumWidth(21);
	table->setCellWidget(row, 1, m_BackgroundColorButton);
	table->item(row, 1)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
	connect(m_BackgroundColorButton, SIGNAL(clicked(bool)), this, SLOT(OnBackgroundColorButtonClicked(bool)), Qt::QueuedConnection);
	row++;
	comboVals.push_back("Step");
	comboVals.push_back("Linear");
	SetupCombo(table, this, row, 1, m_PaletteModeCombo, comboVals, SIGNAL(currentIndexChanged(int)), SLOT(OnPaletteModeComboCurrentIndexChanged(int)));
	m_PaletteModeCombo->SetCurrentIndexStealth(int(ePaletteMode::PALETTE_LINEAR));
	table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
	//Geometry.
	row = 0;
	table = ui.GeometryTable;
	SetupSpinner<SpinBox, int>		   (table, this, row, 1, m_WidthSpin,		spinHeight,	   10,	  2048,    50, SIGNAL(valueChanged(int)),	 SLOT(OnWidthChanged(int)),		     true,  width(),  width(),  width());
	SetupSpinner<SpinBox, int>		   (table, this, row, 1, m_HeightSpin,		spinHeight,	   10,	  2048,	   50, SIGNAL(valueChanged(int)),	 SLOT(OnHeightChanged(int)),		 true, height(), height(), height());
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_CenterXSpin,     spinHeight, -dmax,    dmax,  0.05, SIGNAL(valueChanged(double)), SLOT(OnCenterXChanged(double)),     true,	  0,   0,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_CenterYSpin,     spinHeight, -dmax,    dmax,  0.05, SIGNAL(valueChanged(double)), SLOT(OnCenterYChanged(double)),     true,	  0,   0,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_ScaleSpin,       spinHeight,    10,    dmax,    20, SIGNAL(valueChanged(double)), SLOT(OnScaleChanged(double)),	     true,  240, 240, 240);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_ZoomSpin,        spinHeight,     0,      25,   0.2, SIGNAL(valueChanged(double)), SLOT(OnZoomChanged(double)),	     true,	  0,   0,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_RotateSpin,      spinHeight,  -180,     180,    10, SIGNAL(valueChanged(double)), SLOT(OnRotateChanged(double)),      true,	  0,   0,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_ZPosSpin,        spinHeight, -1000,    1000,   0.1, SIGNAL(valueChanged(double)), SLOT(OnZPosChanged(double)),        true,	  0,   1,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_PerspectiveSpin, spinHeight,  -500,     500,  0.01, SIGNAL(valueChanged(double)), SLOT(OnPerspectiveChanged(double)), true,	  0,   1,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_PitchSpin,       spinHeight, -dmax,    dmax,     1, SIGNAL(valueChanged(double)), SLOT(OnPitchChanged(double)),       true,	  0,  45,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_YawSpin,         spinHeight, -dmax,    dmax,     1, SIGNAL(valueChanged(double)), SLOT(OnYawChanged(double)),         true,	  0,  45,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_DepthBlurSpin,   spinHeight, -dmax,    dmax,  0.01, SIGNAL(valueChanged(double)), SLOT(OnDepthBlurChanged(double)),   true,	  0,   1,	0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_BlurCurveSpin,   spinHeight,     0,    dmax,   0.1, SIGNAL(valueChanged(double)), SLOT(OnBlurCurveChanged(double)),   true,	  0,   1,	0);
	m_WidthSpin->m_DoubleClickNonZeroEvent = [&](SpinBox * sb, int val)
	{
		m_Controller->ResizeAndScale(val, m_HeightSpin->DoubleClickNonZero(), eScaleType::SCALE_WIDTH);
		m_HeightSpin->SetValueStealth(m_HeightSpin->DoubleClickNonZero());
	};
	m_HeightSpin->m_DoubleClickNonZeroEvent = [&](SpinBox * sb, int val)
	{
		m_Controller->ResizeAndScale(m_WidthSpin->DoubleClickNonZero(), val, eScaleType::SCALE_HEIGHT);
		m_WidthSpin->SetValueStealth(m_WidthSpin->DoubleClickNonZero());
	};
	dec = 4;
	m_CenterXSpin->setDecimals(dec);
	m_CenterYSpin->setDecimals(dec);
	m_ZPosSpin->setDecimals(dec);
	m_PerspectiveSpin->setDecimals(dec);
	m_DepthBlurSpin->setDecimals(dec);
	table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
	//Filter.
	row = 0;
	table = ui.FilterTable;
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_SpatialFilterWidthSpin, spinHeight,   0, 2, 0.1, SIGNAL(valueChanged(double)), SLOT(OnSpatialFilterWidthChanged(double)), true, 1.0, 1.0, 0);
	comboVals = SpatialFilterCreator<float>::FilterTypes();
	SetupCombo(table, this, row, 1, m_SpatialFilterTypeCombo, comboVals, SIGNAL(currentIndexChanged(const QString&)), SLOT(OnSpatialFilterTypeComboCurrentIndexChanged(const QString&)));
	m_SpatialFilterTypeCombo->SetCurrentIndexStealth(0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_DEFilterMinRadiusSpin, spinHeight,    0, 25,   1, SIGNAL(valueChanged(double)), SLOT(OnDEFilterMinRadiusWidthChanged(double)), true,   0,   0,   0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_DEFilterMaxRadiusSpin, spinHeight,    0, 25,   1, SIGNAL(valueChanged(double)), SLOT(OnDEFilterMaxRadiusWidthChanged(double)), true, 0.0, 9.0,   0);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_DECurveSpin,			  spinHeight, 0.15,  5, 0.1, SIGNAL(valueChanged(double)), SLOT(OnDEFilterCurveWidthChanged(double)),     true, 0.4, 0.4, 0.4);
	table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
	//Iteration.
	row = 0;
	table = ui.IterationTable;
	auto quality = m_Settings->OpenCL() ? m_Settings->OpenClQuality() : m_Settings->CpuQuality();
	SetupSpinner<SpinBox, int>(			table, this, row, 1, m_SbsSpin,				spinHeight, 1000, 100000, 100, SIGNAL(valueChanged(int)),	 SLOT(OnSbsChanged(int)),		   true, DEFAULT_SBS, DEFAULT_SBS, DEFAULT_SBS);
	SetupSpinner<SpinBox, int>(			table, this, row, 1, m_FuseSpin,			spinHeight, 1,      1000,   5, SIGNAL(valueChanged(int)),	 SLOT(OnFuseChanged(int)),	       true,	     15, 100, 15);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_RandRangeSpin,		spinHeight, 0.01,   1000, 0.1, SIGNAL(valueChanged(double)), SLOT(OnRandRangeChanged(double)), true,	      1,  10,  1);
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_QualitySpin,			spinHeight, 1,      dmax,  50, SIGNAL(valueChanged(double)), SLOT(OnQualityChanged(double)),   true,     quality, 10, 10);
	SetupSpinner<SpinBox, int>(         table, this, row, 1, m_SupersampleSpin,		spinHeight, 1,         4,   1, SIGNAL(valueChanged(int)),	 SLOT(OnSupersampleChanged(int)),  true,           1,  2,  1);
	m_RandRangeSpin->DoubleClickLowVal(1);
	m_RandRangeSpin->setDecimals(4);
	m_FuseSpin->DoubleClickLowVal(15);
	m_SupersampleSpin->DoubleClickLowVal(1);
	table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
	//Animation.
	row = 0;
	table = ui.AnimationTable;
	comboVals.clear();
	comboVals.push_back("Linear");
	comboVals.push_back("Smooth");
	SetupCombo(table, this, row, 1, m_InterpTypeCombo, comboVals, SIGNAL(currentIndexChanged(int)), SLOT(OnInterpTypeComboCurrentIndexChanged(int)));
	m_InterpTypeCombo->SetCurrentIndexStealth(int(eInterp::EMBER_INTERP_SMOOTH));
	comboVals.clear();
	comboVals.push_back("Linear");
	comboVals.push_back("Log");
	SetupCombo(                         table, this, row, 1, m_AffineInterpTypeCombo, comboVals, SIGNAL(currentIndexChanged(int)), SLOT(OnAffineInterpTypeComboCurrentIndexChanged(int)));
	m_AffineInterpTypeCombo->SetCurrentIndexStealth(int(eAffineInterp::AFFINE_INTERP_LOG));
	SetupSpinner<DoubleSpinBox, double>(table, this, row, 1, m_TemporalFilterWidthSpin, spinHeight, 1, 10, 1, SIGNAL(valueChanged(double)), SLOT(OnTemporalFilterWidthChanged(double)), true, 1);
	comboVals = TemporalFilterCreator<float>::FilterTypes();
	SetupCombo(                         table, this, row, 1, m_TemporalFilterTypeCombo, comboVals, SIGNAL(currentIndexChanged(const QString&)), SLOT(OnTemporalFilterTypeComboCurrentIndexChanged(const QString&)));
	m_TemporalFilterTypeCombo->SetCurrentIndexStealth(int(eTemporalFilterType::GAUSSIAN_TEMPORAL_FILTER));
	table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
}

/// <summary>
/// Return whether the apply all checkbox is checked.
/// </summary>
/// <returns>True if checked, else false.</returns>
bool Fractorium::ApplyAll()
{
	return ui.ApplyAllParamsCheckBox->isChecked();
}

/// <summary>
/// Color.
/// </summary>

/// <summary>
/// Set the brightness to be used for calculating K1 and K2 for filtering and final accum.
/// Called when brightness spinner is changed.
/// Resets the rendering process to the filtering stage.
/// </summary>
/// <param name="d">The brightness</param>
template <typename T>
void FractoriumEmberController<T>::BrightnessChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Brightness = d;
	}, true, eProcessAction::FILTER_AND_ACCUM, m_Fractorium->ApplyAll());
}

void Fractorium::OnBrightnessChanged(double d) { m_Controller->BrightnessChanged(d); }

/// <summary>
/// Set the gamma to be used for final accum.
/// Called when gamma spinner is changed.
/// Resets the rendering process if temporal samples is greater than 1,
/// else if early clip is true, filter and accum, else final accum only.
/// </summary>
/// <param name="d">The gamma value</param>
template <typename T> void FractoriumEmberController<T>::GammaChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Gamma = d;
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}
void Fractorium::OnGammaChanged(double d) { m_Controller->GammaChanged(d); }

/// <summary>
/// Set the gamma threshold to be used for final accum.
/// Called when gamma threshold spinner is changed.
/// Resets the rendering process to the final accumulation stage.
/// </summary>
/// <param name="d">The gamma threshold</param>
template <typename T> void FractoriumEmberController<T>::GammaThresholdChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_GammaThresh = d;
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}
void Fractorium::OnGammaThresholdChanged(double d) { m_Controller->GammaThresholdChanged(d); }

/// <summary>
/// Set the vibrancy to be used for final accum.
/// Called when vibrancy spinner is changed.
/// Resets the rendering process to the final accumulation stage if temporal samples is 1, else full reset.
/// </summary>
/// <param name="d">The vibrancy</param>
template <typename T> void FractoriumEmberController<T>::VibrancyChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Vibrancy = d;
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}
void Fractorium::OnVibrancyChanged(double d) { m_Controller->VibrancyChanged(d); }

/// <summary>
/// Set the highlight power to be used for final accum.
/// Called when highlight power spinner is changed.
/// Resets the rendering process to the final accumulation stage.
/// </summary>
/// <param name="d">The highlight power</param>
template <typename T> void FractoriumEmberController<T>::HighlightPowerChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_HighlightPower = d;
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}
void Fractorium::OnHighlightPowerChanged(double d) { m_Controller->HighlightPowerChanged(d); }

/// <summary>
/// Set the k2 brightness value to be used for final accum.
/// Called when k2 is changed.
/// Resets the rendering process to the final accumulation stage.
/// </summary>
/// <param name="d">The k2 value</param>
template <typename T> void FractoriumEmberController<T>::K2Changed(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_K2 = d;
	}, true, eProcessAction::FILTER_AND_ACCUM, m_Fractorium->ApplyAll());
}

void Fractorium::OnK2Changed(double d) { m_Controller->K2Changed(d); }

/// <summary>
/// Show the color selection dialog.
/// Called when background color button is clicked.
/// </summary>
/// <param name="checked">Ignored</param>
void Fractorium::OnBackgroundColorButtonClicked(bool checked)
{
	m_ColorDialog->exec();
}

/// <summary>
/// Set a new ember background color when the user accepts the color dialog.
/// Also change the background and foreground colors of the color cell in the
/// color params table.
/// Resets the rendering process.
/// </summary>
/// <param name="color">The color to set, RGB in the 0-255 range</param>
template <typename T>
void FractoriumEmberController<T>::BackgroundChanged(const QColor& color)
{
	auto itemRow = m_Fractorium->m_BgRow;
	auto colorTable = m_Fractorium->ui.ColorTable;
	colorTable->item(itemRow, 1)->setBackgroundColor(color);
	auto r = ToString(color.red());
	auto g = ToString(color.green());
	auto b = ToString(color.blue());
	colorTable->item(itemRow, 1)->setTextColor(VisibleColor(color));
	colorTable->item(itemRow, 1)->setText("rgb(" + r + ", " + g + ", " + b + ")");
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		//Color is 0-255, normalize to 0-1.
		ember.m_Background.r = color.red()   / 255.0;
		ember.m_Background.g = color.green() / 255.0;
		ember.m_Background.b = color.blue()  / 255.0;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}

void Fractorium::OnColorSelected(const QColor& color) { m_Controller->BackgroundChanged(color); }

/// <summary>
/// Set the palette index interpolation mode.
/// Called when palette mode combo box index is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="index">The index of the palette mode combo box</param>
template <typename T> void FractoriumEmberController<T>::PaletteModeChanged(uint i)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_PaletteMode = i == 0 ? ePaletteMode::PALETTE_STEP : ePaletteMode::PALETTE_LINEAR;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnPaletteModeComboCurrentIndexChanged(int index) { m_Controller->PaletteModeChanged(index); }

/// <summary>
/// Geometry.
/// </summary>

/// <summary>
/// Set the width of the ember in pixels to the passed in value.
/// Called when the width spinner is changed in a manner other than double clicking.
/// Resets the rendering process.
/// </summary>
/// <param name="i">The width value in pixels to set</param>
template <typename T> void FractoriumEmberController<T>::WidthChanged(uint i)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_FinalRasW = i;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}

void Fractorium::OnWidthChanged(int i) { m_Controller->WidthChanged(i); }

/// <summary>
/// Set the height of the ember in pixels to the passed in value.
/// Called when the height spinner is changed in a manner other than double clicking.
/// Resets the rendering process.
/// </summary>
/// <param name="i">The height value in pixels to set</param>
template <typename T> void FractoriumEmberController<T>::HeightChanged(uint i)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_FinalRasH = i;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}

void Fractorium::OnHeightChanged(int i) { m_Controller->HeightChanged(i); }

/// <summary>
/// Set the width and height of the ember in pixels to the passed in values.
/// Called when either the width or height spinners are double clicked.
/// Because this will change the scale value, the scale spinner gets a stealth update.
/// For this reason, the affine scales are reset, even though they are not when doing a manual
/// height or width adjustment.
/// Resets the rendering process.
/// </summary>
/// <param name="width">The width value in pixels to set</param>
/// <param name="height">The height value in pixels to set</param>
/// <param name="scaleType">The height value in pixels to set</param>
template <typename T>
void FractoriumEmberController<T>::ResizeAndScale(int width, int height, eScaleType scaleType)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		m_Ember.SetSizeAndAdjustScale(width, height, false, scaleType);
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
	m_Fractorium->m_ScaleSpin->SetValueStealth(m_Ember.m_PixelsPerUnit);
	m_Fractorium->OnActionResetScale(true);
}

/// <summary>
/// Set the x offset applied to the center of the image.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The x offset value</param>
template <typename T> void FractoriumEmberController<T>::CenterXChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CenterX = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnCenterXChanged(double d) { m_Controller->CenterXChanged(d); }

/// <summary>
/// Set the y offset applied to the center of the image.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The y offset value</param>
template <typename T> void FractoriumEmberController<T>::CenterYChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CenterY = ember.m_RotCenterY = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnCenterYChanged(double d) { m_Controller->CenterYChanged(d); }

/// <summary>
/// Set the scale (pixels per unit) value of the image.
/// Note this will not increase the number of iters ran, but will degrade quality.
/// To preserve quality, but exponentially increase iters, use zoom.
/// Called when scale spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The scale value</param>
template <typename T> void FractoriumEmberController<T>::ScaleChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_PixelsPerUnit = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnScaleChanged(double d) { m_Controller->ScaleChanged(d); }

/// <summary>
/// Set the zoom value of the image.
/// Note this will increase the number of iters ran exponentially.
/// To zoom in without increasing iters, but sacrifice quality, use scale.
/// Called when zoom spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The zoom value</param>
template <typename T> void FractoriumEmberController<T>::ZoomChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Zoom = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnZoomChanged(double d) { m_Controller->ZoomChanged(d); }

/// <summary>
/// Set the angular rotation of the image.
/// Called when rotate spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The rotation in angles</param>
template <typename T> void FractoriumEmberController<T>::RotateChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Rotate = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnRotateChanged(double d) { m_Controller->RotateChanged(d); }

template <typename T> void FractoriumEmberController<T>::ZPosChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CamZPos = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnZPosChanged(double d) {  m_Controller->ZPosChanged(d); }

template <typename T> void FractoriumEmberController<T>::PerspectiveChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CamPerspective = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnPerspectiveChanged(double d) { m_Controller->PerspectiveChanged(d); }

template <typename T> void FractoriumEmberController<T>::PitchChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CamPitch = d * DEG_2_RAD;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnPitchChanged(double d) { m_Controller->PitchChanged(d); }

template <typename T> void FractoriumEmberController<T>::YawChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CamYaw = d * DEG_2_RAD;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnYawChanged(double d) { m_Controller->YawChanged(d); }

template <typename T> void FractoriumEmberController<T>::DepthBlurChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CamDepthBlur = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnDepthBlurChanged(double d) { m_Controller->DepthBlurChanged(d); }

template <typename T> void FractoriumEmberController<T>::BlurCurveChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_BlurCurve = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnBlurCurveChanged(double d) { m_Controller->BlurCurveChanged(d); }

/// <summary>
/// Filter.
/// </summary>

/// <summary>
/// Set the spatial filter width.
/// Called when the spatial filter width spinner is changed.
/// Resets the rendering process to density filtering if early clip is used, else to final accumulation.
/// </summary>
/// <param name="d">The spatial filter width</param>
template <typename T> void FractoriumEmberController<T>::SpatialFilterWidthChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_SpatialFilterRadius = d;
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}

void Fractorium::OnSpatialFilterWidthChanged(double d) { m_Controller->SpatialFilterWidthChanged(d); }

/// <summary>
/// Set the spatial filter type.
/// Called when the spatial filter type combo box index is changed.
/// Resets the rendering process to density filtering if early clip is used, else to final accumulation.
/// </summary>
/// <param name="text">The spatial filter type</param>
template <typename T> void FractoriumEmberController<T>::SpatialFilterTypeChanged(const QString& text)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_SpatialFilterType = SpatialFilterCreator<T>::FromString(text.toStdString());
	}, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
}

void Fractorium::OnSpatialFilterTypeComboCurrentIndexChanged(const QString& text) { m_Controller->SpatialFilterTypeChanged(text); }

/// <summary>
/// Set the density estimation filter min radius value.
/// Resets the rendering process to density filtering.
/// </summary>
/// <param name="d">The min radius value</param>
template <typename T>
void FractoriumEmberController<T>::DEFilterMinRadiusWidthChanged(double d)
{
	if (m_Ember.m_MinRadDE != d)
	{
		UpdateAll([&](Ember<T>& ember, bool isMain)
		{
			ember.m_MinRadDE = d;
		}, true, eProcessAction::FILTER_AND_ACCUM, m_Fractorium->ApplyAll());
	}
}

void Fractorium::OnDEFilterMinRadiusWidthChanged(double d)
{
	ConstrainLow(m_DEFilterMinRadiusSpin, m_DEFilterMaxRadiusSpin);
	m_Controller->DEFilterMinRadiusWidthChanged(m_DEFilterMinRadiusSpin->value());
}

/// <summary>
/// Set the density estimation filter max radius value.
/// Resets the rendering process to density filtering.
/// </summary>
/// <param name="d">The max radius value</param>
template <typename T>
void FractoriumEmberController<T>::DEFilterMaxRadiusWidthChanged(double d)
{
	if (m_Ember.m_MaxRadDE != d)
	{
		UpdateAll([&](Ember<T>& ember, bool isMain)
		{
			ember.m_MaxRadDE = d;
		}, true, eProcessAction::FILTER_AND_ACCUM, m_Fractorium->ApplyAll());
	}
}

void Fractorium::OnDEFilterMaxRadiusWidthChanged(double d)
{
	ConstrainHigh(m_DEFilterMinRadiusSpin, m_DEFilterMaxRadiusSpin);
	m_Controller->DEFilterMaxRadiusWidthChanged(m_DEFilterMaxRadiusSpin->value());
}

/// <summary>
/// Set the density estimation filter curve value.
/// Resets the rendering process to density filtering.
/// </summary>
/// <param name="d">The curve value</param>
template <typename T> void FractoriumEmberController<T>::DEFilterCurveWidthChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CurveDE = d;
	}, true, eProcessAction::FILTER_AND_ACCUM, m_Fractorium->ApplyAll());
}

void Fractorium::OnDEFilterCurveWidthChanged(double d) { m_Controller->DEFilterCurveWidthChanged(d); }

/// <summary>
/// Iteration.
/// </summary>

/// <summary>
/// Set the iteration depth.
/// Called when the sub batch size spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The sub batch size value to set</param>
template <typename T> void FractoriumEmberController<T>::SbsChanged(int d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_SubBatchSize = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnSbsChanged(int d) { m_Controller->SbsChanged(d); }

/// <summary>
/// Set the range from which to chose the starting random points, as well as point resets due to bad points.
/// Called when the rand range spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The sub batch size value to set</param>
template <typename T> void FractoriumEmberController<T>::RandRangeChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_RandPointRange = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}

void Fractorium::OnRandRangeChanged(double d) { m_Controller->RandRangeChanged(d); }

/// <summary>
/// Set the number of samples to disregard for each sub batch.
/// Called when the fuse count spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The fuse count value to set</param>
template <typename T> void FractoriumEmberController<T>::FuseChanged(int d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_FuseCount = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnFuseChanged(int d) { m_Controller->FuseChanged(d); }

/// <summary>
/// Set the quality.
/// 10 is good for interactive rendering on the CPU.
/// 20-50 is good for OpenCL.
/// Above 500 seems to offer little additional value for final renders.
/// Called when the quality spinner is changed.
/// If rendering is done, and the value is greater than the last value,
/// the rendering process is continued, else it's reset.
/// </summary>
/// <param name="d">The quality in terms of iterations per pixel</param>
template <typename T> void FractoriumEmberController<T>::QualityChanged(double d) { }
void Fractorium::OnQualityChanged(double d) { m_Controller->QualityChanged(d); }

/// <summary>
/// Set the supersample.
/// Note this will dramatically degrade performance, especially in
/// OpenCL, while only giving a minor improvement in visual quality.
/// Values above 2 add no noticeable difference.
/// The user should only use this for a final render, or a quick preview, and then
/// reset it back to 1 when done.
/// Called when the supersample spinner is changed.
/// Resets the rendering process.
/// </summary>
/// <param name="d">The supersample value to set</param>
template <typename T> void FractoriumEmberController<T>::SupersampleChanged(int d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_Supersample = d;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
}
void Fractorium::OnSupersampleChanged(int d) { m_Controller->SupersampleChanged(d); }

/// <summary>
/// Set the affine interpolation type.
/// Does not reset anything because this is only used for animation.
/// In the future, when animation is implemented, this will have an effect.
/// Called when the affine interp type combo box index is changed.
/// </summary>
/// <param name="index">The index</param>
template <typename T>
void FractoriumEmberController<T>::AffineInterpTypeChanged(int i)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		eAffineInterp interp;

		if (i == 0)
			interp = eAffineInterp::AFFINE_INTERP_LINEAR;
		else if (i == 1)
			interp = eAffineInterp::AFFINE_INTERP_LOG;
		else
			interp = eAffineInterp::AFFINE_INTERP_LINEAR;

		ember.m_AffineInterp = interp;

		if (!m_Fractorium->ApplyAll())
			if (m_EmberFilePointer)
				m_EmberFilePointer->m_AffineInterp = interp;
	}, false, eProcessAction::NOTHING, m_Fractorium->ApplyAll());
}

void Fractorium::OnAffineInterpTypeComboCurrentIndexChanged(int index) { m_Controller->AffineInterpTypeChanged(index); }

/// <summary>
/// Set the interpolation type.
/// Does not reset anything because this is only used for animation.
/// Called when the interp type combo box index is changed.
/// </summary>
/// <param name="i">The index</param>
template <typename T>
void FractoriumEmberController<T>::InterpTypeChanged(int i)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		eInterp interp;

		if (i == 0)
			interp = eInterp::EMBER_INTERP_LINEAR;
		else if (i == 1)
			interp = eInterp::EMBER_INTERP_SMOOTH;
		else
			interp = eInterp::EMBER_INTERP_LINEAR;

		ember.m_Interp = interp;

		if (!m_Fractorium->ApplyAll())
			if (m_EmberFilePointer)
				m_EmberFilePointer->m_Interp = interp;
	}, false, eProcessAction::NOTHING, m_Fractorium->ApplyAll());
}

void Fractorium::OnInterpTypeComboCurrentIndexChanged(int index) { m_Controller->InterpTypeChanged(index); }

/// <summary>
/// Set the temporal filter width to be used with animation.
/// Called when the temporal filter width spinner is changed.
/// Does not reset anything because this is only used for animation.
/// </summary>
/// <param name="d">The temporal filter width</param>
template <typename T>
void FractoriumEmberController<T>::TemporalFilterWidthChanged(double d)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_TemporalFilterWidth = d;

		if (!m_Fractorium->ApplyAll())
			if (m_EmberFilePointer)
				m_EmberFilePointer->m_TemporalFilterWidth = d;
	}, false, eProcessAction::NOTHING, m_Fractorium->ApplyAll());//Don't do anything until animation is implemented.
}
void Fractorium::OnTemporalFilterWidthChanged(double d) { m_Controller->TemporalFilterWidthChanged(d); }

/// <summary>
/// Set the temporal filter type to be used with animation.
/// Called when the temporal filter combo box index is changed.
/// Does not reset anything because this is only used for animation.
/// </summary>
/// <param name="text">The name of the temporal filter</param>
template <typename T>
void FractoriumEmberController<T>::TemporalFilterTypeChanged(const QString& text)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		auto filter = TemporalFilterCreator<T>::FromString(text.toStdString());
		ember.m_TemporalFilterType = filter;

		if (!m_Fractorium->ApplyAll())
			if (m_EmberFilePointer)
				m_EmberFilePointer->m_TemporalFilterType = filter;
	}, false, eProcessAction::NOTHING, m_Fractorium->ApplyAll());//Don't do anything until animation is implemented.
}
void Fractorium::OnTemporalFilterTypeComboCurrentIndexChanged(const QString& text) { m_Controller->TemporalFilterTypeChanged(text); }

/// <summary>
/// Set the center.
/// This updates the spinners as well as the current ember center.
/// Resets the renering process.
/// </summary>
/// <param name="x">The x offset</param>
/// <param name="y">The y offset</param>
template <typename T>
void FractoriumEmberController<T>::SetCenter(double x, double y)
{
	UpdateAll([&](Ember<T>& ember, bool isMain)
	{
		ember.m_CenterX = x;
		ember.m_CenterY = ember.m_RotCenterY = y;
	}, true, eProcessAction::FULL_RENDER, m_Fractorium->ApplyAll());
	m_Fractorium->m_CenterXSpin->SetValueStealth(x);//Don't trigger a redraw twice.
	m_Fractorium->m_CenterYSpin->SetValueStealth(y);
}

/// <summary>
/// Fill the parameter tables and palette widgets with values from the current ember.
/// This takes ~1-2ms.
/// </summary>
template <typename T>
void FractoriumEmberController<T>::FillParamTablesAndPalette()
{
	m_Fractorium->m_BrightnessSpin->SetValueStealth(m_Ember.m_Brightness);//Color.
	m_Fractorium->m_GammaSpin->SetValueStealth(m_Ember.m_Gamma);
	m_Fractorium->m_GammaThresholdSpin->SetValueStealth(m_Ember.m_GammaThresh);
	m_Fractorium->m_VibrancySpin->SetValueStealth(m_Ember.m_Vibrancy);
	m_Fractorium->m_HighlightSpin->SetValueStealth(m_Ember.m_HighlightPower);
	m_Fractorium->m_K2Spin->SetValueStealth(m_Ember.m_K2);
	m_Fractorium->m_ColorDialog->setCurrentColor(QColor(m_Ember.m_Background.r * 255, m_Ember.m_Background.g * 255, m_Ember.m_Background.b * 255));
	m_Fractorium->ui.ColorTable->item(m_Fractorium->m_BgRow, 1)->setBackgroundColor(m_Fractorium->m_ColorDialog->currentColor());
	BackgroundChanged(m_Fractorium->m_ColorDialog->currentColor());
	m_Fractorium->m_PaletteModeCombo->SetCurrentIndexStealth(int(m_Ember.m_PaletteMode));
	m_Fractorium->m_WidthSpin->SetValueStealth(m_Ember.m_FinalRasW);//Geometry.
	m_Fractorium->m_HeightSpin->SetValueStealth(m_Ember.m_FinalRasH);
	m_Fractorium->m_CenterXSpin->SetValueStealth(m_Ember.m_CenterX);
	m_Fractorium->m_CenterYSpin->SetValueStealth(m_Ember.m_CenterY);
	m_Fractorium->m_ScaleSpin->SetValueStealth(m_Ember.m_PixelsPerUnit);
	m_Fractorium->m_ZoomSpin->SetValueStealth(m_Ember.m_Zoom);
	m_Fractorium->m_RotateSpin->SetValueStealth(m_Ember.m_Rotate);
	m_Fractorium->m_ZPosSpin->SetValueStealth(m_Ember.m_CamZPos);
	m_Fractorium->m_PerspectiveSpin->SetValueStealth(m_Ember.m_CamPerspective);
	m_Fractorium->m_PitchSpin->SetValueStealth(m_Ember.m_CamPitch * RAD_2_DEG_T);
	m_Fractorium->m_YawSpin->SetValueStealth(m_Ember.m_CamYaw * RAD_2_DEG_T);
	m_Fractorium->m_DepthBlurSpin->SetValueStealth(m_Ember.m_CamDepthBlur);
	m_Fractorium->m_BlurCurveSpin->SetValueStealth(m_Ember.m_BlurCurve);
	m_Fractorium->m_SpatialFilterWidthSpin->SetValueStealth(m_Ember.m_SpatialFilterRadius);//Filter.
	m_Fractorium->m_SpatialFilterTypeCombo->SetCurrentIndexStealth(int(m_Ember.m_SpatialFilterType));
	m_Fractorium->m_TemporalFilterWidthSpin->SetValueStealth(m_Ember.m_TemporalFilterWidth);
	m_Fractorium->m_TemporalFilterTypeCombo->SetCurrentIndexStealth(int(m_Ember.m_TemporalFilterType));
	m_Fractorium->m_DEFilterMinRadiusSpin->SetValueStealth(m_Ember.m_MinRadDE);
	m_Fractorium->m_DEFilterMaxRadiusSpin->SetValueStealth(m_Ember.m_MaxRadDE);
	m_Fractorium->m_DECurveSpin->SetValueStealth(m_Ember.m_CurveDE);
	m_Fractorium->m_SbsSpin->SetValueStealth(m_Ember.m_SubBatchSize);//Iteration.
	m_Fractorium->m_FuseSpin->SetValueStealth(m_Ember.m_FuseCount);
	m_Fractorium->m_RandRangeSpin->SetValueStealth(m_Ember.m_RandPointRange);
	m_Fractorium->m_QualitySpin->SetValueStealth(m_Ember.m_Quality);
	m_Fractorium->m_SupersampleSpin->SetValueStealth(m_Ember.m_Supersample);
	m_Fractorium->m_AffineInterpTypeCombo->SetCurrentIndexStealth(int(m_Ember.m_AffineInterp));
	m_Fractorium->m_InterpTypeCombo->SetCurrentIndexStealth(int(m_Ember.m_Interp));
	auto temp = m_Ember.m_Palette.m_Filename;

	if (temp.get())
		m_Fractorium->SetPaletteFileComboIndex(*temp.get());

	//Update the palette preview widget.
	m_Fractorium->ResetPaletteControls();
	//Since the controls were cleared above, the adjusted palette will be identical to the base palette.
	//Callers can set, apply and display palette adjustments after this function exits if needed.
	SetBasePaletteAndAdjust(m_Ember.m_Palette);//Updating the palette GUI will trigger a full render.
	InitLockedScale();
}

/// <summary>
/// Copy all GUI widget values on the parameters tab to the passed in ember.
/// </summary>
/// <param name="ember">The ember to copy values to.</param>
/// <param name="imageParamsOnly">True to get just spatial and density filters plus coloring params.</param>
template <typename T> void FractoriumEmberController<T>::ParamsToEmber(Ember<float>& ember, bool imageParamsOnly) { ParamsToEmberPrivate<float>(ember, imageParamsOnly); }
#ifdef DO_DOUBLE
template <typename T> void FractoriumEmberController<T>::ParamsToEmber(Ember<double>& ember, bool imageParamsOnly) { ParamsToEmberPrivate<double>(ember, imageParamsOnly); }
#endif
template <typename T>
template <typename U>
void FractoriumEmberController<T>::ParamsToEmberPrivate(Ember<U>& ember, bool imageParamsOnly)
{
	ember.m_Brightness = m_Fractorium->m_BrightnessSpin->value();//Color.
	ember.m_Gamma = m_Fractorium->m_GammaSpin->value();
	ember.m_GammaThresh = m_Fractorium->m_GammaThresholdSpin->value();
	ember.m_Vibrancy = m_Fractorium->m_VibrancySpin->value();
	ember.m_HighlightPower = m_Fractorium->m_HighlightSpin->value();
	ember.m_K2 = m_Fractorium->m_K2Spin->value();
	ember.m_SpatialFilterRadius = m_Fractorium->m_SpatialFilterWidthSpin->value();//Filter.
	ember.m_SpatialFilterType = eSpatialFilterType(m_Fractorium->m_SpatialFilterTypeCombo->currentIndex());
	ember.m_MinRadDE = m_Fractorium->m_DEFilterMinRadiusSpin->value();
	ember.m_MaxRadDE = m_Fractorium->m_DEFilterMaxRadiusSpin->value();
	ember.m_CurveDE  = m_Fractorium->m_DECurveSpin->value();

	if (imageParamsOnly)
		return;

	ember.m_TemporalFilterWidth = m_Fractorium->m_TemporalFilterWidthSpin->value();
	ember.m_TemporalFilterType = eTemporalFilterType(m_Fractorium->m_TemporalFilterTypeCombo->currentIndex());
	auto color = m_Fractorium->ui.ColorTable->item(5, 1)->backgroundColor();
	ember.m_Background.r = color.red() / 255.0;
	ember.m_Background.g = color.green() / 255.0;
	ember.m_Background.b = color.blue() / 255.0;
	ember.m_PaletteMode = ePaletteMode(m_Fractorium->m_PaletteModeCombo->currentIndex());
	ember.m_FinalRasW = m_Fractorium->m_WidthSpin->value();//Geometry.
	ember.m_FinalRasH = m_Fractorium->m_HeightSpin->value();
	ember.m_CenterX = m_Fractorium->m_CenterXSpin->value();
	ember.m_CenterY = ember.m_RotCenterY = m_Fractorium->m_CenterYSpin->value();
	ember.m_PixelsPerUnit = m_Fractorium->m_ScaleSpin->value();
	ember.m_Zoom = m_Fractorium->m_ZoomSpin->value();
	ember.m_Rotate = m_Fractorium->m_RotateSpin->value();
	ember.m_CamZPos = m_Fractorium->m_ZPosSpin->value();
	ember.m_CamPerspective = m_Fractorium->m_PerspectiveSpin->value();
	ember.m_CamPitch = m_Fractorium->m_PitchSpin->value() * DEG_2_RAD_T;
	ember.m_CamYaw = m_Fractorium->m_YawSpin->value() * DEG_2_RAD_T;
	ember.m_CamDepthBlur = m_Fractorium->m_DepthBlurSpin->value();
	ember.m_BlurCurve = m_Fractorium->m_BlurCurveSpin->value();
	ember.m_SubBatchSize = m_Fractorium->m_SbsSpin->value();
	ember.m_FuseCount = m_Fractorium->m_FuseSpin->value();
	ember.m_RandPointRange = m_Fractorium->m_RandRangeSpin->value();
	ember.m_Quality = m_Fractorium->m_QualitySpin->value();
	ember.m_Supersample = m_Fractorium->m_SupersampleSpin->value();
	ember.m_AffineInterp = eAffineInterp(m_Fractorium->m_AffineInterpTypeCombo->currentIndex());
	ember.m_Interp = eInterp(m_Fractorium->m_InterpTypeCombo->currentIndex());
	ember.SyncSize();
}

/// <summary>
/// Set the rotation.
/// This updates the spinner, optionally stealth.
/// </summary>
/// <param name="rot">The rotation value in angles to set</param>
/// <param name="stealth">True if stealth to skip re-rendering, else false to trigger a new render</param>
void Fractorium::SetRotation(double rot, bool stealth)
{
	if (stealth)
		m_RotateSpin->SetValueStealth(rot);
	else
		m_RotateSpin->setValue(rot);
}

/// <summary>
/// Set the scale.
/// This is the number of raster pixels that correspond to the distance
/// between 0-1 in the cartesian plane. The higher the number, the more
/// zoomed in the image is.
/// Resets the rendering process.
/// </summary>
/// <param name="scale">The scale value</param>
void Fractorium::SetScale(double scale)
{
	m_ScaleSpin->setValue(scale);
}

template class FractoriumEmberController<float>;

#ifdef DO_DOUBLE
	template class FractoriumEmberController<double>;
#endif
