merge commit

Mon, 24 Aug 2020 23:00:50 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Mon, 24 Aug 2020 23:00:50 +0300
changeset 92
c438a7db7c52
parent 89
7abaf1d64719 (diff)
parent 91
9eb5e06f34c3 (current diff)
child 93
6fe24fd945c0

merge commit

CMakeLists.txt file | annotate | diff | comparison | revisions
src/basics.h file | annotate | diff | comparison | revisions
src/gl/common.h file | annotate | diff | comparison | revisions
src/gl/partrenderer.cpp file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Mon Aug 24 22:55:37 2020 +0300
+++ b/CMakeLists.txt	Mon Aug 24 23:00:50 2020 +0300
@@ -59,7 +59,13 @@
 	src/settingseditor/settingseditor.cpp
 	src/types/boundingbox.cpp
 	src/ui/canvas.cpp
+	src/ui/multiplyfactordialog.cpp
+	src/ui/objecteditor.cpp
+	src/ui/polygonobjecteditor.cpp
 	src/widgets/colorbutton.cpp
+	src/widgets/doublespinbox.cpp
+	src/widgets/matrixeditor.cpp
+	src/widgets/vec3editor.cpp
 )
 set (LDFORGE_HEADERS
 	src/basics.h
@@ -93,6 +99,8 @@
 	src/linetypes/errorline.h
 	src/linetypes/metacommand.h
 	src/linetypes/object.h
+	src/linetypes/polygonobject.h
+	src/linetypes/propertygenerics.h
 	src/linetypes/quadrilateral.h
 	src/linetypes/subfilereference.h
 	src/linetypes/triangle.h
@@ -101,13 +109,22 @@
 	src/settingseditor/settingseditor.h
 	src/types/boundingbox.h
 	src/ui/canvas.h
+	src/ui/multiplyfactordialog.h
+	src/ui/objecteditor.h
+	src/ui/polygonobjecteditor.h
 	src/widgets/colorbutton.h
+	src/widgets/doublespinbox.h
+	src/widgets/matrixeditor.h
+	src/widgets/vec3editor.h
 )
 set (LDFORGE_FORMS
 	src/document.ui
 	src/mainwindow.ui
 	src/settingseditor/librarieseditor.ui
 	src/settingseditor/settingseditor.ui
+	src/ui/multiplyfactordialog.ui
+	src/widgets/matrixeditor.ui
+	src/widgets/vec3editor.ui
 )
 
 set(LDFORGE_LOCALES
--- a/locale/fi.ts	Mon Aug 24 22:55:37 2020 +0300
+++ b/locale/fi.ts	Mon Aug 24 23:00:50 2020 +0300
@@ -136,90 +136,134 @@
         <translation>Näkymä</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="63"/>
+        <location filename="../src/mainwindow.ui" line="64"/>
         <source>Quit</source>
         <translation>Lopeta</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="68"/>
+        <location filename="../src/mainwindow.ui" line="69"/>
         <source>Open…</source>
         <translation>Avaa...</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="71"/>
+        <location filename="../src/mainwindow.ui" line="72"/>
         <source>Ctrl+O</source>
         <translation>Ctrl+O</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="76"/>
+        <location filename="../src/mainwindow.ui" line="77"/>
         <source>New</source>
         <translation>Uusi</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="79"/>
+        <location filename="../src/mainwindow.ui" line="80"/>
         <source>Ctrl+N</source>
         <translation>Ctrl+N</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="84"/>
+        <location filename="../src/mainwindow.ui" line="85"/>
         <source>Preferences…</source>
         <translation>Asetukset...</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="92"/>
+        <location filename="../src/mainwindow.ui" line="93"/>
         <source>Normal colours</source>
         <translation type="unfinished">Perusvärit</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="100"/>
+        <location filename="../src/mainwindow.ui" line="101"/>
         <source>BFC color coding</source>
         <translation type="unfinished">BFC-värikoodaus</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.ui" line="108"/>
+        <location filename="../src/mainwindow.ui" line="109"/>
         <source>Random colours</source>
         <translation type="unfinished">Satunnaiset värit</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="97"/>
+        <location filename="../src/mainwindow.ui" line="117"/>
+        <source>Pick scene colours</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/mainwindow.cpp" line="98"/>
         <source>Open model</source>
         <translation>Avaa malli</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="99"/>
+        <location filename="../src/mainwindow.cpp" line="100"/>
         <source>LDraw models (*.ldr *.dat)</source>
         <translation>LDraw-mallit (*.ldr *.dat)</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="118"/>
+        <location filename="../src/mainwindow.cpp" line="119"/>
         <source>Problem loading references</source>
         <translation type="unfinished">Ongelma viitteiden lataamisessa</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="128"/>
+        <location filename="../src/mainwindow.cpp" line="129"/>
         <source>Problem opening file</source>
         <translation>Ongelma tiedoston avaamisessa</translation>
     </message>
     <message>
-        <location filename="../src/mainwindow.cpp" line="130"/>
+        <location filename="../src/mainwindow.cpp" line="131"/>
         <source>Could not open %1: %2</source>
         <translation>Ei voitu avata %1: %2</translation>
     </message>
 </context>
 <context>
+    <name>MatrixEditor</name>
+    <message>
+        <location filename="../src/widgets/matrixeditor.ui" line="14"/>
+        <source>Form</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/widgets/matrixeditor.ui" line="87"/>
+        <location filename="../src/widgets/matrixeditor.ui" line="94"/>
+        <location filename="../src/widgets/matrixeditor.ui" line="101"/>
+        <location filename="../src/widgets/matrixeditor.ui" line="108"/>
+        <source>×</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MultiplyFactorDialog</name>
+    <message>
+        <location filename="../src/ui/multiplyfactordialog.ui" line="14"/>
+        <source>Multiply with a scalar</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/ui/multiplyfactordialog.ui" line="24"/>
+        <source>Factor:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/ui/multiplyfactordialog.ui" line="34"/>
+        <source>Invert</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/ui/multiplyfactordialog.ui" line="43"/>
+        <source>Preview</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
     <name>QObject</name>
     <message>
-        <location filename="../src/gl/partrenderer.cpp" line="244"/>
+        <location filename="../src/gl/partrenderer.cpp" line="250"/>
         <source>OpenGL error: %1</source>
         <translation>OpenGL-virhe: %1</translation>
     </message>
     <message>
-        <location filename="../src/gl/partrenderer.cpp" line="245"/>
+        <location filename="../src/gl/partrenderer.cpp" line="251"/>
         <source>OpenGL error</source>
         <translation>OpenGL-virhe</translation>
     </message>
     <message>
-        <location filename="../src/gl/partrenderer.cpp" line="247"/>
+        <location filename="../src/gl/partrenderer.cpp" line="253"/>
         <source>Damn it</source>
         <translation>Hemmetti</translation>
     </message>
@@ -328,4 +372,32 @@
         <translation>Näppäinyhdistelmät</translation>
     </message>
 </context>
+<context>
+    <name>Vec3Editor</name>
+    <message>
+        <location filename="../src/widgets/vec3editor.ui" line="14"/>
+        <source>Form</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/widgets/vec3editor.ui" line="20"/>
+        <source>x = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/widgets/vec3editor.ui" line="36"/>
+        <source>y = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/widgets/vec3editor.ui" line="52"/>
+        <source>z = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../src/widgets/vec3editor.ui" line="68"/>
+        <source>×</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 </TS>
--- a/locale/sv.ts	Mon Aug 24 22:55:37 2020 +0300
+++ b/locale/sv.ts	Mon Aug 24 23:00:50 2020 +0300
@@ -224,6 +224,40 @@
         <source>Random colours</source>
         <translation>Slumpmässiga färger</translation>
     </message>
+    <message>
+        <source>Pick scene colours</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MatrixEditor</name>
+    <message>
+        <source>Form</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>×</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>MultiplyFactorDialog</name>
+    <message>
+        <source>Multiply with a scalar</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Factor:</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Invert</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Preview</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>PartRenderer</name>
@@ -335,6 +369,29 @@
     </message>
 </context>
 <context>
+    <name>Vec3Editor</name>
+    <message>
+        <source>Form</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>x = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>y = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>z = </source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>×</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
     <name>gl::Compiler</name>
     <message>
         <source>Vertex shader:</source>
--- a/src/basics.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/basics.h	Mon Aug 24 23:00:50 2020 +0300
@@ -129,6 +129,17 @@
 	return static_cast<double>(x);
 }
 
+/**
+* @brief casts floating point values to qreal, converting non-floating point values causes an error
+* @param[in] x floating point value to cast
+* @returns qreal
+*/
+template<typename T>
+auto toQreal(T x) -> std::enable_if_t<std::is_floating_point_v<T>, qreal>
+{
+	return static_cast<qreal>(x);
+}
+
 template<int N, typename T, glm::qualifier Q>
 inline QPoint toQPoint(const glm::vec<N, T, Q>& vec)
 {
--- a/src/colors.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/colors.h	Mon Aug 24 23:00:50 2020 +0300
@@ -28,9 +28,11 @@
 
 struct ldraw::Color
 {
-	qint32 index;
+	qint32 index = 0;
 };
 
+Q_DECLARE_METATYPE(ldraw::Color)
+
 class ldraw::ColorTable
 {
 public:
--- a/src/document.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/document.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -31,13 +31,15 @@
 	documents{documents},
 	colorTable{colorTable},
 	renderer{new Canvas{model, documents, colorTable, this}},
-	ui{*new Ui::Document}
+	ui{*new Ui::Document},
+	objectEditor{model, ldraw::NULL_ID, this}
 {
 	this->ui.setupUi(this);
 	this->ui.listView->setModel(model);
-	QVBoxLayout* layout = new QVBoxLayout;
-	layout->addWidget(this->renderer);
-	this->ui.viewportFrame->setLayout(layout);
+	this->ui.viewportFrame->setLayout(new QVBoxLayout{this->ui.listView});
+	this->ui.viewportFrame->layout()->addWidget(this->renderer);
+	this->ui.objectEditorFrame->setLayout(new QVBoxLayout{this->ui.objectEditorFrame});
+	this->ui.objectEditorFrame->layout()->addWidget(&this->objectEditor);
 	this->setMouseTracking(true);
 	connect(this->ui.splitter, &QSplitter::splitterMoved, this, &Document::splitterChanged);
 	connect(this->renderer, &Canvas::newStatusText, this, &Document::newStatusText);
@@ -53,7 +55,9 @@
 				selection.select(index, index);
 			}
 		}
+		QSignalBlocker blocker{this};
 		selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
+		this->selectionChanged(newSelection);
 	});
 	connect(this->ui.listView->selectionModel(), &QItemSelectionModel::selectionChanged,
 		[&](const QItemSelection& selected, const QItemSelection& deselected)
@@ -64,7 +68,9 @@
 			return fn::map<QSet<ldraw::id_t>>(selection.indexes(), resolveIndex);
 		};
 		this->renderer->handleSelectionChange(resolve(selected), resolve(deselected));
+		this->selectionChanged(resolve(this->ui.listView->selectionModel()->selection()));
 	});
+	connect(this->model, &Model::dataChanged, this->renderer, qOverload<>(&Canvas::update));
 }
 
 Document::~Document()
@@ -86,3 +92,15 @@
 {
 	this->renderer->setRenderPreferences(newPreferences);
 }
+
+void Document::selectionChanged(const QSet<ldraw::id_t>& newSelection)
+{
+	if (newSelection.size() == 1)
+	{
+		this->objectEditor.setObjectId(*newSelection.begin());
+	}
+	else
+	{
+		this->objectEditor.setObjectId(ldraw::NULL_ID);
+	}
+}
--- a/src/document.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/document.h	Mon Aug 24 23:00:50 2020 +0300
@@ -20,6 +20,7 @@
 #include <memory>
 #include <QWidget>
 #include "ui/canvas.h"
