src/colors.cpp

Thu, 29 Mar 2018 10:31:01 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Thu, 29 Mar 2018 10:31:01 +0300
changeset 1352
55fbbd4bdf86
parent 1326
69a90bd2dba2
child 1397
5d5c11af0268
permissions
-rw-r--r--

refactor

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2018 Teemu Piippo
 *
 *  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/>.
 */

#include <QDir>
#include <QMessageBox>
#include "colors.h"
#include "main.h"

ColorData* LDColor::colorData = nullptr;
const LDColor LDColor::nullColor = -1;

/*
 * Initializes the color information module.
 */
void LDColor::initColors()
{
	static ColorData colors;
	colors.loadFromLdconfig();
	LDColor::colorData = &colors;
}

/*
 * Default-constructs an LDColor to 0(black).
 */
LDColor::LDColor() :
    m_index {0} {}

/*
 * Constructs an LDColor by index.
 */
LDColor::LDColor(qint32 index) :
    m_index {index} {}

/*
 * Constructs a direct color.
 */
LDColor::LDColor(QColor color, bool transparent) :
    m_index {directColorIndex(color, transparent)} {}

/*
 * Returns whether or not the color is valid.
 */
bool LDColor::isValid() const
{
	if (isLDConfigColor() and data().name.isEmpty())
		return false; // Unknown LDConfig color
	else
		return m_index != -1;
}

/*
 * Returns whether or not this color is defined in LDConfig.ldr.
 * This is false for e.g. direct colors.
 */
bool LDColor::isLDConfigColor() const
{
	return colorData->contains(index());
}

/*
 * Returns the ColorData entry for this color.
 */
const ColorData::Entry& LDColor::data() const
{
	return colorData->get(index());
}

/*
 * Returns the name of this color.
 */
QString LDColor::name() const
{
	if (isDirect())
		return "0x" + QString::number(index(), 16).toUpper();
	else if (isLDConfigColor())
		return data().name;
	else if (index() == -1)
		return "null color";
	else
		return "unknown";
}

/*
 * Returns the hexadecimal code of this color.
 */
QString LDColor::hexcode() const
{
	return faceColor().name();
}

/*
 * Returns the color used for surfaces.
 */
QColor LDColor::faceColor() const
{
	if (isDirect())
	{
		// Direct color -- compute from the index.
		QColor color;
		color.setRed((index() & 0x0FF0000) >> 16);
		color.setGreen((index() & 0x000FF00) >> 8);
		color.setBlue(index() & 0x00000FF);

		if (index() >= 0x3000000)
			color.setAlpha(128);

		return color;
	}
	else if (isLDConfigColor())
	{
		return data().faceColor;
	}
	else
	{
		return Qt::black;
	}
}

/*
 * Returns the color used for edge lines.
 */
QColor LDColor::edgeColor() const
{
	if (isDirect())
		return luma(faceColor()) < 48 ? Qt::white : Qt::black;
	else if (isLDConfigColor())
		return data().edgeColor;
	else
		return Qt::black;
}

/*
 * Returns the index number of this color.
 */
qint32 LDColor::index() const
{
	return m_index;
}

/*
 * Returns a string containing the preferred representation of the index.
 */
QString LDColor::indexString() const
{
	if (isDirect())
	{
		// Use hexadecimal notation for direct colors.
		return "0x" + QString::number(index(), 16).toUpper();
	}
	else
	{
		return QString::number(index());
	}
}

/*
 * Returns whether or not this color is a direct color.
 * Direct colors are picked by RGB value and are not defined in LDConfig.ldr.
 */
bool LDColor::isDirect() const
{
	return index() >= 0x02000000;
}

/*
 * Returns the LDraw color index for a direct color.
 */
qint32 LDColor::directColorIndex(QColor color, bool transparent)
{
	qint32 index = transparent ? 0x03000000 : 0x02000000;
	index |= color.red() << 16;
	index |= color.green() << 8;
	index |= color.blue();
	return index;
}

/*
 * LDColors are hashed by their index.
 */
uint qHash(LDColor color)
{
	return color.index();
}

/*
 * Calculates the luma-value for the given color.
 * c.f. https://en.wikipedia.org/wiki/Luma_(video)
 */
int luma(const QColor& color)
{
	return static_cast<int>(round(0.2126 * color.red() + 0.7152 * color.green() + 0.0722 * color.blue()));
}

/*
 * Constructs the color data array.
 */
ColorData::ColorData()
{
	// Initialize main and edge colors, they're special like that.
	m_data[MainColor].faceColor = "#AAAAAA";
	m_data[MainColor].edgeColor = Qt::black;
	m_data[MainColor].name = "Main color";
	m_data[EdgeColor].faceColor = Qt::black;
	m_data[EdgeColor].edgeColor = Qt::black;
	m_data[EdgeColor].name = "Edge color";
}

