src/model.cpp

Tue, 28 Jun 2022 11:51:17 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Tue, 28 Jun 2022 11:51:17 +0300
changeset 281
afed72b544f0
parent 264
76a025db4948
child 309
d862721d19a3
permissions
-rw-r--r--

- Add CMake build type to about page and window title
- Use locale-dependant short date format in window title and long format in about page
- Move version definition into project() call
- Renamed the c++ macros to match CMake variable names

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2020 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 <QPixmap>
#include "src/model.h"

constexpr unsigned int gcd(unsigned int a, unsigned int b)
{
	while (a != b) {
		if (b > a) {
			b -= a;
		}
		else if (a > b) {
			a -= b;
		}
	}
	return a;
}

static_assert(gcd(16, 15) == 1);
static_assert(gcd(16, 4) == 4);
static_assert(gcd(272, 192) == 16);

static constexpr const char* circularPrimitiveTypeString(const CircularPrimitive& circ)
{
	return circularPrimitiveStems[circ.type];
}

static QString circularPrimitiveFilePath(const CircularPrimitive& circ)
{
	QString result;
	if (circ.fraction.divisions != 16) {
		result += QString::number(circ.fraction.divisions) + QStringLiteral("\\");
	}
	const unsigned int factor = gcd(circ.fraction.segments, circ.fraction.divisions);
	unsigned int num = circ.fraction.segments / factor;
	unsigned int denom = circ.fraction.divisions / factor;
	if (denom < 4) {
		num *= 4 / denom;
		denom = 4;
	}
	result += QStringLiteral("%1-%2").arg(num).arg(denom);
	result += QString::fromLatin1(circularPrimitiveTypeString(circ));
	result += QStringLiteral(".dat");
	return result;
}

static const char* iconPathForElement(const ModelElement& element)
{
	return std::visit(overloaded{
		[](const Colored<SubfileReference>&) {
			return ":/icons/linetype-subfile.png";
		},
		[](const Colored<LineSegment>&) {
			return ":/icons/linetype-edgeline.png";
		},
		[](const Colored<Triangle>&) {
			return ":/icons/linetype-triangle.png";
		},
		[](const Colored<Quadrilateral>&) {
			return ":/icons/linetype-quadrilateral.png";
		},
		[](const Colored<ConditionalEdge>&) {
			return ":/icons/linetype-conditionaledge.png";
		},
		[](const Colored<CircularPrimitive>&) {
			return ":/icons/linetype-circularprimitive.png";
		},
		[](const Comment&) {
			return ":/icons/chatbubble-ellipses-outline.png";
		},
		[](const Empty&) {
			return "";
		},
		[](const ParseError&) {
			return ":/icons/linetype-errorline.png";
		},
	}, element);
}

static QPixmap iconForElement(const ModelElement& element)
{
	// We avoid processing the same image over and over again by storing it
	// in a static constant. However, we need one per each possible type
	// of ModelElement, so we put the pixmap constant inside a templated lambda,
	// which gets instiated once for each type of ModelElement.
	return std::visit([](auto&& element){
		static const QPixmap pixmap = QPixmap::fromImage(
			QImage{iconPathForElement(element)}
				.scaledToHeight(24, Qt::SmoothTransformation)
		);
		return pixmap;
	}, element);
}

QString modelElementToString(const ModelElement &element)
{
	return std::visit(overloaded{
		[](const Colored<SubfileReference>& ref) {
			return QStringLiteral("1 %1 %2 %3")
				.arg(ref.color.index)
				.arg(transformToString(ref.transformation))
				.arg(ref.name);
		},
		[](const Colored<LineSegment>& seg) {
			return QStringLiteral("2 %1 %2 %3")
				.arg(seg.color.index)
				.arg(vertexToString(seg.p1))
				.arg(vertexToString(seg.p2));
		},
		[](const Colored<Triangle>& triangle) {
			return QStringLiteral("3 %1 %2 %3 %4")
				.arg(triangle.color.index)
				.arg(vertexToString(triangle.p1))
				.arg(vertexToString(triangle.p2))
				.arg(vertexToString(triangle.p3));
		},
		[](const Colored<Quadrilateral>& quad) {
			return QStringLiteral("4 %1 %2 %3 %4 %5")
				.arg(quad.color.index)
				.arg(vertexToString(quad.p1))
				.arg(vertexToString(quad.p2))
				.arg(vertexToString(quad.p3))
				.arg(vertexToString(quad.p4));
		},
		[](const Colored<ConditionalEdge>& cedge) {
			return QStringLiteral("5 %1 %2 %3 %4 %5")
				.arg(cedge.color.index)
				.arg(vertexToString(cedge.p1))
				.arg(vertexToString(cedge.p2))
				.arg(vertexToString(cedge.c1))
				.arg(vertexToString(cedge.c2));
		},
		[](const Colored<CircularPrimitive>& circ) {
			return QStringLiteral("1 %1 %2 %3")
				.arg(circ.color.index)
				.arg(transformToString(circ.transformation))
				.arg(circularPrimitiveFilePath(circ));
		},
		[](const Comment& comment) {
			return "0 " + comment.text;
		},
		[](const Empty&) {
			return QStringLiteral("");
		},
		[](const ParseError& parseError) {
			return parseError.code;
		},
	}, element);
}