+#include "ui/objecteditor.h"
 
 namespace Ui
 {
@@ -45,9 +46,11 @@
 	void newStatusText(const QString& newStatusText);
 	void splitterChanged();
 private:
+	void selectionChanged(const QSet<ldraw::id_t>& newSelection);
 	Model* model;
 	DocumentManager* const documents;
 	const ldraw::ColorTable& colorTable;
 	Canvas* renderer;
 	Ui::Document& ui;
+	ObjectEditor objectEditor;
 };
--- a/src/document.ui	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/document.ui	Mon Aug 24 23:00:50 2020 +0300
@@ -19,6 +19,14 @@
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
      </property>
+     <widget class="QFrame" name="viewportFrame">
+      <property name="frameShape">
+       <enum>QFrame::StyledPanel</enum>
+      </property>
+      <property name="frameShadow">
+       <enum>QFrame::Raised</enum>
+      </property>
+     </widget>
      <widget class="QListView" name="listView">
       <property name="alternatingRowColors">
        <bool>true</bool>
@@ -30,7 +38,7 @@
        <enum>QAbstractItemView::SelectRows</enum>
       </property>
      </widget>
-     <widget class="QFrame" name="viewportFrame">
+     <widget class="QFrame" name="objectEditorFrame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
--- a/src/geometry.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/geometry.h	Mon Aug 24 23:00:50 2020 +0300
@@ -26,7 +26,7 @@
 	template<int N>
 	struct Polygon
 	{
-		glm::vec3 points[N];
+		std::array<glm::vec3, N> points;
 	};
 
 	template<int N>
--- a/src/gl/common.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/gl/common.h	Mon Aug 24 23:00:50 2020 +0300
@@ -73,8 +73,7 @@
 		Triangle,
 		Quadrilateral,
 		ConditionalEdge
-	};
-	Type type;
+	} type;
 	glm::vec3 vertices[4];
 	ldraw::Color color;
 	ldraw::id_t id;
