Add base code for circular primitives

Mon, 20 Jun 2022 02:04:51 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Mon, 20 Jun 2022 02:04:51 +0300
changeset 232
8efa3a33172e
parent 231
a9bf6bab5ea2
child 233
5509bec02c81

Add base code for circular primitives

CMakeLists.txt file | annotate | diff | comparison | revisions
icons/linetype-circularprimitive.png file | annotate | diff | comparison | revisions
icons/save-as-outline.png file | annotate | diff | comparison | revisions
icons/save-outline.png file | annotate | diff | comparison | revisions
icons_svg/linetype-circularprimitive.svg file | annotate | diff | comparison | revisions
icons_svg/save-as-outline.svg file | annotate | diff | comparison | revisions
icons_svg/save-outline.svg file | annotate | diff | comparison | revisions
src/circularprimitive.h file | annotate | diff | comparison | revisions
src/document.cpp file | annotate | diff | comparison | revisions
src/document.h file | annotate | diff | comparison | revisions
src/gl/partrenderer.cpp file | annotate | diff | comparison | revisions
src/gl/partrenderer.h file | annotate | diff | comparison | revisions
src/ldrawalgorithm.h file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
src/model.cpp file | annotate | diff | comparison | revisions
src/model.h file | annotate | diff | comparison | revisions
src/polygoncache.cpp file | annotate | diff | comparison | revisions
src/ui/circletool.ui file | annotate | diff | comparison | revisions
src/ui/objecteditor.cpp file | annotate | diff | comparison | revisions
--- a/CMakeLists.txt	Wed Jun 15 19:47:02 2022 +0300
+++ b/CMakeLists.txt	Mon Jun 20 02:04:51 2022 +0300
@@ -47,6 +47,7 @@
 	src/settingseditor/settingseditor.cpp
 	src/types/boundingbox.cpp
 #	src/ui/canvas.cpp
+	src/ui/circletooloptions.cpp
 	src/ui/multiplyfactordialog.cpp
 	src/ui/objecteditor.cpp
 	src/widgets/colorbutton.cpp
