src/gui_editactions.cpp

changeset 183
f1b8cb53d2a2
child 184
fae3bc9ce319
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/gui_editactions.cpp	Wed May 08 15:19:06 2013 +0300
@@ -0,0 +1,687 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 2013 Santeri Piippo
+ *  
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *  
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *  
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gui.h"
+#include "common.h"
+#include "file.h"
+#include "history.h"
+#include "colorSelectDialog.h"
+#include "historyDialog.h"
+#include "setContentsDialog.h"
+#include "misc.h"
+#include "bbox.h"
+#include "radiobox.h"
+#include "extprogs.h"
+#include <qspinbox.h>
+#include <qcheckbox.h>
+
+vector<LDObject*> g_Clipboard;
+
+cfg (bool, edit_schemanticinline, false);
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+static bool copyToClipboard () {
+	vector<LDObject*> objs = g_win->sel ();
+	
+	if (objs.size() == 0)
+		return false;
+	
+	// Clear the clipboard first.
+	for (LDObject* obj : g_Clipboard)
+		delete obj;
+	
+	g_Clipboard.clear ();
+	
+	// Now, copy the contents into the clipboard. The objects should be
+	// separate objects so that modifying the existing ones does not affect
+	// the clipboard. Thus, we add clones of the objects to the clipboard, not
+	// the objects themselves.
+	for (ulong i = 0; i < objs.size(); ++i)
+		g_Clipboard.push_back (objs[i]->clone ());
+	
+	return true;
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (cut, "Cut", "cut", "Cut the current selection to clipboard.", CTRL (X)) {
+	vector<ulong> ulaIndices;
+	vector<LDObject*> copies;
+	
+	if (!copyToClipboard ())
+		return;
+	
+	g_win->deleteSelection (&ulaIndices, &copies);
+	History::addEntry (new DelHistory (ulaIndices, copies, DelHistory::Cut));
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (copy, "Copy", "copy", "Copy the current selection to clipboard.", CTRL (C)) {
+	copyToClipboard ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (paste, "Paste", "paste", "Paste clipboard contents.", CTRL (V)) {
+	vector<ulong> historyIndices;
+	vector<LDObject*> historyCopies;
+	
+	ulong idx = g_win->getInsertionPoint ();
+	g_win->sel ().clear ();
+	
+	for (LDObject* obj : g_Clipboard) {
+		historyIndices.push_back (idx);
+		historyCopies.push_back (obj->clone ());
+		
+		LDObject* copy = obj->clone ();
+		g_curfile->insertObj (idx, copy);
+		g_win->sel ().push_back (copy);
+	}
+	
+	History::addEntry (new AddHistory (historyIndices, historyCopies, AddHistory::Paste));
+	g_win->refresh ();
+	g_win->scrollToSelection ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (del, "Delete", "delete", "Delete the selection", KEY (Delete)) {
+	vector<ulong> ulaIndices;
+	vector<LDObject*> copies;
+	
+	g_win->deleteSelection (&ulaIndices, &copies);
+	
+	if (copies.size ())
+		History::addEntry (new DelHistory (ulaIndices, copies));
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+static void doInline (bool bDeep) {
+	vector<LDObject*> sel = g_win->sel ();
+	
+	// History stuff
+	vector<LDSubfile*> paRefs;
+	vector<ulong> ulaRefIndices, ulaBitIndices;
+	
+	for (LDObject* obj : sel) {
+		if (obj->getType() != LDObject::Subfile)
+			continue;
+		
+		ulaRefIndices.push_back (obj->getIndex (g_curfile));
+		paRefs.push_back (static_cast<LDSubfile*> (obj)->clone ());
+	}
+	
+	for (LDObject* obj : sel) {
+		// Get the index of the subfile so we know where to insert the
+		// inlined contents.
+		long idx = obj->getIndex (g_curfile);
+		if (idx == -1)
+			continue;
+		
+		vector<LDObject*> objs;
+		
+		if (obj->getType() == LDObject::Subfile)
+			objs = static_cast<LDSubfile*> (obj)->inlineContents (bDeep, true);
+		else if (obj->getType() == LDObject::Radial)
+			objs = static_cast<LDRadial*> (obj)->decompose (true);
+		else
+			continue;
+		
+		// Merge in the inlined objects
+		for (LDObject* inlineobj : objs) {
+			ulaBitIndices.push_back (idx);
+			
+			// This object is now inlined so it has no parent anymore.
+			inlineobj->parent = null;
+			
+			g_curfile->insertObj (idx++, inlineobj);
+		}
+		
+		// Delete the subfile now as it's been inlined.
+		g_curfile->forgetObject (obj);
+		delete obj;
+	}
+	
+	History::addEntry (new InlineHistory (ulaBitIndices, ulaRefIndices, paRefs, bDeep));
+	g_win->refresh ();
+}
+
+MAKE_ACTION (inlineContents, "Inline", "inline", "Inline selected subfiles.", CTRL (I)) {
+	doInline (false);
+}
+
+MAKE_ACTION (deepInline, "Deep Inline", "inline-deep", "Recursively inline selected subfiles "
+	"down to polygons only.", CTRL_SHIFT (I))
+{
+	doInline (true);
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (splitQuads, "Split Quads", "quad-split", "Split quads into triangles.", (0)) {
+	vector<LDObject*> objs = g_win->sel ();
+	
+	vector<ulong> ulaIndices;
+	vector<LDQuad*> paCopies;
+	
+	// Store stuff first for history archival
+	for (LDObject* obj : objs) {
+		if (obj->getType() != LDObject::Quad)
+			continue;
+		
+		ulaIndices.push_back (obj->getIndex (g_curfile));
+		paCopies.push_back (static_cast<LDQuad*> (obj)->clone ());
+	}
+	
+	for (LDObject* obj : objs) {
+		if (obj->getType() != LDObject::Quad)
+			continue;
+		
+		// Find the index of this quad
+		long lIndex = obj->getIndex (g_curfile);
+		
+		if (lIndex == -1)
+			return;
+		
+		std::vector<LDTriangle*> triangles = static_cast<LDQuad*> (obj)->splitToTriangles ();
+		
+		// Replace the quad with the first triangle and add the second triangle
+		// after the first one.
+		g_curfile->m_objs[lIndex] = triangles[0];
+		g_curfile->insertObj (lIndex + 1, triangles[1]);
+		
+		// Delete this quad now, it has been split.
+		delete obj;
+	}
+	
+	History::addEntry (new QuadSplitHistory (ulaIndices, paCopies));
+	g_win->refresh ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (setContents, "Set Contents", "set-contents", "Set the raw code of this object.", KEY (F9)) {
+	if (g_win->sel ().size() != 1)
+		return;
+	
+	LDObject* obj = g_win->sel ()[0];
+	SetContentsDialog::staticDialog (obj);
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (setColor, "Set Color", "palette", "Set the color on given objects.", KEY (F10)) {
+	if (g_win->sel ().size() <= 0)
+		return;
+	
+	short dColor;
+	short dDefault = -1;
+	
+	std::vector<LDObject*> objs = g_win->sel ();
+	
+	// If all selected objects have the same color, said color is our default
+	// value to the color selection dialog.
+	dDefault = g_win->getSelectedColor ();
+	
+	// Show the dialog to the user now and ask for a color.
+	if (ColorSelectDialog::staticDialog (dColor, dDefault, g_win)) {
+		std::vector<ulong> ulaIndices;
+		std::vector<short> daColors;
+		
+		for (LDObject* obj : objs) {
+			if (obj->dColor != -1) {
+				ulaIndices.push_back (obj->getIndex (g_curfile));
+				daColors.push_back (obj->dColor);
+				
+				obj->dColor = dColor;
+			}
+		}
+		
+		History::addEntry (new SetColorHistory (ulaIndices, daColors, dColor));
+		g_win->refresh ();
+	}
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (makeBorders, "Make Borders", "make-borders", "Add borders around given polygons.",
+	CTRL_SHIFT (B))
+{
+	vector<LDObject*> objs = g_win->sel ();
+	
+	vector<ulong> ulaIndices;
+	vector<LDObject*> paObjs;
+	
+	for (LDObject* obj : objs) {
+		if (obj->getType() != LDObject::Quad && obj->getType() != LDObject::Triangle)
+			continue;
+		
+		short dNumLines;
+		LDLine* lines[4];
+		
+		if (obj->getType() == LDObject::Quad) {
+			dNumLines = 4;
+			
+			LDQuad* quad = static_cast<LDQuad*> (obj);
+			lines[0] = new LDLine (quad->vaCoords[0], quad->vaCoords[1]);
+			lines[1] = new LDLine (quad->vaCoords[1], quad->vaCoords[2]);
+			lines[2] = new LDLine (quad->vaCoords[2], quad->vaCoords[3]);
+			lines[3] = new LDLine (quad->vaCoords[3], quad->vaCoords[0]);
+		} else {
+			dNumLines = 3;
+			
+			LDTriangle* tri = static_cast<LDTriangle*> (obj);
+			lines[0] = new LDLine (tri->vaCoords[0], tri->vaCoords[1]);
+			lines[1] = new LDLine (tri->vaCoords[1], tri->vaCoords[2]);
+			lines[2] = new LDLine (tri->vaCoords[2], tri->vaCoords[0]); 
+		}
+		
+		for (short i = 0; i < dNumLines; ++i) {
+			ulong idx = obj->getIndex (g_curfile) + i + 1;
+			
+			lines[i]->dColor = edgecolor;
+			g_curfile->insertObj (idx, lines[i]);
+			
+			ulaIndices.push_back (idx);
+			paObjs.push_back (lines[i]->clone ());
+		}
+	}
+	
+	History::addEntry (new AddHistory (ulaIndices, paObjs));
+	g_win->refresh ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (makeCornerVerts, "Make Corner Vertices", "corner-verts",
+	"Adds vertex objects to the corners of the given polygons", (0))
+{
+	vector<ulong> ulaIndices;
+	vector<LDObject*> paObjs;
+	
+	for (LDObject* obj : g_win->sel ()) {
+		if (obj->vertices () < 2)
+			continue;
+		
+		ulong idx = obj->getIndex (g_curfile);
+		for (short i = 0; i < obj->vertices(); ++i) {
+			LDVertex* vert = new LDVertex;
+			vert->vPosition = obj->vaCoords[i];
+			vert->dColor = obj->dColor;
+			
+			g_curfile->insertObj (++idx, vert);
+			ulaIndices.push_back (idx);
+			paObjs.push_back (vert->clone ());
+		}
+	}
+	
+	if (ulaIndices.size() > 0) {
+		History::addEntry (new AddHistory (ulaIndices, paObjs));
+		g_win->refresh ();
+	}
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+static void doMoveSelection (const bool bUp) {
+	vector<LDObject*> objs = g_win->sel ();
+	
+	// Get the indices of the objects for history archival
+	vector<ulong> ulaIndices;
+	for (LDObject* obj : objs)
+		ulaIndices.push_back (obj->getIndex (g_curfile));
+	
+	LDObject::moveObjects (objs, bUp);
+	History::addEntry (new ListMoveHistory (ulaIndices, bUp));
+	g_win->buildObjList ();
+}
+
+MAKE_ACTION (moveUp, "Move Up", "arrow-up", "Move the current selection up.", SHIFT (Up)) {
+	doMoveSelection (true);
+}
+
+MAKE_ACTION (moveDown, "Move Down", "arrow-down", "Move the current selection down.", SHIFT (Down)) {
+	doMoveSelection (false);
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (undo, "Undo", "undo", "Undo a step.", CTRL (Z)) {
+	History::undo ();
+}
+
+MAKE_ACTION (redo, "Redo", "redo", "Redo a step.", CTRL_SHIFT (Z)) {
+	History::redo ();
+}
+
+MAKE_ACTION (showHistory, "Edit History", "history", "Show the history dialog.", (0)) {
+	HistoryDialog dlg;
+	dlg.exec ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+void doMoveObjects (vertex vVector) {
+	vector<ulong> ulaIndices;
+	
+	// Apply the grid values
+	vVector[X] *= currentGrid ().confs[Grid::X]->value;
+	vVector[Y] *= currentGrid ().confs[Grid::Y]->value;
+	vVector[Z] *= currentGrid ().confs[Grid::Z]->value;
+	
+	for (LDObject* obj : g_win->sel ()) {
+		ulaIndices.push_back (obj->getIndex (g_curfile));
+		obj->move (vVector);
+	}
+	
+	History::addEntry (new MoveHistory (ulaIndices, vVector));
+	g_win->refresh ();
+}
+
+MAKE_ACTION (moveXNeg, "Move -X", "move-x-neg", "Move selected objects negative on the X axis.", KEY (Left)) {
+	doMoveObjects ({-1, 0, 0});
+}
+
+MAKE_ACTION (moveYNeg, "Move -Y", "move-y-neg", "Move selected objects negative on the Y axis.", KEY (PageUp)) {
+	doMoveObjects ({0, -1, 0});
+}
+
+MAKE_ACTION (moveZNeg, "Move -Z", "move-z-neg", "Move selected objects negative on the Z axis.", KEY (Down)) {
+	doMoveObjects ({0, 0, -1});
+}
+
+MAKE_ACTION (moveXPos, "Move +X", "move-x-pos", "Move selected objects positive on the X axis.", KEY (Right)) {
+	doMoveObjects ({1, 0, 0});
+}
+
+MAKE_ACTION (moveYPos, "Move +Y", "move-y-pos", "Move selected objects positive on the Y axis.", KEY (PageDown)) {
+	doMoveObjects ({0, 1, 0});
+}
+
+MAKE_ACTION (moveZPos, "Move +Z", "move-z-pos", "Move selected objects positive on the Z axis.", KEY (Up)) {
+	doMoveObjects ({0, 0, 1});
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// invert - reverse winding
+// 
+// NOTE: History management gets a little tricky here. For lines, cond-lines,
+// triangles and quads, we edit the object, thus we record an EditHistory. For
+// subfiles and radials we create or delete invertnext objects. Since we have
+// multiple actions of different types, we store a history entry for each of
+// them and wrap them into a ComboHistory, which allows storage of multiple
+// simultaneous edits with different type. This is what we ultimately store into
+// History.
+// =============================================================================
+MAKE_ACTION (invert, "Invert", "invert", "Reverse the winding of given objects.", CTRL_SHIFT (W)) {
+	std::vector<LDObject*> paSelection = g_win->sel ();
+	std::vector<HistoryEntry*> paHistory;
+	
+	for (LDObject* obj : paSelection) {
+		// For the objects we end up editing, we store information into these
+		// variables and we store them into an EditHistory after the switch
+		// block. Subfile and radial management is stored into the history
+		// list immediately.
+		ulong ulHistoryIndex = obj->getIndex (g_curfile);
+		LDObject* pOldCopy, *pNewCopy;
+		bool bEdited = false;
+		
+		switch (obj->getType ()) {
+		case LDObject::Line:
+		case LDObject::CondLine:
+			{
+				// For lines, we swap the vertices. I don't think that a
+				// cond-line's control points need to be swapped, do they?
+				LDLine* pLine = static_cast<LDLine*> (obj);
+				vertex vTemp = pLine->vaCoords[0];
+				
+				pOldCopy = pLine->clone ();
+				pLine->vaCoords[0] = pLine->vaCoords[1];
+				pLine->vaCoords[1] = vTemp;
+				pNewCopy = pLine->clone ();
+				bEdited = true;
+			}
+			break;
+		
+		case LDObject::Triangle:
+			{
+				// Triangle goes 0 -> 1 -> 2, reversed: 0 -> 2 -> 1.
+				// Thus, we swap 1 and 2.
+				LDTriangle* pTri = static_cast<LDTriangle*> (obj);
+				vertex vTemp = pTri->vaCoords[1];
+				
+				pOldCopy = pTri->clone ();
+				pTri->vaCoords[1] = pTri->vaCoords[2];
+				pTri->vaCoords[2] = vTemp;
+				pNewCopy = pTri->clone ();
+				bEdited = true;
+			}
+			break;
+		
+		case LDObject::Quad:
+			{
+				// Quad: 0 -> 1 -> 2 -> 3
+				// rev:  0 -> 3 -> 2 -> 1
+				// Thus, we swap 1 and 3.
+				LDQuad* pQuad = static_cast<LDQuad*> (obj);
+				vertex vTemp = pQuad->vaCoords[1];
+				
+				pOldCopy = pQuad->clone ();
+				pQuad->vaCoords[1] = pQuad->vaCoords[3];
+				pQuad->vaCoords[3] = vTemp;
+				pNewCopy = pQuad->clone ();
+				bEdited = true;
+			}
+			break;
+		
+		case LDObject::Subfile:
+		case LDObject::Radial:
+			{
+				// Subfiles and radials are inverted when they're prefixed with
+				// a BFC INVERTNEXT statement. Thus we need to toggle this status.
+				// For flat primitives it's sufficient that the determinant is
+				// flipped but I don't have a method for checking flatness yet.
+				// Food for thought...
+				
+				bool inverted = false;
+				ulong idx = obj->getIndex (g_curfile);
+				
+				if (idx > 0) {
+					LDObject* prev = g_curfile->object (idx - 1);
+					LDBFC* bfc = dynamic_cast<LDBFC*> (prev);
+					
+					if (bfc && bfc->type == LDBFC::InvertNext) {
+						// Object is prefixed with an invertnext, thus remove it.
+						paHistory.push_back (new DelHistory ({idx - 1}, {bfc->clone ()}));
+						
+						inverted = true;
+						g_curfile->forgetObject (bfc);
+						delete bfc;
+					}
+				}
+				
+				if (!inverted) {
+					// Not inverted, thus prefix it with a new invertnext.
+					LDBFC* bfc = new LDBFC (LDBFC::InvertNext);
+					g_curfile->insertObj (idx, bfc);
+					
+					paHistory.push_back (new AddHistory ({idx}, {bfc->clone ()}));
+				}
+			}
+			break;
+		
+		default:
+			break;
+		}
+		
+		// If we edited this object, store the EditHistory based on collected
+		// information now.
+		if (bEdited == true)
+			paHistory.push_back (new EditHistory ({ulHistoryIndex}, {pOldCopy}, {pNewCopy}));
+	}
+	
+	if (paHistory.size () > 0) {
+		History::addEntry (new ComboHistory (paHistory));
+		g_win->refresh ();
+	}
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+static void doRotate (const short l, const short m, const short n) {
+	std::vector<LDObject*> sel = g_win->sel ();
+	bbox box;
+	vertex origin;
+	std::vector<vertex*> queue;
+	const double angle = (pi * currentGrid ().confs[Grid::Angle]->value) / 360;
+	
+	// ref: http://en.wikipedia.org/wiki/Transformation_matrix#Rotation_2
+	matrix<3> transform ({
+		(l * l * (1 - cos (angle))) + cos (angle),
+		(m * l * (1 - cos (angle))) - (n * sin (angle)),
+		(n * l * (1 - cos (angle))) + (m * sin (angle)),
+		
+		(l * m * (1 - cos (angle))) + (n * sin (angle)),
+		(m * m * (1 - cos (angle))) + cos (angle),
+		(n * m * (1 - cos (angle))) - (l * sin (angle)),
+		
+		(l * n * (1 - cos (angle))) - (m * sin (angle)),
+		(m * n * (1 - cos (angle))) + (l * sin (angle)),
+		(n * n * (1 - cos (angle))) + cos (angle)
+	});
+	
+	// Calculate center vertex
+	for (LDObject* obj : sel) {
+		if (obj->getType () == LDObject::Subfile)
+			box << static_cast<LDSubfile*> (obj)->vPosition;
+		else if (obj->getType () == LDObject::Radial)
+			box << static_cast<LDRadial*> (obj)->vPosition;
+		else
+			box << obj;
+	}
+	
+	origin = box.center ();
+	
+	// Apply the above matrix to everything
+	for (LDObject* obj : sel) {
+		if (obj->vertices ())
+			for (short i = 0; i < obj->vertices (); ++i)
+				queue.push_back (&obj->vaCoords[i]);
+		else if (obj->getType () == LDObject::Subfile) {
+			LDSubfile* ref = static_cast<LDSubfile*> (obj);
+			
+			queue.push_back (&ref->vPosition);
+			ref->mMatrix = ref->mMatrix * transform;
+		} else if (obj->getType () == LDObject::Radial) {
+			LDRadial* rad = static_cast<LDRadial*> (obj);
+			
+			queue.push_back (&rad->vPosition);
+			rad->mMatrix = rad->mMatrix * transform;
+		} else if (obj->getType () == LDObject::Vertex)
+			queue.push_back (&static_cast<LDVertex*> (obj)->vPosition);
+	}
+	
+	for (vertex* v : queue) {
+		v->move (-origin);
+		v->transform (transform, g_origin);
+		v->move (origin);
+	}
+	
+	g_win->refresh ();
+}
+
+MAKE_ACTION (rotateXPos, "Rotate +X", "rotate-x-pos", "Rotate objects around X axis", CTRL (Right)) {
+	doRotate (1, 0, 0);
+}
+
+MAKE_ACTION (rotateYPos, "Rotate +Y", "rotate-y-pos", "Rotate objects around Y axis", CTRL (PageDown)) {
+	doRotate (0, 1, 0);
+}
+
+MAKE_ACTION (rotateZPos, "Rotate +Z", "rotate-z-pos", "Rotate objects around Z axis", CTRL (Up)) {
+	doRotate (0, 0, 1);
+}
+
+MAKE_ACTION (rotateXNeg, "Rotate -X", "rotate-x-neg", "Rotate objects around X axis", CTRL (Left)) {
+	doRotate (-1, 0, 0);
+}
+
+MAKE_ACTION (rotateYNeg, "Rotate -Y", "rotate-y-neg", "Rotate objects around Y axis", CTRL (PageUp)) {
+	doRotate (0, -1, 0);
+}
+
+MAKE_ACTION (rotateZNeg, "Rotate -Z", "rotate-z-neg", "Rotate objects around Z axis", CTRL (Down)) {
+	doRotate (0, 0, -1);
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (roundCoords, "Round Coordinates", "round-coords", "Round coordinates down to 3/4 decimals", (0)) {
+	setlocale (LC_ALL, "C");
+	
+	for (LDObject* obj : g_win->sel ())
+	for (short i = 0; i < obj->vertices (); ++i)
+	for (const Axis ax : g_Axes)
+		obj->vaCoords[i][ax] = atof (fmt ("%.3f", obj->vaCoords[i][ax]));
+	
+	g_win->refresh ();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+MAKE_ACTION (uncolorize, "Uncolorize", "uncolorize", "Reduce colors of everything selected to main and edge colors", (0)) {
+	vector<LDObject*> oldCopies, newCopies;
+	vector<ulong> indices;
+	
+	for (LDObject* obj : g_win->sel ()) {
+		if (obj->isColored () == false)
+			continue;
+		
+		indices.push_back (obj->getIndex (g_curfile));
+		oldCopies.push_back (obj->clone ());
+		
+		obj->dColor = (obj->getType () == LDObject::Line || obj->getType () == LDObject::CondLine) ? edgecolor : maincolor;
+		newCopies.push_back (obj->clone ());
+	}
+	
+	if (indices.size () > 0) {
+		History::addEntry (new EditHistory (indices, oldCopies, newCopies));
+		g_win->refresh ();
+	}
+}
\ No newline at end of file

mercurial