@@ -113,6 +112,16 @@
 
 namespace gl
 {
+	constexpr Polygon::Type POLYGON_TYPES[] =
+	{
+		Polygon::Type::EdgeLine,
+		Polygon::Type::Triangle,
+		Polygon::Type::Quadrilateral,
+		Polygon::Type::ConditionalEdge
+	};
+
+	constexpr int NUM_POLYGON_TYPES = countof(POLYGON_TYPES);
+
 	inline Polygon edgeLine(const glm::vec3& v_1, const glm::vec3& v_2, ldraw::Color color, ldraw::id_t id)
 	{
 		return {Polygon::EdgeLine, {v_1, v_2}, color, id};
--- a/src/gl/compiler.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/gl/compiler.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -171,9 +171,8 @@
 	if (not this->initialized)
 	{
 		this->initializeOpenGLFunctions();
-		for (int i = 0; i < gl::NUM_ARRAY_CLASSES; i += 1)
+		for (auto& object : this->glObjects)
 		{
-			auto& object = this->glObjects[i];
 			object.program = new QOpenGLShaderProgram;
 			gl::buildShaders(object.program, ::vertexShaderSource, ::fragmentShaderSource);
 			object.program->bind();
@@ -186,12 +185,12 @@
 			{
 				object.program->enableAttributeArray(k);
 			}
-			constexpr int stride = sizeof(gl::Vertex);
-			object.program->setAttributeBuffer(0, GL_FLOAT, offsetof(gl::Vertex, position), 3, stride);
-			object.program->setAttributeBuffer(1, GL_FLOAT, offsetof(gl::Vertex, color), 4, stride);
-			object.program->setAttributeBuffer(2, GL_FLOAT, offsetof(gl::Vertex, normal), 3, stride);
-			glVertexAttribIPointer(3, 1, GL_INT, stride, reinterpret_cast<void*>(offsetof(gl::Vertex, id)));
-			glVertexAttribIPointer(4, 1, GL_INT, stride, reinterpret_cast<void*>(offsetof(gl::Vertex, selected)));
+			constexpr int stride = sizeof(Vertex);
+			object.program->setAttributeBuffer(0, GL_FLOAT, offsetof(Vertex, position), 3, stride);
+			object.program->setAttributeBuffer(1, GL_FLOAT, offsetof(Vertex, color), 4, stride);
+			object.program->setAttributeBuffer(2, GL_FLOAT, offsetof(Vertex, normal), 3, stride);
+			glVertexAttribIPointer(3, 1, GL_INT, stride, reinterpret_cast<void*>(offsetof(Vertex, id)));
+			glVertexAttribIPointer(4, 1, GL_INT, stride, reinterpret_cast<void*>(offsetof(Vertex, selected)));
 			object.vertexArray.release();
 			object.buffer.release();
 			object.program->release();
@@ -203,13 +202,13 @@
 void gl::Compiler::build(Model* model, DocumentManager* context, const gl::RenderPreferences& preferences)
 {
 	this->boundingBox = {};
-	std::vector<gl::Vertex> vboData[gl::NUM_ARRAY_CLASSES];
+	std::vector<Vertex> vboData[gl::NUM_POLYGON_TYPES];
 	const std::vector<gl::Polygon> polygons = model->getPolygons(context);
 	for (const gl::Polygon& polygon : polygons)
 	{
 		this->buildPolygon(polygon, vboData, preferences);
 	}
-	for (int arrayId = 0; arrayId < gl::NUM_ARRAY_CLASSES; arrayId += 1)
+	for (int arrayId = 0; arrayId < gl::NUM_POLYGON_TYPES; arrayId += 1)
 	{
 		auto& buffer = this->glObjects[arrayId].buffer;
 		auto& vector = vboData[arrayId];
@@ -237,18 +236,18 @@
 	return gl::ArrayClass::Lines;
 }
 
-ldraw::id_t gl::Compiler::idFromColor(const std::array<GLbyte, 3>& data)
+ldraw::id_t gl::Compiler::idFromColor(const std::array<GLubyte, 3>& data)
 {
 	return {data[0] * std::int32_t{0x10000} + data[1] * std::int32_t{0x100} + data[2]};
 }
 
 void gl::Compiler::buildPolygon(
 	gl::Polygon polygon,
-	std::vector<gl::Vertex>* vboData,
+	std::vector<Vertex>* vboData,
 	const gl::RenderPreferences& preferences)
 {
 	const gl::ArrayClass vboClass = classifyPolygon(polygon);
-	std::vector<gl::Vertex>& vertexBuffer = vboData[static_cast<int>(vboClass)];
+	std::vector<Vertex>& vertexBuffer = vboData[static_cast<int>(vboClass)];
 	auto vertexRing = iter::ring(polygon.vertices, polygon.numPolygonVertices());
 	reserveMore(vertexBuffer, polygon.numPolygonVertices());
 	const QColor color = this->getColorForPolygon(polygon, preferences);
@@ -258,7 +257,7 @@
 		const glm::vec3& v2 = vertexRing[i];
 		const glm::vec3& v3 = vertexRing[i + 1];
 		this->boundingBox.consider(polygon.vertices[i]);
-		gl::Vertex& vertex = vertexBuffer.emplace_back();
+		Vertex& vertex = vertexBuffer.emplace_back();
 		vertex.position = polygon.vertices[i];
 		vertex.normal = glm::normalize(glm::cross(v1 - v2, v3 - v2));
 		vertex.color = glm::vec4{color.redF(), color.greenF(), color.blueF(), color.alphaF()};
@@ -313,16 +312,17 @@
 
 void gl::Compiler::setSelectedObjects(const QSet<ldraw::id_t> ids)
 {
-	for (int i = 0; i < gl::NUM_ARRAY_CLASSES; i += 1)
+	for (auto& object : this->glObjects)
 	{
-		auto& vector = this->glObjects[i].cachedData;
-		for (gl::Vertex& vertex : vector)
+		std::vector<Vertex>& vector = object.cachedData;
+		for (Vertex& vertex : vector)
 		{
 			vertex.selected = (ids.contains({vertex.id})) ? 1 : 0;
 		}
 		const GLsizeiptr size = static_cast<int>(vector.size() * sizeof vector[0]);
-		this->glObjects[i].buffer.bind();
+		object.buffer.bind();
 		glBufferSubData(GL_ARRAY_BUFFER, 0, size, vector.data());
+		object.buffer.release();
 	}
 }
 
--- a/src/gl/compiler.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/gl/compiler.h	Mon Aug 24 23:00:50 2020 +0300
@@ -34,15 +34,6 @@
 namespace gl
 {
 	class Compiler;
-
-	struct Vertex
-	{
-		glm::vec3 position;
-		glm::vec4 color;
-		glm::vec3 normal;
-		glm::int32 id;
-		glm::int32 selected = 0;
-	};
 }
 
 class gl::Compiler : public QObject, protected QOpenGLExtraFunctions
@@ -52,7 +43,6 @@
 	Compiler(const ldraw::ColorTable& colorTable, QObject* parent);
 	~Compiler();
 	void build(Model* model, DocumentManager* context, const RenderPreferences& preferences);
-	void buildPolygon(Polygon polygon, std::vector<Vertex>* vboData, const gl::RenderPreferences& preferences);
 	std::size_t vertexCount(gl::ArrayClass arrayClass) const;
 	QColor getColorForPolygon(const gl::Polygon& polygon, const RenderPreferences& preferences);
 	glm::vec3 modelCenter() const;
@@ -63,7 +53,7 @@
 	void buildShaders(int arrayId);
 	void setSelectedObjects(const QSet<ldraw::id_t> ids);
 
-	static ldraw::id_t idFromColor(const std::array<GLbyte, 3>& data);
+	static ldraw::id_t idFromColor(const std::array<GLubyte, 3>& data);
 
 	template<typename T>
 	void setUniform(const char* uniformName, T&& value)
@@ -86,7 +76,16 @@
 		this->setUniform(uniformName, *array);
 	}
 private:
-	std::size_t storedVertexCounts[gl::NUM_ARRAY_CLASSES] = {0_z};
+	struct Vertex
+	{
+		glm::vec3 position;
+		glm::vec4 color;
+		glm::vec3 normal;
+		glm::int32 id;
+		glm::int32 selected = 0;
+	};
+	void buildPolygon(Polygon polygon, std::vector<Vertex>* vboData, const gl::RenderPreferences& preferences);
+	std::size_t storedVertexCounts[gl::NUM_POLYGON_TYPES] = {0};
 	bool initialized = false;
 	BoundingBox boundingBox;
 	const ldraw::ColorTable& colorTable;
@@ -97,8 +96,8 @@
 		QOpenGLShaderProgram* pickSceneProgram = nullptr;
 		QOpenGLBuffer buffer{QOpenGLBuffer::VertexBuffer};
 		QOpenGLVertexArrayObject vertexArray;
-		std::vector<gl::Vertex> cachedData;
-	} glObjects[gl::NUM_ARRAY_CLASSES];
+		std::vector<Vertex> cachedData;
+	} glObjects[gl::NUM_POLYGON_TYPES];
 };
 
 #define CHECK_GL_ERROR() { checkGLError(__FILE__, __LINE__); }
--- a/src/gl/partrenderer.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/gl/partrenderer.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -60,11 +60,7 @@
 		abort();
 	}
 	this->compiler->initialize();
-	this->compiler->build(this->model, this->documents, this->renderPreferences);
-	connect(this->model, &Model::dataChanged, [&]()
-	{
-		this->compiler->build(this->model, this->documents, this->renderPreferences);
-	});
+	connect(this->model, &Model::dataChanged, this, &PartRenderer::build);
 	this->initialized = true;
 	this->modelQuaternion = glm::angleAxis(glm::radians(30.0f), glm::vec3{-1, 0, 0});
 	this->modelQuaternion *= glm::angleAxis(glm::radians(225.0f), glm::vec3{-0, 1, 0});
@@ -111,6 +107,11 @@
 
 void PartRenderer::renderScene()
 {
+	if (this->needBuild)
+	{
+		this->compiler->build(this->model, this->documents, this->renderPreferences);
+		this->needBuild = false;
+	}
 	this->checkForGLErrors();
 	if (this->renderPreferences.lineAntiAliasing && this->renderPreferences.style != gl::RenderStyle::PickScene)
 	{
@@ -211,6 +212,11 @@
 {
 }
 
+void PartRenderer::build()
+{
+	this->needBuild = true;
+}
+
 void PartRenderer::renderVao(const gl::ArrayClass arrayClass)
 {
 	this->compiler->bindVertexArray(arrayClass);
@@ -340,7 +346,7 @@
 	this->renderPreferences.style = gl::RenderStyle::PickScene;
 	this->makeCurrent();
 	this->renderScene();
-	std::array<GLbyte, 3> data;
+	std::array<GLubyte, 3> data;
 	this->checkForGLErrors();
 	glReadPixels(where.x(), this->height() - where.y(), 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &data[0]);
 	this->checkForGLErrors();
@@ -369,7 +375,7 @@
 	this->renderPreferences = newPreferences;
 	if (mainColorChanged or backgroundColorChanged)
 	{
-		this->compiler->build(this->model, this->documents, this->renderPreferences);
+		this->build();
 		this->setupBackgroundColor();
 	}
 	emit this->renderPreferencesChanged();
--- a/src/gl/partrenderer.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/gl/partrenderer.h	Mon Aug 24 23:00:50 2020 +0300
@@ -57,10 +57,12 @@
 	void updateViewMatrix();
 	void updateModelMatrix();
 	void setupBackgroundColor();
+	Q_SLOT void build();
 	static constexpr double MIN_ZOOM = 0.0;
 	static constexpr double MAX_ZOOM = 3.0;
 	double zoom = 1.0;
 	bool initialized = false;
+	bool needBuild = true;
 	void renderVao(const gl::ArrayClass arrayClass);
 	void checkForGLErrors();
 };
--- a/src/linetypes/conditionaledge.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/conditionaledge.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -1,56 +1,10 @@
 #include "conditionaledge.h"
 
-ldraw::ConditionalEdge::ConditionalEdge(
-	const glm::vec3& point_1,
-	const glm::vec3& point_2,
-	const glm::vec3& controlPoint_1,
-	const glm::vec3& controlPoint_2,
-	const Color color_index) :
-	Edge{point_1, point_2, color_index},
-	controlPoint_1{controlPoint_1},
-	controlPoint_2{controlPoint_2}
-{
-}
-
-ldraw::ConditionalEdge::ConditionalEdge(const std::array<glm::vec3, 4>& vertices, const Color color) :
-	Edge{vertices[0], vertices[1], color},
-	controlPoint_1{vertices[2]},
-	controlPoint_2{vertices[3]}
-{
-}
-
-QVariant ldraw::ConditionalEdge::getProperty(Property property) const
-{
-	switch (property)
-	{
-	case Property::ControlPoint1:
-		return QVariant::fromValue(controlPoint_1);
-	case Property::ControlPoint2:
-		return QVariant::fromValue(controlPoint_2);
-	default:
-		return Edge::getProperty(property);
-	}
-}
-
-auto ldraw::ConditionalEdge::setProperty(
-	Property property,
-	const QVariant& value)
-	-> SetPropertyResult
-{
-	switch (property)
-	{
-	case Property::ControlPoint1:
-		controlPoint_1 = value.value<glm::vec3>();
-	case Property::ControlPoint2:
-		controlPoint_2 = value.value<glm::vec3>();
-	default:
-		return Edge::setProperty(property, value);
-	}
-}
-
 QString ldraw::ConditionalEdge::textRepresentation() const
 {
-	return Edge::textRepresentation() + utility::format("%1 %2",
-		utility::vertexToStringParens(controlPoint_1),
-		utility::vertexToStringParens(controlPoint_2));
+	return utility::format("%1 %2 %3 %4",
+		utility::vertexToStringParens(this->points[0]),
+		utility::vertexToStringParens(this->points[1]),
+		utility::vertexToStringParens(this->points[2]),
+		utility::vertexToStringParens(this->points[3]));
 }
--- a/src/linetypes/conditionaledge.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/conditionaledge.h	Mon Aug 24 23:00:50 2020 +0300
@@ -1,27 +1,14 @@
 #pragma once
-#include "edge.h"
+#include "polygonobject.h"
 
 namespace ldraw
 {
 	class ConditionalEdge;
 }
 
-class ldraw::ConditionalEdge : public Edge
+class ldraw::ConditionalEdge : public PolygonObject<4>
 {
 public:
-	ConditionalEdge() = default;
-	ConditionalEdge(
-		const glm::vec3& point_1,
-		const glm::vec3& point_2,
-		const glm::vec3& controlPoint_1,
-		const glm::vec3& controlPoint_2,
-		const Color colorIndex = ldraw::edgeColor);
-	ConditionalEdge(const std::array<glm::vec3, 4>& vertices, const Color color);
-	QVariant getProperty(Property property) const override;
-	SetPropertyResult setProperty(
-		Property property,
-		const QVariant& value) override;
+	using PolygonObject<4>::PolygonObject;
 	QString textRepresentation() const override;
-	glm::vec3 controlPoint_1 = {};
-	glm::vec3 controlPoint_2 = {};
 };
--- a/src/linetypes/edge.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/edge.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -1,52 +1,11 @@
 #include "edge.h"
 
-ldraw::Edge::Edge(
-	const glm::vec3& point_1,
-	const glm::vec3& point_2,
-	const Color color_index) :
-	ColoredObject{color_index},
-	point_1{point_1},
-	point_2{point_2} {}
-
-ldraw::Edge::Edge(const std::array<glm::vec3, 2>& vertices, const Color color) :
-	ColoredObject{color},
-	point_1{vertices[0]},
-	point_2{vertices[1]}
-{
-}
-
-QVariant ldraw::Edge::getProperty(Property property) const
-{
-	switch (property)
-	{
-	case Property::Point1:
-		return QVariant::fromValue(point_1);
-	case Property::Point2:
-		return QVariant::fromValue(point_2);
-	default:
-		return BaseClass::getProperty(property);
-	}
-}
-
-auto ldraw::Edge::setProperty(Property property, const QVariant& value)
-	-> SetPropertyResult
-{
-	switch (property)
-	{
-	case Property::Point1:
-		point_1 = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point2:
-		point_2 = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	default:
-		return BaseClass::setProperty(property, value);
-	}
-}
-
 QString ldraw::Edge::textRepresentation() const
 {
-	return utility::format("%1 %2", utility::vertexToStringParens(point_1), utility::vertexToStringParens(point_2));
+	return utility::format(
+		"%1 %2",
+		utility::vertexToStringParens(this->points[0]),
+		utility::vertexToStringParens(this->points[1]));
 }
 
 void ldraw::Edge::getPolygons(
@@ -54,5 +13,5 @@
 	GetPolygonsContext* context) const
 {
 	Q_UNUSED(context)
-	polygons.push_back(gl::edgeLine(this->point_1, this->point_2, this->colorIndex, this->id));
+	polygons.push_back(gl::edgeLine(this->points[0], this->points[1], this->colorIndex, this->id));
 }
--- a/src/linetypes/edge.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/edge.h	Mon Aug 24 23:00:50 2020 +0300
@@ -1,25 +1,15 @@
 #pragma once
-#include "object.h"
+#include "polygonobject.h"
 
 namespace ldraw
 {
 	class Edge;
 }
 
-class ldraw::Edge : public ColoredObject
+class ldraw::Edge : public PolygonObject<2>
 {
 public:
-	using BaseClass = ColoredObject;
-	Edge() = default;
-	Edge(const glm::vec3& point_1, const glm::vec3& point_2,
-		 const Color colorIndex = ldraw::edgeColor);
-	Edge(const std::array<glm::vec3, 2>& vertices, const Color color);
-	QVariant getProperty(Property property) const override;
-	SetPropertyResult setProperty(
-		Property property,
-		const QVariant& value) override;
+	using PolygonObject::PolygonObject;
 	QString textRepresentation() const override;
 	void getPolygons(std::vector<gl::Polygon>& polygons, GetPolygonsContext* context) const override;
-	glm::vec3 point_1 = {};
-	glm::vec3 point_2 = {};
 };
--- a/src/linetypes/errorline.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/errorline.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -20,22 +20,11 @@
 	}
 }
 
-auto ldraw::ErrorLine::setProperty(
-	Property property,
-	const QVariant& value)
-	-> SetPropertyResult
+void ldraw::ErrorLine::setProperty(SetPropertyResult* result, const PropertyKeyValue& pair)
 {
-	switch (property)
-	{
-	case Property::Text:
-		this->text = value.toString();
-		return SetPropertyResult::Success;
-	case Property::ErrorMessage:
-		this->message = value.toString();
-		return SetPropertyResult::Success;
-	default:
-		return Object::setProperty(property, value);
-	}
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(Text, {this->text = value;});
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(ErrorMessage, {this->message = value;});
+	BaseClass::setProperty(result, pair);
 }
 
 QString ldraw::ErrorLine::textRepresentation() const
--- a/src/linetypes/errorline.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/errorline.h	Mon Aug 24 23:00:50 2020 +0300
@@ -9,14 +9,14 @@
 class ldraw::ErrorLine : public Object
 {
 public:
+	using BaseClass = Object;
 	ErrorLine(QStringView text = u"", QStringView message = u"");
 	QVariant getProperty(Property property) const override;
-	SetPropertyResult setProperty(
-		Property property,
-		const QVariant& value) override;
 	QString textRepresentation() const override;
 	QBrush textRepresentationForeground() const override;
 	QBrush textRepresentationBackground() const override;
 	QString text;
 	QString message;
+protected:
+	void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair) override;
 };
--- a/src/linetypes/metacommand.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/metacommand.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -15,20 +15,14 @@
 	}
 }
 
-auto ldraw::MetaCommand::setProperty(Property property, const QVariant& value)
-	-> SetPropertyResult
+void ldraw::MetaCommand::setProperty(ldraw::Object::SetPropertyResult* result, const PropertyKeyValue& pair)
 {
-	switch (property)
-	{
-	case Property::Text:
-		storedText = value.toString();
-		return SetPropertyResult::Success;
-	default:
-		return Object::setProperty(property, value);
-	}
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(Text, {this->storedText = value;});
+	BaseClass::setProperty(result, pair);
 }
 
 QString ldraw::MetaCommand::textRepresentation() const
 {
 	return this->storedText;
 }
+
--- a/src/linetypes/metacommand.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/metacommand.h	Mon Aug 24 23:00:50 2020 +0300
@@ -8,12 +8,12 @@
 class ldraw::MetaCommand : public Object
 {
 public:
+	using BaseClass = Object;
 	MetaCommand() = default;
 	MetaCommand(QStringView text);
 	QVariant getProperty(Property property) const override;
-	SetPropertyResult setProperty(
-		Property property,
-		const QVariant& value) override;
 	QString textRepresentation() const override;
 	QString storedText = "";
+protected:
+	void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair) override;
 };
--- a/src/linetypes/object.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/object.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -1,6 +1,8 @@
 #include <QBrush>
 #include <QFont>
 #include "object.h"
+#include "widgets/vec3editor.h"
+#include "modeleditcontext.h"
 
 static std::int32_t getIdForNewObject()
 {
@@ -29,12 +31,20 @@
 	return {};
 }
 