@@ -58,6 +59,7 @@
 )
 set (LDFORGE_HEADERS
 	src/basics.h
+	src/circularprimitive.h
 	src/colors.h
 	src/document.h
 	src/documentmanager.h
@@ -88,6 +90,7 @@
 	src/settingseditor/settingseditor.h
 	src/types/boundingbox.h
 #	src/ui/canvas.h
+	src/ui/circletooloptions.h
 	src/ui/multiplyfactordialog.h
 	src/ui/objecteditor.h
 	src/widgets/colorbutton.h
@@ -101,6 +104,7 @@
 	src/mainwindow.ui
 	src/settingseditor/librarieseditor.ui
 	src/settingseditor/settingseditor.ui
+	src/ui/circletool.ui
 	src/ui/multiplyfactordialog.ui
 	src/ui/objecteditor.ui
 	src/widgets/colorselectdialog.ui
@@ -117,6 +121,7 @@
 set (LDFORGE_OTHER_FILES
 )
 
+set(CMAKE_AUTOUIC_SEARCH_PATHS src/ui)
 set(LDFORGE_RESOURCES ldforge.qrc)
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
Binary file icons/linetype-circularprimitive.png has changed
Binary file icons/save-as-outline.png has changed
Binary file icons/save-outline.png has changed
--- a/icons_svg/linetype-circularprimitive.svg	Wed Jun 15 19:47:02 2022 +0300
+++ b/icons_svg/linetype-circularprimitive.svg	Mon Jun 20 02:04:51 2022 +0300
@@ -1,59 +1,62 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    width="512"
    height="512"
    viewBox="0 0 512 512"
    version="1.1"
    id="svg6"
    sodipodi:docname="linetype-circularprimitive.svg"
-   inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     id="namedview8"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="1.0820313"
+     inkscape:cx="225.5018"
+     inkscape:cy="256.92419"
+     inkscape:window-width="1920"
+     inkscape:window-height="1047"
+     inkscape:window-x="1920"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg6" />
+  <title
+     id="title2">ionicons-v5-q</title>
+  <path
+     style="fill:#ffff0a;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
+     d="m 242.42911,69.470162 -1.30366,19.592296 -85.52192,32.997432 -61.197671,80.83969 -4.516995,95.56456 39.263636,78.35865 90.59169,43.64258 L 324.15608,407.88121 398.94611,335.5168 417.7623,251.705 380.34973,157.84476 327.446,107.09009 259.81574,82.478128 278.92587,64.288364 435.88495,71.005695 447.27619,435.10552 66.095826,442.8476 70.646482,68.812661 Z"
+     id="path1444" />
+  <path
+     d="M416,448H96a32.09,32.09,0,0,1-32-32V96A32.09,32.09,0,0,1,96,64H416a32.09,32.09,0,0,1,32,32V416A32.09,32.09,0,0,1,416,448Z"
+     style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"
+     id="path4" />
+  <circle
+     cx="253.52237"
+     cy="256.59106"
+     r="165.04449"
+     style="fill:none;stroke:#000000;stroke-width:27.5074px;stroke-linecap:round;stroke-linejoin:round"
+     id="circle4" />
   <metadata
-     id="metadata12">
+     id="metadata1900">
     <rdf:RDF>
       <cc:Work
          rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
         <dc:title>ionicons-v5-q</dc:title>
       </cc:Work>
     </rdf:RDF>
   </metadata>
-  <defs
-     id="defs10" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1920"
-     inkscape:window-height="962"
-     id="namedview8"
-     showgrid="false"
-     inkscape:zoom="0.39453125"
-     inkscape:cx="165.06309"
-     inkscape:cy="214.80274"
-     inkscape:window-x="0"
-     inkscape:window-y="29"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="svg6" />
-  <title
-     id="title2">ionicons-v5-q</title>
-  <circle
-     cx="256"
-     cy="256"
-     r="192"
-     style="fill:#ffff44;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1"
-     id="circle4" />
 </svg>
--- a/icons_svg/save-as-outline.svg	Wed Jun 15 19:47:02 2022 +0300
+++ b/icons_svg/save-as-outline.svg	Mon Jun 20 02:04:51 2022 +0300
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    width="512"
    height="512"
    viewBox="0 0 512 512"
    version="1.1"
    id="svg3862"
    sodipodi:docname="save-as-outline.svg"
-   inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
   <metadata
      id="metadata3868">
     <rdf:RDF>
@@ -38,18 +38,26 @@
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
      inkscape:window-width="1920"
-     inkscape:window-height="970"
+     inkscape:window-height="967"
      id="namedview3864"
      showgrid="false"
-     inkscape:zoom="0.27345145"
-     inkscape:cx="-30.25538"
-     inkscape:cy="-228.93654"
+     inkscape:zoom="0.7734375"
+     inkscape:cx="277.9798"
+     inkscape:cy="265.05051"
      inkscape:window-x="0"
-     inkscape:window-y="29"
+     inkscape:window-y="40"
      inkscape:window-maximized="1"
-     inkscape:current-layer="svg3862" />
+     inkscape:current-layer="svg3862"
+     inkscape:pagecheckerboard="0" />
   <title
      id="title3858">ionicons-v5-p</title>
+  <rect
+     style="fill:#ffffff"
+     id="rect845"
+     width="224.55817"
+     height="91.655327"
+     x="93.8778"
+     y="100.38228" />
   <path
      d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z"
      style="fill:#ffa0a0;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1"
--- a/icons_svg/save-outline.svg	Wed Jun 15 19:47:02 2022 +0300
+++ b/icons_svg/save-outline.svg	Mon Jun 20 02:04:51 2022 +0300
@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    width="512"
    height="512"
    viewBox="0 0 512 512"
    version="1.1"
    id="svg3862"
    sodipodi:docname="save-outline.svg"
-   inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
   <metadata
      id="metadata3868">
     <rdf:RDF>
@@ -37,18 +37,26 @@
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
      inkscape:window-width="1920"
-     inkscape:window-height="970"
+     inkscape:window-height="967"
      id="namedview3864"
      showgrid="false"
-     inkscape:zoom="0.27345145"
-     inkscape:cx="-30.25538"
-     inkscape:cy="-228.93654"
+     inkscape:zoom="1.546875"
+     inkscape:cx="146.10101"
+     inkscape:cy="10.343434"
      inkscape:window-x="0"
-     inkscape:window-y="29"
+     inkscape:window-y="40"
      inkscape:window-maximized="1"
-     inkscape:current-layer="svg3862" />
+     inkscape:current-layer="svg3862"
+     inkscape:pagecheckerboard="0" />
   <title
      id="title3858">ionicons-v5-p</title>
+  <rect
+     style="fill:#ffffff"
+     id="rect845"
+     width="221.13829"
+     height="92.165718"
+     x="98.009506"
+     y="103.8269" />
   <path
      d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z"
      style="fill:#40a0ff;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/circularprimitive.h	Mon Jun 20 02:04:51 2022 +0300
@@ -0,0 +1,88 @@
+#pragma once
+#include <glm/gtc/matrix_transform.hpp>
+#include "basics.h"
+#include "model.h"
+#include "ldrawalgorithm.h"
+
+template<typename Fn>
+void rasterize(const CircularPrimitive& circ, Fn&& fn)
+{
+	std::vector<ModelElement> result;
+	const auto xform = [&circ](const glm::vec2& p, float y){
+		return glm::vec3{circ.transformation * glm::vec4{p.x, y, p.y, 1}};
+	};
+	const glm::vec3 origin = xform({0, 0}, 0);
+	switch(circ.type) {
+	case CircularPrimitive::Circle:
+		ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&]
+		(const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){
+			fn(edge(xform(p1, 0), xform(p2, 0)));
+		});
+		break;
+	case CircularPrimitive::Disc:
+		ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&]
+		(const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){
+			fn(triangle(origin, xform(p1, 0), xform(p2, 0)));
+		});
+		break;
+	case CircularPrimitive::Cylinder:
+		ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&]
+		(const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){
+			fn(quadrilateral(xform(p1, 1), xform(p2, 1), xform(p2, 0), xform(p1, 0)));
+		});
+		break;
+	case CircularPrimitive::CylinderOpen:
+		rasterize(CircularPrimitive{
+			.type = CircularPrimitive::Cylinder,
+			.fraction = circ.fraction,
+			.transformation = circ.transformation,
+		}, fn);
+		rasterize(CircularPrimitive{
+			.type = CircularPrimitive::Circle,
+			.fraction = circ.fraction,
+			.transformation = circ.transformation,
+		}, fn);
+		rasterize(CircularPrimitive{
+			.type = CircularPrimitive::Circle,
+			.fraction = circ.fraction,
+			.transformation = glm::translate(circ.transformation, {0, 1, 0}),
+		}, fn);
+		break;
+	case CircularPrimitive::CylinderClosed:
+		rasterize(CircularPrimitive{
+			.type = CircularPrimitive::CylinderOpen,
+			.fraction = circ.fraction,
+			.transformation = circ.transformation,
+		}, fn);
+		rasterize(CircularPrimitive{
+			.type = CircularPrimitive::Disc,
+			.fraction = circ.fraction,
+			.transformation = circ.transformation,
+		}, fn);
+		break;
+	case CircularPrimitive::DiscNegative:
+		{
+			unsigned int i = 0;
+			ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&]
+			(const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){
+				constexpr glm::vec2 corners[4] = {
+					{+1, +1},
+					{-1, +1},
+					{-1, -1},
+					{+1, -1},
+				};
+				const glm::vec2& corner = corners[i * 4 / circ.fraction.divisions];
+				fn(triangle(xform(p2, 0), xform(p1, 0), xform(corner, 0)));
+				++i;
+			});
+		}
+		break;
+	case CircularPrimitive::Chord:
+		for (unsigned int i = 1; i < circ.fraction.segments; ++i) {
+			const glm::vec2& p1 = ldraw::rimpoint(circ.fraction.divisions, i);
+			const glm::vec2& p2 = ldraw::rimpoint(circ.fraction.divisions, i + 1);
+			fn(triangle(xform(p2, 0), xform(p1, 0), xform({1, 0}, 0)));
+		}
+		break;
+	}
+}
--- a/src/document.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/document.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -23,6 +23,7 @@
 #include "model.h"
 #include "ui/objecteditor.h"
 #include "gl/partrenderer.h"