Model::Model(QObject *parent) :
	QAbstractListModel{parent}
{
}

Model::~Model()
{
}

ModelId Model::append(const ModelElement &value)
{
	const std::size_t position = this->size();
	const ModelId id = this->runningId;
	this->runningId.value += 1;
	const int row = narrow<int>(signed_cast(this->size()));
	Q_EMIT this->beginInsertRows({}, row, row);
	this->body.push_back({value, id});
	this->positions[id] = position;
	Q_EMIT this->endInsertRows();
	return id;
}

const ModelElement &Model::at(std::size_t position) const
{
	return this->body[position].data;
}

ModelId Model::idAt(std::size_t position) const
{
	return this->body[position].id;
}

void Model::assignAt(std::size_t position, const ModelElement &element)
{
	this->body[position].data = element;
	const QModelIndex index = this->index(narrow<int>(signed_cast(position)));
	Q_EMIT this->dataChanged(index, index);
}

std::optional<std::size_t> Model::find(ModelId id) const
{
	return pointerToOptional(findInMap(this->positions, id));
}

template<typename K, typename V>
void removeFromMap(std::map<K, V>& map, const K& key)
{
	const auto it = map.find(key);
	if (it != map.end()) {
		map.erase(it);
	}
}

void Model::remove(const std::size_t index)
{
	if (index <  this->body.size()) {
		const int row = narrow<int>(signed_cast(index));
		Q_EMIT this->beginRemoveRows({}, row, row);
		removeFromMap(this->positions, this->body[index].id);
		this->body.erase(this->body.begin() + row);
		for (std::size_t i = index; i < this->body.size(); ++i) {
			this->positions[this->body[i].id] = i;
		}
		Q_EMIT this->endRemoveRows();
	}
}

int Model::rowCount(const QModelIndex &) const
{
	return narrow<int>(signed_cast(this->size()));
}

QVariant Model::data(const QModelIndex &index, int role) const
{
	const std::size_t i = unsigned_cast(index.row());
	const ModelElement& element = this->body[i].data;
	switch(role)
	{
	case Qt::DecorationRole:
		return iconForElement(element);
	case Qt::DisplayRole:
		return modelElementToString(element);
	/*
	case Qt::ForegroundRole:
		return object->textRepresentationForeground();
	case Qt::BackgroundRole:
		return object->textRepresentationBackground();
	case Qt::FontRole:
		return object->textRepresentationFont();
	*/
	default:
		return {};
	}
}

const ModelElement &Model::operator[](std::size_t index) const
{
	return this->body[index].data;
}

std::size_t Model::size() const
{
	return this->body.size();
}

void save(const Model &model, QIODevice *device)
{
	QTextStream out{device};
	for (std::size_t i = 0; i < model.size(); ++i) {
		out << modelElementToString(model[i]) << "\r\n";
	}
}

/**
 * @brief Sets the path to the model
 * @param path New path to use
 */
void updateHeaderNameField(Model& model, const QString &name)
{
	// Update the "Name: 1234.dat" comment
	if (model.size() >= 2) {
		if (const Comment* nameObject = std::get_if<Comment>(&model[1])) {
			if (nameObject->text.startsWith("Name: ")) {
				model[1] = Comment{"Name: " + name};
			}
		}
	}
}

mercurial