-auto ldraw::Object::setProperty(Property id, const QVariant& value)
-	-> SetPropertyResult
+void ldraw::Object::setProperty(SetPropertyResult* result, const PropertyKeyValue& pair)
 {
-	Q_UNUSED(id)
-	Q_UNUSED(value)
-	return SetPropertyResult::PropertyNotHandled;
+	Q_UNUSED(result)
+	Q_UNUSED(pair)
+}
+
+/**
+ * @brief public interface to setProperty
+ */
+ldraw::Object::SetPropertyResult ldraw::Object::setProperty(const PropertyKeyValue& pair)
+{
+	SetPropertyResult result;
+	this->setProperty(&result, pair);
+	return result;
 }
 
 QBrush ldraw::Object::textRepresentationForeground() const
@@ -58,6 +68,12 @@
 	Q_UNUSED(context)
 }
 
+const glm::vec3& ldraw::Object::getPoint(int index) const
+{
+	Q_UNUSED(index);
+	throw BadPointIndex{};
+}
+
 ldraw::ColoredObject::ColoredObject(const Color color_index) :
 	colorIndex{color_index}
 {
@@ -73,34 +89,16 @@
 	switch (id)
 	{
 	case Property::Color:
-		return colorIndex.index;
+		return QVariant::fromValue<Color>(colorIndex);
 	default:
 		return Object::getProperty(id);
 	}
 }
 
-auto ldraw::ColoredObject::setProperty(Property id, const QVariant& value)
-	-> SetPropertyResult
+void ldraw::ColoredObject::setProperty(SetPropertyResult* result, const PropertyKeyValue& pair)
 {
-	switch (id)
-	{
-	case Property::Color:
-		{
-			bool ok;
-			const int value_int = value.toInt(&ok);
-			if (ok)
-			{
-				colorIndex.index = value_int;
-				return SetPropertyResult::Success;
-			}
-			else
-			{
-				return SetPropertyResult::InvalidValue;
-			}
-		}
-	default:
-		return Object::setProperty(id, value);
-	}
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(Color, {colorIndex = value;});
+	Object::setProperty(result, pair);
 }
 
 QString ldraw::Empty::textRepresentation() const
--- a/src/linetypes/object.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/object.h	Mon Aug 24 23:00:50 2020 +0300
@@ -5,11 +5,13 @@
 #include "main.h"
 #include "colors.h"
 #include "gl/common.h"
+#include "linetypes/propertygenerics.h"
+
+class Model;
 
 namespace ldraw
 {
 	struct GetPolygonsContext;
-	enum class Property;
 	class Object;
 	class ColoredObject;
 	class Empty;
@@ -23,88 +25,71 @@
 	::DocumentManager* documents;
 };
 
-/**
- * @brief Different properties that can be queried with getProperty
- */
-enum class ldraw::Property
-{
-	Color, // Color of the object
-	Text, // Text contained in a comment
-	Point1, // First vertex in a polygon or edge line
-	Point2, // Second vertex in a polygon or edge line
-	Point3, // Third vertex in a polygon
-	Point4, // Fourth vertex in a quadrilateral
-	ControlPoint1, // First control point in a conditional edge line
-	ControlPoint2, // Second control point in a conditional edge line
-	Transformation, // 4x4 transformation matrix of a subfile reference
-	ReferenceName, // Subfile reference name
-	IsInverted, // Whether or not the object has been inverted with BFC INVERTNEXT
-	ErrorMessage // For error lines, why parsing failed
-};
-
-// Mapping of properties to types
-#define LDFORGE_DEFINE_PROPERTY_TYPE(PROPERTY, TYPE) \
-	namespace ldraw { \
-		template<> struct PropertyType<ldraw::Property::PROPERTY> { using type = TYPE; }; \
-	}
-
-namespace ldraw
-{
-	template<ldraw::Property property>
-	struct PropertyType
-	{
-	};
-
-	template<ldraw::Property property>
-	using PropertyType_t = typename PropertyType<property>::type;
-}
-
-LDFORGE_DEFINE_PROPERTY_TYPE(Color, int)
-LDFORGE_DEFINE_PROPERTY_TYPE(Text, QString)
-LDFORGE_DEFINE_PROPERTY_TYPE(Point1, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(Point2, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(Point3, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(Point4, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(ControlPoint1, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(ControlPoint2, glm::vec3)
-LDFORGE_DEFINE_PROPERTY_TYPE(Transformation, glm::mat4)
-LDFORGE_DEFINE_PROPERTY_TYPE(ReferenceName, QString)
-LDFORGE_DEFINE_PROPERTY_TYPE(IsInverted, bool)
-LDFORGE_DEFINE_PROPERTY_TYPE(ErrorMessage, QString)
-
 class ldraw::Object
 {
 public:
 	enum class SetPropertyResult
 	{
 		Success = 0,
-		PropertyNotHandled,
-		InvalidValue
+		PropertyNotHandled
+	};
+	friend bool handled(SetPropertyResult result)
+	{
+		return result == SetPropertyResult::Success;
+	}
+	class BadPointIndex : public std::exception
+	{
 	};
 	Object();
 	Object(const Object&) = delete;
 	virtual ~Object();
 	const id_t id;
-	//virtual void toString(QTextStream &out) = 0;
 	virtual bool hasColor() const;
 	virtual QVariant getProperty(Property id) const;
-	virtual SetPropertyResult setProperty(Property id, const QVariant& value);
+	template<ldraw::Property property>
+	SetPropertyResult setProperty(const PropertyType<property>& value);
+	SetPropertyResult setProperty(const PropertyKeyValue& pair);
 	virtual QString textRepresentation() const = 0;
 	virtual QBrush textRepresentationForeground() const;
 	virtual QBrush textRepresentationBackground() const;
 	virtual QFont textRepresentationFont() const;
 	virtual void getPolygons(std::vector<gl::Polygon>& polygons, GetPolygonsContext* context) const;
 	virtual void invert() {}
+	virtual int numPoints() const { return 0; }
+	virtual const glm::vec3& getPoint(int index) const;
+protected:
+	template<Property property, typename Function>
+	void handle(SetPropertyResult* result, const PropertyKeyValue& pair, Function function);
+	virtual void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair);
 };
 
+template<ldraw::Property property>
+ldraw::Object::SetPropertyResult ldraw::Object::setProperty(const ldraw::PropertyType<property>& value)
+{
+	SetPropertyResult result = SetPropertyResult::PropertyNotHandled;
+	this->setProperty(&result, PropertyKeyValue{property, QVariant::fromValue(value)});
+	return result;
+}
+
+template<ldraw::Property property, typename Function>
+void ldraw::Object::handle(SetPropertyResult* result, const PropertyKeyValue& pair, Function function)
+{
+	if (pair.key == property)
+	{
+		function(pair.value.value<ldraw::PropertyType<property>>());
+		*result = SetPropertyResult::Success;
+	}
+}
+
 class ldraw::ColoredObject : public Object
 {
 public:
 	ColoredObject(const Color colorIndex = ldraw::mainColor);
 	bool hasColor() const override final;
 	QVariant getProperty(Property id) const override;
-	SetPropertyResult setProperty(Property id, const QVariant& value) override;
 	Color colorIndex = ldraw::mainColor;
+protected:
+	void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair) override;
 };
 
 /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/linetypes/polygonobject.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,70 @@
+#pragma once
+#include "object.h"
+#include "widgets/vec3editor.h"
+#include "model.h"
+#include "modeleditcontext.h"
+
+namespace ldraw
+{
+	template<int N, typename>
+	class PolygonObject;
+}
+
+template<int N, typename = std::enable_if_t<(N > 0 and N <= 4)>>
+class ldraw::PolygonObject : public ColoredObject
+{
+public:
+	using BaseClass = ColoredObject;
+	PolygonObject(const std::array<glm::vec3, N>& points, const Color color) :
+		ColoredObject{color},
+		points{points} {}
+	int numPoints() const override
+	{
+		return N;
+	}
+	const glm::vec3& getPoint(int index) const override
+	{
+		Q_ASSERT(index >= 0 and index < N);
+		return this->points[index];
+	}
+	QVariant getProperty(const Property id) const override
+	{
+		switch (id)
+		{
+		case Property::Point0:
+			return QVariant::fromValue(points[0]);
+		case Property::Point1:
+			return QVariant::fromValue(points[1]);
+		case Property::Point2:
+			if (N >= 3)
+			{
+				return QVariant::fromValue(points[2]);
+			}
+			break;
+		case Property::Point3:
+			if (N >= 4)
+			{
+				return QVariant::fromValue(points[3]);
+			}
+			break;
+		default:
+			break;
+		}
+		return BaseClass::getProperty(id);
+	}
+	void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair)
+	{
+		LDRAW_OBJECT_HANDLE_SET_PROPERTY(Point0, {points[0] = value;})
+		LDRAW_OBJECT_HANDLE_SET_PROPERTY(Point1, {points[1] = value;})
+		if constexpr (N >= 3)
+		{
+			LDRAW_OBJECT_HANDLE_SET_PROPERTY(Point2, {points[2] = value;})
+		}
+		if constexpr (N >= 4)
+		{
+			LDRAW_OBJECT_HANDLE_SET_PROPERTY(Point3, {points[3] = value;})
+		}
+		ColoredObject::setProperty(result, pair);
+	}
+	std::array<glm::vec3, N> points;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/linetypes/propertygenerics.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,153 @@
+#pragma once
+#include "main.h"
+#include "colors.h"
+
+class Vec3Editor;
+class MatrixEditor;
+
+namespace ldraw
+{
+	enum class Property;
+	struct PropertyKeyValue;
+	template<Property property>
+	struct PropertyTraits
+	{
+		static constexpr bool defined = false;
+	};
+}
+
+/**
+ * Different properties
+ */
+enum class ldraw::Property
+{
+	Color, // Color of the object
+	Text, // Text contained in a comment
+	Point0, // First vertex in a polygon or edge line
+	Point1, // Second vertex in a polygon or edge line
+	Point2, // Third vertex in a polygon
+	Point3, // Fourth vertex in a quadrilateral
+	Transformation, // 4x4 transformation matrix of a subfile reference
+	ReferenceName, // Subfile reference name
+	IsInverted, // Whether or not the object has been inverted with BFC INVERTNEXT
+	ErrorMessage // For error lines, why parsing failed
+};
+
+Q_DECLARE_METATYPE(ldraw::Property)
+
+// Mapping of properties to types
+#define LDFORGE_DEFINE_PROPERTY_TYPE(PROPERTY, TYPE) \
+	namespace ldraw \
+	{ \
+		template<> struct PropertyTraits<ldraw::Property::PROPERTY> \
+		{ \
+			using type = TYPE; \
+			static constexpr std::array<char, 256> name{#PROPERTY}; \
+			static constexpr bool defined = true; \
+		}; \
+	}
+
+LDFORGE_DEFINE_PROPERTY_TYPE(Color, ldraw::Color)
+LDFORGE_DEFINE_PROPERTY_TYPE(Text, QString)
+LDFORGE_DEFINE_PROPERTY_TYPE(Point0, glm::vec3)
+LDFORGE_DEFINE_PROPERTY_TYPE(Point1, glm::vec3)
+LDFORGE_DEFINE_PROPERTY_TYPE(Point2, glm::vec3)
+LDFORGE_DEFINE_PROPERTY_TYPE(Point3, glm::vec3)
+LDFORGE_DEFINE_PROPERTY_TYPE(Transformation, glm::mat4)
+LDFORGE_DEFINE_PROPERTY_TYPE(ReferenceName, QString)
+LDFORGE_DEFINE_PROPERTY_TYPE(IsInverted, bool)
+LDFORGE_DEFINE_PROPERTY_TYPE(ErrorMessage, QString)
+
+#define LDRAW_OBJECT_HANDLE_SET_PROPERTY(PROPERTY, HANDLER) \
+	{this->handle<ldraw::Property::PROPERTY>(result, pair, \
+		[&](const ldraw::PropertyType<ldraw::Property::PROPERTY>& value) HANDLER);}
+
+// Generics
+namespace ldraw
+{
+	template<ldraw::Property property>
+	using PropertyType = typename PropertyTraits<property>::type;
+
+	template<ldraw::Property property>
+	inline const char* PROPERTY_NAME = PropertyTraits<property>::name;
+
+	struct PropertyKeyValue
+	{
+		Property key;
+		QVariant value;
+	};
+
+	constexpr Property pointProperty(int n)
+	{
+		Q_ASSERT(n >= 0 and n < 4);
+		return static_cast<Property>(static_cast<int>(Property::Point0) + n);
+	}
+
+	struct PropertyTrait
+	{
+		ldraw::Property property;
+		std::array<char, 256> name;
+		int type;
+	};
+
+	namespace detail
+	{
+		template<int N>
+		constexpr int propertyCountHelper()
+		{
+			if constexpr (ldraw::PropertyTraits<static_cast<Property>(N)>::defined)
+			{
+				return propertyCountHelper<N + 1>();
+			}
+			else
+			{
+				return N;
+			}
+		}
+
+		template<int k>
+		constexpr PropertyTrait getPropertyTrait()
+		{
+			constexpr auto property = static_cast<ldraw::Property>(k);
+			using trait = ldraw::PropertyTraits<property>;
+			return PropertyTrait{
+				property,
+				trait::name,
+				qMetaTypeId<typename trait::type>()
+			};
+		}
+
+		template<int... Ints>
+		auto getPropertyTraits(std::integer_sequence<int, Ints...>)
+		{
+			return std::array<PropertyTrait, sizeof...(Ints)>{getPropertyTrait<Ints>()...};
+		}
+	}
+
+	constexpr int NUM_PROPERTIES = detail::propertyCountHelper<0>();
+	inline const auto& traits()
+	{
+		static std::array<PropertyTrait, NUM_PROPERTIES> result =
+			detail::getPropertyTraits(std::make_integer_sequence<int, NUM_PROPERTIES>());
+		return result;
+	}
+
+	inline const auto& traits(ldraw::Property property)
+	{
+		return traits()[static_cast<int>(property)];
+	}
+
+	template<typename T, std::size_t... Ints>
+	constexpr auto makeIndexArray(std::index_sequence<Ints...>)
+	{
+		return std::array{static_cast<T>(Ints)...};
+	}
+
+	constexpr auto ALL_PROPERTIES = makeIndexArray<Property>(std::make_index_sequence<NUM_PROPERTIES>{});
+
+	template<typename T>
+	bool testPropertyType(ldraw::Property property)
+	{
+		return qMetaTypeId<T>() == ldraw::traits(property).type;
+	}
+}
--- a/src/linetypes/quadrilateral.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/quadrilateral.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -1,70 +1,12 @@
 #include "quadrilateral.h"
 
-ldraw::Quadrilateral::Quadrilateral(
-	const glm::vec3& point_1,
-	const glm::vec3& point_2,
-	const glm::vec3& point_3,
-	const glm::vec3& point_4,
-	Color color_index) :
-	ColoredObject{color_index},
-	points{point_1, point_2, point_3, point_4}
-{
-}
-
-ldraw::Quadrilateral::Quadrilateral(const std::array<glm::vec3, 4>& vertices, const Color color) :
-	ColoredObject{color},
-	points{vertices[0], vertices[1], vertices[2], vertices[3]}
-{
-}
-
-QVariant ldraw::Quadrilateral::getProperty(const Property id) const
-{
-	switch (id)
-	{
-	case Property::Point1:
-		return QVariant::fromValue(points[0]);
-	case Property::Point2:
-		return QVariant::fromValue(points[1]);
-	case Property::Point3:
-		return QVariant::fromValue(points[2]);
-	case Property::Point4:
-		return QVariant::fromValue(points[3]);
-	default:
-		return ColoredObject::getProperty(id);
-	}
-}
-
-auto ldraw::Quadrilateral::setProperty(
-	const Property id,
-	const QVariant& value)
-	-> SetPropertyResult
-{
-	switch (id)
-	{
-	case Property::Point1:
-		points[0] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point2:
-		points[1] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point3:
-		points[2] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point4:
-		points[3] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	default:
-		return ColoredObject::setProperty(id, value);
-	}
-}
-
 QString ldraw::Quadrilateral::textRepresentation() const
 {
 	return utility::format("%1 %2 %3 %4",
-		utility::vertexToStringParens(points[0]),
-		utility::vertexToStringParens(points[1]),
-		utility::vertexToStringParens(points[2]),
-		utility::vertexToStringParens(points[3]));
+		utility::vertexToStringParens(this->points[0]),
+		utility::vertexToStringParens(this->points[1]),
+		utility::vertexToStringParens(this->points[2]),
+		utility::vertexToStringParens(this->points[3]));
 }
 
 void ldraw::Quadrilateral::getPolygons(
@@ -78,7 +20,7 @@
 		this->points[2],
 		this->points[3],
 		this->colorIndex,
-			this->id));
+		this->id));
 }
 
 void ldraw::Quadrilateral::invert()
