added work done on cylinders

Mon, 04 Jun 2018 23:12:40 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Mon, 04 Jun 2018 23:12:40 +0300
changeset 1391
5fa4bf1fc781
parent 1390
3eace926af7f
child 1392
0541d9b21968

added work done on cylinders

CMakeLists.txt file | annotate | diff | comparison | revisions
src/dialogs/cylindereditor.cpp file | annotate | diff | comparison | revisions
src/dialogs/cylindereditor.h file | annotate | diff | comparison | revisions
src/dialogs/cylindereditor.ui file | annotate | diff | comparison | revisions
src/dialogs/subfilereferenceeditor.cpp file | annotate | diff | comparison | revisions
src/dialogs/subfilereferenceeditor.h file | annotate | diff | comparison | revisions
src/dialogs/subfilereferenceeditor.ui file | annotate | diff | comparison | revisions
src/generics/vectormodel.h file | annotate | diff | comparison | revisions
src/glcompiler.cpp file | annotate | diff | comparison | revisions
src/glcompiler.h file | annotate | diff | comparison | revisions
src/glrenderer.cpp file | annotate | diff | comparison | revisions
src/lddocument.cpp file | annotate | diff | comparison | revisions
src/lddocument.h file | annotate | diff | comparison | revisions
src/linetypes/cylinder.cpp file | annotate | diff | comparison | revisions
src/linetypes/cylinder.h file | annotate | diff | comparison | revisions
src/linetypes/modelobject.cpp file | annotate | diff | comparison | revisions
src/linetypes/modelobject.h file | annotate | diff | comparison | revisions
src/parser.cpp file | annotate | diff | comparison | revisions
src/primitives.cpp file | annotate | diff | comparison | revisions
src/primitives.h file | annotate | diff | comparison | revisions
src/toolsets/basictoolset.cpp file | annotate | diff | comparison | revisions
src/toolsets/extprogramtoolset.cpp file | annotate | diff | comparison | revisions
src/widgets/matrixeditor.cpp file | annotate | diff | comparison | revisions
src/widgets/matrixeditor.h file | annotate | diff | comparison | revisions
src/widgets/matrixeditor.ui file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Wed May 30 22:31:06 2018 +0300
+++ b/CMakeLists.txt	Mon Jun 04 23:12:40 2018 +0300
@@ -79,6 +79,7 @@
 	src/geometry/plane.cpp
 	src/linetypes/comment.cpp
 	src/linetypes/conditionaledge.cpp
+	src/linetypes/cylinder.cpp
 	src/linetypes/edgeline.cpp
 	src/linetypes/empty.cpp
 	src/linetypes/modelobject.cpp
@@ -96,6 +97,7 @@
 	src/types/vertex.cpp
 	src/widgets/doublespinbox.cpp
 	src/widgets/headeredit.cpp
+	src/widgets/matrixeditor.cpp
 	src/widgets/vertexobjecteditor.cpp
 )
 
@@ -157,6 +159,7 @@
 	src/geometry/plane.h
 	src/linetypes/comment.h
 	src/linetypes/conditionaledge.h
+	src/linetypes/cylinder.h
 	src/linetypes/edgeline.h
 	src/linetypes/empty.h
 	src/linetypes/modelobject.h
@@ -175,6 +178,7 @@
 	src/types/vertex.h
 	src/widgets/doublespinbox.h
 	src/widgets/headeredit.h
+	src/widgets/matrixeditor.h
 	src/widgets/vertexobjecteditor.h
 )
 
@@ -202,6 +206,7 @@
 	src/partdownloader.ui
 	src/widgets/vertexobjecteditor.ui
 	src/widgets/headeredit.ui
+	src/widgets/matrixeditor.ui
 )
 
 set (LDFORGE_OTHER_FILES
@@ -234,6 +239,7 @@
 	if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
 		set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDEBUG")
 	endif()
+	set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wall -Wpedantic -Werror=switch")
 endif()
 
 qt5_add_resources (LDFORGE_QRC ${LDFORGE_RESOURCES})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialogs/cylindereditor.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,14 @@
+#include "cylindereditor.h"
+#include "ui_cylindereditor.h"
+
+CylinderEditor::CylinderEditor(QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::CylinderEditor)
+{
+	ui->setupUi(this);
+}
+
+CylinderEditor::~CylinderEditor()
+{
+	delete ui;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialogs/cylindereditor.h	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,22 @@
+#ifndef CYLINDEREDITOR_H
+#define CYLINDEREDITOR_H
+
+#include <QDialog>
+
+namespace Ui {
+class CylinderEditor;
+}
+
+class CylinderEditor : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit CylinderEditor(QWidget *parent = 0);
+	~CylinderEditor();
+
+private:
+	Ui::CylinderEditor *ui;
+};
+
+#endif // CYLINDEREDITOR_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/dialogs/cylindereditor.ui	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CylinderEditor</class>
+ <widget class="QDialog" name="CylinderEditor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <widget class="QDialogButtonBox" name="buttonBox">
+   <property name="geometry">
+    <rect>
+     <x>30</x>
+     <y>240</y>
+     <width>341</width>
+     <height>32</height>
+    </rect>
+   </property>
+   <property name="orientation">
+    <enum>Qt::Horizontal</enum>
+   </property>
+   <property name="standardButtons">
+    <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+   </property>
+  </widget>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>CylinderEditor</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>CylinderEditor</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>
--- a/src/dialogs/subfilereferenceeditor.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/dialogs/subfilereferenceeditor.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -30,11 +30,10 @@
 {
 	this->ui.setupUi(this);
 	this->ui.referenceName->setText(reference->referenceName());
+	this->ui.matrixEditor->setPosition(reference->position());
+	this->ui.matrixEditor->setMatrix(reference->transformationMatrix());
 	this->color = reference->color();
 	::setColorButton(this->ui.colorButton, this->color);
-	this->ui.positionX->setValue(reference->position().x);
-	this->ui.positionY->setValue(reference->position().y);
-	this->ui.positionZ->setValue(reference->position().z);
 	connect(
 		this->ui.colorButton,
 		&QPushButton::clicked,
@@ -44,22 +43,6 @@
 				::setColorButton(this->ui.colorButton, this->color);
 		}
 	);
-	for (int i : {0, 1, 2})
-	for (int j : {0, 1, 2})
-	{
-		QLayoutItem* item = this->ui.matrixLayout->itemAtPosition(i, j);
-		QDoubleSpinBox* spinbox = item ? qobject_cast<QDoubleSpinBox*>(item->widget()) : nullptr;
-		withSignalsBlocked(spinbox, [&]()
-		{
-			spinbox->setValue(reference->transformationMatrix()(i, j));
-		});
-		connect(
-			spinbox,
-			qOverload<double>(&QDoubleSpinBox::valueChanged),
-			this,
-			&SubfileReferenceEditor::matrixChanged
-		);
-	}
 	connect(
 		this->ui.primitivesTreeView,
 		&QTreeView::clicked,
@@ -72,26 +55,6 @@
 				this->ui.referenceName->setText(primitiveName.toString());
 		}
 	);
