Continue giant refactor

Tue, 07 Jun 2022 01:37:26 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Tue, 07 Jun 2022 01:37:26 +0300
changeset 201
5d201ee4a9c3
parent 200
ca23936b455b
child 202
b05af0bab735

Continue giant refactor

CMakeLists.txt file | annotate | diff | comparison | revisions
src/document.cpp file | annotate | diff | comparison | revisions
src/documentmanager.cpp file | annotate | diff | comparison | revisions
src/geometry.cpp file | annotate | diff | comparison | revisions
src/geometry.h file | annotate | diff | comparison | revisions
src/gl/partrenderer.cpp file | annotate | diff | comparison | revisions
src/gl/partrenderer.h file | annotate | diff | comparison | revisions
src/libraries.cpp file | annotate | diff | comparison | revisions
src/libraries.h file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
src/mainwindow.cpp file | annotate | diff | comparison | revisions
src/mainwindow.h file | annotate | diff | comparison | revisions
src/parser.cpp file | annotate | diff | comparison | revisions
src/settingseditor/librarieseditor.cpp file | annotate | diff | comparison | revisions
src/ui/canvas.cpp file | annotate | diff | comparison | revisions
src/ui/canvas.h file | annotate | diff | comparison | revisions
src/utility.h file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Mon Jun 06 22:01:22 2022 +0300
+++ b/CMakeLists.txt	Tue Jun 07 01:37:26 2022 +0300
@@ -31,7 +31,6 @@
 	src/libraries.cpp
 	src/invert.cpp
 	src/main.cpp
-	src/mainwindow.cpp
 	src/model.cpp
 	src/parser.cpp
 	src/polygoncache.cpp
@@ -70,7 +69,6 @@
 	src/ldrawalgorithm.h
 	src/libraries.h
 	src/main.h
-	src/mainwindow.h
 	src/model.h
 	src/parser.h
 	src/polygoncache.h
--- a/src/document.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/document.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -154,7 +154,7 @@
 	drawState->previewPolygon.back() = drawState->previewPoint;
 	if (drawState->previewPolygon.size() > 2)
 	{
-		drawState->isconcave = not geom::isConvex(drawState->previewPolygon);
+		drawState->isconcave = not isConvex(drawState->previewPolygon);
 	}
 }
 
@@ -169,7 +169,7 @@
 
 bool isCloseToExistingPoints(const std::vector<glm::vec3>& points, const glm::vec3 &pos)
 {
-	return any(points, std::bind(geom::isclose, std::placeholders::_1, pos));
+	return any(points, std::bind(isclose, std::placeholders::_1, pos));
 }
 
 void EditorTabWidget::canvasMouseClick(QMouseEvent *event)
--- a/src/documentmanager.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/documentmanager.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -188,8 +188,8 @@
 	if (not bag.missing.empty())
 	{
 		bag.missing.sort(Qt::CaseInsensitive);
-		errorStream << utility::format(
-			"The following files could not be opened: %1",
+		errorStream << format(
+			tr("The following files could not be opened: %1"),
 			bag.missing.join(", "));
 	}
 }
--- a/src/geometry.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/geometry.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -9,9 +9,9 @@
  * @param plane
  * @return point of intersection. Does not return a value if the line is in parallel to the plane.
  */