--- a/src/linetypes/quadrilateral.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/quadrilateral.h	Mon Aug 24 23:00:50 2020 +0300
@@ -1,26 +1,16 @@
 #pragma once
-#include "object.h"
+#include "polygonobject.h"
 
 namespace ldraw
 {
 	class Quadrilateral;
 }
 
-class ldraw::Quadrilateral : public ColoredObject
+class ldraw::Quadrilateral : public PolygonObject<4>
 {
 public:
-	Quadrilateral() = default;
-	Quadrilateral(
-		const glm::vec3 &point_1,
-		const glm::vec3 &point_2,
-		const glm::vec3 &point_3,
-		const glm::vec3 &point_4,
-		Color colorIndex = ldraw::mainColor);
-	Quadrilateral(const std::array<glm::vec3, 4>& vertices, const Color color);
-	QVariant getProperty(Property id) const override;
-	SetPropertyResult setProperty(Property id, const QVariant& value) override;
+	using PolygonObject<4>::PolygonObject;
 	QString textRepresentation() const override;
 	void getPolygons(std::vector<gl::Polygon>& polygons, GetPolygonsContext* context) const override;
 	void invert() override;
-	glm::vec3 points[4] = {{}};
 };
--- a/src/linetypes/subfilereference.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/subfilereference.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -2,9 +2,12 @@
 #include "documentmanager.h"
 #include "invert.h"
 
-ldraw::SubfileReference::SubfileReference(const glm::mat4& transformation,
+ldraw::SubfileReference::SubfileReference
+(
+	const glm::mat4& transformation,
 	const QString& referenceName,
-	const Color color) :
+	const Color color
+) :
 	ColoredObject{color},
 	transformation{transformation},
 	referenceName{referenceName}
@@ -24,22 +27,11 @@
 	}
 }
 
-auto ldraw::SubfileReference::setProperty(
-	Property property,
-	const QVariant& value)
-	-> SetPropertyResult
+void ldraw::SubfileReference::setProperty(SetPropertyResult* result, const PropertyKeyValue& pair)
 {
-	switch (property)
-	{
-	case Property::Transformation:
-		this->transformation = value.value<glm::mat4>();
-		return SetPropertyResult::Success;
-	case Property::ReferenceName:
-		this->referenceName = value.toString();
-		return SetPropertyResult::Success;
-	default:
-		return ColoredObject::setProperty(property, value);
-	}
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(Transformation, {this->transformation = value;});
+	LDRAW_OBJECT_HANDLE_SET_PROPERTY(ReferenceName, {this->referenceName = value;});
+	ldraw::ColoredObject::setProperty(result, pair);
 }
 
 QString ldraw::SubfileReference::textRepresentation() const
@@ -47,9 +39,11 @@
 	return referenceName + " " + utility::vertexToStringParens(this->position());
 }
 
-void ldraw::SubfileReference::getPolygons(
+void ldraw::SubfileReference::getPolygons
+(
 	std::vector<gl::Polygon>& polygons,
-	GetPolygonsContext* context) const
+	GetPolygonsContext* context
+) const
 {
 	Model* model = this->resolve(context->documents);
 	if (model != nullptr)
@@ -81,7 +75,7 @@
 
 glm::vec3 ldraw::SubfileReference::position() const
 {
-	return {this->transformation[3][0], this->transformation[3][1], this->transformation[3][2]};
+	return this->transformation[3];
 }
 
 void ldraw::SubfileReference::invert()
--- a/src/linetypes/subfilereference.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/subfilereference.h	Mon Aug 24 23:00:50 2020 +0300
@@ -17,7 +17,6 @@
 		const QString &referenceName,
 		const Color color = ldraw::mainColor);
 	QVariant getProperty(Property property) const override;
-	SetPropertyResult setProperty(Property property, const QVariant& value) override;
 	QString textRepresentation() const override;
 	void getPolygons(std::vector<gl::Polygon>& polygons, GetPolygonsContext* context) const override;
 	glm::vec3 position() const;
@@ -26,4 +25,6 @@
 	glm::mat4 transformation;
 	QString referenceName;
 	bool isInverted = false;
+protected:
+	void setProperty(SetPropertyResult* result, const PropertyKeyValue& pair) override;
 };
--- a/src/linetypes/triangle.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/triangle.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -1,61 +1,5 @@
 #include "triangle.h"
 
-ldraw::Triangle::Triangle(
-	const glm::vec3& point_1,
-	const glm::vec3& point_2,
-	const glm::vec3& point_3,
-	Color color_index) :
-	ColoredObject{color_index},
-	points{point_1, point_2, point_3}
-{
-}
-
-ldraw::Triangle::Triangle(const std::array<glm::vec3, 3>& vertices, const Color color) :
-	ColoredObject{color},
-	points{vertices[0], vertices[1], vertices[2]}
-{
-}
-
-ldraw::Triangle::Triangle(const glm::vec3 (&vertices)[3], const Color color) :
-	ColoredObject{color},
-	points{vertices[0], vertices[1], vertices[2]}
-{
-}
-
-QVariant ldraw::Triangle::getProperty(const Property id) const
-{
-	switch (id)
-	{
-	case Property::Point1:
-		return QVariant::fromValue(points[0]);
-	case Property::Point2:
-		return QVariant::fromValue(points[1]);
-	case Property::Point3:
-		return QVariant::fromValue(points[2]);
-	default:
-		return ColoredObject::getProperty(id);
-	}
-}
-
-auto ldraw::Triangle::setProperty(Property id, const QVariant& value)
-	-> SetPropertyResult
-{
-	switch (id)
-	{
-	case Property::Point1:
-		points[0] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point2:
-		points[1] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	case Property::Point3:
-		points[2] = value.value<glm::vec3>();
-		return SetPropertyResult::Success;
-	default:
-		return ColoredObject::setProperty(id, value);
-	}
-}
-
 QString ldraw::Triangle::textRepresentation() const
 {
 	return utility::format("%1 %2 %3",
@@ -74,7 +18,7 @@
 		this->points[1],
 		this->points[2],
 		this->colorIndex,
-			this->id));
+		this->id));
 }
 
 void ldraw::Triangle::invert()
--- a/src/linetypes/triangle.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/linetypes/triangle.h	Mon Aug 24 23:00:50 2020 +0300
@@ -1,27 +1,17 @@
 #pragma once
-#include "object.h"
+#include "polygonobject.h"
 
 namespace ldraw
 {
 	class Triangle;
 }
 
