fractorium/Source/Ember/PaletteList.cpp
Person 1dfbd4eff2 --User changes
-Add new preset dimensions to the right click menu of the width and height fields in the editor.
-Change QSS stylesheets to properly handle tabs.
-Make tabs rectangular by default. For some reason, they had always been triangular.

--Bug fixes
 -Incremental rendering times in the editor were wrong.

--Code changes
 -Migrate to Qt6. There is probably more work to be done here.
-Migrate to VS2022.
-Migrate to Wix 4 installer.
-Change installer to install to program files for all users.
-Fix many VS2022 code analysis warnings.
-No longer use byte typedef, because std::byte is now a type. Revert all back to unsigned char.
-Upgrade OpenCL headers to version 3.0 and keep locally now rather than trying to look for system files.
-No longer link to Nvidia or AMD specific OpenCL libraries. Use the generic installer located at OCL_ROOT too.
-Add the ability to change OpenCL grid dimensions. This was attempted for investigating possible performance improvments, but made no difference.

This has not been verified on Linux or Mac yet.
2023-04-25 17:59:54 -06:00

793 lines
24 KiB
C++

#include "EmberPch.h"
#include "PaletteList.h"
#include "XmlToEmber.h"
namespace EmberNs
{
/// <summary>
/// Empty constructor which initializes the palette map with the default palette file.
/// </summary>
template <typename T>
PaletteList<T>::PaletteList()
{
}
/// <summary>
/// Destructor which saves any modifiable palettes to file, just in case they were modified.
/// </summary>
template <typename T>
PaletteList<T>::~PaletteList()
{
for (auto& palFile : s_Palettes)
{
if (IsModifiable(palFile.first))
Save(palFile.first);
}
}
/// <summary>
/// Create a new palette file with the given name and vector of palettes, and save it.
/// </summary>
/// <param name="filename">The full path to the file to add</param>
/// <param name="palettes">The list of palettes which comprise the file</param>
/// <returns>True if the file did not exist, was successfully added and saved, else false.</returns>
template <typename T>
bool PaletteList<T>::AddPaletteFile(const string& filename, const vector<Palette<T>>& palettes)
{
if (!GetPaletteListByFullPath(filename))
{
s_Palettes.insert(make_pair(filename, palettes));
Save(filename);
return true;
}
return false;
}
/// <summary>
/// Create an new empty palette file with the given name with a single modifiable palette in it.
/// </summary>
/// <param name="filename">The full path to the file to add</param>
/// <param name="palettes">The list of palettes which comprise the file</param>
/// <returns>True if the file did not exist, was successfully added and saved, else false.</returns>
template <typename T>
bool PaletteList<T>::AddEmptyPaletteFile(const string& filename)
{
if (!GetPaletteListByFullPath(filename))
{
const auto item = s_Palettes.insert(make_pair(filename, vector<Palette<T>>()));
Palette<T> p;
p.m_Index = 0;
p.m_Name = "empty-default";
p.m_Filename = make_shared<string>(filename);
p.m_SourceColors = map<T, v4T>
{
{ T(0), v4T(T(0), T(0), T(0), T(1)) },
{ T(1), v4T(T(0), T(0), T(0), T(1)) }
};
item.first->second.push_back(p);
Save(filename);
return true;
}
return false;
}
/// <summary>
/// Add a new palette to an existing palette file and save the file.
/// </summary>
/// <param name="filename">The full path to the existing palette file to add</param>
/// <param name="palette">The new palette to add to the file</param>
/// <returns>True if the palette file existed, the palette was added, and the file was successfully saved, else false.</returns>
template <typename T>
bool PaletteList<T>::AddPaletteToFile(const string& filename, const Palette<T>& palette)
{
if (const auto p = GetPaletteListByFullPathOrFilename(filename))
{
p->push_back(palette);
p->back().m_Filename = make_shared<string>(filename);//Ensure the filename matches because this could have been duplicated from another palette file.
p->back().m_Index = static_cast<int>(p->size()) - 1;
Save(filename);
return true;
}
return false;
}
/// <summary>
/// Replace an existing palette in a palette file with a new one and save the file.
/// The match is done based on palette name, so if there are duplicate names in
/// the file, only the first one will be replaced.
/// </summary>
/// <param name="filename">The full path to the existing palette file to replace a palette in</param>
/// <param name="palette">The new palette to use to replace an existing one in the file</param>
/// <returns>True if the palette file existed, the palette was replaced, and the file was successfully saved, else false.</returns>
template <typename T>
bool PaletteList<T>::Replace(const string& filename, const Palette<T>& palette)
{
if (const auto p = GetPaletteListByFullPathOrFilename(filename))
{
for (auto& pal : *p)
{
if (pal.m_Name == palette.m_Name)
{
const auto index = pal.m_Index;
pal = palette;
pal.m_Index = index;
Save(filename);
return true;
}
}
}
return false;
}
/// <summary>
/// Replace an existing palette in a palette file with a new one and save the file.
/// The match is done based on the passed in index.
/// </summary>
/// <param name="filename">The full path to the existing palette file to replace a palette in</param>
/// <param name="palette">The new palette to use to replace an existing one in the file</param>
/// <param name="index">The 0-based index of the palette to replace</param>
/// <returns>True if the palette file existed, the palette was replaced, and the file was successfully saved, else false.</returns>
template <typename T>
bool PaletteList<T>::Replace(const string& filename, const Palette<T>& palette, int index)
{
if (const auto p = GetPaletteListByFullPathOrFilename(filename))
{
if (index < p->size())
{
(*p)[index] = palette;
(*p)[index].m_Index = index;
Save(filename);
return true;
}
}
return false;
}
/// <summary>
/// Delete an existing palette from a palette file.
/// The match is done based on the passed in index.
/// </summary>
/// <param name="filename">The full path to the existing palette file to delete a palette from</param>
/// <param name="index">The 0-based index of the palette to delete</param>
/// <returns>True if the palette file existed, the palette was deleted, and the file was successfully saved, else false.</returns>
template <typename T>
bool PaletteList<T>::Delete(const string& filename, int index)
{
int i = 0;
if (const auto p = GetPaletteListByFullPathOrFilename(filename))
{
if (index < p->size())
{
p->erase(p->begin() + index);
for (auto& pal : *p)
pal.m_Index = i++;
Save(filename);
return true;
}
}
return false;
}
/// <summary>
/// Read an Xml palette file into memory.
/// This must be called before any palette file usage.
/// </summary>
/// <param name="filename">The full path to the file to read</param>
/// <param name="force">If true, override the initialization state and force a read, else observe the initialization state.</param>
/// <returns>Whether anything was read</returns>
template <typename T>
bool PaletteList<T>::Add(const string& filename, bool force)
{
bool added = true;
const auto contains = GetPaletteListByFullPathOrFilename(filename) != nullptr;
const auto filenameonly = GetFilename(filename);
if (contains && !force)//Don't allow any palettes with the same name, even if they reside in different paths.
return false;
const auto palettes = s_Palettes.insert(make_pair(filename, vector<Palette<T>>()));
if (force || palettes.second)
{
string buf;
const char* loc = __FUNCTION__;
if (ReadFile(filename.c_str(), buf))
{
const auto lower = ToLower(filename);
const auto pfilename = shared_ptr<string>(new string(filename));
if (EndsWith(lower, ".xml"))
{
const auto doc = xmlReadMemory(static_cast<const char*>(buf.data()), static_cast<int>(buf.size()), filename.c_str(), nullptr, XML_PARSE_NONET);
if (doc)
{
auto rootNode = xmlDocGetRootElement(doc);
if (!Compare(rootNode->name, "palettes"))
{
palettes.first->second.clear();
palettes.first->second.reserve(buf.size() / 2048);//Roughly the size in bytes it takes to store the xml text of palette.
ParsePalettes(rootNode, pfilename, palettes.first->second);
xmlFreeDoc(doc);
}
if (palettes.first->second.empty())
{
added = false;//Reading failed, likely not a valid palette file.
s_Palettes.erase(filename);
AddToReport(string(loc) + " : Couldn't parse xml doc");
}
}
else
{
added = false;
s_Palettes.erase(filename);
AddToReport(string(loc) + " : Couldn't load xml doc");
}
}
else if (EndsWith(lower, ".ugr") || EndsWith(lower, ".gradient") || EndsWith(lower, ".gradients"))
{
if (!ParsePalettes(buf, pfilename, palettes.first->second))
{
added = false;
s_Palettes.erase(filename);
AddToReport(string(loc) + " : Couldn't read palette file " + filename);
}
}
}
else
{
added = false;
s_Palettes.erase(filename);
AddToReport(string(loc) + " : Couldn't read palette file " + filename);
}
}
return added;
}
/// <summary>
/// Get the palette at a random index in a random file in the map.
/// Attempt to avoid selecting a palette which is all black.
/// </summary>
/// <returns>A pointer to a random palette in a random file if successful, else nullptr.</returns>
template <typename T>
Palette<T>* PaletteList<T>::GetRandomPalette()
{
size_t attempts = 0;
while (attempts < Size() * 10)
{
auto p = s_Palettes.begin();
const auto paletteFileIndex = QTIsaac<ISAAC_SIZE, ISAAC_INT>::LockedRand(Size());
size_t i = 0;
//Move p forward i elements.
while (i < paletteFileIndex && p != s_Palettes.end())
{
++i;
++p;
}
if (i < Size())
{
const size_t paletteIndex = QTIsaac<ISAAC_SIZE, ISAAC_INT>::LockedRand(p->second.size());
if (paletteIndex < p->second.size() && !p->second[paletteIndex].IsEmpty())
return &p->second[paletteIndex];
}
attempts++;
}
return Size() ? (&s_Palettes.begin()->second[0]) : nullptr;
}
/// <summary>
/// Get the palette at a specified index in the specified file in the map.
/// </summary>
/// <param name="filename">The filename of the palette to retrieve</param>
/// <param name="i">The index of the palette to read. A value of -1 indicates a random palette.</param>
/// <returns>A pointer to the requested palette if the index was in range, else nullptr.</returns>
template <typename T>
Palette<T>* PaletteList<T>::GetPaletteByFilename(const string& filename, size_t i)
{
if (const auto palettes = GetPaletteListByFilename(filename))
if (i < palettes->size())
return &(*palettes)[i];
return nullptr;
}
/// <summary>
/// Get the palette at a specified index in the specified file in the map.
/// </summary>
/// <param name="filename">The full path and filename of the palette to retrieve</param>
/// <param name="i">The index of the palette to read. A value of -1 indicates a random palette.</param>
/// <returns>A pointer to the requested palette if the index was in range, else nullptr.</returns>
template <typename T>
Palette<T>* PaletteList<T>::GetPaletteByFullPath(const string& filename, size_t i)
{
if (const auto palettes = GetPaletteListByFullPath(filename))
if (i < palettes->size())
return &(*palettes)[i];
return nullptr;
}
/// <summary>
/// Get a pointer to a palette with a specified name in the specified full path and filename in the map.
/// </summary>
/// <param name="filename">The filename of the palette to retrieve</param>
/// <param name="name">The name of the palette to retrieve</param>
/// <returns>A pointer to the requested palette if found, else nullptr.</returns>
template <typename T>
Palette<T>* PaletteList<T>::GetPaletteByName(const string& filename, const string& name)
{
if (const auto palettes = GetPaletteListByFullPathOrFilename(filename))
for (auto& palette : *palettes)
if (palette.m_Name == name)
return &palette;
return nullptr;
}
/// <summary>
/// Get the palette file with the specified filename in the map.
/// </summary>
/// <param name="filename">The filename of the palette to retrieve</param>
/// <returns>A pointer to the requested palette if found, else nullptr.</returns>
template <typename T>
vector<Palette<T>>* PaletteList<T>::GetPaletteListByFilename(const string& filename)
{
const auto filenameonly = GetFilename(filename);
for (auto& palettes : s_Palettes)
if (GetFilename(palettes.first) == filenameonly)
return &palettes.second;
return nullptr;
}
/// <summary>
/// Get the palette file with the specified full path and filename in the map.
/// </summary>
/// <param name="filename">The full path and filename of the palette to retrieve</param>
/// <returns>A pointer to the requested palette if found, else nullptr.</returns>
template <typename T>
vector<Palette<T>>* PaletteList<T>::GetPaletteListByFullPath(const string& filename)
{
const auto palettes = s_Palettes.find(filename);
if (palettes != s_Palettes.end() && !palettes->second.empty())
return &palettes->second;
return nullptr;
}
/// <summary>
/// Get the palette file with the specified full path and filename in the map.
/// If that does not work, try getting it with the filename alone.
/// </summary>
/// <param name="filename">The full path and filename or just the filename of the palette to retrieve</param>
/// <returns>A pointer to the requested palette if found, else nullptr.</returns>
template <typename T>
vector<Palette<T>>* PaletteList<T>::GetPaletteListByFullPathOrFilename(const string& filename)
{
auto p = GetPaletteListByFullPath(filename);
if (!p)
p = GetPaletteListByFilename(filename);
return p;
}
/// <summary>
/// Get full path and filename of the pallete with the specified filename
/// </summary>
/// <param name="filename">The filename only of the palette to retrieve</param>
/// <returns>A pointer to the requested palette if found, else nullptr.</returns>
template <typename T>
string PaletteList<T>::GetFullPathFromFilename(const string& filename)
{
const auto filenameonly = GetFilename(filename);
for (auto& palettes : s_Palettes)
if (GetFilename(palettes.first) == filenameonly)
return palettes.first;
return "";
}
/// <summary>
/// Get a copy of the palette at a specified index in the specified file in the map
/// with its hue adjusted by the specified amount.
/// </summary>
/// <param name="filename">The filename of the palette to retrieve</param>
/// <param name="i">The index of the palette to read.</param>
/// <param name="hue">The hue adjustment to apply</param>
/// <param name="palette">The palette to store the output</param>
/// <returns>True if successful, else false.</returns>
template <typename T>
bool PaletteList<T>::GetHueAdjustedPalette(const string& filename, size_t i, T hue, Palette<T>& palette)
{
if (const auto unadjustedPal = GetPaletteByFullPath(filename, i))
{
unadjustedPal->MakeHueAdjustedPalette(palette, hue);
return true;
}
return false;
}
/// <summary>
/// Clear the palette list and reset the initialization state.
/// </summary>
template <typename T>
void PaletteList<T>::Clear()
{
s_Palettes.clear();
}
/// <summary>
/// Get the size of the palettes map.
/// This will be the number of files read.
/// </summary>
/// <returns>The size of the palettes map</returns>
template <typename T>
size_t PaletteList<T>::Size() { return s_Palettes.size(); }
/// <summary>
/// Get the size of specified palette vector in the palettes map.
/// </summary>
/// <param name="index">The index of the palette in the map to retrieve</param>
/// <returns>The size of the palette vector at the specified index in the palettes map</returns>
template <typename T>
size_t PaletteList<T>::Size(size_t index)
{
size_t i = 0;
auto p = s_Palettes.begin();
while (i < index && p != s_Palettes.end())
{
++i;
++p;
}
return p->second.size();
}
/// <summary>
/// Get the size of specified palette vector in the palettes map.
/// </summary>
/// <param name="s">The filename of the palette in the map to retrieve</param>
/// <returns>The size of the palette vector at the specified index in the palettes map</returns>
template <typename T>
size_t PaletteList<T>::Size(const string& s)
{
return s_Palettes[s].size();
}
/// <summary>
/// Get the name of specified palette in the palettes map.
/// </summary>
/// <param name="index">The index of the palette in the map to retrieve</param>
/// <returns>The name of the palette vector at the specified index in the palettes map</returns>
template <typename T>
const string& PaletteList<T>::Name(size_t index)
{
size_t i = 0;
auto p = s_Palettes.begin();
while (i < index && p != s_Palettes.end())
{
++i;
++p;
}
return p->first;
}
/// <summary>
/// Determines whether all palettes in the passed in palette file are modifiable,
/// meaning whether the source colors have at least one element in them.
/// </summary>
/// <param name="filename">The full path to the existing palette file to search for a modifiable palette in</param>
/// <returns>True if at all palettes in the file were modifiable, else false.</returns>
template <typename T>
bool PaletteList<T>::IsModifiable(const string& filename)
{
if (const auto palFile = GetPaletteListByFullPathOrFilename(filename))
for (auto& pal : *palFile)
if (pal.m_SourceColors.empty())
return false;
return true;
}
/// <summary>
/// Get a const ref to the underlying static palette structure.
/// </summary>
/// <returns>s_Palettes</returns>
template <typename T>
const map<string, vector<Palette<T>>>& PaletteList<T>::Palettes() const
{
return s_Palettes;
}
/// <summary>
/// Saves an existing file to disk.
/// </summary>
/// <param name="filename">The full path to the existing palette file to save</param>
/// <returns>True if successful, else false.</returns>
template <typename T>
bool PaletteList<T>::Save(const string& filename)
{
const auto fullpath = GetFullPathFromFilename(filename);
try
{
size_t index = 0;
ostringstream os;
if (const auto palFile = GetPaletteListByFullPathOrFilename(filename))
{
ofstream f(fullpath);
os << "<palettes>\n";
if (f.is_open())
{
for (auto& pal : *palFile)
{
os << "<palette number=\"" << index++ << "\" name=\"" << pal.m_Name << "\"";
if (!pal.m_SourceColors.empty())
{
os << " source_colors=\"";
for (auto& sc : pal.m_SourceColors)//Need to clamp these each from 0 to 1. Use our custom clamp funcs.//TODO
os << Clamp<T>(sc.first, 0, 1) << "," << Clamp<T>(sc.second.r, 0, 1) << "," << Clamp<T>(sc.second.g, 0, 1) << "," << Clamp<T>(sc.second.b, 0, 1) << " ";
os << "\"";
}
os << " data=\"";
for (int i = 0; i < 32; i++)
{
for (int j = 0; j < 8; j++)
{
const size_t idx = 8 * i + j;
os << "00";
os << hex << setw(2) << setfill('0') << static_cast<int>(std::rint(pal[idx][0] * 255));
os << hex << setw(2) << setfill('0') << static_cast<int>(std::rint(pal[idx][1] * 255));
os << hex << setw(2) << setfill('0') << static_cast<int>(std::rint(pal[idx][2] * 255));
}
os << "\n";
}
os << "\"/>\n";
}
}
os << "</palettes>";
string s = os.str();
f.write(s.c_str(), s.size());
return true;
}
}
catch (const std::exception& e)
{
cout << "Error: Writing palette file " << fullpath << " failed: " << e.what() << "\n";
return false;
}
catch (...)
{
cout << "Error: Writing palette file " << fullpath << " failed.\n";
return false;
}
return false;
}
/// <summary>
/// Parses an Xml node for all palettes present and stores them in the passed in palette vector.
/// Note that although the Xml color values are expected to be 0-255, they are converted and
/// stored as normalized colors, with values from 0-1.
/// </summary>
/// <param name="node">The parent note of all palettes in the Xml file.</param>
/// <param name="filename">The name of the Xml file.</param>
/// <param name="palettes">The vector to store the parsed palettes associated with this file in.</param>
template <typename T>
void PaletteList<T>::ParsePalettes(xmlNode* node, const shared_ptr<string>& filename, vector<Palette<T>>& palettes)
{
char* val;
xmlAttrPtr attr;
int index = 0;
Locale lcl;//This is required to properly read commas in the custom palette file. Because foreign locales treat a comma as the decimal point, which causes errors.
while (node)
{
if (node->type == XML_ELEMENT_NODE && !Compare(node->name, "palette"))
{
attr = node->properties;
Palette<T> palette;
while (attr)
{
val = reinterpret_cast<char*>(xmlGetProp(node, attr->name));
if (!Compare(attr->name, "data"))
{
string s1, s;
size_t tmp, colorCount = 0;
stringstream ss, temp(val); ss >> std::hex;
s.reserve(2048);
while (temp >> s1)
s += s1;
auto length = s.size();
for (size_t strIndex = 0; strIndex < length;)
{
strIndex += 2;//Skip past the 00 at the beginning of each RGB.
for (glm::length_t i = 0; i < 3 && colorCount < palette.Size(); i++)
{
const char tmpStr[3] = { s[strIndex++], s[strIndex++], 0 };//Read out and convert the string two characters at a time.
ss.clear();//Reset and fill the string stream.
ss.str(tmpStr);
ss >> tmp;//Do the conversion.
palette.m_Entries[colorCount][i] = static_cast<T>(tmp) / static_cast<T>(255);//Hex palette is [0..255], convert to [0..1].
}
colorCount++;
}
}
else if (!Compare(attr->name, "source_colors"))
{
const string s(val);
const auto vec1 = Split(s, ' ');
for (auto& v : vec1)
{
const auto vec2 = Split(v, ',');
if (vec2.size() == 4)
{
float d1 = Clamp(std::stof(vec2[0]), 0.0f, 1.0f);
palette.m_SourceColors[d1] = v4F(Clamp(std::stof(vec2[1]), 0.0f, 1.0f),
Clamp(std::stof(vec2[2]), 0.0f, 1.0f),
Clamp(std::stof(vec2[3]), 0.0f, 1.0f), 1.0f);
}
}
}
else if (!Compare(attr->name, "name"))
{
palette.m_Name = string(val);
}
xmlFree(val);
attr = attr->next;
}
palette.m_Index = index++;
palette.m_Filename = filename;
palettes.push_back(palette);
}
else
{
ParsePalettes(node->children, filename, palettes);
}
node = node->next;
}
}
/// <summary>
/// Parses a gradient file for all palettes present and stores them in the passed in palette vector.
/// Note that although the Xml color values are expected to be 0-255, they are converted and
/// stored as normalized colors, with values from 0-1.
/// This format is from Ultra Fractal and Apophysis.
/// </summary>
/// <param name="buf">The data to parse.</param>
/// <param name="filename">The name of the gradient file.</param>
/// <param name="palettes">The vector to store the parsed palettes associated with this file in.</param>
/// <returns>True if at least one palette is read, else false.</returns>
template <typename T>
bool PaletteList<T>::ParsePalettes(const string& buf, const shared_ptr<string>& filename, vector<Palette<T>>& palettes)
{
int paletteIndex = 0;
size_t index = 0;
string line;
string name;
bool reading = false;
bool found = false;
istringstream iss(buf);
vector<string> splitVec;
const char leftBrace = '{';
const char rightBrace = '}';
const string titleStr = "title=";
const string indexStr = "index=";
const string colorStr = "color=";
const string titleDelStr = " =\"";
const string colorDelStr = " =";
Palette<T> palette;
Color<T> col;
palettes.clear();
while (std::getline(iss, line))
{
if (!reading && Contains(line, leftBrace))
{
reading = true;
}
else if (Contains(line, rightBrace))
{
if (found)
palettes.push_back(palette);
reading = false;
found = false;
}
if (reading)
{
if (Find(line, titleStr))
{
splitVec = Split(line, titleDelStr, true);
if (splitVec.size() > 2)
name = splitVec[1];
}
else if (Find(line, indexStr) && Find(line, colorStr))
{
if (!found)
{
index = 0;
found = true;
palette.Clear();
palette.m_Index = paletteIndex++;
palette.m_Name = name;
palette.m_Filename = filename;
}
splitVec = Split(line, colorDelStr, true);
if (splitVec.size() > 3 && index < 256)
{
int val = std::stoi(splitVec[3]);
col.r = (val & 0xFF) / T(255);//Hex palette is [0..255], convert to [0..1].
col.g = ((val >> 8) & 0xFF) / T(255);
col.b = ((val >> 16) & 0xFF) / T(255);
palette[index] = col;
}
index++;
}
}
}
return !palettes.empty();
}
template EMBER_API class PaletteList<float>;
#ifdef DO_DOUBLE
template EMBER_API class PaletteList<double>;
#endif
}