-std::optional<glm::vec3> geom::linePlaneIntersection(
-	const geom::Line<3>& line,
-	const geom::Plane& plane,
+std::optional<glm::vec3> linePlaneIntersection(
+	const Line<3>& line,
+	const Plane& plane,
 	const float epsilon)
 {
 	const float denominator = glm::dot(line.direction, plane.normal);
@@ -31,9 +31,9 @@
  * @param triangle
  * @return plane
  */
-geom::Plane geom::planeFromTriangle(const geom::Triangle& triangle)
+Plane planeFromTriangle(const Triangle& triangle)
 {
-	return geom::Plane{normalVector(triangle), triangle.p1};
+	return Plane{normalVector(triangle), triangle.p1};
 }
 
 /**
@@ -41,7 +41,7 @@
  * @param triangle
  * @return normal vector
  */
-glm::vec3 geom::normalVector(const geom::Triangle& triangle)
+glm::vec3 normalVector(const Triangle& triangle)
 {
 	return glm::normalize(
 		glm::cross(
@@ -55,10 +55,10 @@
  * @param matrix Matrix to compute
  * @return scaling vector and unscaled matrix
  */
-geom::ScalingExtract geom::extractScaling(const glm::mat4& matrix)
+ScalingExtract extractScaling(const glm::mat4& matrix)
 {
-	geom::ScalingExtract result;
-	result.scaling = geom::scalingVector(matrix);
+	ScalingExtract result;
+	result.scaling = scalingVector(matrix);
 	result.unscaled = glm::scale(matrix, 1.0f / result.scaling);
 	return result;
 }
@@ -68,7 +68,7 @@
  * @param matrix
  * @return scaling vector
  */
-glm::vec3 geom::scalingVector(const glm::mat4 matrix)
+glm::vec3 scalingVector(const glm::mat4 matrix)
 {
 	auto component = [](const glm::mat4& matrix, const int i) -> float
 	{
@@ -77,7 +77,7 @@
 	return glm::vec3{component(matrix, 0), component(matrix, 1), component(matrix, 2)};
 }
 
-std::optional<glm::vec2> geom::lineLineIntersection(const Line<2>& line_1, const Line<2>& line_2)
+std::optional<glm::vec2> lineLineIntersection(const Line<2>& line_1, const Line<2>& line_2)
 {
 	const float denominator = (line_1.direction.x * line_2.direction.y) - (line_1.direction.y * line_2.direction.x);
 	constexpr float epsilon = 1e-6f;
@@ -103,7 +103,7 @@
 	}
 }
 
-std::optional<glm::vec2> geom::rayLineSegmentIntersection(const Ray<2>& ray, const LineSegment2D& line)
+std::optional<glm::vec2> rayLineSegmentIntersection(const Ray<2>& ray, const LineSegment2D& line)
 {
 	std::optional<glm::vec2> result = lineLineIntersection(
 		rayToLine(ray),
@@ -127,7 +127,7 @@
 	return result;
 }
 
-std::optional<geom::PointOnRectagle> geom::rayRectangleIntersection(const Ray<2>& ray, const QRectF& rectangle)
+std::optional<PointOnRectagle> rayRectangleIntersection(const Ray<2>& ray, const QRectF& rectangle)
 {
 	std::optional<glm::vec2> position;
 	std::optional<PointOnRectagle> result;
@@ -167,7 +167,7 @@
 	return result;
 }
 
-geom::LineSegment2D geom::top(const QRectF& rectangle)
+LineSegment2D top(const QRectF& rectangle)
 {
 	return {
 		glm::vec2{rectangle.left(), rectangle.top()},
@@ -175,7 +175,7 @@
 	};
 }
 
-geom::LineSegment2D geom::bottom(const QRectF& rectangle)
+LineSegment2D bottom(const QRectF& rectangle)
 {
 	return {
 		glm::vec2{rectangle.left(), rectangle.bottom()},
@@ -183,7 +183,7 @@
 	};
 }
 
-geom::LineSegment2D geom::left(const QRectF& rectangle)
+LineSegment2D left(const QRectF& rectangle)
 {
 	return {
 		glm::vec2{rectangle.left(), rectangle.top()},
@@ -191,7 +191,7 @@
 	};
 }
 
-geom::LineSegment2D geom::right(const QRectF& rectangle)
+LineSegment2D right(const QRectF& rectangle)
 {
 	return {
 		glm::vec2{rectangle.right(), rectangle.top()},
@@ -199,7 +199,7 @@
 	};
 }
 
-bool geom::isConvex(const std::vector<glm::vec3>& polygon)
+bool isConvex(const std::vector<glm::vec3>& polygon)
 {
 	const int n = polygon.size();
 	auto polygonRing = iter::ring(polygon, n);
@@ -223,7 +223,7 @@
  * @param polygon
  * @return winding
  */
-Winding geom::winding(const QPolygonF &polygon)
+Winding winding(const QPolygonF &polygon)
 {
 	// based on https://stackoverflow.com/a/1165943
 	double sum = 0.0;
@@ -242,7 +242,7 @@
  * @param t scalar between 0 and 1, with t=0 being P0 and t=1 being P3
  * @return point on curve
  */
-glm::vec3 geom::pointOnCurve(const BezierCurve &curve, float t)
+glm::vec3 pointOnCurve(const BezierCurve &curve, float t)
 {
 	// clamp t as rounding errors might make it slightly out of bounds
 	t = std::clamp(t, 0.0f, 1.0f);
@@ -262,7 +262,7 @@
  * @param t scalar between 0 and 1, with t=0 being P0 and t=1 being P3
  * @return point on curve
  */
-glm::vec3 geom::derivativeOnCurve(const BezierCurve &curve, float t)
+glm::vec3 derivativeOnCurve(const BezierCurve &curve, float t)
 {
 	// clamp t as rounding errors might make it slightly out of bounds
 	t = std::clamp(t, 0.0f, 1.0f);
--- a/src/geometry.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/geometry.h	Tue Jun 07 01:37:26 2022 +0300
@@ -2,176 +2,172 @@
 #include <QPolygonF>
 #include "basics.h"
 
-namespace geom
+struct Plane
+{
+	glm::vec3 normal;
+	glm::vec3 anchor;
+};
+
+template<int N, typename T = float, glm::qualifier Q = glm::defaultp>
+struct Line
+{
+	glm::vec<N, T, Q> direction;
+	glm::vec<N, T, Q> anchor;
+};
+
+template<int N, typename T = float, glm::qualifier Q = glm::defaultp>
+struct Ray
+{
+	glm::vec<N, T, Q> direction;
+	glm::vec<N, T, Q> anchor;
+};
+
+inline const glm::vec3 origin = {0, 0, 0};
+inline const Plane XY = {{0, 0, 1}, origin};
+inline const Plane XZ = {{0, 1, 0}, origin};
+inline const Plane YZ = {{1, 0, 0}, origin};
+
+struct LineSegment
+{
+	glm::vec3 p1, p2;
+};
+struct Triangle
+{
+	glm::vec3 p1, p2, p3;
+};
+struct Quadrilateral
 {
-	struct Plane
-	{
-		glm::vec3 normal;
-		glm::vec3 anchor;
-	};
+	glm::vec3 p1, p2, p3, p4;
+};
+struct ConditionalEdge
+{
+	glm::vec3 p1, p2;
+	glm::vec3 c1, c2;
+};
+struct LineSegment2D
+{
+	glm::vec2 p1, p2;
+};
 
-	template<int N, typename T = float, glm::qualifier Q = glm::defaultp>
-	struct Line
-	{
-		glm::vec<N, T, Q> direction;
-		glm::vec<N, T, Q> anchor;
-	};
+// get polygon type from amount of points
+template<int N>
+struct PolygonType {};
+template<>
+struct PolygonType<2> { using type = LineSegment; };
+template<>
+struct PolygonType<3> { using type = Triangle; };
+template<>
+struct PolygonType<4> { using type = Quadrilateral; };
+template<int N>
+using Polygon = typename PolygonType<N>::type;
+
+/**
+ * @brief Computes a line from two points
+ * @param point_1
+ * @param point_2
+ * @return line
+ */
+template<int N, typename T, glm::qualifier Q>
+Line<N, T, Q> lineFromPoints(const glm::vec<N, T, Q>& point_1, const glm::vec<N, T, Q>& point_2)
+{
+	return {point_2 - point_1, point_1};
+}
 
-	template<int N, typename T = float, glm::qualifier Q = glm::defaultp>
-	struct Ray
-	{
-		glm::vec<N, T, Q> direction;
-		glm::vec<N, T, Q> anchor;
-	};
+template<int N, typename T, glm::qualifier Q>
+Ray<N, T, Q> rayFromPoints(const glm::vec<N, T, Q>& point_1, const glm::vec<N, T, Q>& point_2)
+{
+	return {point_2 - point_1, point_1};
+}
+
+template<int N, typename T, glm::qualifier Q>
+Line<N, T, Q> rayToLine(const Ray<N, T, Q>& ray)
+{
+	return {ray.direction, ray.anchor};
+}
 
-	inline const glm::vec3 origin = {0, 0, 0};
-	inline const Plane XY = {{0, 0, 1}, origin};
-	inline const Plane XZ = {{0, 1, 0}, origin};
-	inline const Plane YZ = {{1, 0, 0}, origin};
+enum class RectangleSide
+{
+	Top,
+	Left,
+	Bottom,
+	Right
+};
+
+struct PointOnRectagle
+{
+	glm::vec2 position;
+	RectangleSide side;
+};
 
-	struct LineSegment
-	{
-		glm::vec3 p1, p2;
-	};
-	struct Triangle
-	{
-		glm::vec3 p1, p2, p3;
-	};
-	struct Quadrilateral
-	{
-		glm::vec3 p1, p2, p3, p4;
+std::optional<glm::vec2> lineLineIntersection(const Line<2>& line_1, const Line<2>& line_2);
+std::optional<glm::vec2> rayLineSegmentIntersection(const Ray<2>& ray, const LineSegment2D& line);
+std::optional<PointOnRectagle> rayRectangleIntersection(const Ray<2>& ray, const QRectF& rectangle);
+Plane planeFromTriangle(const Triangle& triangle);
+glm::vec3 normalVector(const Triangle& triangle);
+std::optional<glm::vec3> linePlaneIntersection(
+	const Line<3>& line,
+	const Plane& plane, const float epsilon = 1e-6f);
+glm::vec3 scalingVector(const glm::mat4 matrix);
+LineSegment2D top(const QRectF& rectangle);
+LineSegment2D bottom(const QRectF& rectangle);
+LineSegment2D left(const QRectF& rectangle);
+LineSegment2D right(const QRectF& rectangle);
+bool isConvex(const std::vector<glm::vec3>& polygon);
+Winding winding(const QPolygonF& polygon);
+struct ScalingExtract
+{
+	glm::vec3 scaling;
+	glm::mat4 unscaled;
+};
+ScalingExtract extractScaling(const glm::mat4& matrix);
+
+struct NPolygon
+{
+	std::vector<glm::vec3> points;
+};
+
+inline constexpr bool isclose(const glm::vec3& a, const glm::vec3& b)
+{
+	return qFuzzyCompare(a.x, b.x)
+		and qFuzzyCompare(a.y, b.y)
+		and qFuzzyCompare(a.z, b.z);
+}
+
+struct CircleF
+{
+	QPointF center;
+	qreal radius;
+};
+
+/**
+ * @brief Inscribes a circle
+ * @param circle
+ * @return a QRectF that inscribes the specified circle
+ */
+inline constexpr QRectF inscribe(const CircleF& circle)
+{
+	return {
+		circle.center.x() - circle.radius,
+		circle.center.y() - circle.radius,
+		circle.radius * 2,
+		circle.radius * 2
 	};
-	struct ConditionalEdge
-	{
-		glm::vec3 p1, p2;
-		glm::vec3 c1, c2;
-	};
-	struct LineSegment2D
-	{
-		glm::vec2 p1, p2;
-	};
-
-	// get polygon type from amount of points
-	template<int N>
-	struct PolygonType {};
-	template<>
-	struct PolygonType<2> { using type = LineSegment; };
-	template<>
-	struct PolygonType<3> { using type = Triangle; };
-	template<>
-	struct PolygonType<4> { using type = Quadrilateral; };
-	template<int N>
-	using Polygon = typename PolygonType<N>::type;
+}
 
-	/**
-	 * @brief Computes a line from two points
-	 * @param point_1
-	 * @param point_2
-	 * @return line
-	 */
-	template<int N, typename T, glm::qualifier Q>
-	Line<N, T, Q> lineFromPoints(const glm::vec<N, T, Q>& point_1, const glm::vec<N, T, Q>& point_2)
-	{
-		return {point_2 - point_1, point_1};
-	}
-
-	template<int N, typename T, glm::qualifier Q>
-	Ray<N, T, Q> rayFromPoints(const glm::vec<N, T, Q>& point_1, const glm::vec<N, T, Q>& point_2)
-	{
-		return {point_2 - point_1, point_1};
-	}
-
-	template<int N, typename T, glm::qualifier Q>
-	Line<N, T, Q> rayToLine(const Ray<N, T, Q>& ray)
-	{
-		return {ray.direction, ray.anchor};
-	}
-
-	enum class RectangleSide
+struct BezierCurve
+{
+	glm::vec3 points[4];
+	const glm::vec3& operator[](int x) const
 	{
-		Top,
-		Left,
-		Bottom,
-		Right
-	};
-
-	struct PointOnRectagle
-	{
-		glm::vec2 position;
-		RectangleSide side;
-	};
-
-	std::optional<glm::vec2> lineLineIntersection(const Line<2>& line_1, const Line<2>& line_2);
-	std::optional<glm::vec2> rayLineSegmentIntersection(const Ray<2>& ray, const LineSegment2D& line);
-	std::optional<PointOnRectagle> rayRectangleIntersection(const Ray<2>& ray, const QRectF& rectangle);
-	Plane planeFromTriangle(const Triangle& triangle);
-	glm::vec3 normalVector(const Triangle& triangle);
-	std::optional<glm::vec3> linePlaneIntersection(
-		const Line<3>& line,
-		const Plane& plane, const float epsilon = 1e-6f);
-	glm::vec3 scalingVector(const glm::mat4 matrix);
-	LineSegment2D top(const QRectF& rectangle);
-	LineSegment2D bottom(const QRectF& rectangle);
-	LineSegment2D left(const QRectF& rectangle);
-	LineSegment2D right(const QRectF& rectangle);
-	bool isConvex(const std::vector<glm::vec3>& polygon);
-	Winding winding(const QPolygonF& polygon);
-	struct ScalingExtract
-	{
-		glm::vec3 scaling;
-		glm::mat4 unscaled;
-	};
-	ScalingExtract extractScaling(const glm::mat4& matrix);
-
-	struct NPolygon
-	{
-		std::vector<glm::vec3> points;
-	};
-
-	inline constexpr bool isclose(const glm::vec3& a, const glm::vec3& b)
+		Q_ASSERT(x >= 0 and x < 4);
+		return this->points[x];
+	}
+	glm::vec3& operator[](int x)
 	{
-		return qFuzzyCompare(a.x, b.x)
-			and qFuzzyCompare(a.y, b.y)
-			and qFuzzyCompare(a.z, b.z);
+		Q_ASSERT(x >= 0 and x < 4);
+		return this->points[x];
 	}
-
-	struct CircleF
-	{
-		QPointF center;
-		qreal radius;
-	};
+};
 
-	/**
-	 * @brief Inscribes a circle
-	 * @param circle
-	 * @return a QRectF that inscribes the specified circle
-	 */
-	inline constexpr QRectF inscribe(const CircleF& circle)
-	{
-		return {
-			circle.center.x() - circle.radius,
-			circle.center.y() - circle.radius,
-			circle.radius * 2,
-			circle.radius * 2
-		};
-	}
-
-	struct BezierCurve
-	{
-		glm::vec3 points[4];
-		const glm::vec3& operator[](int x) const
-		{
-			Q_ASSERT(x >= 0 and x < 4);
-			return this->points[x];
-		}
-		glm::vec3& operator[](int x)
-		{
-			Q_ASSERT(x >= 0 and x < 4);
-			return this->points[x];
-		}
-	};
-
-	glm::vec3 pointOnCurve(const BezierCurve& curve, float t);
-	glm::vec3 derivativeOnCurve(const BezierCurve& curve, float t);
-}
-using namespace geom;
+glm::vec3 pointOnCurve(const BezierCurve& curve, float t);
+glm::vec3 derivativeOnCurve(const BezierCurve& curve, float t);
--- a/src/gl/partrenderer.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/gl/partrenderer.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -304,11 +304,11 @@
  * @param plane Plane to raycast against
  * @return world co-ordinates, or no value if the point is behind the camera.
  */
-std::optional<glm::vec3> PartRenderer::screenToModelCoordinates(const QPoint& point, const geom::Plane& plane) const
+std::optional<glm::vec3> PartRenderer::screenToModelCoordinates(const QPoint& point, const Plane& plane) const
 {
-	const geom::Line line = this->cameraLine(point);
+	const Line line = this->cameraLine(point);
 	std::optional<glm::vec3> result;
-	result = geom::linePlaneIntersection(line, plane, 0.01f);
+	result = linePlaneIntersection(line, plane, 0.01f);
 	// If the point lies behind the camera, do not return a result.
 	if (result.has_value() and glm::dot(line.direction, *result - line.anchor) < 0)
 	{
@@ -332,11 +332,11 @@
 	return toQPointF(glm::vec2{projected.x, this->height() - projected.y});
 }
 
-geom::Line<3> PartRenderer::cameraLine(const QPoint& point) const
+Line<3> PartRenderer::cameraLine(const QPoint& point) const
 {
 	const glm::vec3 p1 = this->unproject({point.x(), point.y(), 0});
 	const glm::vec3 p2 = this->unproject({point.x(), point.y(), 1});
-	return geom::lineFromPoints(p1, p2);
+	return lineFromPoints(p1, p2);
 }
 
 /**
--- a/src/gl/partrenderer.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/gl/partrenderer.h	Tue Jun 07 01:37:26 2022 +0300
@@ -31,9 +31,9 @@
 	BoundingBox boundingBox;
 	gl::ModelShaders shaders;
 	ModelId highlighted = {0};
-	std::optional<glm::vec3> screenToModelCoordinates(const QPoint& point, const geom::Plane& plane) const;
+	std::optional<glm::vec3> screenToModelCoordinates(const QPoint& point, const Plane& plane) const;
 	QPointF modelToScreenCoordinates(const glm::vec3& point) const;
-	geom::Line<3> cameraLine(const QPoint& point) const;
+	Line<3> cameraLine(const QPoint& point) const;
 	glm::vec3 unproject(const glm::vec3& win) const;
 	glm::mat4 projectionMatrix;
 	glm::mat4 viewMatrix;
--- a/src/libraries.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/libraries.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -220,7 +220,7 @@
  * @param errors Where to stream any encountered errors
  * @return color table
  */
-ldraw::ColorTable LibraryManager::loadColorTable(QTextStream& errors)
+ldraw::ColorTable LibraryManager::loadColorTable(QTextStream& errors) const
 {
 	ldraw::ColorTable result;
 	for (const Library& library : this->libraries)
--- a/src/libraries.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/libraries.h	Tue Jun 07 01:37:26 2022 +0300
@@ -78,7 +78,7 @@
 	int rowCount(const QModelIndex&) const override;
 	int columnCount(const QModelIndex&) const override;
 	bool isValidIndex(const int libraryIndex) const;
-	ldraw::ColorTable loadColorTable(QTextStream& errors);
+	ldraw::ColorTable loadColorTable(QTextStream& errors) const;
 private:
 	enum Column
 	{
--- a/src/main.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/main.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -1,24 +1,14 @@
-/*
- *  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 <QApplication>
+#include <QFileDialog>
+#include <QMessageBox>
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include "version.h"
+#include "document.h"
+#include "settingseditor/settingseditor.h"
+#include "widgets/colorselectdialog.h"
 
-#include <QApplication>
-#include "mainwindow.h"
-#include "version.h"
+static const QDir LOCALE_DIR {":/locale"};
 
 static void doQtRegistrations()
 {
@@ -29,11 +19,408 @@
 	qRegisterMetaTypeStreamOperators<Libraries>("Libraries");
 }
 
+template<typename BaseType, typename MemberType, typename DataType>
+struct MemberData
+{
+	std::size_t member;
+	DataType payload;
+	constexpr MemberType memberInstance(BaseType* instance) const
+	{
+		return *reinterpret_cast<MemberType*>(reinterpret_cast<char*>(instance) + this->member);
+	}
+};
+
+static constexpr MemberData<Ui_MainWindow, QAction*, gl::RenderStyle> renderStyleButtons[] = {
+	{ offsetof(Ui_MainWindow, actionRenderStyleNormal), gl::RenderStyle::Normal },
+	{ offsetof(Ui_MainWindow, actionRenderStyleBfc), gl::RenderStyle::BfcRedGreen },
+	{ offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors },
+	{ offsetof(Ui_MainWindow, actionRenderStylePickScene), gl::RenderStyle::PickScene },
+};
+
+static std::optional<ModelId> openModelFromPath(
+	const QString& path,
+	const LibraryManager* libraries,
+	DocumentManager* documents,
+	QWidget* parent)
+{
+	QString errorString;
+	QTextStream errorStream{&errorString};
+	const std::optional<ModelId> modelIdOpt = documents->openModel(
+		path,
+		errorStream,
+		DocumentManager::OpenType::ManuallyOpened);
+	if (modelIdOpt.has_value()) {
+		documents->loadDependenciesForModel(modelIdOpt.value(), path, *libraries, errorStream);
+		if (not errorString.isEmpty()) {
+			QMessageBox::warning(
+				parent,
+				QObject::tr("Problem loading references"),
+				errorString);
+		}
+	}
+	else {
+		QMessageBox::critical(
+			parent,
+			QObject::tr("Problem opening file"),
+			format(QObject::tr("Could not open %1: %2"), path, errorString));
+	}
+	return modelIdOpt;
+}
+
+static QString getOpenModelPath(QWidget* parent)
+{
+	return QFileDialog::getOpenFileName(
+		parent,
+		QObject::tr("Open model"),
+		"",
+		QObject::tr("LDraw models (*.ldr *.dat)"));
+}
+
+static const QString localeCode(const QString& locale)
+{
+	if (locale == "system") {
+		return QLocale::system().name();
+	}
+	else {
+		return locale;
+	}
+}
+
+/**
+ * @brief Changes the application language to the specified language
+ */
+static void changeLanguage(const QString& locale, QTranslator* translator)
+{
+	if (not locale.isEmpty()) {
+		const QString localeCode = ::localeCode(locale);
+		QLocale::setDefault({localeCode});
+		qApp->removeTranslator(translator);
+		const QString path = LOCALE_DIR.filePath(localeCode + ".qm");
+		const bool loadSuccessful = translator->load(path);
+		if (loadSuccessful)
+		{
+			qApp->installTranslator(translator);
+		}
+	}
+}
+
+/*
+void MainWindow::handleDocumentSplitterChange()
+{
+	EditorTabWidget* currentDocument = this->currentDocument();
+	if (currentDocument != nullptr)
+	{
+		this->documentSplitterState = currentDocument->saveSplitterState();
+		for (int i = 0; i < this->ui->tabs->count(); i += 1)
+		{
+			EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
+			if (document != nullptr and document != currentDocument)
+			{
+				document->restoreSplitterState(this->documentSplitterState);
+			}
+		}
+		this->settings.setMainSplitterState(this->documentSplitterState);
+	}
+}
+*/
+
+static EditorTabWidget* currentTabWidget(Ui_MainWindow* ui)
+{
+	return qobject_cast<EditorTabWidget*>(ui->tabs->currentWidget());
+};
+
+
+static void closeDocument(DocumentManager* documents, EditorTabWidget *document)
+{
+	std::optional<ModelId> modelId = documents->findIdForModel(document->model);
+	if (modelId.has_value()) {
+		documents->closeDocument(modelId.value());
+		delete document;
+	}
+}
+
+static void handleTabCloseButton(Ui_MainWindow* ui, DocumentManager* documents, int tabIndex)
+{
+	if (tabIndex >= 0 and tabIndex < ui->tabs->count()) {
+		EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(tabIndex));
+		if (tab != nullptr) {
+			closeDocument(documents, tab);
+		}
+	}
+}
+
+static std::optional<ModelId> findCurrentModelId(Ui_MainWindow* ui, DocumentManager* documents)
+{
+	const EditorTabWidget* tab = currentTabWidget(ui);
+	if (tab != nullptr) {
+		return documents->findIdForModel(tab->model);
+	}
+	else {
+		return {};
+	}
+}
+
+/**
+ * @brief Updates the title of the main window so to contain the app's name
+ * and version as well as the open document name.
+ */
+static QString title()
+{
+	QString title = ::appName;
+	title += " ";
+	title += fullVersionString();
+	return title;
+}
+
+static ldraw::ColorTable loadColors(const LibraryManager* libraries)
+{
+	QTextStream errors;
+	return libraries->loadColorTable(errors);
+}
+
+static QString tabName(const QFileInfo& fileInfo)
+{
+	QString result = fileInfo.baseName();
+	if (result.isEmpty()) {
+		result = QObject::tr("<unnamed>");
+	}
+	return result;
+}
+
+void rebuildRecentFilesMenu(QMenu* menu, const QStringList& strings, QWidget* parent)
+{
+	menu->clear();
+	for (const QString& path : strings) {
+		QAction* action = new QAction{path, parent};
+		action->setData(path);
+		menu->addAction(action);
+	}
+}
+
+static void updateRenderPreferences(
+	Ui_MainWindow* ui,
+	const gl::RenderPreferences* renderPreferences)
+{
+	for (int i = 0; i < ui->tabs->count(); i += 1) {
+		EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(i));
+		if (tab != nullptr) {
+			tab->canvas->setRenderPreferences(*renderPreferences);
+		}
+	}
+	for (auto data : ::renderStyleButtons) {
+		QAction* action = data.memberInstance(ui);
+		action->setChecked(renderPreferences->style == data.payload);
+	}
+	ui->actionDrawAxes->setChecked(renderPreferences->drawAxes);
+};
+
+static gl::RenderPreferences loadRenderPreferences(Configuration* settings)
+{
+	return gl::RenderPreferences{
+		.style = static_cast<gl::RenderStyle>(settings->renderStyle()),
+		.mainColor = settings->mainColor(),
+		.backgroundColor = settings->backgroundColor(),
+		.selectedColor = settings->selectedColor(),
+		.lineThickness = settings->lineThickness(),
+		.lineAntiAliasing = settings->lineAntiAliasing(),
+		.drawAxes = settings->drawAxes(),
+	};
+}
+
 int main(int argc, char *argv[])
 {
 	doQtRegistrations();
 	QApplication app{argc, argv};
-	MainWindow mainwindow;
-	mainwindow.show();
-	return app.exec();
+	QMainWindow mainWindow;
+	Ui_MainWindow ui;
+	DocumentManager documents{&mainWindow};
+	QString currentLanguage = "en";
+	QTranslator translator{&mainWindow};
+	Configuration settings;
+	LibraryManager libraries{&mainWindow};
+	QByteArray documentSplitterState;
+	QStringList recentlyOpenedFiles;
+	ldraw::ColorTable colorTable;
+	gl::RenderPreferences renderPreferences;
+	ui.setupUi(&mainWindow);
+	const uiutilities::KeySequenceMap defaultKeyboardShortcuts =
+		uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow));
+	const auto saveSettings = [&]{
+		settings.setMainWindowGeometry(mainWindow.saveGeometry());
+		settings.setRecentFiles(recentlyOpenedFiles);
+		settings.setMainSplitterState(documentSplitterState);
+		settings.setRenderStyle(static_cast<int>(renderPreferences.style));
+		settings.setDrawAxes(renderPreferences.drawAxes);
+		libraries.storeToSettings(&settings);
+	};
+	const auto updateRecentlyOpenedDocumentsMenu = [&]{
+		rebuildRecentFilesMenu(ui.menuRecentFiles, recentlyOpenedFiles, &mainWindow);
+		for (QAction* action : ui.menuRecentFiles->findChildren<QAction*>()) {
+			QString path = action->data().toString();
+			QObject::connect(
+				action,
+				&QAction::triggered,
+				[path, &libraries, &documents, &mainWindow]() {
+					openModelFromPath(path, &libraries, &documents, &mainWindow);
+				}
+			);
+		}
+	};
+	const auto restoreSettings = [&]{
+		recentlyOpenedFiles = settings.recentFiles();
+		documentSplitterState = settings.mainSplitterState();
+		renderPreferences = loadRenderPreferences(&settings);
+		changeLanguage(settings.locale(), &translator);
+		libraries.restoreFromSettings(&settings);
+		updateRecentlyOpenedDocumentsMenu();
+		colorTable = loadColors(&libraries);
+		updateRenderPreferences(&ui, &renderPreferences);
+		ui.retranslateUi(&mainWindow);
+	};
+	const auto addRecentlyOpenedFile = [&](const QString& path){
+		constexpr int maxRecentlyOpenedFiles = 10;
+		recentlyOpenedFiles.removeAll(path);
+		recentlyOpenedFiles.insert(0, path);
+		while (recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
+		{
+			recentlyOpenedFiles.removeLast();
+		}
+		saveSettings();
+		updateRecentlyOpenedDocumentsMenu();
+	};
+	const auto openModelForEditing = [&](const ModelId modelId){
+		EditorTabWidget* document = new EditorTabWidget{
+			documents.getModelById(modelId),
+			&documents,
+			colorTable,
+		};
+		document->canvas->setRenderPreferences(renderPreferences);
+		QObject::connect(
+			document,
+			&EditorTabWidget::newStatusText,
+			[&](const QString& newStatusText) {
+				mainWindow.statusBar()->showMessage(newStatusText);
+			});
+		const QFileInfo fileInfo{*documents.modelPath(modelId)};
+		ui.tabs->addTab(document, tabName(fileInfo));
+		ui.tabs->setCurrentWidget(document);
+		document->restoreSplitterState(documentSplitterState);
+	};
+	const auto newModel = [&openModelForEditing](DocumentManager* documents){
+		openModelForEditing(documents->newModel());
+	};
+	QObject::connect(ui.actionNew, &QAction::triggered, [&newModel, &documents]{
+		newModel(&documents);
+	});
+	QObject::connect(ui.actionOpen, &QAction::triggered, [&]{
+		const QString path = getOpenModelPath(&mainWindow);
+		if (not path.isEmpty())
+		{
+			const std::optional<ModelId> id = openModelFromPath(path, &libraries, &documents, &mainWindow);
+			if (id.has_value()) {
+				openModelForEditing(id.value());
+				addRecentlyOpenedFile(path);
+			}
+		}
+	});
+	QObject::connect(ui.actionSettingsEditor, &QAction::triggered, [&]{
+		SettingsEditor settingsEditor{&settings, defaultKeyboardShortcuts, &mainWindow};
+		const int result = settingsEditor.exec();
+		if (result == QDialog::Accepted)
+		{
+			restoreSettings();
+		}
+	});
+	QObject::connect(ui.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close);
+	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&ui]{
+		EditorTabWidget* tab = currentTabWidget(&ui);
+		if (tab != nullptr)
+		{
+			adjustGridToView(tab->canvas);
+		}
+	});
+	QObject::connect(ui.actionClose, &QAction::triggered, [&ui, &documents]{
+		EditorTabWidget* tab = currentTabWidget(&ui);
+		if (tab != nullptr)
+		{
+			closeDocument(&documents, tab);
+		}
+	});
+	const auto save = [&](ModelId modelId){
+		QString error;
+		QTextStream errorStream{&error};
+		const bool succeeded = documents.saveModel(modelId, errorStream);
+		if (not succeeded)
+		{
+			QMessageBox::critical(&mainWindow, QObject::tr("Save error"), error);
+		}
+		else
+		{
+			const QString* pathPtr = documents.modelPath(modelId);
+			if (pathPtr != nullptr) {
+				addRecentlyOpenedFile(*pathPtr);
+			}
+		}
+	};
+	const auto actionSaveAs = [&]{
+		const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents);
+		if (modelId.has_value())
+		{
+			const QString* pathPtr = documents.modelPath(*modelId);
+			QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
+			const QString newPath = QFileDialog::getSaveFileName(
+				&mainWindow,
+				QObject::tr("Save as…"),
+				QFileInfo{defaultPath}.absoluteDir().path(), 
+				QObject::tr("LDraw files (*.ldr *dat);;All files (*)")
+			);
+			if (not newPath.isEmpty()) {
+				QString error;
+				QTextStream errorStream{&error};
+				documents.setModelPath(*modelId, newPath, libraries, errorStream);
+				ui.tabs->setTabText(ui.tabs->currentIndex(), QFileInfo{newPath}.fileName());
+				save(*modelId);
+			}
+		}
+	};
+	QObject::connect(ui.actionSaveAs, &QAction::triggered, actionSaveAs);
+	QObject::connect(ui.actionSave, &QAction::triggered, [&]{
+		if (currentTabWidget(&ui) != nullptr) {
+			const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents);
+			if (modelId.has_value()) {
+				const QString* path = documents.modelPath(*modelId);
+				if (path == nullptr or path->isEmpty()) {
+					actionSaveAs();
+				}
+				else {
+					save(*modelId);
+				}
+			}
+		}
+	});
+	QObject::connect(ui.tabs, &QTabWidget::tabCloseRequested, [&](int index){
+		handleTabCloseButton(&ui, &documents, index);
+	});
+	QObject::connect(ui.actionDrawAxes, &QAction::triggered, [&](bool drawAxes){
+		renderPreferences.drawAxes = drawAxes;
+		saveSettings();
+		updateRenderPreferences(&ui, &renderPreferences);
+	});
+	for (auto data : ::renderStyleButtons) {
+		QAction* action = data.memberInstance(&ui);
+		QObject::connect(action, &QAction::triggered, [&, data]{
+			renderPreferences.style = data.payload;
+			saveSettings();
+			updateRenderPreferences(&ui, &renderPreferences);
+		});
+	}
+	mainWindow.setWindowTitle(title());
+	mainWindow.restoreGeometry(settings.mainWindowGeometry());
+	restoreSettings();
+	updateRenderPreferences(&ui, &renderPreferences);
+	newModel(&documents);
+	mainWindow.show();
+	const int result = app.exec();
+	saveSettings();
+	return result;
 }
--- a/src/mainwindow.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/mainwindow.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -15,564 +15,3 @@
  *  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 <QLabel>
-#include <QVBoxLayout>
-#include <QCloseEvent>
-#include <QFileDialog>
-#include <QMessageBox>
-#include "mainwindow.h"
-#include "ui_mainwindow.h"
-#include "settingseditor/settingseditor.h"
-#include "version.h"
-#include "document.h"
-#include "uiutilities.h"
-#include "widgets/colorselectdialog.h"
-
-template<typename BaseType, typename MemberType, typename DataType>
-struct MemberData
-{
-	std::size_t member;
-	DataType payload;
-	constexpr MemberType memberInstance(BaseType* instance) const
-	{
-		return *reinterpret_cast<MemberType*>(reinterpret_cast<char*>(instance) + this->member);
-	}
-};
-
-static constexpr MemberData<Ui_MainWindow, QAction*, gl::RenderStyle> renderStyleButtons[] = {
-	{ offsetof(Ui_MainWindow, actionRenderStyleNormal), gl::RenderStyle::Normal },
-	{ offsetof(Ui_MainWindow, actionRenderStyleBfc), gl::RenderStyle::BfcRedGreen },
-	{ offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors },
-	{ offsetof(Ui_MainWindow, actionRenderStylePickScene), gl::RenderStyle::PickScene },
-};
-
-class A : public QSettings
-{
-	using QSettings::QSettings;
-};
-
-MainWindow::MainWindow(QWidget *parent) :
-	QMainWindow{parent},
-	ui{std::make_unique<Ui_MainWindow>()},
-	documents{this},
-	settings{},
-	libraries{this}
-{
-	this->ui->setupUi(this);
-	defaultKeyboardShortcuts = uiutilities::makeKeySequenceMap(uiutilities::collectActions(this));
-	connect(ui->actionNew, &QAction::triggered, this, &MainWindow::newModel);
-	connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::openModel);
-	connect(ui->actionQuit, &QAction::triggered, this, &QMainWindow::close);
-	connect(ui->actionSettingsEditor, &QAction::triggered, this, &MainWindow::runSettingsEditor);
-	connect(ui->actionAdjustGridToView, &QAction::triggered, [&]()
-	{
-		if (this->currentDocument() != nullptr)
-		{
-			adjustGridToView(this->currentDocument()->canvas);
-		}
-	});
-	connect(this->ui->actionSave, &QAction::triggered,
-		this, &MainWindow::actionSave);
-	connect(this->ui->actionSaveAs, &QAction::triggered,
-		this, &MainWindow::actionSaveAs);
-	connect(this->ui->actionClose, &QAction::triggered, this, &MainWindow::actionClose);
-	connect(this->ui->actionDelete, &QAction::triggered, this, &MainWindow::actionDelete);
-	connect(this->ui->actionInvert, &QAction::triggered, this, &MainWindow::actionInvert);
-	connect(this->ui->tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::handleTabCloseButton);
-	for (auto data : ::renderStyleButtons)
-	{
-		QAction* action = data.memberInstance(this->ui.get());
-		connect(action, &QAction::triggered, [this, data]()
-		{
-			this->setRenderStyle(data.payload);
-		});
-	}
-	connect(this->ui->actionDrawAxes, &QAction::triggered, this, &MainWindow::setDrawAxes);
-	this->updateTitle();
-	this->restoreStartupSettings();
-	this->restoreSettings();
-	this->updateRenderPreferences();
-	this->newModel();
-}
-
-// MainWindow needs a destructor even if it is empty because otherwise the destructor of the
-// std::unique_ptr is resolved in the header file, where it will complain about Ui_MainWindow
-// being incomplete.
-MainWindow::~MainWindow()
-{
-}
-
-void MainWindow::newModel()
-{
-	this->openModelForEditing(documents.newModel());
-}
-
-void MainWindow::openModel()
-{
-	const QString path = QFileDialog::getOpenFileName(
-		this,
-		tr("Open model"),
-		"",
-		tr("LDraw models (*.ldr *.dat)"));
-	if (not path.isEmpty())
-	{
-		this->openModelFromPath(path);
-	}
-}
-
-void MainWindow::openModelFromPath(const QString& path)
-{
-	QString errorString;
-	QTextStream errorStream{&errorString};
-	std::optional<ModelId> modelIdOpt = this->documents.openModel(
-		path,
-		errorStream,
-		DocumentManager::OpenType::ManuallyOpened);
-	if (modelIdOpt.has_value())
-	{
-		const ModelId modelId = modelIdOpt.value();
-		this->documents.loadDependenciesForModel(modelId, path, this->libraries, errorStream);
-		if (not errorString.isEmpty())
-		{
-			QMessageBox::warning(
-				this,
-				tr("Problem loading references"),
-				errorString);
-		}
-		this->openModelForEditing(modelId);
-		this->addRecentlyOpenedFile(path);
-	}
-	else
-	{
-		QMessageBox::critical(
-			this,
-			tr("Problem opening file"),
-			utility::format(
-				tr("Could not open %1: %2"),
-				path,
-				errorString));
-	}
-}
-
-/**
- * @brief Changes the application language to the specified language
- * @param localeCode Code of the locale to translate to
- */
-void MainWindow::changeLanguage(QString localeCode)
-{
-	if (not localeCode.isEmpty() and localeCode != this->currentLanguage)
-	{
-		this->currentLanguage = localeCode;
-		if (localeCode == "system")
-		{
-			localeCode = QLocale::system().name();
-		}
-		QLocale::setDefault({localeCode});
-		qApp->removeTranslator(&this->translator);
-		const bool loadSuccessful = this->translator.load(pathToTranslation(localeCode));
-		if (loadSuccessful)
-		{
-			qApp->installTranslator(&this->translator);
-		}
-	}
-}
-
-void MainWindow::addRecentlyOpenedFile(const QString& path)
-{
-	this->recentlyOpenedFiles.removeAll(path);
-	this->recentlyOpenedFiles.insert(0, path);
-	while (this->recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
-	{
-		this->recentlyOpenedFiles.removeLast();
-	}
-	this->saveSettings();
-	this->updateRecentlyOpenedDocumentsMenu();
-}
-
-void MainWindow::openModelForEditing(const ModelId modelId)
-{
-	EditorTabWidget* document = new EditorTabWidget{
-		this->documents.getModelById(modelId),
-		&this->documents,
-		this->colorTable,
-	};
-	document->canvas->setRenderPreferences(this->renderPreferences);
-	connect(document, &EditorTabWidget::newStatusText, [&](const QString& newStatusText)
-	{
-		this->statusBar()->showMessage(newStatusText);
-	});
-	const QFileInfo fileInfo{*this->documents.modelPath(modelId)};
-	QString tabName = fileInfo.baseName();
-	if (tabName.isEmpty())
-	{
-		tabName = tr("<unnamed>");
-	}
-	this->ui->tabs->addTab(document, tabName);
-	this->ui->tabs->setCurrentWidget(document);
-	document->restoreSplitterState(this->documentSplitterState);
-}
-
-void MainWindow::runSettingsEditor()
-{
-	SettingsEditor settingsEditor{&this->settings, this->defaultKeyboardShortcuts, this};
-	const int result = settingsEditor.exec();
-	if (result == QDialog::Accepted)
-	{
-		this->restoreSettings();
-	}
-}
-
-EditorTabWidget* MainWindow::currentDocument()
-{
-	return qobject_cast<EditorTabWidget*>(this->ui->tabs->currentWidget());
-}
-
-const EditorTabWidget* MainWindow::currentDocument() const
-{
-	return qobject_cast<const EditorTabWidget*>(this->ui->tabs->currentWidget());
-}
-
-void MainWindow::handleDocumentSplitterChange()
-{
-	EditorTabWidget* currentDocument = this->currentDocument();
-	if (currentDocument != nullptr)
-	{
-		this->documentSplitterState = currentDocument->saveSplitterState();
-		for (int i = 0; i < this->ui->tabs->count(); i += 1)
-		{
-			EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
-			if (document != nullptr and document != currentDocument)
-			{
-				document->restoreSplitterState(this->documentSplitterState);
-			}
-		}
-		this->settings.setMainSplitterState(this->documentSplitterState);
-	}
-}
-
-void MainWindow::updateRecentlyOpenedDocumentsMenu()
-{
-	this->ui->menuRecentFiles->clear();
-	for (const QString& path : this->recentlyOpenedFiles)
-	{
-		QAction* action = new QAction{path, this};
-		action->setData(path);
-		this->ui->menuRecentFiles->addAction(action);
-		connect(action, &QAction::triggered, this, &MainWindow::openRecentFile);
-	}
-}
-
-void MainWindow::openRecentFile()
-{
-	QAction* action = qobject_cast<QAction*>(this->sender());
-	if (action != nullptr)
-	{
-		const QString path = action->data().toString();
-		this->openModelFromPath(path);
-	}
-}
-
-void MainWindow::setRenderStyle(gl::RenderStyle renderStyle)
-{
-	this->renderPreferences.style = renderStyle;
-	this->saveSettings();
-	this->updateRenderPreferences();
-}
-
-void MainWindow::setDrawAxes(bool drawAxes)
-{
-	this->renderPreferences.drawAxes = drawAxes;
-	this->saveSettings();
-	this->updateRenderPreferences();
-}
-
-/**
- * @brief Handles the "Save" (Ctrl+S) action
- */
-void MainWindow::actionSave()
-{
-	if (this->currentDocument() != nullptr)
-	{
-		const std::optional<ModelId> modelId = this->findCurrentModelId();
-		if (modelId.has_value())
-		{
-			const QString* path = this->documents.modelPath(*modelId);
-			if (path == nullptr or path->isEmpty())
-			{
-				this->actionSaveAs();
-			}
-			else
-			{
-				QString error;
-				QTextStream errorStream{&error};
-				const bool succeeded = this->documents.saveModel(*modelId, errorStream);
-				if (not succeeded)
-				{
-					QMessageBox::critical(this, tr("Save error"), error);
-				}
-				else
-				{
-					this->addRecentlyOpenedFile(*path);
-				}
-			}
-		}
-	}
-}
-
-/**
- * @brief Handles the "Save as…" (Ctrl+Shift+S) action
- */
-void MainWindow::actionSaveAs()
-{
-	if (this->currentDocument() != nullptr)
-	{
-		const std::optional<ModelId> modelId = this->findCurrentModelId();
-		if (modelId.has_value())
-		{
-			const QString* pathPtr = this->documents.modelPath(*modelId);
-			QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
-			const QString newPath = QFileDialog::getSaveFileName(
-				this,
-				tr("Save as…"),
-				QFileInfo{defaultPath}.absoluteDir().path(), 
-				tr("LDraw files (*.ldr *dat);;All files (*)")
-			);
-			if (not newPath.isEmpty())
-			{
-				QString error;
-				QTextStream errorStream{&error};
-				this->documents.setModelPath(*modelId, newPath, this->libraries, errorStream);
-				this->ui->tabs->setTabText(this->ui->tabs->currentIndex(), QFileInfo{newPath}.fileName());
-				this->actionSave();
-			}
-		}
-	}
-}
-
-/**
- * @brief Handles the "Close" (Ctrl+W) action
- */
-void MainWindow::actionClose()
-{
-	if (this->currentDocument() != nullptr)
-	{
-		this->closeDocument(this->currentDocument());
-	}
-}
-
-/**
- * @brief Handles the "Delete" (Del) action
- */
-void MainWindow::actionDelete()
-{
-	/*
-	EditorTabWidget* document = this->currentDocument();
-	if (document != nullptr)
-	{
-		std::unique_ptr<ModelEditor> modelEditor = document->editModel();
-		QSet<ldraw::id_t> ids = document->selectedObjects(); // copy
-		for (const ldraw::id_t id : ids)
-		{
-			const QModelIndex index = modelEditor->model().find(id);
-			if (index.isValid())
-			{
-				modelEditor->remove(index.row());
-			}
-		}
-	}
-	*/
-}
-
-/**
- * @brief Handles the "Invert" action
- */
-void MainWindow::actionInvert()
-{
-	/*
-	EditorTabWidget* document = this->currentDocument();
-	if (document != nullptr)
-	{
-		// TODO: simplify
-		std::unique_ptr<ModelEditor> modelEditor = document->editModel();
-		const std::optional<ModelId> modelId = this->documents.findIdForModel(&modelEditor->model());
-		if (modelId.has_value())
-		{
-			ldraw::GetPolygonsContext context = {
-				.modelId = modelId.value(),
-				.documents = &this->documents,
-			};
-			for (const ldraw::id_t id : document->selectedObjects())
-			{
-				modelEditor->modifyObject(id, [&context](ldraw::Object* object)
-				{
-					object->invert(&context);
-				});
-			}
-		}
-	}
-	*/
-}
-
-/**
- * @brief Removes the document at the specified tab index
- * @param index
- */
-void MainWindow::handleTabCloseButton(int tabIndex)
-{
-	if (tabIndex >= 0 and tabIndex < this->ui->tabs->count())
-	{
-		EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(tabIndex));
-		if (document != nullptr)
-		{
-			this->closeDocument(document);
-		}
-	}
-}
-
-/**
- * @brief Closes the specified document
- * @param document
- */
-void MainWindow::closeDocument(EditorTabWidget *document)
-{
-	std::optional<ModelId> modelId = this->documents.findIdForModel(document->model);
-	if (modelId.has_value())
-	{
-		this->documents.closeDocument(modelId.value());
-		delete document;
-	}
-}
-
-std::optional<ModelId> MainWindow::findCurrentModelId() const
-{
-	const EditorTabWidget* document = this->currentDocument();
-	if (document != nullptr)
-	{
-		return this->documents.findIdForModel(document->model);
-	}
-	else
-	{
-		return {};
-	}
-}
-
-void MainWindow::changeEvent(QEvent* event)
-{
-	if (event != nullptr)
-	{
-		switch (event->type())
-		{
-		case QEvent::LanguageChange:
-			this->ui->retranslateUi(this);
-			break;
-		default:
-			break;
-		}
-	}
-	QMainWindow::changeEvent(event);
-}
-
-/**
- * @brief Handles closing the main window
- * @param event Event information
- */
-void MainWindow::closeEvent(QCloseEvent* event)
-{
-	saveSettings();
-	event->accept();
-}
-
-/**
- * @brief Updates the title of the main window so to contain the app's name
- * and version as well as the open document name.
- */
-void MainWindow::updateTitle()
-{
-	QString title = ::appName;
-	title += " ";
-	title += fullVersionString();
-	setWindowTitle(title);
-}
-
-void MainWindow::updateRenderPreferences()
-{
-	for (int i = 0; i < this->ui->tabs->count(); i += 1)
-	{
-		EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
-		if (document != nullptr)
-		{
-			document->canvas->setRenderPreferences(this->renderPreferences);
-		}
-	}
-	for (auto data : ::renderStyleButtons)
-	{
-		QAction* action = data.memberInstance(this->ui.get());
-		action->setChecked(this->renderPreferences.style == data.payload);
-	}
-	this->ui->actionDrawAxes->setChecked(this->renderPreferences.drawAxes);
-}
-
-/**
- * @brief Stores the settings of the main window, storing geometry, etc
- */
-void MainWindow::saveSettings()
-{
-	this->settings.setMainWindowGeometry(this->saveGeometry());
-	this->settings.setRecentFiles(this->recentlyOpenedFiles);
-	this->settings.setMainSplitterState(this->documentSplitterState);
-	this->settings.setRenderStyle(static_cast<int>(this->renderPreferences.style));
-	this->settings.setDrawAxes(this->renderPreferences.drawAxes);
-	this->libraries.storeToSettings(&this->settings);
-}
-
-void MainWindow::restoreStartupSettings()
-{
-	this->restoreGeometry(this->settings.mainWindowGeometry());
-}
-
-/**
- * @brief Restores saved settings relating to the main window
- */
-void MainWindow::restoreSettings()
-{
-	this->recentlyOpenedFiles = this->settings.recentFiles();
-	this->documentSplitterState = this->settings.mainSplitterState();
-	this->renderPreferences.style = static_cast<gl::RenderStyle>(this->settings.renderStyle());
-	this->renderPreferences.mainColor = this->settings.mainColor();
-	this->renderPreferences.backgroundColor = this->settings.backgroundColor();
-	this->renderPreferences.lineThickness = this->settings.lineThickness();
-	this->renderPreferences.lineAntiAliasing = this->settings.lineAntiAliasing();
-	this->renderPreferences.selectedColor = this->settings.selectedColor();
-	this->renderPreferences.drawAxes = this->settings.drawAxes();
-	const QString systemLocale = QLocale::system().name();
-	const QVariant defaultLocale = this->settings.locale();
-	this->changeLanguage(defaultLocale.toString());
-	this->libraries.restoreFromSettings(&this->settings);
-	this->updateRecentlyOpenedDocumentsMenu();
-	this->loadColors();
-	this->updateRenderPreferences();
-}
-
-QString MainWindow::pathToTranslation(const QString& localeCode)
-{
-	QDir dir {":/locale"};
-	return dir.filePath(localeCode + ".qm");
-}
-
-void MainWindow::loadColors()
-{
-	QTextStream errors;
-	this->colorTable = this->libraries.loadColorTable(errors);
-}
-
-void MainWindow::keyReleaseEvent(QKeyEvent* /*event*/)
-{
-	/*
-	Document* document = this->currentDocument();
-	if (document != nullptr)
-	{
-		document->handleKeyPress(event);
-	}
-	*/
-}
--- a/src/mainwindow.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/mainwindow.h	Tue Jun 07 01:37:26 2022 +0300
@@ -26,60 +26,3 @@
 #include "libraries.h"
 #include "uiutilities.h"
 #include "ui/canvas.h"
-
-class EditorTabWidget;
-
-class MainWindow : public QMainWindow
-{
-	Q_OBJECT
-public:
-	MainWindow(QWidget *parent = nullptr);
-	~MainWindow() override;
-private Q_SLOTS:
-	void newModel();
-	void openModel();
-	void openModelFromPath(const QString& path);
-	void runSettingsEditor();
-	void handleDocumentSplitterChange();
-	void updateRecentlyOpenedDocumentsMenu();
-	void openRecentFile();
-	void setRenderStyle(gl::RenderStyle renderStyle);
-	Q_SLOT void setDrawAxes(bool drawAxes);
-	void actionSave();
-	void actionSaveAs();
-	void actionClose();
-	void actionDelete();
-	void actionInvert();
-	void handleTabCloseButton(int tabIndex);
-protected:
-	void changeEvent(QEvent* event) override;
-	void closeEvent(QCloseEvent* event) override;
-	void keyReleaseEvent(QKeyEvent *event) override;
-private:
-	std::unique_ptr<class Ui_MainWindow> ui;
-	DocumentManager documents;
-	QString currentLanguage = "en";
-	QTranslator translator;
-	Configuration settings;
-	LibraryManager libraries;
-	QByteArray documentSplitterState;
-	uiutilities::KeySequenceMap defaultKeyboardShortcuts;
-	static constexpr int maxRecentlyOpenedFiles = 10;
-	QStringList recentlyOpenedFiles;
-	ldraw::ColorTable colorTable;
-	gl::RenderPreferences renderPreferences;
-	void updateTitle();
-	void updateRenderPreferences();
-	void saveSettings();
-	void restoreStartupSettings();
-	void restoreSettings();
-	void changeLanguage(QString localeCode);
-	void addRecentlyOpenedFile(const QString& path);
-	void openModelForEditing(const ModelId modelId);
-	static QString pathToTranslation(const QString& localeCode);
-	void loadColors();
-	EditorTabWidget *currentDocument();
-	const EditorTabWidget *currentDocument() const;
-	void closeDocument(EditorTabWidget* document);
-	std::optional<ModelId> findCurrentModelId() const;
-};
--- a/src/parser.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/parser.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -225,7 +225,7 @@
 			return Colored<ConditionalEdge>{cedge, pair.second};
 		}
 		default:
-			throw BodyParseError{utility::format("bad line type '%1'", code)};
+			throw BodyParseError{format("bad line type '%1'", code)};
 		}
 	}
 	catch(const BodyParseError& error)
--- a/src/settingseditor/librarieseditor.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/settingseditor/librarieseditor.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -50,9 +50,9 @@
 	{
 		QMessageBox::critical(this,
 			tr("Library does not exist"),
-			utility::format(
+			format(
 				tr("The directory %1 does not exist."),
-				utility::quoted(dir.path())));
+				quoted(dir.path())));
 	}
 	else
 	{
@@ -60,9 +60,9 @@
 		{
 			QMessageBox::warning(this,
 				tr("Unreadable library"),
-				utility::format(
+				format(
 					tr("The directory %1 cannot be read."),
-					utility::quoted(dir.path())));
+					quoted(dir.path())));
 		}
 		this->libraries.addLibrary({Library::OfficialLibrary, dir});
 		this->ui.newLibraryPath->clear();