-
-	for (QDoubleSpinBox* spinbox : {this->ui.scalingX, this->ui.scalingY, this->ui.scalingZ})
-	{
-		connect(
-			spinbox,
-			qOverload<double>(&QDoubleSpinBox::valueChanged),
-			this,
-			&SubfileReferenceEditor::scalingChanged
-		);
-	}
-
-	// Fill in the initial scaling values
-	for (int column : {0, 1, 2})
-	{
-		QDoubleSpinBox* spinbox = this->vectorElement(column);
-		withSignalsBlocked(spinbox, [&]()
-		{
-			spinbox->setValue(this->matrixScaling(column));
-		});
-	}
 }
 
 SubfileReferenceEditor::~SubfileReferenceEditor()
@@ -99,61 +62,12 @@
 	delete &this->ui;
 }
 
-/*
- * Returns a spinbox from the matrix grid at position (row, column).
- * Row and column must be within [0, 2].
- */
-QDoubleSpinBox* SubfileReferenceEditor::matrixCell(int row, int column) const
-{
-	if (qBound(0, row, 2) != row or qBound(0, column, 2) != column)
-	{
-		throw std::out_of_range {"bad row and column values"};
-	}
-	else
-	{
-		QLayoutItem* item = this->ui.matrixLayout->itemAtPosition(row, column);
-		return item ? qobject_cast<QDoubleSpinBox*>(item->widget()) : nullptr;
-	}
-}
-
-/*
- * Returns a spinbox for the vector element at the given position
- * Index must be within [0, 2]
- */
-QDoubleSpinBox* SubfileReferenceEditor::vectorElement(int index)
-{
-	switch (index)
-	{
-	case 0:
-		return this->ui.scalingX;
-
-	case 1:
-		return this->ui.scalingY;
-
-	case 2:
-		return this->ui.scalingZ;
-
-	default:
-		throw std::out_of_range {"bad index"};
-	}
-}
-
 void SubfileReferenceEditor::accept()
 {
 	this->reference->setReferenceName(this->ui.referenceName->text());
-	Matrix transformationMatrix;
-	for (int i : {0, 1, 2})
-	for (int j : {0, 1, 2})
-	{
-		transformationMatrix(i, j) = this->matrixCell(i, j)->value();
-	}
-	this->reference->setTransformationMatrix(transformationMatrix);
-	this->reference->setPosition({
-		this->ui.positionX->value(),
-		this->ui.positionY->value(),
-		this->ui.positionZ->value()
-	});
 	this->reference->setColor(this->color);
+	this->reference->setTransformationMatrix(this->ui.matrixEditor->matrix());
+	this->reference->setPosition(this->ui.matrixEditor->position());
 	QDialog::accept();
 }
 
@@ -161,77 +75,3 @@
 {
 	this->ui.primitivesTreeView->setModel(primitives);
 }
-
-double SubfileReferenceEditor::matrixScaling(int column) const
-{
-	return sqrt(
-		pow(this->matrixCell(0, column)->value(), 2) +
-		pow(this->matrixCell(1, column)->value(), 2) +
-		pow(this->matrixCell(2, column)->value(), 2)
-	);
-}
-
-/*
- * Updates the appropriate matrix column when a scaling vector element is changed.
- */
-void SubfileReferenceEditor::scalingChanged()
-{
-	for (int column : {0, 1, 2})
-	{
-		if (this->sender() == this->vectorElement(column))
-		{
-			double oldScaling = this->matrixScaling(column);
-			double newScaling = static_cast<QDoubleSpinBox*>(this->sender())->value();
-
-			if (not qFuzzyCompare(newScaling, 0.0))
-			{
-				for (int row : {0, 1, 2})
-				{
-					double cellValue = this->matrixCell(row, column)->value();
-					cellValue *= newScaling / oldScaling;
-					QDoubleSpinBox* cellWidget = this->matrixCell(row, column);
-					withSignalsBlocked(cellWidget, [&]()
-					{
-						cellWidget->setValue(cellValue);
-					});
-				}
-			}
-
-			break;
-		}
-	}
-}
-
-/*
- * Finds the position for the given cell widget.
- */
-QPair<int, int> SubfileReferenceEditor::cellPosition(QDoubleSpinBox* cellWidget)
-{
-	for (int row : {0, 1, 2})
-	for (int column : {0, 1, 2})
-	{
-		if (this->matrixCell(row, column) == cellWidget)
-			return {row, column};
-	}
-
-	throw std::out_of_range {"widget is not in the matrix"};
-}
-
-/*
- * Updates the appropriate scaling vector element when a matrix cell is changed.
- */
-void SubfileReferenceEditor::matrixChanged()
-{
-	QDoubleSpinBox* cellWidget = static_cast<QDoubleSpinBox*>(this->sender());
-
-	try
-	{
-		int column = this->cellPosition(cellWidget).second;
-		QDoubleSpinBox* spinbox = this->vectorElement(column);
-		withSignalsBlocked(spinbox, [&]()
-		{
-			spinbox->setValue(this->matrixScaling(column));
-		});
-	}
-	catch (const std::out_of_range&) {}
-}
--- a/src/dialogs/subfilereferenceeditor.h	Wed May 30 22:31:06 2018 +0300
+++ b/src/dialogs/subfilereferenceeditor.h	Mon Jun 04 23:12:40 2018 +0300
@@ -33,16 +33,7 @@
 	void accept() override;
 	void setPrimitivesTree(class PrimitiveManager* primitives);
 