+#include "circularprimitive.h"
 
 // Make mapbox::earcut work with glm::vec3
 namespace mapbox {
@@ -63,6 +64,11 @@
 	});
 }
 
+void EditTools::setCircleToolOptions(const CircleToolOptions& options)
+{
+	this->circleToolOptions = options;
+}
+
 void EditTools::mvpMatrixChanged(const glm::mat4& matrix)
 {
 	this->mvpMatrix = matrix;
@@ -138,19 +144,25 @@
 	painter->drawPolygon(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
 }
 
-static opt<std::vector<glm::vec3>> modelActionPoints(const ModelAction& action)
+static std::vector<std::vector<glm::vec3>> modelActionPoints(const ModelAction& action)
 {
-	opt<std::vector<glm::vec3>> result;
+	std::vector<std::vector<glm::vec3>> result;
 	if (const AppendToModel* append = std::get_if<AppendToModel>(&action)) {
 		const ModelElement& newElement = append->newElement;
 		if (const LineSegment* seg = std::get_if<Colored<LineSegment>>(&newElement)) {
-			result = {seg->p1, seg->p2};
+			result.push_back({seg->p1, seg->p2});
 		}
 		else if (const Triangle* tri = std::get_if<Colored<Triangle>>(&newElement)) {
-			result = {tri->p1, tri->p2, tri->p3};
+			result.push_back({tri->p1, tri->p2, tri->p3});
 		}
 		else if (const Quadrilateral* quad = std::get_if<Colored<Quadrilateral>>(&newElement)) {
-			result = {quad->p1, quad->p2, quad->p3, quad->p4};
+			result.push_back({quad->p1, quad->p2, quad->p3, quad->p4});
+		}
+		else if (const CircularPrimitive* circ = std::get_if<Colored<CircularPrimitive>>(&newElement)) {
+			rasterize(*circ, [&](const ModelElement& element){
+				const auto& subpoints = modelActionPoints(AppendToModel{element});
+				std::copy(subpoints.begin(), subpoints.end(), std::back_inserter(result));
+			});
 		}
 	}
 	return result;
@@ -199,23 +211,36 @@
 	}
 }
 