/*
 * ColorData :: contains
 *
 * Returns whether or not the given color index is present in the array.
 */
bool ColorData::contains(int code) const
{
	return code >= 0 and code < countof(m_data);
}

/*
 * Returns an entry in the color array.
 */
const ColorData::Entry& ColorData::get(int code) const
{
	if (not contains(code))
		throw std::runtime_error {"Attempted to get non-existant color information"};

	return m_data[code];
}

/*
 * Loads color information from LDConfig.ldr.
 */
void ColorData::loadFromLdconfig()
{
	*this = {};

	for (const Library& library : config::libraries())
	{
		QDir dir {library.path};

		if (dir.exists("LDConfig.ldr"))
		{
			QFile file {dir.filePath("LDConfig.ldr")};

			if (file.open(QIODevice::ReadOnly))
			{
				this->loadFromFile(file);
			}
			else
			{
				QMessageBox::critical(
					nullptr,
					QObject::tr("Error"),
					format(
						QObject::tr("Unable to open LDConfig.ldr for parsing: %1"),
						file.errorString()
					)
				);
			}
		}
	}
}

void ColorData::loadFromFile(QIODevice& device)
{
	// TODO: maybe LDConfig can be loaded as a Document? Or would that be overkill?
	while (not device.atEnd())
	{
		QString line = QString::fromUtf8(device.readLine());

		if (line.isEmpty() or line[0] != '0')
			continue; // empty or illogical

		line.remove('\r');
		line.remove('\n');

		// Parse the line
		LDConfigParser parser = {line};
		QString name;
		QString facename;
		QString edgename;
		QString codestring;

		// Check 0 !COLOUR, parse the name
		if (not parser.compareToken(0, "0") or not parser.compareToken(1, "!COLOUR") or not parser.getToken(name, 2))
			continue;

		// Replace underscores in the name with spaces for readability
		name.replace("_", " ");

		if (not parser.parseTag("CODE", codestring))
			continue;

		bool ok;
		int code = codestring.toShort(&ok);

		if (not ok or not contains(code))
			continue;

		if (not parser.parseTag("VALUE", facename) or not parser.parseTag("EDGE", edgename))
			continue;

		// Ensure that our colors are correct
		QColor faceColor = {facename};
		QColor edgeColor = {edgename};

		if (not faceColor.isValid() or not edgeColor.isValid())
			continue;

		// Fill in the entry now.
		Entry& entry = m_data[code];
		entry.name = name;
		entry.faceColor = faceColor;
		entry.edgeColor = edgeColor;

		// If the alpha tag is present, fill in that too.
		if (parser.parseTag("ALPHA", codestring))
			entry.faceColor.setAlpha(qBound(0, codestring.toInt(), 255));
	}
}

/*
 * Constructs the LDConfig.ldr parser.
 */
LDConfigParser::LDConfigParser(QString inputText)
{
	m_tokens = inputText.split(' ', QString::SkipEmptyParts);
}

/*
 * Returns whether or not there is a token at the given position.
 * If there is, fills in the value parameter with it.
 */
bool LDConfigParser::getToken(QString& tokenText, int position)
{
	if (position >= countof(m_tokens))
	{
		return false;
	}
	else
	{
		tokenText = m_tokens[position];
		return true;
	}
}

/*
 * Attempts to find the provided token in the parsed LDConfig.ldr line.
 * If found, fills in the first parameter with the position of the token.
 *
 * The args parameter specifies how many arguments (i.e. following tokens) the token needs to have.
 */
bool LDConfigParser::findToken(int& tokenPosition, QString needle, int args)
{
	for (int i = 0; i < (countof(m_tokens) - args); ++i)
	{
		if (m_tokens[i] == needle)
		{
			tokenPosition = i;
			return true;
		}
	}

	return false;
}

/*
 * Returns whether or not the token at the given position has the given text value.
 */
bool LDConfigParser::compareToken(int position, QString text)
{
	QString token;
	return getToken(token, position) and (token == text);
}

/*
 * Finds an attribute in the line, and fills in its value.
 * For instance, if the line contains "ALPHA 128", this function can find the "128" for "ALPHA".
 * Returns whether or not the attribute was found.
 */
bool LDConfigParser::parseTag(QString key, QString& value)
{
	int position;

	// Try find the token and get its position
	if (not findToken(position, key, 1))
	{
		return false;
	}
	else
	{
		// Get the token after it and store it in.
		return getToken(value, position + 1);
	}
}

mercurial