-private slots:
-	void scalingChanged();
-	void matrixChanged();
-
 private:
-	QDoubleSpinBox* matrixCell(int row, int column) const;
-	double matrixScaling(int column) const;
-	QPair<int, int> cellPosition(QDoubleSpinBox* cellWidget);
-	QDoubleSpinBox* vectorElement(int index);
-
 	class Ui_SubfileReferenceEditor& ui;
 	class LDSubfileReference* const reference;
 	LDColor color;
--- a/src/dialogs/subfilereferenceeditor.ui	Wed May 30 22:31:06 2018 +0300
+++ b/src/dialogs/subfilereferenceeditor.ui	Mon Jun 04 23:12:40 2018 +0300
@@ -13,7 +13,7 @@
   <property name="windowTitle">
    <string>Dialog</string>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
+  <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0,0">
    <item>
     <widget class="QTreeView" name="primitivesTreeView"/>
    </item>
@@ -60,269 +60,21 @@
        </item>
       </layout>
      </item>
-     <item row="2" column="0">
-      <widget class="QLabel" name="label_2">
-       <property name="text">
-        <string>Position:</string>
-       </property>
-      </widget>
-     </item>
-     <item row="2" column="1">
-      <layout class="QHBoxLayout" name="horizontalLayout">
-       <item>
-        <widget class="DoubleSpinBox" name="positionX">
-         <property name="prefix">
-          <string>𝑥 = </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="DoubleSpinBox" name="positionY">
-         <property name="prefix">
-          <string>𝑦 = </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="DoubleSpinBox" name="positionZ">
-         <property name="prefix">
-          <string>𝑧 = </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
-     <item row="3" column="1">
-      <widget class="Line" name="line">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-      </widget>
-     </item>
-     <item row="4" column="0">
-      <widget class="QLabel" name="label_3">
-       <property name="text">
-        <string>Transformation matrix:</string>
-       </property>
-      </widget>
-     </item>
-     <item row="4" column="1">
-      <layout class="QGridLayout" name="matrixLayout">
-       <item row="1" column="1">
-        <widget class="DoubleSpinBox" name="matrixE">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="0">
-        <widget class="DoubleSpinBox" name="matrixA">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="0">
-        <widget class="DoubleSpinBox" name="matrixG">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="1">
-        <widget class="DoubleSpinBox" name="matrixH">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="2" column="2">
-        <widget class="DoubleSpinBox" name="matrixI">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="1" column="2">
-        <widget class="DoubleSpinBox" name="matrixF">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="2">
-        <widget class="DoubleSpinBox" name="matrixC">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="1" column="0">
-        <widget class="DoubleSpinBox" name="matrixD">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item row="0" column="1">
-        <widget class="DoubleSpinBox" name="matrixB">
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>-10000.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
-     <item row="6" column="0">
-      <widget class="QLabel" name="label_5">
-       <property name="text">
-        <string>Scaling vector:</string>
-       </property>
-      </widget>
-     </item>
-     <item row="6" column="1">
-      <layout class="QHBoxLayout" name="horizontalLayout_3">
-       <item>
-        <widget class="DoubleSpinBox" name="scalingX">
-         <property name="prefix">
-          <string>𝑥 × </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>0.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="DoubleSpinBox" name="scalingY">
-         <property name="prefix">
-          <string>𝑦 × </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>0.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="DoubleSpinBox" name="scalingZ">
-         <property name="prefix">
-          <string>𝑧 × </string>
-         </property>
-         <property name="decimals">
-          <number>5</number>
-         </property>
-         <property name="minimum">
-          <double>0.000000000000000</double>
-         </property>
-         <property name="maximum">
-          <double>10000.000000000000000</double>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
-     <item row="5" column="1">
-      <widget class="Line" name="line_2">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-      </widget>
-     </item>
     </layout>
    </item>
    <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Transformation</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <item>
+       <widget class="MatrixEditor" name="matrixEditor" native="true"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
     <widget class="QDialogButtonBox" name="buttonBox">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
@@ -336,30 +88,16 @@
  </widget>
  <customwidgets>
   <customwidget>
-   <class>DoubleSpinBox</class>
-   <extends>QDoubleSpinBox</extends>
-   <header>widgets/doublespinbox.h</header>
+   <class>MatrixEditor</class>
+   <extends>QWidget</extends>
+   <header>widgets/matrixeditor.h</header>
+   <container>1</container>
   </customwidget>
  </customwidgets>
  <tabstops>
   <tabstop>primitivesTreeView</tabstop>
   <tabstop>referenceName</tabstop>
   <tabstop>colorButton</tabstop>
-  <tabstop>positionX</tabstop>
-  <tabstop>positionY</tabstop>
-  <tabstop>positionZ</tabstop>
-  <tabstop>matrixA</tabstop>
-  <tabstop>matrixB</tabstop>
-  <tabstop>matrixC</tabstop>
-  <tabstop>matrixD</tabstop>
-  <tabstop>matrixE</tabstop>
-  <tabstop>matrixF</tabstop>
-  <tabstop>matrixG</tabstop>
-  <tabstop>matrixH</tabstop>
-  <tabstop>matrixI</tabstop>
-  <tabstop>scalingX</tabstop>
-  <tabstop>scalingY</tabstop>
-  <tabstop>scalingZ</tabstop>
  </tabstops>
  <resources/>
  <connections>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/generics/vectormodel.h	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,4 @@
+#ifndef VECTORMODEL_H
+#define VECTORMODEL_H
+
+#endif // VECTORMODEL_H
--- a/src/glcompiler.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/glcompiler.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -373,20 +373,6 @@
 			break;
 		}
 
-	// TODO: try use interfaces to remove these special treatments?
-	case LDObjectType::SubfileReference:
-		{
-			LDSubfileReference* subfileReference = static_cast<LDSubfileReference*>(object);
-			auto data = subfileReference->inlinePolygons(
-				m_documents,
-				m_renderer->model()->winding()
-			);
-
-			for (LDPolygon& poly : data)
-				compilePolygon (poly, index, info);
-			break;
-		}
-
 	case LDObjectType::BezierCurve:
 		{
 			LDBezierCurve* curve = static_cast<LDBezierCurve*>(object);
@@ -396,6 +382,13 @@
 		break;
 
 	default:
+		if (object->isRasterizable())
+		{
+			auto data = object->rasterizePolygons(m_documents, m_renderer->model()->winding());
+
+			for (LDPolygon& poly : data)
+				compilePolygon(poly, index, info);
+		}
 		break;
 	}
 