+const std::vector<ModelAction> EditTools::modelActions() const
+{
+	switch(this->mode) {
+	case SelectMode:
+		return {};
+	case DrawMode:
+		return drawModeActions();
+	case CircleMode:
+		return circleModeActions();
+	}
+}
+
 void EditTools::renderPreview(QPainter* painter, const void* pensptr)
 {
 	const Pens& pens = *reinterpret_cast<const Pens*>(pensptr);
 	painter->setPen(pens.polygonPen);
-	for (const ModelAction& action : this->actions()) {
-		const std::vector<glm::vec3> points = modelActionPoints(action).value_or(std::vector<glm::vec3>{});
-		if (points.size() == 2) {
-			drawWorldPolyline(painter, points, renderer);
-		}
-		else {
-			if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) {
-				painter->setBrush(pens.greenPolygonBrush);
+	for (const ModelAction& action : this->modelActions()) {
+		for (const std::vector<glm::vec3>& points : modelActionPoints(action)) {
+			if (points.size() == 2) {
+				drawWorldPolyline(painter, points, renderer);
 			}
 			else {
-				painter->setBrush(pens.redPolygonBrush);
+				if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) {
+					painter->setBrush(pens.greenPolygonBrush);
+				}
+				else {
+					painter->setBrush(pens.redPolygonBrush);
+				}
+				drawWorldPolygon(painter, points, this->renderer);
 			}
-			drawWorldPolygon(painter, points, this->renderer);
 		}
 	}
 	painter->setBrush(pens.pointBrush);
@@ -268,39 +293,21 @@
 				this->polygon.push_back(*this->worldPosition);
 			}
 		}
-		else if (true
-			and event->button() == Qt::RightButton
-			and this->polygon.size() > 1
-		) {
-			this->removeLastPoint();
+		break;
+	case CircleMode:
+		if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) {
+			if (this->polygon.size() == 2) {
+				this->closeShape();
+			}
+			else {
+				this->polygon.push_back(*this->worldPosition);
+			}
 		}
 		break;
 	}
-}
-
-constexpr float distancesquared(const glm::vec3& p1, const glm::vec3& p2)
-{
-	const float dx = p2.x - p1.x;
-	const float dy = p2.y - p1.y;
-	const float dz = p2.z - p1.z;
-	return (dx * dx) + (dy * dy) + (dz * dz);
-}
-
-inline float area(const Quadrilateral& q)
-{
-	return 0.5 * (
-		glm::length(glm::cross(q.p2 - q.p1, q.p3 - q.p1)) +
-		glm::length(glm::cross(q.p3 - q.p2, q.p4 - q.p2))
-	);
-}
-
-inline float energy(const Quadrilateral& q)
-{
-	const float L2 = distancesquared(q.p1, q.p2)
-		+ distancesquared(q.p2, q.p3)
-		+ distancesquared(q.p3, q.p4)
-		+ distancesquared(q.p4, q.p1);
-	return 1 - 6.928203230275509 * area(q) / L2;
+	if (event->button() == Qt::RightButton and this->polygon.size() > 1) {
+		this->removeLastPoint();
+	}
 }
 
 struct MergedTriangles
@@ -365,7 +372,32 @@
 	return result;
 }
 