--- a/src/ui/canvas.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/ui/canvas.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -234,21 +234,21 @@
 	//font.setStyle(QFont::StyleItalic);
 	painter.setFont(font);
 	QFontMetrics fontMetrics{font};
-	const auto renderText = [&](const QString& text, const geom::PointOnRectagle& intersection)
+	const auto renderText = [&](const QString& text, const PointOnRectagle& intersection)
 	{
 		QPointF position = toQPointF(intersection.position);
-		const geom::RectangleSide side = intersection.side;
+		const RectangleSide side = intersection.side;
 		switch (side)
 		{
-		case geom::RectangleSide::Top:
+		case RectangleSide::Top:
 			position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
 			break;
-		case geom::RectangleSide::Left:
+		case RectangleSide::Left:
 			break;
-		case geom::RectangleSide::Bottom:
+		case RectangleSide::Bottom:
 			position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
 			break;
-		case geom::RectangleSide::Right:
+		case RectangleSide::Right:
 			position += QPointF{static_cast<qreal>(-fontMetrics.horizontalAdvance(text)), 0};
 			break;
 		}
@@ -272,8 +272,8 @@
 	for (const auto& axis : directions)
 	{
 		const QPointF x_p = this->modelToScreenCoordinates(axis.direction);
-		const auto intersection = geom::rayRectangleIntersection(
-			geom::rayFromPoints(toVec2(p1), toVec2(x_p)),
+		const auto intersection = rayRectangleIntersection(
+			rayFromPoints(toVec2(p1), toVec2(x_p)),
 			box);
 		if (intersection.has_value())
 		{
@@ -304,7 +304,7 @@
 
 Winding Canvas::worldPolygonWinding(const std::vector<glm::vec3> &points) const
 {
-	return geom::winding(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
+	return winding(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
 }
 
 /**
@@ -356,7 +356,7 @@
 void Canvas::drawWorldPoint(QPainter* painter, const glm::vec3& worldPoint) const
 {
 	const QPointF center = this->modelToScreenCoordinates(worldPoint);
-	painter->drawEllipse(geom::inscribe(geom::CircleF{center, 5}));
+	painter->drawEllipse(inscribe(CircleF{center, 5}));
 }
 
 /**
@@ -366,12 +366,12 @@
 void Canvas::setGridMatrix(const glm::mat4& newMatrix)
 {
 	this->gridMatrix = newMatrix;
-	const geom::Triangle triangle {
+	const Triangle triangle {
 		this->gridMatrix * glm::vec4{0, 0, 0, 1},
 		this->gridMatrix * glm::vec4{1, 0, 0, 1},
 		this->gridMatrix * glm::vec4{0, 1, 0, 1},
 	};
-	this->gridPlane = geom::planeFromTriangle(triangle);
+	this->gridPlane = planeFromTriangle(triangle);
 	this->gridProgram->setGridMatrix(this->gridMatrix);
 	this->update();
 }
--- a/src/ui/canvas.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/ui/canvas.h	Tue Jun 07 01:37:26 2022 +0300
@@ -67,7 +67,7 @@
 	std::optional<AxesProgram> axesProgram;
 	std::optional<VertexProgram> vertexProgram;
 	glm::mat4 gridMatrix;
-	geom::Plane gridPlane;
+	Plane gridPlane;
 	int totalMouseMove = 0;
 	bool isDark = true;
 	QSet<ModelId> selection;
--- a/src/utility.h	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/utility.h	Tue Jun 07 01:37:26 2022 +0300
@@ -19,84 +19,79 @@
 #pragma once
 #include "basics.h"
 
-namespace utility
+// http://stackoverflow.com/a/18204188/3629665
+template<typename T>
+inline T rotl10(T x)
 {
-	// http://stackoverflow.com/a/18204188/3629665
-	template<typename T>
-	inline T rotl10(T x)
-	{
-		return (x << 10) | ((x >> 22) & 0x000000ff);
-	}
+	return (x << 10) | ((x >> 22) & 0x000000ff);
+}
+
+template<typename T>
+inline T rotl20(T x)
+{
+	return (x << 20) | ((x >> 12) & 0x000000ff);
+}
 
-	template<typename T>
-	inline T rotl20(T x)
-	{
-		return (x << 20) | ((x >> 12) & 0x000000ff);
-	}
+inline QString format(const QString& format_string)
+{
+	return format_string;
+}
 
-	inline QString format(const QString& format_string)
-	{
-		return format_string;
-	}
+template<typename T, typename... Rest>
+QString format(const QString& format_string, T&& arg, Rest&&... rest)
+{
+	return format(format_string.arg(arg), std::forward<Rest>(rest)...);
+}
 
-	template<typename T, typename... Rest>
-	QString format(const QString& format_string, T&& arg, Rest&&... rest)
-	{
-		return format(format_string.arg(arg), std::forward<Rest>(rest)...);
-	}
-
-	inline QString quoted(QString string)
+inline QString quoted(QString string)
+{
+	if (string.contains("'"))
 	{
-		if (string.contains("'"))
-		{
-			string.replace("\"", "\\\"");
-			string = "\"" + string + "\"";
-		}
-		else
-		{
-			string = "'" + string + "'";
-		}
-		return string;
-	}
-
-	/**
-	 * @brief Converts the specified vertex to a simple string
-	 * @param vertex vertex to convert
-	 * @return "x y z"-formatted string
-	 */
-	inline QString vertexToString(const glm::vec3& vertex)
-	{
-		return utility::format("%1 %2 %3", vertex.x, vertex.y, vertex.z);
+		string.replace("\"", "\\\"");
+		string = "\"" + string + "\"";
 	}
-
-	inline QString vertexToStringParens(const glm::vec3& vertex)
-	{
-		return utility::format("(%1, %2, %3)", vertex.x, vertex.y, vertex.z);
-	}
-
-	inline QString transformToString(const glm::mat4& matrix)
+	else
 	{
-		return utility::format(
-			"%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12",
-			matrix[3][0],
-			matrix[3][1],
-			matrix[3][2],
-			matrix[0][0],
-			matrix[1][0],
-			matrix[2][0],
-			matrix[0][1],
-			matrix[1][1],
-			matrix[2][1],
-			matrix[0][2],
-			matrix[1][2],
-			matrix[2][2]);
+		string = "'" + string + "'";
 	}
+	return string;
 }
 
-using namespace utility;
+/**
+ * @brief Converts the specified vertex to a simple string
+ * @param vertex vertex to convert
+ * @return "x y z"-formatted string
+ */
+inline QString vertexToString(const glm::vec3& vertex)
+{
+	return format("%1 %2 %3", vertex.x, vertex.y, vertex.z);
+}
+
+inline QString vertexToStringParens(const glm::vec3& vertex)
+{
+	return format("(%1, %2, %3)", vertex.x, vertex.y, vertex.z);
+}
+
+inline QString transformToString(const glm::mat4& matrix)
+{
+	return format(
+		"%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12",
+		matrix[3][0],
+		matrix[3][1],
+		matrix[3][2],
+		matrix[0][0],
+		matrix[1][0],
+		matrix[2][0],
+		matrix[0][1],
+		matrix[1][1],
+		matrix[2][1],
+		matrix[0][2],
+		matrix[1][2],
+		matrix[2][2]);
+}
 
 template<typename T, glm::qualifier Q>
-inline unsigned int qHash(const glm::vec<3, T, Q>& key)
+constexpr unsigned int qHash(const glm::vec<3, T, Q>& key)
 {
-	return qHash(key.x) ^ utility::rotl10(qHash(key.y)) ^ utility::rotl20(qHash(key.z));
+	return qHash(key.x) ^ rotl10(qHash(key.y)) ^ rotl20(qHash(key.z));
 }

mercurial