--- a/src/glcompiler.h	Wed May 30 22:31:06 2018 +0300
+++ b/src/glcompiler.h	Mon Jun 04 23:12:40 2018 +0300
@@ -64,7 +64,7 @@
 		const QModelIndex& polygonOwnerIndex,
 		ObjectVboData& objectInfo
 	);
-	Q_SLOT void compileObject (const QModelIndex &index);
+	Q_SLOT void compileObject(const QModelIndex &index);
 	QColor getColorForPolygon(
 		LDPolygon& poly,
 		const QModelIndex& polygonOwnerIndex,
--- a/src/glrenderer.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/glrenderer.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -247,6 +247,12 @@
 void GLRenderer::initializeGL()
 {
 	initializeOpenGLFunctions();
+
+	if (glGetError() != GL_NO_ERROR)
+	{
+		abort();
+	}
+
 	setBackground();
 	glLineWidth (config::lineThickness());
 	glLineStipple (1, 0x6666);
--- a/src/lddocument.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/lddocument.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -105,7 +105,7 @@
 	m_tabIndex = value;
 }
 
-const QList<LDPolygon>& LDDocument::polygonData() const
+const QVector<LDPolygon>& LDDocument::polygonData() const
 {
 	return m_polygonData;
 }
@@ -505,7 +505,7 @@
 
 // =============================================================================
 //
-QList<LDPolygon> LDDocument::inlinePolygons()
+QVector<LDPolygon> LDDocument::inlinePolygons()
 {
 	initializeCachedData();
 	return polygonData();
@@ -534,7 +534,7 @@
 					if (deep and object->type() == LDObjectType::SubfileReference)
 					{
 						LDSubfileReference* reference = static_cast<LDSubfileReference*>(object);
-						reference->inlineContents(
+						reference->rasterize(
 							documentManager(),
 							this->winding(),
 							model,
--- a/src/lddocument.h	Wed May 30 22:31:06 2018 +0300
+++ b/src/lddocument.h	Mon Jun 04 23:12:40 2018 +0300
@@ -100,13 +100,13 @@
 	EditHistory* history() const;
 	void initializeCachedData();
 	void inlineContents(Model& model, bool deep, bool renderinline);
-	QList<LDPolygon> inlinePolygons();
+	QVector<LDPolygon> inlinePolygons();
 	const QSet<Vertex>& inlineVertices();
 	bool isFrozen() const;
 	bool isSafeToClose();
 	QString name() const;
 	void objectRemoved(LDObject* object, int index);
-	const QList<LDPolygon>& polygonData() const;
+	const QVector<LDPolygon>& polygonData() const;
 	void recountTriangles();
 	void redo();
 	void redoVertices();
@@ -139,7 +139,7 @@
 	long m_savePosition;
 	int m_tabIndex;
 	int m_triangleCount;
-	QList<LDPolygon> m_polygonData;
+	QVector<LDPolygon> m_polygonData;
 	QMap<LDObject*, QSet<Vertex>> m_objectVertices;
 	QSet<Vertex> m_vertices;
 	DocumentManager* m_manager;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/linetypes/cylinder.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,133 @@
+#include "../algorithms/geometry.h"
+#include "../glShared.h"
+#include "../model.h"
+#include "../algorithms/invert.h"
+#include "cylinder.h"
+#include "quadrilateral.h"
+#include "primitives.h"
+
+QString LDCylinder::buildFilename() const
+{
+	int numerator = this->m_segments;
+	int denominator = this->m_divisions;
+	QString prefix;
+
+	if (m_divisions != MediumResolution)
+		prefix = QString::number(m_divisions) + '\\';
+
+	simplify(numerator, denominator);
+	return format("%1%2-%3cyli.dat", prefix, numerator, denominator);
+}
+
+LDCylinder::LDCylinder(
+	int segments,
+	int divisions,
+	const Matrix& transformationMatrix,
+	const Vertex& position
+) :
+	LDMatrixObject {transformationMatrix, position},
+	m_segments {segments},
+	m_divisions {divisions} {}
+
+QString LDCylinder::asText() const
+{
+	return LDSubfileReference(buildFilename(), transformationMatrix(), position()).asText();
+}
+
+void LDCylinder::getVertices(DocumentManager* /* context */, QSet<Vertex>& vertices) const
+{
+	int endSegment = (m_segments == m_divisions) ? m_segments : m_segments + 1;
+
+	for (int i = 0; i < endSegment; i += 1)
+	{
+		QPointF point2d = pointOnLDrawCircumference(i, m_divisions);
+
+		for (double y_value : {0.0, 1.0})
+		{
+			Vertex vertex {point2d.x(), y_value, point2d.y()};
+			vertex.transform(transformationMatrix(), position());
+			vertices.insert(vertex);
+		}
+	}
+}
+
+void LDCylinder::rasterize(
+	DocumentManager* context,
+	Winding /* parentWinding */,
+	Model& model,
+	bool /* deep */,
+	bool /* render */
+) {
+	Model cylinderBody {context};
+	buildPrimitiveBody(cylinderBody);
+
+	for (LDObject* object : cylinderBody.objects())
+	{
+		for (int i = 0; i < object->numVertices(); i += 1)
+		{
+			Vertex vertex = object->vertex(i);
+			vertex.transform(transformationMatrix(), position());
+			object->setVertex(i, vertex);
+		}
+	}
+
+	model.merge(cylinderBody);
+}
+
+QVector<LDPolygon> LDCylinder::rasterizePolygons(DocumentManager* context, Winding winding)
+{
+	Model cylinderBody {context};
+	buildPrimitiveBody(cylinderBody, winding);
+	QVector<LDPolygon> result;
+
+	for (LDObject* object : cylinderBody.objects())
+	{
+		for (int i = 0; i < object->numVertices(); i += 1)
+		{
+			Vertex vertex = object->vertex(i);
+			vertex.transform(transformationMatrix(), position());
+			object->setVertex(i, vertex);
+		}
+
+		LDPolygon* polygon = object->getPolygon();
+
+		if (polygon)
+		{
+			if (shouldInvert(winding, context))
+				invertPolygon(*polygon);
+
+			result.append(*polygon);
+		}
+
+		delete polygon;
+	}
+
+	return result;
+}
+
+void LDCylinder::buildPrimitiveBody(Model& model, Winding winding) const
+{
+	PrimitiveModel primitive;
+	primitive.type = PrimitiveModel::Cylinder;
+	primitive.segments = m_segments;
+	primitive.divisions = m_divisions;
+	primitive.ringNumber = 0;
+	primitive.generateCylinder(model, winding);
+}
+
+QString LDCylinder::objectListText() const
+{
+	QString result = format("Cylinder %1 %2, (", m_segments / m_divisions, position().toString(true));
+
+	for (int i = 0; i < 9; ++i)
+		result += format("%1%2", transformationMatrix().value(i), (i != 8) ? " " : "");
+
+	result += ')';
+	return result;
+}
+
+void LDCylinder::serialize(class Serializer& serializer)
+{
+	LDMatrixObject::serialize(serializer);
+	serializer << m_segments << m_divisions;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/linetypes/cylinder.h	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,39 @@
+#pragma once
+#include "modelobject.h"
+
+class LDCylinder : public LDMatrixObject
+{
+public:
+	static const LDObjectType SubclassType = LDObjectType::Cylinder;
+
+	LDCylinder() = default;
+	LDCylinder(int segments, int divisions, const Matrix& transformationMatrix, const Vertex& position);
+
+	virtual LDObjectType type() const override
+	{
+		return SubclassType;
+	}
+
+	virtual QString asText() const override;
+	virtual void getVertices(DocumentManager *context, QSet<Vertex>& verts) const override;
+	bool isRasterizable() const override { return true; }
+	void rasterize(
+		DocumentManager* context,
+		Winding parentWinding,
+		Model& model,
+		bool deep,
+		bool render
+	) override;
+	QVector<LDPolygon> rasterizePolygons(DocumentManager* context, Winding parentWinding) override;
+	QString objectListText() const override;
+	int triangleCount(DocumentManager*) const override { return 2 * m_segments; }
+	QString typeName() const override { return "cylinder"; }
+	void serialize(class Serializer& serializer) override;
+
+private:
+	QString buildFilename() const;
+	void buildPrimitiveBody(Model& model, Winding winding = CounterClockwise) const;
+
+	int m_segments;
+	int m_divisions;
+};
--- a/src/linetypes/modelobject.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/linetypes/modelobject.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -31,6 +31,7 @@
 #include "conditionaledge.h"
 #include "comment.h"
 #include "empty.h"
+#include "cylinder.h"
 
 // List of all LDObjects
 QMap<qint32, LDObject*> g_allObjects;
@@ -146,18 +147,41 @@
 		obj->setColor (parentcolor);
 }
 
-bool shouldInvert(LDSubfileReference* reference, Winding winding, DocumentManager* context)
+/*
+ * Returns whether or not a compound object should be inverted.
+ */
+bool LDMatrixObject::shouldInvert(Winding winding, DocumentManager* context)
 {
 	bool result = false;
-	result ^= (reference->isInverted());
-	result ^= (reference->transformationMatrix().determinant() < 0);
-	result ^= (reference->fileInfo(context)->winding() != winding);
+	result ^= (isInverted());
+	result ^= (transformationMatrix().determinant() < 0);
+	result ^= (nativeWinding(context) != winding);
 	return result;
 }
 
+/*
+ * The winding used by the object's geometry. By default it's CCW but some documents referenced by
+ * a subfile reference may use CW geometry.
+ *
+ * Since the native winding of a subfile reference depends on the actual document it references,
+ * determining the winding requires the libraries for reference.
+ */
+Winding LDObject::nativeWinding(DocumentManager* /*context*/) const
+{
+	return CounterClockwise;
+}
+
+/*
+ * Reimplementation of LDObject::nativeWinding for subfile references
+ */
+Winding LDSubfileReference::nativeWinding(DocumentManager* context) const
+{
+	return fileInfo(context)->winding();
+}
+
 // =============================================================================
 // -----------------------------------------------------------------------------
-void LDSubfileReference::inlineContents(
+void LDSubfileReference::rasterize(
 	DocumentManager* context,
 	Winding parentWinding,
 	Model& model,
@@ -174,7 +198,7 @@
 		// Transform the objects
 		for (LDObject* object : inlined)
 		{
-			if (::shouldInvert(this, parentWinding, context))
+			if (shouldInvert(parentWinding, context))
 				::invert(object, context);
 
 			TransformObject(object, transformationMatrix(), position(), color());
@@ -230,20 +254,20 @@
 
 // =============================================================================
 //
-QList<LDPolygon> LDSubfileReference::inlinePolygons(DocumentManager* context, Winding parentWinding)
+QVector<LDPolygon> LDSubfileReference::rasterizePolygons(DocumentManager* context, Winding parentWinding)
 {
 	LDDocument* file = fileInfo(context);
 
 	if (file)
 	{
-		QList<LDPolygon> data = fileInfo(context)->inlinePolygons();
+		QVector<LDPolygon> data = fileInfo(context)->inlinePolygons();
 
 		for (LDPolygon& entry : data)
 		{
 			for (int i = 0; i < entry.numVertices(); ++i)
 				entry.vertices[i].transform (transformationMatrix(), position());
 
-			if (::shouldInvert(this, parentWinding, context))
+			if (shouldInvert(parentWinding, context))
 				::invertPolygon(entry);
 		}
 
@@ -332,6 +356,9 @@
 	case LDObjectType::BezierCurve:
 		return new LDBezierCurve {};
 
+	case LDObjectType::Cylinder:
+		return new LDCylinder {};
+
 	case LDObjectType::_End:
 		break;
 	}
@@ -518,6 +545,11 @@
 	}
 }
 
+QVector<LDPolygon> LDObject::rasterizePolygons(DocumentManager*, Winding)
+{
+	return {};
+}
+
 QString LDError::objectListText() const
 {
 	return "ERROR: " + asText();
--- a/src/linetypes/modelobject.h	Wed May 30 22:31:06 2018 +0300
+++ b/src/linetypes/modelobject.h	Mon Jun 04 23:12:40 2018 +0300
@@ -40,6 +40,7 @@
 	Error,				//	Object is the result of failed parsing
 	Empty,				//	Object represents an empty line
 	BezierCurve,		//	Object represents a Bézier curve
+	Cylinder,
 	_End
 };
 
@@ -69,12 +70,15 @@
 	virtual bool hasMatrix() const; // Does this object have a matrix and position? (see LDMatrixObject)
 	virtual bool isColored() const;
 	bool isHidden() const;
+	virtual bool isRasterizable() const { return false; } // Can this object be rasterized?
 	virtual bool isScemantic() const; // Does this object have meaning in the part model?
 	void move (const QVector3D vector);
 	virtual int numVertices() const;
 	virtual int numPolygonVertices() const;
 	virtual QString objectListText() const;
 	QColor randomColor() const;
+	virtual void rasterize(DocumentManager*, Winding, Model&, bool, bool) {}
+	virtual QVector<LDPolygon> rasterizePolygons(DocumentManager*, Winding);
 	void setColor (LDColor color);
 	void setHidden (bool value);
 	void setVertex (int i, const Vertex& vert);
@@ -92,6 +96,7 @@
 	void modified(const LDObjectState& before, const LDObjectState& after);
 
 protected:
+	virtual Winding nativeWinding(DocumentManager*) const;
 	template<typename T>
 	void changeProperty(T* property, const T& value);
 
@@ -114,6 +119,7 @@
 	LDMatrixObject() = default;
 	LDMatrixObject(const Matrix& transformationMatrix, const Vertex& pos);
 
+	bool hasMatrix() const override { return true; }
 	const Vertex& position() const;
 	void setCoordinate (const Axis ax, double value);
 	void setPosition (const Vertex& a);
@@ -121,6 +127,9 @@
 	const Matrix& transformationMatrix() const;
 	void serialize(class Serializer& serializer) override;
 
+protected:
+	bool shouldInvert(Winding winding, DocumentManager* context);
+
 private:
 	Vertex m_position;
 	Matrix m_transformationMatrix = Matrix::identity;
@@ -171,23 +180,26 @@
 
 	virtual QString asText() const override;
 	LDDocument* fileInfo(DocumentManager *context) const;
+	bool isRasterizable() const override { return true; }
 	virtual void getVertices(DocumentManager *context, QSet<Vertex>& verts) const override;
-	void inlineContents(
+	void rasterize(
 		DocumentManager* context,
 		Winding parentWinding,
 		Model& model,
 		bool deep,
 		bool render
-	);
-	QList<LDPolygon> inlinePolygons(DocumentManager* context, Winding parentWinding);
+	) override;
+	QVector<LDPolygon> rasterizePolygons(DocumentManager* context, Winding parentWinding) override;
 	QString objectListText() const override;
 	QString referenceName() const;
 	int triangleCount(DocumentManager *context) const override;
-	bool hasMatrix() const override { return true; }
 	QString typeName() const override { return "subfilereference"; }
 	void serialize(class Serializer& serializer) override;
 	void setReferenceName(const QString& newReferenceName);
 
+protected:
+	Winding nativeWinding(DocumentManager* context) const override;
+
 private:
 	QString m_referenceName;
 };
--- a/src/parser.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/parser.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -24,6 +24,7 @@
 #include "linetypes/empty.h"
 #include "linetypes/quadrilateral.h"
 #include "linetypes/triangle.h"
+#include "linetypes/cylinder.h"
 
 /*
  * Constructs an LDraw parser
@@ -381,13 +382,33 @@
 				CheckTokenCount (tokens, 15);
 				CheckTokenNumbers (tokens, 1, 13);
 
-				Vertex referncePosition = parseVertex (tokens, 2);  // 2 - 4
+				Vertex displacement = parseVertex (tokens, 2);  // 2 - 4
 				Matrix transform;
+				QString referenceName = tokens[14];
 
 				for (int i = 0; i < 9; ++i)
 					transform.value(i) = tokens[i + 5].toDouble(); // 5 - 13
 
-				LDSubfileReference* obj = model.emplaceAt<LDSubfileReference>(position, tokens[14], transform, referncePosition);
+				static const QRegExp cylinderRegexp {R"((?:(\d+)\\)?(\d+)-(\d+)cyli\.dat)"};
+				LDObject* obj;
+
+				if (cylinderRegexp.exactMatch(referenceName))
+				{
+					int resolution = MediumResolution;
+
+					if (not cylinderRegexp.capturedTexts()[1].isEmpty())
+						resolution = cylinderRegexp.capturedTexts()[1].toInt();
+
+					int numerator = cylinderRegexp.capturedTexts()[2].toInt();
+					int denominator = cylinderRegexp.capturedTexts()[3].toInt();
+					int segments = (numerator * resolution) / denominator;
+					obj = model.emplaceAt<LDCylinder>(position, segments, resolution, transform, displacement);
+				}
+				else
+				{
+					obj = model.emplaceAt<LDSubfileReference>(position, referenceName, transform, displacement);
+				}
+
 				obj->setColor (tokens[1].toInt(nullptr, 0));
 				return obj;
 			}
--- a/src/primitives.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/primitives.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -263,7 +263,7 @@
 // This actual value is given by: hypot(0.0761, 0.3827)
 static const double chordLength = 0.3901929010117944;
 
-void PrimitiveModel::generateCylinder(Model& model) const
+void PrimitiveModel::generateCylinder(Model& model, Winding winding) const
 {
 	Q_ASSERT(this->type == Cylinder);
 	auto circle = makeCircle(this->segments, this->divisions, 1);
@@ -289,12 +289,14 @@
 		double x1 = circle[i].x2();
 		double z0 = circle[i].y1();
 		double z1 = circle[i].y2();
+		double y1 = (winding == CounterClockwise) ? 0.0 : 1.0;
+		double y2 = (winding == CounterClockwise) ? 1.0 : 0.0;
 
 		LDQuadrilateral* quad = model.emplace<LDQuadrilateral>(
-			Vertex {x1, 0.0, z1},
-			Vertex {x0, 0.0, z0},
-			Vertex {x0, 1.0, z0},
-			Vertex {x1, 1.0, z1}
+			Vertex {x1, y1, z1},
+			Vertex {x0, y1, z0},
+			Vertex {x0, y2, z0},
+			Vertex {x1, y2, z1}
 		);
 		quad->setColor(MainColor);
 	}
--- a/src/primitives.h	Wed May 30 22:31:06 2018 +0300
+++ b/src/primitives.h	Mon Jun 04 23:12:40 2018 +0300
@@ -57,7 +57,7 @@
 
 	QString typeName() const;
 	void generateBody(Model& model) const;
-	void generateCylinder(Model& model) const;
+	void generateCylinder(Model& model, Winding winding = CounterClockwise) const;
 	static QString typeName(Type type);
 	QString makeFileName(FilenameStyle style) const;
 };
--- a/src/toolsets/basictoolset.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/toolsets/basictoolset.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -104,34 +104,37 @@
 
 void BasicToolset::doInline (bool deep)
 {
-	for (LDSubfileReference* reference : filterByType<LDSubfileReference> (selectedObjects()))
+	for (LDObject* object : selectedObjects())
 	{
-		// Get the index of the subfile so we know where to insert the
-		// inlined contents.
-		QPersistentModelIndex referenceIndex = currentDocument()->indexOf(reference);
-		int row = referenceIndex.row();
-
-		if (referenceIndex.isValid())
+		if (object->isRasterizable())
 		{
-			Model inlined {m_documents};
-			reference->inlineContents(
-				m_documents,
-				currentDocument()->winding(),
-				inlined,
-				deep,
-				false
-			);
+			// Get the index of the subfile so we know where to insert the
+			// inlined contents.
+			QPersistentModelIndex referenceIndex = currentDocument()->indexOf(object);
+			int row = referenceIndex.row();
 
-			// Merge in the inlined objects
-			for (LDObject* inlinedObject : inlined.objects())
+			if (referenceIndex.isValid())
 			{
-				currentDocument()->insertCopy(row, inlinedObject);
-				mainWindow()->select(currentDocument()->index(row));
-				row += 1;
+				Model inlined {m_documents};
+				object->rasterize(
+					m_documents,
+					currentDocument()->winding(),
+					inlined,
+					deep,
+					false
+				);
+
+				// Merge in the inlined objects
+				for (LDObject* inlinedObject : inlined.objects())
+				{
+					currentDocument()->insertCopy(row, inlinedObject);
+					mainWindow()->select(currentDocument()->index(row));
+					row += 1;
+				}
+
+				// Delete the subfile now as it's been inlined.
+				currentDocument()->removeRow(referenceIndex.row());
 			}
-
-			// Delete the subfile now as it's been inlined.
-			currentDocument()->removeRow(referenceIndex.row());
 		}
 	}
 
--- a/src/toolsets/extprogramtoolset.cpp	Wed May 30 22:31:06 2018 +0300
+++ b/src/toolsets/extprogramtoolset.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -152,11 +152,10 @@
 {
 	for (LDObject* obj : objects)
 	{
-		if (obj->type() == LDObjectType::SubfileReference)
+		if (obj->isRasterizable())
 		{
-			LDSubfileReference* ref = static_cast<LDSubfileReference*> (obj);
 			Model model {m_documents};
-			ref->inlineContents(m_documents, CounterClockwise, model, true, false);
+			obj->rasterize(m_documents, CounterClockwise, model, true, false);
 			writeObjects(model.objects(), f);
 		}
 		else if (obj->type() == LDObjectType::BezierCurve)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.cpp	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,202 @@
+#include "matrixeditor.h"
+#include "ui_matrixeditor.h"
+
+MatrixEditor::MatrixEditor(const Matrix& matrix, const Vertex& position, QWidget *parent) :
+	QWidget {parent},
+	ui {*new Ui_MatrixEditor}
+{
+	this->ui.setupUi(this);
+	this->setPosition(position);
+	this->setMatrix(matrix);
+
+	for (int i : {0, 1, 2})
+	for (int j : {0, 1, 2})
+	{
+		connect(
+			matrixCell(i, j),
+			qOverload<double>(&QDoubleSpinBox::valueChanged),
+			this,
+			&MatrixEditor::matrixChanged
+		);
+	}
+
+	for (QDoubleSpinBox* spinbox : {this->ui.scalingX, this->ui.scalingY, this->ui.scalingZ})
+	{
+		connect(
+			spinbox,
+			qOverload<double>(&QDoubleSpinBox::valueChanged),
+			this,
+			&MatrixEditor::scalingChanged
+		);
+	}
+}
+
+MatrixEditor::MatrixEditor(QWidget* parent) :
+	MatrixEditor {Matrix::identity, {0, 0, 0}, parent} {}
+
+MatrixEditor::~MatrixEditor()
+{
+	delete &this->ui;
+}
+
+/*
+ * Returns a spinbox from the matrix grid at position (row, column).
+ * Row and column must be within [0, 2].
+ */
+QDoubleSpinBox* MatrixEditor::matrixCell(int row, int column) const
+{
+	if (qBound(0, row, 2) != row or qBound(0, column, 2) != column)
+	{
+		throw std::out_of_range {"bad row and column values"};
+	}
+	else
+	{
+		QLayoutItem* item = this->ui.matrixLayout->itemAtPosition(row, column);
+		return item ? qobject_cast<QDoubleSpinBox*>(item->widget()) : nullptr;
+	}
+}
+
+/*
+ * Returns a spinbox for the vector element at the given position
+ * Index must be within [0, 2]
+ */
+QDoubleSpinBox* MatrixEditor::vectorElement(int index)
+{
+	switch (index)
+	{
+	case 0:
+		return this->ui.scalingX;
+
+	case 1:
+		return this->ui.scalingY;
+
+	case 2:
+		return this->ui.scalingZ;
+
+	default:
+		throw std::out_of_range {"bad index"};
+	}
+}
+
+double MatrixEditor::matrixScaling(int column) const
+{
+	return sqrt(
+		pow(this->matrixCell(0, column)->value(), 2) +
+		pow(this->matrixCell(1, column)->value(), 2) +
+		pow(this->matrixCell(2, column)->value(), 2)
+	);
+}
+
+/*
+ * Updates the appropriate matrix column when a scaling vector element is changed.
+ */
+void MatrixEditor::scalingChanged()
+{
+	for (int column : {0, 1, 2})
+	{
+		if (this->sender() == this->vectorElement(column))
+		{
+			double oldScaling = this->matrixScaling(column);
+			double newScaling = static_cast<QDoubleSpinBox*>(this->sender())->value();
+
+			if (not qFuzzyCompare(newScaling, 0.0))
+			{
+				for (int row : {0, 1, 2})
+				{
+					double cellValue = this->matrixCell(row, column)->value();
+					cellValue *= newScaling / oldScaling;
+					QDoubleSpinBox* cellWidget = this->matrixCell(row, column);
+					withSignalsBlocked(cellWidget, [&]()
+					{
+						cellWidget->setValue(cellValue);
+					});
+				}
+			}
+
+			break;
+		}
+	}
+}
+
+/*
+ * Finds the position for the given cell widget.
+ */
+QPair<int, int> MatrixEditor::cellPosition(QDoubleSpinBox* cellWidget)
+{
+	for (int row : {0, 1, 2})
+	for (int column : {0, 1, 2})
+	{
+		if (this->matrixCell(row, column) == cellWidget)
+			return {row, column};
+	}
+
+	throw std::out_of_range {"widget is not in the matrix"};
+}
+
+/*
+ * Updates the appropriate scaling vector element when a matrix cell is changed.
+ */
+void MatrixEditor::matrixChanged()
+{
+	QDoubleSpinBox* cellWidget = static_cast<QDoubleSpinBox*>(this->sender());
+
+	try
+	{
+		int column = this->cellPosition(cellWidget).second;
+		QDoubleSpinBox* spinbox = this->vectorElement(column);
+		withSignalsBlocked(spinbox, [&]()
+		{
+			spinbox->setValue(this->matrixScaling(column));
+		});
+	}
+	catch (const std::out_of_range&) {}
+}
+
+Vertex MatrixEditor::position() const
+{
+	return {
+		this->ui.positionX->value(),
+		this->ui.positionY->value(),
+		this->ui.positionZ->value()
+	};
+}
+
+Matrix MatrixEditor::matrix() const
+{
+	Matrix transformationMatrix;
+
+	for (int i : {0, 1, 2})
+	for (int j : {0, 1, 2})
+	{
+		transformationMatrix(i, j) = this->matrixCell(i, j)->value();
+	}
+
+	return transformationMatrix;
+}
+
+void MatrixEditor::setPosition(const Vertex& position)
+{
+	this->ui.positionX->setValue(position.x);
+	this->ui.positionY->setValue(position.y);
+	this->ui.positionZ->setValue(position.z);
+}
+
+void MatrixEditor::setMatrix(const Matrix& matrix)
+{
+	for (int i : {0, 1, 2})
+	for (int j : {0, 1, 2})
+	{
+		QDoubleSpinBox* spinbox = matrixCell(i, j);
+		withSignalsBlocked(spinbox, [&](){ spinbox->setValue(matrix(i, j)); });
+	}
+
+	// Fill in the initial scaling values
+	for (int column : {0, 1, 2})
+	{
+		QDoubleSpinBox* spinbox = this->vectorElement(column);
+		withSignalsBlocked(spinbox, [&]()
+		{
+			spinbox->setValue(this->matrixScaling(column));
+		});
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.h	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,37 @@
+#pragma once
+#include <QWidget>
+#include "../linetypes/modelobject.h"
+
+class QDoubleSpinBox;
+class Ui_MatrixEditor;
+
+class MatrixEditor : public QWidget
+{
+	Q_OBJECT
+
+public:
+	MatrixEditor(
+		const Matrix& matrix = Matrix::identity,
+		const Vertex& position = {0, 0, 0},
+		QWidget* parent = nullptr
+	);
+	MatrixEditor(QWidget* parent);
+	~MatrixEditor();
+
+	Vertex position() const;
+	Matrix matrix() const;
+	void setPosition(const Vertex& position);
+	void setMatrix(const Matrix& matrix);
+
+private slots:
+	void scalingChanged();
+	void matrixChanged();
+
+private:
+	QDoubleSpinBox* matrixCell(int row, int column) const;
+	double matrixScaling(int column) const;
+	QPair<int, int> cellPosition(QDoubleSpinBox* cellWidget);
+	QDoubleSpinBox* vectorElement(int index);
+
+	Ui_MatrixEditor& ui;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/widgets/matrixeditor.ui	Mon Jun 04 23:12:40 2018 +0300
@@ -0,0 +1,274 @@
+<?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>697</width>
+    <height>231</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <item row="5" column="0">
+    <widget class="QLabel" name="label_5">
+     <property name="text">
+      <string>Scaling vector:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="DoubleSpinBox" name="scalingX">
+       <property name="prefix">
+        <string>𝑥 × </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>0.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="DoubleSpinBox" name="scalingY">
+       <property name="prefix">
+        <string>𝑦 × </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>0.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="DoubleSpinBox" name="scalingZ">
+       <property name="prefix">
+        <string>𝑧 × </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>0.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="4" column="1">
+    <layout class="QGridLayout" name="matrixLayout">
+     <item row="1" column="2">
+      <widget class="DoubleSpinBox" name="matrixF">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="2">
+      <widget class="DoubleSpinBox" name="matrixI">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <widget class="DoubleSpinBox" name="matrixA">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="2">
+      <widget class="DoubleSpinBox" name="matrixC">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="0">
+      <widget class="DoubleSpinBox" name="matrixG">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="2" column="1">
+      <widget class="DoubleSpinBox" name="matrixH">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="DoubleSpinBox" name="matrixB">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="0">
+      <widget class="DoubleSpinBox" name="matrixD">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="DoubleSpinBox" name="matrixE">
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="2" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="DoubleSpinBox" name="positionX">
+       <property name="prefix">
+        <string>𝑥 = </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="DoubleSpinBox" name="positionY">
+       <property name="prefix">
+        <string>𝑦 = </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="DoubleSpinBox" name="positionZ">
+       <property name="prefix">
+        <string>𝑧 = </string>
+       </property>
+       <property name="decimals">
+        <number>5</number>
+       </property>
+       <property name="minimum">
+        <double>-10000.000000000000000</double>
+       </property>
+       <property name="maximum">
+        <double>10000.000000000000000</double>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Position:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Transformation matrix:</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