-class ldraw::Triangle : public ColoredObject
+class ldraw::Triangle : public PolygonObject<3>
 {
 public:
-	Triangle() = default;
-	Triangle(
-		const glm::vec3 &point_1,
-		const glm::vec3 &point_2,
-		const glm::vec3 &point_3,
-		Color colorIndex = ldraw::mainColor);
-	Triangle(const std::array<glm::vec3, 3>& vertices, const Color color);
-	Triangle(const glm::vec3 (&vertices)[3], const Color color);
-	QVariant getProperty(Property id) const override;
-	SetPropertyResult setProperty(Property id, const QVariant& value) override;
+	using PolygonObject<3>::PolygonObject;
 	QString textRepresentation() const override;
 	void getPolygons(std::vector<gl::Polygon>& polygons, GetPolygonsContext* context) const override;
 	void invert() override;
-	glm::vec3 points[3] = {{}};
 };
 
--- a/src/main.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/main.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -19,6 +19,8 @@
 #include <QApplication>
 #include "mainwindow.h"
 #include "version.h"
+#include <iostream>
+#include <QMessageBox>
 
 int main(int argc, char *argv[])
 {
@@ -28,6 +30,9 @@
 	::qRegisterMetaTypeStreamOperators<Library>("Library");
 	::qRegisterMetaTypeStreamOperators<Libraries>("Libraries");
 	QApplication app{argc, argv};
+	/*
+	QMessageBox::information(nullptr, "", QMetaType::typeName( qMetaTypeId<ldraw::Color>() ));
+	*/
 	MainWindow mainwindow;
 	mainwindow.show();
 	return app.exec();
--- a/src/main.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/main.h	Mon Aug 24 23:00:50 2020 +0300
@@ -74,13 +74,14 @@
 	using conditionaledgeid_t = Id<class ConditionalEdge>;
 	using subfileid_t = Id<class SubfileReference>;
 
-	constexpr struct
+	constexpr struct NullId
 	{
 		template<typename T>
 		constexpr operator Id<T>() const
 		{
 			return Id<T>{0};
 		}
+		static constexpr decltype(ldraw::id_t::value) value = 0;
 	} NULL_ID = {};
 
 	template<typename T>
--- a/src/mainwindow.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/mainwindow.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -43,6 +43,7 @@
 	{ 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
--- a/src/mainwindow.ui	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/mainwindow.ui	Mon Aug 24 23:00:50 2020 +0300
@@ -26,7 +26,7 @@
      <x>0</x>
      <y>0</y>
      <width>800</width>
-     <height>22</height>
+     <height>24</height>
     </rect>
    </property>
    <widget class="QMenu" name="menuFile">
@@ -53,6 +53,7 @@
     <addaction name="actionRenderStyleNormal"/>
     <addaction name="actionRenderStyleBfc"/>
     <addaction name="actionRenderStyleRandom"/>
+    <addaction name="actionRenderStylePickScene"/>
    </widget>
    <addaction name="menuFile"/>
    <addaction name="menuView"/>
@@ -108,6 +109,14 @@
     <string>Random colours</string>
    </property>
   </action>
+  <action name="actionRenderStylePickScene">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Pick scene colours</string>
+   </property>
+  </action>
  </widget>
  <resources/>
  <connections/>
--- a/src/model.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/model.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -22,7 +22,10 @@
 #include "modeleditcontext.h"
 
 Model::Model(QObject* parent) :
-	QAbstractListModel{parent} {}
+	QAbstractListModel{parent}
+{
+	connect(this, &Model::dataChanged, [&](){ this->needRecache = true; });
+}
 
 int Model::size() const
 {
--- a/src/model.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/model.h	Mon Aug 24 23:00:50 2020 +0300
@@ -135,5 +135,12 @@
 	{
 		*index_out = index;
 	}
-	return static_cast<const R*>(this->objectAt(index));
+	if (index.isValid())
+	{
+		return static_cast<const R*>(this->objectAt(index));
+	}
+	else
+	{
+		return nullptr;
+	}
 }
--- a/src/modeleditcontext.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/modeleditcontext.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -17,12 +17,23 @@
  */
 
 #include "modeleditcontext.h"
+#include "linetypes/triangle.h"
+#include "linetypes/quadrilateral.h"
 
 Model::EditContext::EditContext(Model& model) :
 	storedModel{model}
 {
 }
 
+Model::EditContext::~EditContext()
+{
+	for (ldraw::id_t id : this->modifiedObjects)
+	{
+		const QModelIndex index = this->model().lookup(id);
+		emit this->model().dataChanged(index, index);
+	}
+}
+
 ldraw::id_t Model::EditContext::append(std::unique_ptr<ldraw::Object>&& object)
 {
 	const ldraw::id_t id = object->id;
@@ -36,15 +47,32 @@
 	this->model().remove(position);
 }
 
-void Model::EditContext::setObjectProperty(
-	ldraw::id_t id,
-	ldraw::Property property,
+auto Model::EditContext::setObjectProperty(
+	const ldraw::id_t id,
+	const ldraw::Property property,
 	const QVariant& value)
+	-> ldraw::Object::SetPropertyResult
+{
+	ldraw::Object* const object = this->model().objectAt(id);
+	if (object != nullptr)
+	{
+		const ldraw::Object::SetPropertyResult result = object->setProperty(ldraw::PropertyKeyValue{property, value});
+		modifiedObjects.insert(id);
+		return result;
+	}
+	else
+	{
+		return ldraw::Object::SetPropertyResult::PropertyNotHandled;
+	}
+}
+
+void Model::EditContext::setObjectPoint(ldraw::id_t id, int pointId, const glm::vec3& value)
 {
 	ldraw::Object* object = this->model().objectAt(id);
 	if (object != nullptr)
 	{
-		object->setProperty(property, value);
+		object->setProperty(ldraw::PropertyKeyValue{ldraw::pointProperty(pointId), QVariant::fromValue(value)});
+		modifiedObjects.insert(id);
 	}
 }
 
@@ -63,7 +91,7 @@
 	return this->storedModel;
 }
 
-static std::array<geom::Triangle, 2> splitTriangles(ldraw::Diagonal diagonal, const glm::vec3(&points)[4])
+static std::array<geom::Triangle, 2> splitTriangles(ldraw::Diagonal diagonal, const std::array<glm::vec3, 4>& points)
 {
 	std::array<geom::Triangle, 2> result;
 	switch (diagonal)
--- a/src/modeleditcontext.h	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/modeleditcontext.h	Mon Aug 24 23:00:50 2020 +0300
@@ -18,31 +18,42 @@
 
 #pragma once
 #include "model.h"
-#include "linetypes/object.h"
-#include "linetypes/quadrilateral.h"
-#include "linetypes/triangle.h"
 
 class Model::EditContext
 {
 public:
+	~EditContext();
 	template<typename T, typename... Args>
 	ldraw::Id<T> append(Args&&... args);
 	ldraw::id_t append(std::unique_ptr<ldraw::Object>&& object);
 	template<typename T, typename... Args>
 	ldraw::Id<T> insert(int position, Args&&... args);
 	void remove(int position);
-	void setObjectProperty(
-		ldraw::id_t object,
-		ldraw::Property property,
-		const QVariant &value);
+	template<ldraw::Property property>
+	void setObjectProperty(ldraw::id_t id, const ldraw::PropertyType<property>& value);
+	auto setObjectProperty(ldraw::id_t id, ldraw::Property property, const QVariant& value)
+		-> ldraw::Object::SetPropertyResult;
+	void setObjectPoint(ldraw::id_t id, int pointId, const glm::vec3& value);
 	void invertObject(ldraw::id_t id);
 	Model& model();
 private:
 	EditContext(Model& model);
 	friend class Model;
+	QSet<ldraw::id_t> modifiedObjects;
 	Model& storedModel;
 };
 
+template<ldraw::Property Property>
+void Model::EditContext::setObjectProperty(const ldraw::id_t id, const ldraw::PropertyType<Property>& value)
+{
+	ldraw::Object* object = this->model().objectAt(id);
+	if (object != nullptr)
+	{
+		object->setProperty<Property>(value);
+		modifiedObjects.insert(id);
+	}
+}
+
 template<typename T, typename... Args>
 ldraw::Id<T> Model::EditContext::append(Args&&... args)
 {
--- a/src/ui/canvas.cpp	Mon Aug 24 22:55:37 2020 +0300
+++ b/src/ui/canvas.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -154,84 +154,87 @@
 void Canvas::paintGL()
 {
 	PartRenderer::paintGL();
-	// Render axes
-	{
-		glLineWidth(5);
-		glEnable(GL_LINE_SMOOTH);
-		glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
-		this->axesProgram->draw();
-		glDisable(GL_LINE_SMOOTH);
-	}
-	// Render grid
-	{
-		glEnable(GL_BLEND);
-		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-		this->gridProgram->draw();
-		glDisable(GL_BLEND);
-	}
-	if (this->worldPosition.has_value())
+	if (this->renderPreferences.style != gl::RenderStyle::PickScene)
 	{
-		QPainter painter{this};
-		painter.setRenderHint(QPainter::Antialiasing);
-		painter.setPen(Qt::black);
-		painter.setBrush(Qt::green);
-		const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition);
-		painter.drawEllipse(pos, 5, 5);
-		painter.setPen(Qt::white);
-		painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
-	}
-	{
-		QPainter painter{this};
-		QFont font;
-		//font.setStyle(QFont::StyleItalic);
-		painter.setFont(font);
-		QFontMetrics fontMetrics{font};
-		const auto renderText = [&](const QString& text, const geom::PointOnRectagle& intersection)
+		// Render axes
+		{
+			glLineWidth(5);
+			glEnable(GL_LINE_SMOOTH);
+			glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
+			this->axesProgram->draw();
+			glDisable(GL_LINE_SMOOTH);
+		}
+		// Render grid
+		{
+			glEnable(GL_BLEND);
+			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+			this->gridProgram->draw();
+			glDisable(GL_BLEND);
+		}
+		if (this->worldPosition.has_value())
 		{
-			QPointF position = toQPointF(intersection.position);
-			const geom::RectangleSide side = intersection.side;
-			switch (side)
+			QPainter painter{this};
+			painter.setRenderHint(QPainter::Antialiasing);
+			painter.setPen(Qt::black);
+			painter.setBrush(Qt::green);
+			const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition);
+			painter.drawEllipse(pos, 5, 5);
+			painter.setPen(Qt::white);
+			painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
+		}
+		{
+			QPainter painter{this};
+			QFont font;
+			//font.setStyle(QFont::StyleItalic);
+			painter.setFont(font);
+			QFontMetrics fontMetrics{font};
+			const auto renderText = [&](const QString& text, const geom::PointOnRectagle& intersection)
 			{
-			case geom::RectangleSide::Top:
-				position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
-				break;
-			case geom::RectangleSide::Left:
-				break;
-			case geom::RectangleSide::Bottom:
-				position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
-				break;
-			case geom::RectangleSide::Right:
-				position += QPointF{static_cast<qreal>(-fontMetrics.width(text)), 0};
-				break;
-			}
-			painter.drawText(position, text);
-		};
-		const QRectF box {
-			QPointF{0, 0},
-			QPointF{static_cast<qreal>(this->width()), static_cast<qreal>(this->height())}
-		};
-		const QPointF p1 = this->modelToScreenCoordinates(glm::vec3{0, 0, 0});
+				QPointF position = toQPointF(intersection.position);
+				const geom::RectangleSide side = intersection.side;
+				switch (side)
+				{
+				case geom::RectangleSide::Top:
+					position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
+					break;
+				case geom::RectangleSide::Left:
+					break;
+				case geom::RectangleSide::Bottom:
+					position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
+					break;
+				case geom::RectangleSide::Right:
+					position += QPointF{static_cast<qreal>(-fontMetrics.width(text)), 0};
+					break;
+				}
+				painter.drawText(position, text);
+			};
+			const QRectF box {
+				QPointF{0, 0},
+				QPointF{static_cast<qreal>(this->width()), static_cast<qreal>(this->height())}
+			};
+			const QPointF p1 = this->modelToScreenCoordinates(glm::vec3{0, 0, 0});
 
-		static const struct
-		{
-			QString text;
-			glm::vec3 direction;
-		} directions[] =
-		{
-			{"+𝑥", {1, 0, 0}},
-			{"-𝑥", {-1, 0, 0}},
-			{"+𝑦", {0, 1, 0}},
-			{"-𝑦", {0, -1, 0}},
-			{"+𝑧", {0, 0, 1}},
-			{"-𝑧", {0, 0, -1}},
-		};
-		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)), box);
-			if (intersection.has_value())
+			static const struct
+			{
+				QString text;
+				glm::vec3 direction;
+			} directions[] =
 			{
-				renderText(axis.text, *intersection);
+				{"+𝑥", {1, 0, 0}},
+				{"-𝑥", {-1, 0, 0}},
+				{"+𝑦", {0, 1, 0}},
+				{"-𝑦", {0, -1, 0}},
+				{"+𝑧", {0, 0, 1}},
+				{"-𝑧", {0, 0, -1}},
+			};
+			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)), box);
+				if (intersection.has_value())
+				{
+					renderText(axis.text, *intersection);
+				}
 			}
 		}
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/multiplyfactordialog.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,94 @@
+#include "multiplyfactordialog.h"
+#include "ui_multiplyfactordialog.h"
+
+MultiplyFactorDialog::MultiplyFactorDialog(const glm::vec3& baseVector, QWidget* parent) :
+	QDialog{parent},
+	baseVector{baseVector},
+	preview{baseVector, parent, Vec3Editor::NoMultiplyButton}
+{
+	ui = std::make_unique<Ui::MultiplyFactorDialog>();
+	ui->setupUi(this);
+	this->preview.setEnabled(false);
+	this->ui->previewGroupBox->setLayout(new QVBoxLayout{parent});
+	this->ui->previewGroupBox->layout()->addWidget(&this->preview);
+	connect(this->ui->invert, &QCheckBox::clicked, this, &MultiplyFactorDialog::updatePreview);
+	connect(this->ui->factor, qOverload<double>(&DoubleSpinBox::valueChanged), this, &MultiplyFactorDialog::updatePreview);
+}
+
+/**
+ * @brief empty destructor, necessary because std::unique_ptr is used with a forward declaration
+ */
+MultiplyFactorDialog::~MultiplyFactorDialog()
+{
+}
+
+/**
+ * @brief Computes the resulting vector
+ * @return the input vector multiplied by the specified vector
+ */
+glm::vec3 MultiplyFactorDialog::value() const
+{
+	glm::vec3 result = baseVector;
+	if (this->ui->invert->isChecked())
+	{
+		if (qFuzzyIsNull(this->ui->factor->value()))
+		{
+			constexpr double infinity = std::numeric_limits<double>::quiet_NaN();
+			result = {infinity, infinity, infinity};
+		}
+		else
+		{
+			result /= this->ui->factor->value();
+		}
+	}
+	else
+	{
+		result *= this->ui->factor->value();
+	}
+	return result;
+}
+
+/**
+ * @brief Makes a string that is prefixed to the factor input.
+ * @param ui
+ * @return prefix string
+ */
+QString prefixForFactorInput(const Ui::MultiplyFactorDialog& ui)
+{
+	if (ui.invert->isChecked())
+	{
+		return "1 : ";
+	}
+	else
+	{
+		return "";
+	}
+}
+
+/**
+ * @brief Makes a string that is suffixed to the factor input.
+ * @param ui
+ * @return prefix string
+ */
+QString suffixForFactorInput(const Ui::MultiplyFactorDialog& ui)
+{
+	if (ui.invert->isChecked())
+	{
+		// render the actual factor that stuff gets effectively multiplied by
+		return " = " + QString::number(1.0 / (ui.factor->value()));
+	}
+	else
+	{
+		return "";
+	}
+}
+
+/**
+ * @brief Responds to changes in the value and updates previews accordingly
+ */
+void MultiplyFactorDialog::updatePreview()
+{
+	this->ui->factor->setPrefix(::prefixForFactorInput(*this->ui));
+	this->ui->factor->setSuffix(::suffixForFactorInput(*this->ui));
+	this->preview.setValue(this->value());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/multiplyfactordialog.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,23 @@
+#pragma once
+#include <QDialog>
+#include "../main.h"
+#include "../widgets/vec3editor.h"
+
+namespace Ui
+{
+	class MultiplyFactorDialog;
+}
+
+class MultiplyFactorDialog : public QDialog
+{
+	Q_OBJECT
+public:
+	explicit MultiplyFactorDialog(const glm::vec3& baseVector = glm::vec3{}, QWidget *parent = nullptr);
+	~MultiplyFactorDialog();
+	glm::vec3 value() const;
+private:
+	Q_SLOT void updatePreview();
+	std::unique_ptr<Ui::MultiplyFactorDialog> ui;
+	const glm::vec3 baseVector;
+	Vec3Editor preview;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/multiplyfactordialog.ui	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MultiplyFactorDialog</class>
+ <widget class="QDialog" name="MultiplyFactorDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>286</width>
+    <height>169</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Multiply with a scalar</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Factor:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="DoubleSpinBox" name="factor">
+       <property name="value">
+        <double>1.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QCheckBox" name="invert">
+       <property name="text">
+        <string>Invert</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="previewGroupBox">
+     <property name="title">
+      <string>Preview</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>DoubleSpinBox</class>
+   <extends>QDoubleSpinBox</extends>
+   <header>widgets/doublespinbox.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>MultiplyFactorDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>MultiplyFactorDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/objecteditor.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,32 @@
+#include <QVBoxLayout>
+#include "objecteditor.h"
+
+ObjectEditor::ObjectEditor(Model* model, const ldraw::id_t id, QWidget *parent) :
+	QWidget{parent},
+	model{model}
+{
+	this->setObjectId(id);
+	this->setLayout(new QVBoxLayout{this});
+}
+
+void ObjectEditor::setObjectId(const ldraw::id_t id)
+{
+	this->objectId = id;
+	const ldraw::Object* object = this->model->get(id);
+	if (object != nullptr and object->numPoints() > 0)
+	{
+		if (not this->polygonEditor.has_value())
+		{
+			this->polygonEditor.emplace(this->model, id);
+			this->layout()->addWidget(&*this->polygonEditor);
+		}
+		else
+		{
+			this->polygonEditor->setObjectId(id);
+		}
+	}
+	else
+	{
+		this->polygonEditor.reset();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/objecteditor.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,17 @@
+#pragma once
+#include <QWidget>
+#include "../main.h"
+#include "../model.h"
+#include "polygonobjecteditor.h"
+
+class ObjectEditor : public QWidget
+{
+	Q_OBJECT
+public:
+	explicit ObjectEditor(Model* model, ldraw::id_t id = ldraw::NULL_ID, QWidget* parent = nullptr);
+	void setObjectId(ldraw::id_t id);
+private:
+	Model* const model;
+	ldraw::id_t objectId = ldraw::NULL_ID;
+	std::optional<PolygonObjectEditor> polygonEditor;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/polygonobjecteditor.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,77 @@
+#include <QFormLayout>
+#include <QSplitter>
+#include "model.h"
+#include "modeleditcontext.h"
+#include "widgets/vec3editor.h"
+#include "ui/polygonobjecteditor.h"
+
+static constexpr char INDEX_NAME[] = "_ldforge_index";
+static constexpr char PROPERTY_NAME[] = "_ldforge_property";
+static constexpr char OBJECT_ID_NAME[] = "_ldforge_id";
+static constexpr char LABEL_NAME[] = "_ldforge_label";
+
+PolygonObjectEditor::PolygonObjectEditor(Model* model, ldraw::id_t id, QWidget* parent) :
+	QWidget{parent},
+	model{model},
+	storedObjectId{ldraw::NULL_ID.value}
+{
+	this->splitter.emplace(Qt::Vertical, this);
+	this->setObjectId(id);
+}
+
+// destructor needed for std::unique_ptr
+PolygonObjectEditor::~PolygonObjectEditor()
+{
+}
+
+ldraw::id_t PolygonObjectEditor::objectId() const
+{
+	return this->storedObjectId;
+}
+
+void PolygonObjectEditor::setObjectId(ldraw::id_t id)
+{
+	this->storedObjectId = id;
+	this->buildWidgets();
+}
+
+void PolygonObjectEditor::buildWidgets()
+{
+	this->widgets.clear();
+	delete this->layout();
+	QFormLayout* layout = new QFormLayout{this};
+	this->setLayout(layout);
+	for (int n : {0, 1, 2, 3})
+	{
+		this->setupPointWidget(n);
+	}
+	for (std::unique_ptr<QWidget>& widget : this->widgets)
+	{
+		const QString label = widget->property(LABEL_NAME).toString();
+		layout->addRow(label, widget.get());
+	}
+	layout->addRow("", &*this->splitter);
+}
+
+void PolygonObjectEditor::setupPointWidget(int n)
+{
+	const ldraw::Object* const object = this->model->get(this->objectId());
+	const ldraw::Property property = ldraw::pointProperty(n);
+	const QVariant value = object->getProperty(property);
+	if (value.isValid())
+	{
+		std::unique_ptr<Vec3Editor> editor = std::make_unique<Vec3Editor>(value.value<glm::vec3>(), this);
+		QObject::connect(editor.get(), &Vec3Editor::valueChanged, this, &PolygonObjectEditor::pointChanged);
+		editor->setProperty(INDEX_NAME, QVariant::fromValue(n));
+		editor->setProperty(LABEL_NAME, &ldraw::traits(property).name[0]);
+		this->widgets.push_back(std::move(editor));
+	}
+}
+
+void PolygonObjectEditor::pointChanged(const glm::vec3& value)
+{
+	Model::EditContext editcontext = this->model->edit();
+	const int n = this->sender()->property(INDEX_NAME).toInt();
+	const ldraw::Property property = ldraw::pointProperty(n);
+	editcontext.setObjectProperty(this->objectId(), property, QVariant::fromValue(value));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/polygonobjecteditor.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,23 @@
+#pragma once
+#include <QWidget>
+#include "main.h"
+#include "../widgets/vec3editor.h"
+
+class Model;
+
+class PolygonObjectEditor : public QWidget
+{
+public:
+	PolygonObjectEditor(Model* model, ldraw::id_t id, QWidget* parent = nullptr);
+	~PolygonObjectEditor();
+	ldraw::id_t objectId() const;
+	void setObjectId(ldraw::id_t id);
+private:
+	void buildWidgets();
+	void setupPointWidget(int n);
+	Q_SLOT void pointChanged(const glm::vec3& value);
+	Model* model;
+	ldraw::id_t storedObjectId;
+	std::vector<std::unique_ptr<QWidget>> widgets;
+	std::optional<class QSplitter> splitter;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/doublespinbox.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,58 @@
+/*
+ *  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 "doublespinbox.h"
+
+/*
+ * Constructs a new double spin box. The locale is fixed to system default "C".
+ */
+DoubleSpinBox::DoubleSpinBox(QWidget* parent) :
+	QDoubleSpinBox {parent}
+{
+	this->setLocale({"C"});
+	this->setRange(-1e6, 1e6);
+	this->setDecimals(4);
+}
+
+/*
+ * Reimplementation of QDoubleSpinBox::textFromValue to remove trailing zeros.
+ */
+QString DoubleSpinBox::textFromValue(double value) const
+{
+	QString result = QDoubleSpinBox::textFromValue(value);
+	if (result.contains("."))
+	{
+		// Remove trailing zeros
+		while (result.endsWith("0"))
+			result.chop(1);
+		// Remove trailing decimal point if we just removed all the zeros.
+		if (result.endsWith("."))
+			result.chop(1);
+	}
+	return result;
+}
+
+/*
+ * Reimplementation of QDoubleSpinBox::validate to fix the decimal point if the locale-specific
+ * decimal point was used.
+ */
+QValidator::State DoubleSpinBox::validate(QString& input, int& pos) const
+{
+	input.replace(QLocale().decimalPoint(), this->locale().decimalPoint());
+	return QDoubleSpinBox::validate(input, pos);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/doublespinbox.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,34 @@
+/*
+ *  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/>.
+ */
+
+#pragma once
+#include <QDoubleSpinBox>
+
+/*
+ * A version of QDoubleSpinBox that consistently uses "." as the decimal separator
+ * and does not display trailing zeros.
+ */
+class DoubleSpinBox : public QDoubleSpinBox
+{
+public:
+	DoubleSpinBox(QWidget* parent = nullptr);
+protected:
+	QString textFromValue(double value) const override;
+	QValidator::State validate(QString& input, int& pos) const override;
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,89 @@
+#include "main.h"
+#include "matrixeditor.h"
+#include "ui_matrixeditor.h"
+#include "../ui/multiplyfactordialog.h"
+
+constexpr char BUTTON_COLUMN_PROPERTY[] = "_ldforge_column";
+
+MatrixEditor::MatrixEditor(const glm::mat4 value, QWidget* parent) :
+	QWidget(parent),
+	ui(new Ui::MatrixEditor)
+{
+	ui->setupUi(this);
+	for (int column = 0; column < countof(this->spinboxes); column += 1)
+	{
+		for (int row = 0; row < countof(this->spinboxes[0]); row += 1)
+		{
+			const QString name = "cell"_q + QString::number(column) + QString::number(row);
+			QDoubleSpinBox** spinbox = &this->spinboxes[column][row];
+			*spinbox = this->findChild<QDoubleSpinBox*>(name);
+			connect(*spinbox, qOverload<double>(&QDoubleSpinBox::valueChanged), [&]()
+			{
+				emit this->valueChanged(this->value());
+			});
+			Q_ASSERT(*spinbox != nullptr);
+		}
+		QAbstractButton* button = this->findChild<QAbstractButton*>("multiply"_q + QString::number(column));
+		button->setProperty(BUTTON_COLUMN_PROPERTY, column);
+		connect(button, &QAbstractButton::clicked, this, &MatrixEditor::multiplyButtonPressed);
+	}
+	this->setValue(value);
+}
+
+MatrixEditor::MatrixEditor(QWidget *parent) :
+	MatrixEditor{glm::mat4{1}, parent}
+{
+}
+
+MatrixEditor::~MatrixEditor()
+{
+	delete ui;
+}
+
+glm::mat4 MatrixEditor::value() const
+{
+	glm::mat4 result{1};
+	for (int column = 0; column < countof(this->spinboxes); column += 1)
+	{
+		for (int row = 0; row < countof(this->spinboxes[0]); row += 1)
+		{
+			result[column][row] = this->spinboxes[column][row]->value();
+		}
+	}
+	return result;
+}
+
+void MatrixEditor::setValue(const glm::mat4& value)
+{
+	for (int column = 0; column < countof(this->spinboxes); column += 1)
+	{
+		for (int row = 0; row < countof(this->spinboxes[0]); row += 1)
+		{
+			QDoubleSpinBox* spinbox = this->spinboxes[column][row];
+			QSignalBlocker blocker{spinbox};
+			spinbox->setValue(value[column][row]);
+		}
+	}
+}
+
+void MatrixEditor::multiplyButtonPressed()
+{
+	QAbstractButton* button = qobject_cast<QAbstractButton*>(this->sender());
+	if (button != nullptr)
+	{
+		bool ok;
+		const int column = button->property(BUTTON_COLUMN_PROPERTY).toInt(&ok);
+		if (ok and column >= 0 and column < this->matrixSize())
+		{
+			glm::mat4 newValue = this->value();
+			MultiplyFactorDialog dialog{newValue[column], this};
+			const int result = dialog.exec();
+			if (result == QDialog::Accepted)
+			{
+				newValue[column] = glm::vec4{dialog.value(), (column == 3) ? 1 : 0};
+				this->setValue(newValue);
+				emit valueChanged(newValue);
+			}
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,30 @@
+#pragma once
+#include <QWidget>
+#include "main.h"
+
+namespace Ui {
+class MatrixEditor;
+}
+
+class MatrixEditor : public QWidget
+{
+	Q_OBJECT
+public:
+	explicit MatrixEditor(QWidget *parent = nullptr);
+	explicit MatrixEditor(const glm::mat4 value, QWidget* parent = nullptr);
+	~MatrixEditor();
+	glm::mat4 value() const;
+	void setValue(const glm::mat4& value);
+Q_SIGNALS:
+	void valueChanged(const glm::mat4& value);
+private:
+	constexpr int matrixSize() const;
+	Q_SLOT void multiplyButtonPressed();
+	class QDoubleSpinBox* spinboxes[4][3];
+	Ui::MatrixEditor *ui;
+};
+
+constexpr int MatrixEditor::matrixSize() const
+{
+	return 4;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.ui	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MatrixEditor</class>
+ <widget class="QWidget" name="MatrixEditor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>356</width>
+    <height>172</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="DoubleSpinBox" name="cell00"/>
+   </item>
+   <item row="0" column="1">
+    <widget class="DoubleSpinBox" name="cell10"/>
+   </item>
+   <item row="0" column="2">
+    <widget class="DoubleSpinBox" name="cell20"/>
+   </item>
+   <item row="0" column="3">
+    <widget class="DoubleSpinBox" name="cell30"/>
+   </item>
+   <item row="1" column="0">
+    <widget class="DoubleSpinBox" name="cell01"/>
+   </item>
+   <item row="1" column="1">
+    <widget class="DoubleSpinBox" name="cell11"/>
+   </item>
+   <item row="1" column="2">
+    <widget class="DoubleSpinBox" name="cell21"/>
+   </item>
+   <item row="1" column="3">
+    <widget class="DoubleSpinBox" name="cell31"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="DoubleSpinBox" name="cell02"/>
+   </item>
+   <item row="2" column="1">
+    <widget class="DoubleSpinBox" name="cell12"/>
+   </item>
+   <item row="2" column="2">
+    <widget class="DoubleSpinBox" name="cell22"/>
+   </item>
+   <item row="2" column="3">
+    <widget class="DoubleSpinBox" name="cell32"/>
+   </item>
+   <item row="3" column="0">
+    <widget class="DoubleSpinBox" name="doubleSpinBox">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="DoubleSpinBox" name="doubleSpinBox_2">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="2">
+    <widget class="DoubleSpinBox" name="doubleSpinBox_3">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="3">
+    <widget class="DoubleSpinBox" name="doubleSpinBox_4">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="value">
+      <double>1.000000000000000</double>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0">
+    <widget class="QPushButton" name="multiply0">
+     <property name="text">
+      <string>×</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="1">
+    <widget class="QPushButton" name="multiply1">
+     <property name="text">
+      <string>×</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="2">
+    <widget class="QPushButton" name="multiply2">
+     <property name="text">
+      <string>×</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="3">
+    <widget class="QPushButton" name="multiply3">
+     <property name="text">
+      <string>×</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>DoubleSpinBox</class>
+   <extends>QDoubleSpinBox</extends>
+   <header>widgets/doublespinbox.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/vec3editor.cpp	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,67 @@
+#include <QDialog>
+#include <QCheckBox>
+#include <QSignalBlocker>
+#include "vec3editor.h"
+#include "ui_vec3editor.h"
+#include "../ui/multiplyfactordialog.h"
+
+Vec3Editor::Vec3Editor(const glm::vec3& value, QWidget *parent, QFlags<Flag> flags) :
+	QWidget{parent},
+	ui{new Ui::Vec3Editor}
+{
+	this->ui->setupUi(this);
+	this->setValue(value);
+	if (flags.testFlag(NoMultiplyButton))
+	{
+		this->ui->multiply->setVisible(false);
+	}
+	else
+	{
+		connect(this->ui->multiply, &QPushButton::clicked, this, &Vec3Editor::multiplyPressed);
+	}
+	for (QDoubleSpinBox* spinbox : this->spinboxes())
+	{
+		connect(spinbox, qOverload<double>(&QDoubleSpinBox::valueChanged), [&](double)
+		{
+			Q_EMIT this->valueChanged(this->value());
+		});
+	}
+}
+
+Vec3Editor::~Vec3Editor()
+{
+}
+
+glm::vec3 Vec3Editor::value() const
+{
+	auto get = [](DoubleSpinBox* spinbox){ return toFloat(spinbox->value()); };
+	return {get(this->ui->x), get(this->ui->y), get(this->ui->z)};
+}
+
+void Vec3Editor::setValue(const glm::vec3& value)
+{
+	auto set = [](DoubleSpinBox* spinbox, float value)
+	{
+		QSignalBlocker blocker{spinbox};
+		spinbox->setValue(toQreal(value));
+	};
+	set(this->ui->x, value.x);
+	set(this->ui->y, value.y);
+	set(this->ui->z, value.z);
+	Q_EMIT this->valueChanged(value);
+}
+
+std::array<DoubleSpinBox*, 3> Vec3Editor::spinboxes()
+{
+	return {this->ui->x, this->ui->y, this->ui->z};
+}
+
+void Vec3Editor::multiplyPressed()
+{
+	MultiplyFactorDialog dialog{this->value(), this};
+	const int dialogResult = dialog.exec();
+	if (dialogResult == QDialog::Accepted)
+	{
+		this->setValue(dialog.value());
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/vec3editor.h	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,30 @@
+#pragma once
+#include <QWidget>
+#include "main.h"
+
+namespace Ui
+{
+	class Vec3Editor;
+}
+
+class Vec3Editor : public QWidget
+{
+	Q_OBJECT
+public:
+	enum Flag
+	{
+		NoMultiplyButton = 0x1
+	};
+	explicit Vec3Editor(const glm::vec3& value, QWidget* parent = nullptr, QFlags<Flag> flags = 0);
+	~Vec3Editor();
+	glm::vec3 value() const;
+	void setValue(const glm::vec3& value);
+Q_SIGNALS:
+	void valueChanged(const glm::vec3& value);
+private:
+	std::array<class DoubleSpinBox*, 3> spinboxes();
+	Q_SLOT void multiplyPressed();
+	std::unique_ptr<Ui::Vec3Editor> ui;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(QFlags<Vec3Editor::Flag>)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/vec3editor.ui	Mon Aug 24 23:00:50 2020 +0300
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Vec3Editor</class>
+ <widget class="QWidget" name="Vec3Editor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>613</width>
+    <height>46</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,1,1,0">
+   <item>
+    <widget class="DoubleSpinBox" name="x">
+     <property name="prefix">
+      <string>x = </string>
+     </property>
+     <property name="decimals">
+      <number>4</number>
+     </property>
+     <property name="minimum">
+      <double>-1000000.000000000000000</double>
+     </property>
+     <property name="maximum">
+      <double>1000000.000000000000000</double>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="DoubleSpinBox" name="y">
+     <property name="prefix">
+      <string>y = </string>
+     </property>
+     <property name="decimals">
+      <number>4</number>
+     </property>
+     <property name="minimum">
+      <double>-1000000.000000000000000</double>
+     </property>
+     <property name="maximum">
+      <double>1000000.000000000000000</double>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="DoubleSpinBox" name="z">
+     <property name="prefix">
+      <string>z = </string>
+     </property>
+     <property name="decimals">
+      <number>4</number>
+     </property>
+     <property name="minimum">
+      <double>-1000000.000000000000000</double>
+     </property>
+     <property name="maximum">
+      <double>1000000.000000000000000</double>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QPushButton" name="multiply">
+     <property name="text">
+      <string>×</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>DoubleSpinBox</class>
+   <extends>QDoubleSpinBox</extends>
+   <header>widgets/doublespinbox.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>

mercurial