-const std::vector<ModelAction> EditTools::actions() const
+
+const std::vector<ModelAction> EditTools::circleModeActions() const
+{
+	std::vector<ModelAction> result;
+	if (this->numpoints == 2) {
+		const glm::vec3 x = polygon[1] - polygon[0];
+		glm::mat4 transform{
+			glm::vec4{x, 0},
+			this->gridMatrix[2],
+			glm::vec4{glm::cross(glm::vec3{-this->gridMatrix[2]}, x), 0},
+			glm::vec4{this->polygon[0], 1},
+		};
+		Colored<CircularPrimitive> circ{
+			CircularPrimitive{
+				.type = this->circleToolOptions.type,
+				.fraction = this->circleToolOptions.fraction,
+				.transformation = transform,
+			},
+			MAIN_COLOR
+		};
+		result.push_back(AppendToModel{.newElement = circ});
+	}
+	return result;
+}
+
+const std::vector<ModelAction> EditTools::drawModeActions() const
 {
 	std::vector<ModelAction> result;
 	if (this->numpoints == 2) {
@@ -415,7 +447,7 @@
 
 void EditTools::closeShape()
 {
-	for (const ModelAction& action : this->actions()) {
+	for (const ModelAction& action : this->modelActions()) {
 		Q_EMIT this->modelAction(action);
 	}
 	this->polygon.clear();
--- a/src/document.h	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/document.h	Mon Jun 20 02:04:51 2022 +0300
@@ -27,7 +27,8 @@
 enum EditingMode
 {
 	SelectMode,
-	DrawMode
+	DrawMode,
+	CircleMode
 };
 
 Q_DECLARE_METATYPE(EditingMode);
@@ -56,6 +57,10 @@
 	glm::mat4 gridMatrix{1};
 	Plane gridPlane;
 	opt<glm::vec3> worldPosition;
+	CircleToolOptions circleToolOptions = {
+		.fraction = {16, 16},
+		.type = CircularPrimitive::Circle,
+	};
 public:
 	explicit EditTools(QObject *parent = nullptr);
 	~EditTools() override;
@@ -64,6 +69,7 @@
 	EditingMode currentEditingMode() const;
 	Q_SLOT void setEditMode(EditingMode mode);
 	Q_SLOT void setGridMatrix(const glm::mat4& gridMatrix);
+	Q_SLOT void setCircleToolOptions(const CircleToolOptions& options);
 Q_SIGNALS:
 	void newStatusText(const QString& newStatusText);
 	void modelAction(const ModelAction& action);
@@ -74,7 +80,9 @@
 	void mouseClick(const QMouseEvent* event) override;
 	void overpaint(QPainter* painter) override;
 private:
-	const std::vector<ModelAction> actions() const;
+	const std::vector<ModelAction> modelActions() const;
+	const std::vector<ModelAction> circleModeActions() const;
+	const std::vector<ModelAction> drawModeActions() const;
 	void closeShape();
 	void renderPreview(QPainter* painter, const void* pensptr);
 	void removeLastPoint();
--- a/src/gl/partrenderer.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/gl/partrenderer.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -43,6 +43,7 @@
 	colorTable{colorTable}
 {
 	this->setMouseTracking(true);
+	this->setFocusPolicy(Qt::WheelFocus);
 	connect(model, &Model::rowsInserted, [&]{
 		this->needBuild = true;
 	});
@@ -294,38 +295,42 @@
 
 void PartRenderer::mouseMoveEvent(QMouseEvent* event)
 {
-	const bool left = event->buttons() & Qt::LeftButton;
-	const QPoint move = event->pos() - this->lastMousePosition;
-	this->totalMouseMove += move.manhattanLength();
-	if (left and not move.isNull())
-	{
-		// q_x is the rotation of the brick along the vertical y-axis, because turning the
-		// vertical axis causes horizontal (=x) rotation. Likewise q_y is the rotation of the
-		// brick along the horizontal x-axis, which causes vertical rotation.
-		const auto scalar = 0.006f;
-		const float move_x = static_cast<float>(move.x());
-		const float move_y = static_cast<float>(move.y());
-		const glm::quat q_x = glm::angleAxis(scalar * move_x, glm::vec3{0, -1, 0});
-		const glm::quat q_y = glm::angleAxis(scalar * move_y, glm::vec3{-1, 0, 0});
-		this->modelQuaternion = q_x * q_y * this->modelQuaternion;
-		this->updateModelMatrix();
+	if (not this->frozen) {
+		const bool left = event->buttons() & Qt::LeftButton;
+		const QPoint move = event->pos() - this->lastMousePosition;
+		this->totalMouseMove += move.manhattanLength();
+		if (left and not move.isNull())
+		{
+			// q_x is the rotation of the brick along the vertical y-axis, because turning the
+			// vertical axis causes horizontal (=x) rotation. Likewise q_y is the rotation of the
+			// brick along the horizontal x-axis, which causes vertical rotation.
+			const auto scalar = 0.006f;
+			const float move_x = static_cast<float>(move.x());
+			const float move_y = static_cast<float>(move.y());
+			const glm::quat q_x = glm::angleAxis(scalar * move_x, glm::vec3{0, -1, 0});
+			const glm::quat q_y = glm::angleAxis(scalar * move_y, glm::vec3{-1, 0, 0});
+			this->modelQuaternion = q_x * q_y * this->modelQuaternion;
+			this->updateModelMatrix();
+		}
+		this->lastMousePosition = event->pos();
+		for (RenderLayer* layer : this->activeRenderLayers) {
+			layer->mouseMoved(event);
+		}
+		this->update();
 	}
-	this->lastMousePosition = event->pos();
-	for (RenderLayer* layer : this->activeRenderLayers) {
-		layer->mouseMoved(event);
-	}
-	this->update();
 }
 
 void PartRenderer::mousePressEvent(QMouseEvent* event)
 {
-	this->totalMouseMove = 0;
-	this->lastMousePosition = event->pos();
+	if (not this->frozen) {
+		this->totalMouseMove = 0;
+		this->lastMousePosition = event->pos();
+	}
 }
 
 void PartRenderer::mouseReleaseEvent(QMouseEvent* event)
 {
-	if (this->totalMouseMove < (2.0 / sqrt(2)) * 5.0)
+	if (not frozen and this->totalMouseMove < (2.0 / sqrt(2)) * 5.0)
 	{
 		for (RenderLayer* layer : this->activeRenderLayers) {
 			layer->mouseClick(event);
@@ -334,13 +339,22 @@
 	}
 }
 
+void PartRenderer::keyReleaseEvent(QKeyEvent* event)
+{
+	if (event->key() == Qt::Key_Pause) {
+		this->frozen = not this->frozen;
+	}
+}
+
 void PartRenderer::wheelEvent(QWheelEvent* event)
 {
-	static constexpr double WHEEL_STEP = 1 / 1000.0;
-	const double move = (-event->angleDelta().y()) * WHEEL_STEP;
-	this->zoom = std::clamp(this->zoom + move, MIN_ZOOM, MAX_ZOOM);
-	this->updateViewMatrix();
-	this->update();
+	if (not this->frozen) {
+		static constexpr double WHEEL_STEP = 1 / 1000.0;
+		const double move = (-event->angleDelta().y()) * WHEEL_STEP;
+		this->zoom = std::clamp(this->zoom + move, MIN_ZOOM, MAX_ZOOM);
+		this->updateViewMatrix();
+		this->update();
+	}
 }
 
 void PartRenderer::addRenderLayer(RenderLayer* layer)
--- a/src/gl/partrenderer.h	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/gl/partrenderer.h	Mon Jun 20 02:04:51 2022 +0300
@@ -28,6 +28,7 @@
 	bool needBuild = true;
 	std::vector<RenderLayer*> activeRenderLayers;
 	std::vector<RenderLayer*> inactiveRenderLayers;
+	bool frozen = false;
 public:
 	PartRenderer(
 		Model* model,
@@ -56,6 +57,7 @@
 	void mouseMoveEvent(QMouseEvent* event) override;
 	void mousePressEvent(QMouseEvent* event) override;
 	void mouseReleaseEvent(QMouseEvent* event) override;
+	void keyReleaseEvent(QKeyEvent* event) override;
 	void wheelEvent(QWheelEvent* event) override;
 	Line<3> cameraLine(const QPointF& point) const;
 	glm::vec3 unproject(const glm::vec3& win) const;
--- a/src/ldrawalgorithm.h	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/ldrawalgorithm.h	Mon Jun 20 02:04:51 2022 +0300
@@ -19,18 +19,45 @@
 	void makeUnofficial(ModelEditor &editor);
 	*/
 
+	constexpr float circleAngle(int divisions, int i)
+	{
+		constexpr float ofs = 0.5 * pi<>;
+		float factor = -2.0f * pi<> / divisions;
+		return i * factor + ofs;
+	}
+
+	constexpr glm::vec2 rimpoint(int divisions, int i)
+	{
+		const float angle = circleAngle(divisions, i);
+		return glm::vec2{std::sin(angle), std::cos(angle)};
+	}
+
+	template<typename Fn>
+	void circleAngles(int segments, int divisions, Fn&& fn)
+	{
+		for (int i = 0; i < segments; i += 1)
+		{
+			const float a1 = circleAngle(divisions, i - 1);
+			const float a2 = circleAngle(divisions, i);
+			const float a3 = circleAngle(divisions, i + 1);
+			fn(a1, a2, a3);
+		}
+	}
+
 	template<typename Fn>
 	void circle(int segments, int divisions, Fn&& fn)
 	{
-		float factor = 2.0f * pi<> / divisions;
-		for (int i = 0; i < segments; i += 1)
-		{
+		circleAngles(segments, divisions, [&fn](
+			const float a1,
+			const float a2,
+			const float a3
+		){
 			fn(
-				glm::vec2{std::sin((i - 1) * factor), std::cos((i - 1) * factor)},
-				glm::vec2{std::sin(i * factor), std::cos(i * factor)},
-				glm::vec2{std::sin((i + 1) * factor), std::cos((i - 1) * factor)}
+				glm::vec2{std::sin(a1), std::cos(a1)},
+				glm::vec2{std::sin(a2), std::cos(a2)},
+				glm::vec2{std::sin(a3), std::cos(a3)}
 			);
-		}
+		});
 	}
 }
 
--- a/src/main.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/main.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -14,6 +14,7 @@
 #include "settingseditor/settingseditor.h"
 #include "widgets/colorselectdialog.h"
 #include "settings.h"
+#include "ui/circletooloptions.h"
 
 static const QDir LOCALE_DIR {":/locale"};
 
@@ -269,8 +270,9 @@
 	};
 }
 
-void initializeTools(Ui_MainWindow* ui, QWidget* parent)
+void initializeTools(Ui_MainWindow* ui, DocumentManager* documents, QWidget* parent)
 {
+	CircleToolOptionsWidget* circleToolOptions = new CircleToolOptionsWidget{parent};
 	const struct
 	{
 		QString name, tooltip;
@@ -289,6 +291,12 @@
 			.icon = {":/icons/pencil-outline.png"},
 			.widget = nullptr,
 		},
+		{
+			.name = QObject::tr("Circle"),
+			.tooltip = QObject::tr("Draw circular primitives."),
+			.icon = {":/icons/linetype-circularprimitive.png"},
+			.widget = circleToolOptions,
+		},
 	};
 	for (int i = 0; i < countof(editingModesInfo); ++i) {
 		const auto& editingModeInfo = editingModesInfo[i];
@@ -304,7 +312,18 @@
 			widget = new QWidget{parent};
 		}
 		ui->toolWidgetStack->addWidget(widget);
+		QObject::connect(action, &QAction::triggered, [ui, i]{
+			ui->toolWidgetStack->setCurrentIndex(i);
+		});
 	}
+	QObject::connect(
+		circleToolOptions,
+		&CircleToolOptionsWidget::optionsChanged,
+		[ui, documents](const CircleToolOptions& options) {
+			if (ModelData* data = currentModelData(ui, documents)) {
+				data->tools->setCircleToolOptions(options);
+			}
+		});
 }
 
 constexpr bool sortModelIndexesByRow(const QModelIndex& a, const QModelIndex& b)
@@ -583,7 +602,7 @@
 			action->setChecked(action->data().value<EditingMode>() == mode);
 		}
 	};
-	initializeTools(&ui, &mainWindow);
+	initializeTools(&ui, &documents, &mainWindow);
 	for (QAction* action : ui.editingModesToolBar->actions()) {
 		QObject::connect(action, &QAction::triggered, [&, action]{
 			if (ModelData* data = currentModelData(&ui, &documents)) {
--- a/src/model.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/model.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -19,6 +19,63 @@
 #include <QPixmap>
 #include "model.h"
 
+constexpr unsigned int gcd(unsigned int a, unsigned int b)
+{
+	while (a != b) {
+		if (b > a) {
+			b -= a;
+		}
+		else if (a > b) {
+			a -= b;
+		}
+	}
+	return a;
+}
+
+static_assert(gcd(16, 15) == 1);
+static_assert(gcd(16, 4) == 4);
+static_assert(gcd(272, 192) == 16);
+
+static constexpr const char* circularPrimitiveTypeString(const CircularPrimitive& circ)
+{
+	switch (circ.type) {
+	case CircularPrimitive::Circle:
+		return "edge";
+	case CircularPrimitive::Disc:
+		return "disc";
+	case CircularPrimitive::Cylinder:
+		return "cyli";
+	case CircularPrimitive::CylinderOpen:
+		return "cylo";
+	case CircularPrimitive::CylinderClosed:
+		return "cylc";
+	case CircularPrimitive::DiscNegative:
+		return "ndis";
+	case CircularPrimitive::Chord:
+		return "chrd";
+	}
+	return "";
+}
+
+static QString circularPrimitiveFilePath(const CircularPrimitive& circ)
+{
+	QString result;
+	if (circ.fraction.divisions != 16) {
+		result += QString::number(circ.fraction.divisions) + QStringLiteral("\\");
+	}
+	const int factor = gcd(circ.fraction.segments, circ.fraction.divisions);
+	int num = circ.fraction.segments / factor;
+	int denom = circ.fraction.divisions / factor;
+	if (denom < 4) {
+		num *= 4 / denom;
+		denom = 4;
+	}
+	result += QStringLiteral("%1-%2").arg(num).arg(denom);
+	result += QString::fromLatin1(circularPrimitiveTypeString(circ));
+	result += QStringLiteral(".dat");
+	return result;
+}
+
 static const char* iconPathForElement(const ModelElement& element)
 {
 	return std::visit(overloaded{
@@ -37,6 +94,9 @@
 		[](const Colored<ConditionalEdge>&) {
 			return ":/icons/linetype-conditionaledge.png";
 		},
+		[](const Colored<CircularPrimitive>&) {
+			return ":/icons/linetype-circularprimitive.png";
+		},
 		[](const Comment&) {
 			return ":/icons/chatbubble-ellipses-outline.png";
 		},
@@ -102,6 +162,12 @@
 				.arg(vertexToString(cedge.c1))
 				.arg(vertexToString(cedge.c2));
 		},
+		[](const Colored<CircularPrimitive>& circ) {
+			return QStringLiteral("1 %1 %2 %3")
+				.arg(circ.color.index)
+				.arg(transformToString(circ.transformation))
+				.arg(circularPrimitiveFilePath(circ));
+		},
 		[](const Comment& comment) {
 			return "0 " + comment.text;
 		},
--- a/src/model.h	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/model.h	Mon Jun 20 02:04:51 2022 +0300
@@ -47,12 +47,50 @@
 
 struct Empty {};
 
+struct CircularFraction
+{
+	unsigned int segments;
+	unsigned int divisions;
+};
+
+constexpr bool operator<(const CircularFraction& p, const CircularFraction& q)
+{
+	// a/b < c/d
+	// a < c * b / d
+	// a * d < c * b
+	return p.segments * q.divisions < q.segments / p.divisions;
+}
+
+struct CircularPrimitive
+{
+	enum Type
+	{
+		Circle,
+		Disc,
+		Cylinder,
+		CylinderOpen,
+		CylinderClosed,
+		DiscNegative,
+		Chord,
+	} type;
+	static constexpr int NUM_TYPES = Chord + 1;
+	CircularFraction fraction;
+	glm::mat4 transformation;
+};
+
+struct CircleToolOptions
+{
+	CircularFraction fraction;
+	CircularPrimitive::Type type;
+};
+
 using ModelElement = std::variant<
 	Colored<SubfileReference>,
 	Colored<LineSegment>,
 	Colored<Triangle>,
 	Colored<Quadrilateral>,
 	Colored<ConditionalEdge>,
+	Colored<CircularPrimitive>,
 	Comment,
 	Empty,
 	ParseError>;
@@ -206,3 +244,22 @@
 		}
 	}
 }
+
+constexpr Colored<LineSegment> edge(const glm::vec3& p1, const glm::vec3& p2)
+{
+	return Colored<LineSegment>{{.p1 = p1, .p2 = p2}, EDGE_COLOR};
+}
+
+constexpr Colored<Triangle> triangle(const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3)
+{
+	return Colored<Triangle>{{.p1 = p1, .p2 = p2, .p3 = p3}, MAIN_COLOR};
+}
+
+constexpr Colored<Quadrilateral> quadrilateral(
+	const glm::vec3& p1,
+	const glm::vec3& p2,
+	const glm::vec3& p3,
+	const glm::vec3& p4)
+{
+	return Colored<Quadrilateral>{{.p1 = p1, .p2 = p2, .p3 = p3, .p4 = p4}, MAIN_COLOR};
+}
--- a/src/polygoncache.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/polygoncache.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -1,6 +1,7 @@
 #include "polygoncache.h"
 #include "documentmanager.h"
 #include "invert.h"
+#include "circularprimitive.h"
 
 Model* resolve(const QString& name, const ModelId callingModelId, DocumentManager* documents)
 {
@@ -76,6 +77,26 @@
 					}
 				}
 			},
+			[&result, id](const Colored<CircularPrimitive>& circ) {
+				rasterize(circ, [&](const ModelElement& element){
+					std::visit<void>(overloaded{
+						// TODO: :-(
+						[&](const Colored<LineSegment>& edge) {
+							result.push_back({{edge, edge.color}, id});
+						},
+						[&](const Colored<Triangle>& triangle) {
+							result.push_back({{triangle, triangle.color}, id});
+						},
+						[&](const Colored<Quadrilateral>& quad) {
+							result.push_back({{quad, quad.color}, id});
+						},
+						[&](const Colored<ConditionalEdge>& cedge) {
+							result.push_back({{cedge, cedge.color}, id});
+						},
+						[&](const auto&){},
+					}, element);
+				});
+			},
 			[](const ModelElement&) {}
 		}, element);
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ui/circletool.ui	Mon Jun 20 02:04:51 2022 +0300
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CircleToolOptions</class>
+ <widget class="QWidget" name="CircleToolOptions">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>651</width>
+    <height>57</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <widget class="QSpinBox" name="segments">
+     <property name="minimum">
+      <number>1</number>
+     </property>
+     <property name="maximum">
+      <number>16</number>
+     </property>
+     <property name="value">
+      <number>16</number>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>/</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QComboBox" name="divisions">
+     <property name="editable">
+      <bool>true</bool>
+     </property>
+     <property name="currentIndex">
+      <number>1</number>
+     </property>
+     <property name="insertPolicy">
+      <enum>QComboBox::NoInsert</enum>
+     </property>
+     <item>
+      <property name="text">
+       <string>8</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>16</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>48</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="ratio">
+     <property name="minimumSize">
+      <size>
+       <width>64</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="text">
+      <string>1</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QComboBox" name="type"/>
+   </item>
+   <item>
+    <spacer name="horizontalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>40</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
--- a/src/ui/objecteditor.cpp	Wed Jun 15 19:47:02 2022 +0300
+++ b/src/ui/objecteditor.cpp	Mon Jun 20 02:04:51 2022 +0300
@@ -12,7 +12,8 @@
 	const glm::vec3*,
 	const glm::mat4*,
 	const QString*,
-	ldraw::Color>;
+	ldraw::Color,
+	const CircularFraction*>;
 
 enum PropertyKey
 {
@@ -27,6 +28,7 @@
 	Name,
 	Text,
 	Code,
+	Fraction,
 };
 
 std::map<PropertyKey, PropertyValue> getProperties(const ModelElement& element)
@@ -63,6 +65,11 @@
 			result[Name] = &ref.name;
 			result[Color] = ref.color;
 		},
+		[&](const Colored<CircularPrimitive>& circ) {
+			result[Transformation] = &circ.transformation;
+			result[Fraction] = &circ.fraction;
+			result[Color] = circ.color;
+		},
 		[&](Empty) {},
 		[&](const Comment& comment) {
 			result[Text] = &comment.text;

mercurial