From 1a01ac55f2c5b2e328ac43b071fdba009b6370f7 Mon Sep 17 00:00:00 2001 From: twanvl Date: Tue, 10 Jul 2007 02:47:27 +0000 Subject: [PATCH] Rotation and reflection should now work correctly; Finished the symmetry editor git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@538 0fc631ac-6414-0410-93d0-97cfa31319b6 --- src/data/action/symbol.cpp | 3 +- src/data/action/symbol.hpp | 2 + src/data/action/symbol_part.cpp | 87 ++++++++ src/data/action/symbol_part.hpp | 55 +++++ src/data/symbol.cpp | 5 + src/data/symbol.hpp | 6 +- src/gui/symbol/control.cpp | 25 ++- src/gui/symbol/control.hpp | 5 +- src/gui/symbol/part_list.cpp | 6 +- src/gui/symbol/selection.cpp | 21 +- src/gui/symbol/selection.hpp | 8 + src/gui/symbol/symmetry_editor.cpp | 163 +++++++++------ src/gui/symbol/symmetry_editor.hpp | 27 ++- src/gui/symbol/window.cpp | 8 +- src/render/symbol/filter.cpp | 27 +-- src/render/symbol/filter.hpp | 2 +- src/render/symbol/viewer.cpp | 194 ++++++++++-------- src/render/symbol/viewer.hpp | 20 +- .../common/icon_symmetry_reflection.png | Bin 117 -> 120 bytes .../common/icon_symmetry_rotation.png | Bin 123 -> 126 bytes src/resource/msw/mse.rc | 2 + src/resource/msw/tool/symmetry_add.png | Bin 0 -> 330 bytes src/util/window_id.hpp | 5 +- 23 files changed, 476 insertions(+), 195 deletions(-) create mode 100644 src/resource/msw/tool/symmetry_add.png diff --git a/src/data/action/symbol.cpp b/src/data/action/symbol.cpp index 7113e85e..1a3bebb6 100644 --- a/src/data/action/symbol.cpp +++ b/src/data/action/symbol.cpp @@ -519,6 +519,7 @@ void GroupSymbolPartsActionBase::perform(bool to_undo) { GroupSymbolPartsAction::GroupSymbolPartsAction(SymbolGroup& root, const set& parts, const SymbolGroupP& group) : GroupSymbolPartsActionBase(root) + , group(group) { // group parts in the old parts list bool done = false; @@ -539,7 +540,7 @@ GroupSymbolPartsAction::GroupSymbolPartsAction(SymbolGroup& root, const setcalculateBounds(); } String GroupSymbolPartsAction::getName(bool to_undo) const { - return _ACTION_("group parts"); + return group->isSymbolSymmetry() ? _ACTION_("add symmetry") : _ACTION_("group parts"); } UngroupSymbolPartsAction::UngroupSymbolPartsAction(SymbolGroup& root, const set& parts) diff --git a/src/data/action/symbol.hpp b/src/data/action/symbol.hpp index 18d959c1..a4f5f437 100644 --- a/src/data/action/symbol.hpp +++ b/src/data/action/symbol.hpp @@ -301,6 +301,8 @@ class GroupSymbolPartsAction : public GroupSymbolPartsActionBase { GroupSymbolPartsAction(SymbolGroup& root, const set& parts, const SymbolGroupP& group); virtual String getName(bool to_undo) const; + private: + SymbolGroupP group; }; /// Break up one or more SymbolGroups diff --git a/src/data/action/symbol_part.cpp b/src/data/action/symbol_part.cpp index 01254065..750b5eb1 100644 --- a/src/data/action/symbol_part.cpp +++ b/src/data/action/symbol_part.cpp @@ -427,3 +427,90 @@ Action* control_point_remove_action(const SymbolShapeP& shape, const set& to_delete); + + +// ----------------------------------------------------------------------------- : Move symmetry center/handle + +/// Moving the handle or the center of a symbol symmetry +class SymmetryMoveAction : public Action { + public: + SymmetryMoveAction(SymbolSymmetry& symmetry, bool is_handle); + + virtual String getName(bool to_undo) const; + virtual void perform(bool to_undo); + + /// Update this action to move some more + void move(const Vector2D& delta); + + private: + SymbolSymmetry& symmetry; ///< Affected part + bool is_handle; ///< Move the handle or the center? + Vector2D delta; ///< Amount we moved + Vector2D original; ///< Original value + public: + bool constrain; ///< Constrain movement? + int snap; ///< Snap to grid? +}; + +// ----------------------------------------------------------------------------- : Change symmetry kind + +/// Change the type of symmetry +class SymmetryTypeAction : public Action { + public: + SymmetryTypeAction(SymbolSymmetry& symmetry, SymbolSymmetryType type); + + virtual String getName(bool to_undo) const; + virtual void perform(bool to_undo); + private: + SymbolSymmetry& symmetry; + SymbolSymmetryType type; + String old_name; +}; + +// ----------------------------------------------------------------------------- : Change symmetry copies + +/// Change the number of copies of a symmetry +class SymmetryCopiesAction : public Action { + public: + SymmetryCopiesAction(SymbolSymmetry& symmetry, int copies); + + virtual String getName(bool to_undo) const; + virtual void perform(bool to_undo); + private: + SymbolSymmetry& symmetry; + int copies; + String old_name; +}; + // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/data/symbol.cpp b/src/data/symbol.cpp index d6b223e3..17857f66 100644 --- a/src/data/symbol.cpp +++ b/src/data/symbol.cpp @@ -212,6 +212,11 @@ SymbolPartP SymbolSymmetry::clone() const { return part; } +String SymbolSymmetry::expectedName() const { + return capitalize(kind == SYMMETRY_ROTATION ? _TYPE_("rotation") : _TYPE_("reflection")) + + String::Format(_(" (%d)"), copies); +} + IMPLEMENT_REFLECTION(SymbolSymmetry) { REFLECT_BASE(SymbolPart); REFLECT(kind); diff --git a/src/data/symbol.hpp b/src/data/symbol.hpp index 5de54202..b981bd9b 100644 --- a/src/data/symbol.hpp +++ b/src/data/symbol.hpp @@ -222,7 +222,7 @@ class SymbolGroup : public SymbolPart { // ----------------------------------------------------------------------------- : SymbolSymmetry enum SymbolSymmetryType -{ SYMMETRY_ROTATION = SYMBOL_COMBINE_BORDER + 1 // for icons +{ SYMMETRY_ROTATION , SYMMETRY_REFLECTION }; @@ -240,10 +240,12 @@ class SymbolSymmetry : public SymbolGroup { virtual String typeName() const; virtual SymbolPartP clone() const; - virtual int icon() const { return kind; } + virtual int icon() const { return kind + SYMBOL_COMBINE_BORDER + 1; } virtual SymbolSymmetry* isSymbolSymmetry() { return this; } virtual const SymbolSymmetry* isSymbolSymmetry() const { return this; } + String expectedName() const; + DECLARE_REFLECTION(); }; diff --git a/src/gui/symbol/control.cpp b/src/gui/symbol/control.cpp index 0101d9c3..bf6c9b91 100644 --- a/src/gui/symbol/control.cpp +++ b/src/gui/symbol/control.cpp @@ -23,7 +23,7 @@ SymbolControl::SymbolControl(SymbolWindow* parent, int id, const SymbolP& symbol) : wxControl(parent, id) - , SymbolViewer(symbol) + , SymbolViewer(symbol, true) , parent(parent) { onChangeSymbol(); @@ -66,7 +66,7 @@ void SymbolControl::onModeChange(wxCommandEvent& ev) { switchEditor(new_intrusive1(this)); break; case ID_MODE_SYMMETRY: - switchEditor(new_intrusive1(this)); + switchEditor(new_intrusive2(this, selected_parts.getASymmetry())); break; } } @@ -113,6 +113,24 @@ void SymbolControl::onUpdateSelection() { Refresh(false); } break; + } case ID_MODE_SYMMETRY: { + // can only select a single part! + SymbolSymmetryP symmetry = selected_parts.getASymmetry(); + if (!symmetry) { + if (selected_symmetry && selected_parts.select(selected_symmetry)) { + signalSelectionChange(); + } + break; + } + if (symmetry != selected_symmetry) { + if (symmetry && selected_parts.select(symmetry)) { + signalSelectionChange(); + } + // begin editing another part + selected_symmetry = symmetry; + Refresh(false); + } + break; } case ID_MODE_SHAPES: if (!selected_parts.empty()) { // there can't be a selection @@ -136,6 +154,9 @@ void SymbolControl::activatePart(const SymbolPartP& part) { if (part->isSymbolShape()) { selected_parts.select(part); switchEditor(new_intrusive2(this, static_pointer_cast(part))); + } else if (part->isSymbolSymmetry()) { + selected_parts.select(part); + switchEditor(new_intrusive2(this, static_pointer_cast(part))); } } diff --git a/src/gui/symbol/control.hpp b/src/gui/symbol/control.hpp index a64116fb..4d506301 100644 --- a/src/gui/symbol/control.hpp +++ b/src/gui/symbol/control.hpp @@ -73,8 +73,9 @@ class SymbolControl : public wxControl, public SymbolViewer { public: /// What parts are selected? SymbolPartsSelection selected_parts; - SymbolPartP highlight_part; ///< part the mouse cursor is over - SymbolShapeP selected_shape; ///< if there is a single selection + SymbolPartP highlight_part; ///< part the mouse cursor is over + SymbolShapeP selected_shape; ///< if there is a single selection + SymbolSymmetryP selected_symmetry; ///< if there is a single selection /// Parent window SymbolWindow* parent; diff --git a/src/gui/symbol/part_list.cpp b/src/gui/symbol/part_list.cpp index c5ae0d80..5aabab9e 100644 --- a/src/gui/symbol/part_list.cpp +++ b/src/gui/symbol/part_list.cpp @@ -486,13 +486,13 @@ const Image& SymbolPartList::itemPreview(int i, const SymbolPartP& part) { if (s->combine == SYMBOL_COMBINE_SUBTRACT) { // temporarily render using subtract instead, otherwise we don't see anything s->combine = SYMBOL_COMBINE_BORDER; - img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4); + img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4, true); s->combine = SYMBOL_COMBINE_SUBTRACT; } else { - img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4); + img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4, true); } } else { - img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4); + img = render_symbol(sym, filter, 0.08, ITEM_HEIGHT * 4, true); } resample(img, p.image); p.up_to_date = true; diff --git a/src/gui/symbol/selection.cpp b/src/gui/symbol/selection.cpp index 4fb3007d..641b9c0d 100644 --- a/src/gui/symbol/selection.cpp +++ b/src/gui/symbol/selection.cpp @@ -55,13 +55,6 @@ bool SymbolPartsSelection::select(const SymbolPartP& part, SelectMode mode) { return true; } -SymbolShapeP SymbolPartsSelection::getAShape() const { - FOR_EACH(s, selection) { - if (s->isSymbolShape()) return static_pointer_cast(s); - } - return SymbolShapeP(); -} - void SymbolPartsSelection::clearChildren(SymbolPart* part) { if (SymbolGroup* g = part->isSymbolGroup()) { FOR_EACH(p, g->parts) { @@ -71,6 +64,20 @@ void SymbolPartsSelection::clearChildren(SymbolPart* part) { } } +SymbolShapeP SymbolPartsSelection::getAShape() const { + FOR_EACH(s, selection) { + if (s->isSymbolShape()) return static_pointer_cast(s); + } + return SymbolShapeP(); +} + +SymbolSymmetryP SymbolPartsSelection::getASymmetry() const { + FOR_EACH(s, selection) { + if (s->isSymbolSymmetry()) return static_pointer_cast(s); + } + return SymbolSymmetryP(); +} + // ----------------------------------------------------------------------------- : Position based diff --git a/src/gui/symbol/selection.hpp b/src/gui/symbol/selection.hpp index af38b955..33b21776 100644 --- a/src/gui/symbol/selection.hpp +++ b/src/gui/symbol/selection.hpp @@ -15,6 +15,7 @@ class Vector2D; DECLARE_POINTER_TYPE(Symbol); DECLARE_POINTER_TYPE(SymbolPart); DECLARE_POINTER_TYPE(SymbolShape); +DECLARE_POINTER_TYPE(SymbolSymmetry); class SymbolGroup; // ----------------------------------------------------------------------------- : Selection @@ -61,6 +62,13 @@ class SymbolPartsSelection { /// Get any SymbolShape if there is one selected SymbolShapeP getAShape() const; + /// Get any SymbolSymmetry if there is one selected + SymbolSymmetryP getASymmetry() const; + /// Get the only selected thing + inline SymbolPartP getOnlyOne() const { + assert(selection.size() == 1); + return *selection.begin(); + } private: Symbol* root; diff --git a/src/gui/symbol/symmetry_editor.cpp b/src/gui/symbol/symmetry_editor.cpp index 247d8eec..362b60c3 100644 --- a/src/gui/symbol/symmetry_editor.cpp +++ b/src/gui/symbol/symmetry_editor.cpp @@ -16,11 +16,13 @@ // ----------------------------------------------------------------------------- : SymbolSymmetryEditor -SymbolSymmetryEditor::SymbolSymmetryEditor(SymbolControl* control) +SymbolSymmetryEditor::SymbolSymmetryEditor(SymbolControl* control, const SymbolSymmetryP& sym) : SymbolEditorBase(control) - , mode(ID_SYMMETRY_ROTATION) - , drawing(false) + , symmetry(control->selected_symmetry) + , symmetryMoveAction(nullptr) { + symmetry = sym; + control->selected_symmetry = symmetry; control->SetCursor(*wxCROSS_CURSOR); } @@ -28,7 +30,23 @@ SymbolSymmetryEditor::SymbolSymmetryEditor(SymbolControl* control) void SymbolSymmetryEditor::draw(DC& dc) { if (symmetry) { - control.highlightPart(dc, *symmetry); + control.highlightPart(dc, *symmetry, HIGHLIGHT_BORDER); + Color color(255,100,0); + Vector2D center = control.rotation.tr(symmetry->center); + Vector2D handle = control.rotation.tr(symmetry->center + symmetry->handle); + if (symmetry->kind == SYMMETRY_REFLECTION) { + // draw line to handle + dc.SetPen(wxPen(color,1,wxDOT)); + dc.DrawLine(center.x, center.y, handle.x, handle.y); + // draw handle + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(color); + dc.DrawCircle(handle.x, handle.y, hovered == SELECTION_HANDLE ? 7 : 4); + } + // draw center + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(color); + dc.DrawCircle(center.x, center.y, hovered == SELECTION_CENTER ? 8 : 5); } } @@ -39,29 +57,41 @@ void SymbolSymmetryEditor::initUI(wxToolBar* tb, wxMenuBar* mb) { copies->SetHelpText(_HELP_("copies")); copies->SetSize(50, -1); tb->AddSeparator(); + tb->AddTool(ID_ADD_SYMMETRY, _TOOL_("add symmetry"), load_resource_tool_image(_("symmetry_add")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("add symmetry"), _HELP_("add symmetry")); + tb->AddSeparator(); tb->AddTool(ID_SYMMETRY_ROTATION, _TOOL_("rotation"), load_resource_image(_("symmetry_rotation")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("rotation"), _HELP_("rotation")); tb->AddTool(ID_SYMMETRY_REFLECTION, _TOOL_("reflection"), load_resource_image(_("symmetry_reflection")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("reflection"), _HELP_("reflection")); + tb->AddSeparator(); tb->AddControl(copies); tb->Realize(); control.SetCursor(*wxCROSS_CURSOR); - stopActions(); // set status text + resetActions(); // set status text } void SymbolSymmetryEditor::destroyUI(wxToolBar* tb, wxMenuBar* mb) { tb->DeleteTool(ID_SYMMETRY_REFLECTION); tb->DeleteTool(ID_SYMMETRY_ROTATION); + tb->DeleteTool(ID_ADD_SYMMETRY); // HACK: hardcoded size of rest of toolbar tb->DeleteToolByPos(7); // delete separator - tb->DeleteTool(ID_COPIES); // delete sides + tb->DeleteToolByPos(7); // delete separator + tb->DeleteToolByPos(7); // delete separator + tb->DeleteTool(ID_COPIES); // delete copies #if wxVERSION_NUMBER < 2600 - delete sides; + delete copies; #endif } void SymbolSymmetryEditor::onUpdateUI(wxUpdateUIEvent& ev) { if (ev.GetId() >= ID_SYMMETRY && ev.GetId() < ID_SYMMETRY_MAX) { - ev.Check(ev.GetId() == mode); + ev.Enable(symmetry); + ev.Check(symmetry && symmetry->kind == ev.GetId() - ID_SYMMETRY); } else if (ev.GetId() == ID_COPIES) { + ev.Enable(symmetry); + if (symmetry) { + copies->SetValue(symmetry->copies); + } + } else if (ev.GetId() == ID_ADD_SYMMETRY) { ev.Enable(true); } else { ev.Enable(false); // we don't know about this item @@ -70,8 +100,28 @@ void SymbolSymmetryEditor::onUpdateUI(wxUpdateUIEvent& ev) { void SymbolSymmetryEditor::onCommand(int id) { if (id >= ID_SYMMETRY && id < ID_SYMMETRY_MAX) { - mode = id; - stopActions(); + SymbolSymmetryType kind = id == ID_SYMMETRY_ROTATION ? SYMMETRY_ROTATION : SYMMETRY_REFLECTION; + if (symmetry && symmetry->kind != kind) { + getSymbol()->actions.add(new SymmetryTypeAction(*symmetry, kind)); + control.Refresh(false); + } + resetActions(); + } else if (id == ID_COPIES) { + if (symmetry && symmetry->copies != copies->GetValue()) { + getSymbol()->actions.add(new SymmetryCopiesAction(*symmetry, copies->GetValue())); + control.Refresh(false); + } + resetActions(); + } else if (id == ID_ADD_SYMMETRY) { + symmetry = new_intrusive(); + symmetry->kind = SYMMETRY_ROTATION; + symmetry->copies = 2; + symmetry->center = Vector2D(0.5,0.5); + symmetry->handle = Vector2D(0.2,0); + symmetry->name = symmetry->expectedName(); + getSymbol()->actions.add(new GroupSymbolPartsAction(*getSymbol(), control.selected_parts.get(), symmetry)); + control.selected_parts.select(symmetry); + control.Refresh(false); } } @@ -80,81 +130,70 @@ int SymbolSymmetryEditor::modeToolId() { return ID_MODE_SYMMETRY; } // ----------------------------------------------------------------------------- : Mouse events void SymbolSymmetryEditor::onLeftDown (const Vector2D& pos, wxMouseEvent& ev) { - // Start drawing - drawing = true; - center = handle = pos; - SetStatusText(_HELP_("drag to draw symmetry")); + if (!symmetry) return; + selection = findSelection(pos); } void SymbolSymmetryEditor::onLeftUp (const Vector2D& pos, wxMouseEvent& ev) { - if (drawing && symmetry) { - // Finalize the symmetry - getSymbol()->actions.add(new AddSymbolPartAction(*getSymbol(), symmetry)); - // Select the part - control.selectPart(symmetry); - // No need to clean up, this editor gets destroyed - //stopActions(); + if (!symmetry) return; + if (isEditing()) { + resetActions(); } } void SymbolSymmetryEditor::onMouseDrag (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev) { + if (!symmetry) return; // Resize the object - if (drawing) { - handle = to; - makePart(center, handle, ev.ControlDown(), settings.symbol_grid_snap != ev.ShiftDown()); - control.Refresh(false); + if (selection == SELECTION_NONE) return; + if (!symmetryMoveAction) { + symmetryMoveAction = new SymmetryMoveAction(*symmetry, selection == SELECTION_HANDLE); + symmetryMoveAction->constrain = ev.ControlDown(); + symmetryMoveAction->snap = ev.ShiftDown() != settings.symbol_grid_snap ? settings.symbol_grid_size : 0; + getSymbol()->actions.add(symmetryMoveAction); } + symmetryMoveAction->move(to - from); + control.Refresh(false); +} + +void SymbolSymmetryEditor::onMouseMove (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev) { + Selection old_hovered = hovered; + hovered = findSelection(to); + if (hovered != old_hovered) control.Refresh(false); + // TODO: set status text +} + +SymbolSymmetryEditor::Selection SymbolSymmetryEditor::findSelection(const Vector2D& pos) { + if (!symmetry) return SELECTION_NONE; + Vector2D pos_pixel = control.rotation.tr(pos); + Vector2D center = control.rotation.tr(symmetry->center); + if ((center - pos_pixel).lengthSqr() < 5*5) return SELECTION_CENTER; + if (symmetry->kind == SYMMETRY_REFLECTION) { + Vector2D handle = control.rotation.tr(symmetry->center + symmetry->handle); + if ((handle - pos_pixel).lengthSqr() < 5*5) return SELECTION_HANDLE; + } + return SELECTION_NONE; } // ----------------------------------------------------------------------------- : Other events void SymbolSymmetryEditor::onKeyChange(wxKeyEvent& ev) { - if (drawing) { + if (symmetryMoveAction) { if (ev.GetKeyCode() == WXK_CONTROL || ev.GetKeyCode() == WXK_SHIFT) { // changed constrains - makePart(center, handle, ev.ControlDown(), settings.symbol_grid_snap != ev.ShiftDown()); + symmetryMoveAction->constrain = ev.ControlDown(); + symmetryMoveAction->snap = ev.ShiftDown() != settings.symbol_grid_snap ? settings.symbol_grid_size : 0; control.Refresh(false); } else if (ev.GetKeyCode() == WXK_ESCAPE) { // cancel drawing - stopActions(); + resetActions(); } } } -bool SymbolSymmetryEditor::isEditing() { return drawing; } - -// ----------------------------------------------------------------------------- : Generating shapes - -void SymbolSymmetryEditor::stopActions() { - symmetry = SymbolSymmetryP(); - drawing = false; - SetStatusText(_HELP_("draw symmetry")); - control.Refresh(false); +bool SymbolSymmetryEditor::isEditing() { + return symmetryMoveAction; } -void SymbolSymmetryEditor::makePart(Vector2D a, Vector2D b, bool constrained, bool snap) { - // snap - if (snap) { - a = snap_vector(a, settings.symbol_grid_size); - b = snap_vector(b, settings.symbol_grid_size); - } - // constrain - Vector2D dir = b - a; - if (constrained) { - double angle = atan2(dir.y, dir.x); - // multiples of 2pi/24 i.e. 24 stops - double mult = (2 * M_PI) / 24; - angle = floor(angle / mult + 0.5) * mult; - dir = Vector2D(cos(angle), sin(angle)) * dir.length(); - } - // make part - if (!symmetry) { - symmetry = new_intrusive(); - } - symmetry->kind = mode == ID_SYMMETRY_ROTATION ? SYMMETRY_ROTATION : SYMMETRY_REFLECTION; - symmetry->copies = copies->GetValue(); - symmetry->center = a; - symmetry->handle = dir; - symmetry->name = capitalize(mode == ID_SYMMETRY_ROTATION ? _TYPE_("rotation") : _TYPE_("reflection")) - + String::Format(_(" (%d)"), symmetry->copies); +void SymbolSymmetryEditor::resetActions() { + symmetryMoveAction = nullptr; } diff --git a/src/gui/symbol/symmetry_editor.hpp b/src/gui/symbol/symmetry_editor.hpp index 1bafcb5d..409561b7 100644 --- a/src/gui/symbol/symmetry_editor.hpp +++ b/src/gui/symbol/symmetry_editor.hpp @@ -12,14 +12,15 @@ #include #include +class SymmetryMoveAction; + // ----------------------------------------------------------------------------- : SymbolSymmetryEditor /// Editor for adding symmetries class SymbolSymmetryEditor : public SymbolEditorBase { public: /** The symmetry parameter is optional, if it is not set, then only new ones can be created */ - //%SymbolSymmetryEditor(SymbolControl* control, SymbolSymmetryP symmetry); - SymbolSymmetryEditor(SymbolControl* control); + SymbolSymmetryEditor(SymbolControl* control, const SymbolSymmetryP& symmetry); // --------------------------------------------------- : Drawing @@ -37,6 +38,7 @@ class SymbolSymmetryEditor : public SymbolEditorBase { virtual void onLeftDown (const Vector2D& pos, wxMouseEvent& ev); virtual void onLeftUp (const Vector2D& pos, wxMouseEvent& ev); + virtual void onMouseMove (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev); virtual void onMouseDrag (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev); // --------------------------------------------------- : Other events @@ -47,18 +49,23 @@ class SymbolSymmetryEditor : public SymbolEditorBase { // --------------------------------------------------- : Data private: - int mode; - SymbolSymmetryP symmetry; - bool drawing; - Vector2D center, handle; + SymbolSymmetryP& symmetry; // controls wxSpinCtrl* copies; + // Actions + SymmetryMoveAction* symmetryMoveAction; - /// Cancel the drawing - void stopActions(); + // What is selected? + enum Selection { + SELECTION_NONE, + SELECTION_HANDLE, // dragging a handle + SELECTION_CENTER, // dragging the rotation center + } selection, hovered; - /// Make the symmetry object - void makePart(Vector2D a, Vector2D b, bool constrained, bool snap); + Selection findSelection(const Vector2D& pos); + + /// Done with dragging + void resetActions(); }; diff --git a/src/gui/symbol/window.cpp b/src/gui/symbol/window.cpp index ee2a3b9d..6a3eab65 100644 --- a/src/gui/symbol/window.cpp +++ b/src/gui/symbol/window.cpp @@ -297,14 +297,15 @@ void SymbolWindow::onUpdateUI(wxUpdateUIEvent& ev) { void SymbolWindow::onSelectFromList(wxCommandEvent& ev) { - if (inSelectionEvent) return ; + if (inSelectionEvent) return; inSelectionEvent = true; control->onUpdateSelection(); inSelectionEvent = false; } void SymbolWindow::onActivateFromList(wxCommandEvent& ev) { -//% control->activatePart(control->getSymbol()->parts.at(ev.GetIndex())); - // TODO + if (control->selected_parts.size() == 1) { + control->activatePart(control->selected_parts.getOnlyOne()); + } } void SymbolWindow::onSelectFromControl() { @@ -329,6 +330,7 @@ BEGIN_EVENT_TABLE(SymbolWindow, wxFrame) EVT_TOOL_RANGE (ID_MODE_MIN, ID_MODE_MAX, SymbolWindow::onModeChange) EVT_TOOL_RANGE (ID_CHILD_MIN, ID_CHILD_MAX, SymbolWindow::onExtraTool) EVT_UPDATE_UI (wxID_ANY, SymbolWindow::onUpdateUI) + EVT_COMMAND_RANGE(ID_CHILD_MIN, ID_CHILD_MAX, wxEVT_COMMAND_SPINCTRL_UPDATED, SymbolWindow::onExtraTool) EVT_PART_SELECT (ID_PART_LIST, SymbolWindow::onSelectFromList) EVT_PART_ACTIVATE (ID_PART_LIST, SymbolWindow::onActivateFromList) diff --git a/src/render/symbol/filter.cpp b/src/render/symbol/filter.cpp index e7fd7dd9..20b2df29 100644 --- a/src/render/symbol/filter.cpp +++ b/src/render/symbol/filter.cpp @@ -44,8 +44,7 @@ void filter_symbol(Image& symbol, const SymbolFilter& filter) { // HACK: wxGTK seems to fail sometimes if you ask it to allocate the alpha channel. // This manually allocates the memory and gives it to the image to handle. if (!alpha) { - alpha = (Byte*) malloc (sizeof(Byte) * width * height); - memset(alpha, 255, width * height); + alpha = (Byte*) malloc(width * height); symbol.SetAlpha(alpha); } for (UInt y = 0 ; y < width ; ++y) { @@ -53,14 +52,18 @@ void filter_symbol(Image& symbol, const SymbolFilter& filter) { // Determine set // green -> border or outside // green+red=white -> border - SymbolSet point = data[1] ? (data[0] ? SYMBOL_BORDER : SYMBOL_OUTSIDE) : SYMBOL_INSIDE; - // Call filter - AColor result = filter.color((double)x / width, (double)y / height, point); - // Store color - data[0] = result.Red(); - data[1] = result.Green(); - data[2] = result.Blue(); - alpha[0] = result.alpha; + if (data[1] != data[2]) { + // yellow/blue = editing hint, leave alone + } else { + SymbolSet point = data[1] ? (data[0] ? SYMBOL_BORDER : SYMBOL_OUTSIDE) : SYMBOL_INSIDE; + // Call filter + AColor result = filter.color((double)x / width, (double)y / height, point); + // Store color + data[0] = result.Red(); + data[1] = result.Green(); + data[2] = result.Blue(); + alpha[0] = result.alpha; + } // next data += 3; alpha += 1; @@ -68,8 +71,8 @@ void filter_symbol(Image& symbol, const SymbolFilter& filter) { } } -Image render_symbol(const SymbolP& symbol, const SymbolFilter& filter, double border_radius, int size) { - Image i = render_symbol(symbol, border_radius, size); +Image render_symbol(const SymbolP& symbol, const SymbolFilter& filter, double border_radius, int size, bool edit_hints) { + Image i = render_symbol(symbol, border_radius, size, edit_hints); filter_symbol(i, filter); return i; } diff --git a/src/render/symbol/filter.hpp b/src/render/symbol/filter.hpp index c25b0b1a..2f213761 100644 --- a/src/render/symbol/filter.hpp +++ b/src/render/symbol/filter.hpp @@ -35,7 +35,7 @@ class AColor : public Color { void filter_symbol(Image& symbol, const SymbolFilter& filter); /// Render a Symbol to an Image and filter it -Image render_symbol(const SymbolP& symbol, const SymbolFilter& filter, double border_radius = 0.05, int size = 100); +Image render_symbol(const SymbolP& symbol, const SymbolFilter& filter, double border_radius = 0.05, int size = 100, bool edit_hints = false); /// Is a point inside a symbol? enum SymbolSet diff --git a/src/render/symbol/viewer.cpp b/src/render/symbol/viewer.cpp index 32fa7b88..0683e874 100644 --- a/src/render/symbol/viewer.cpp +++ b/src/render/symbol/viewer.cpp @@ -14,7 +14,7 @@ DECLARE_TYPEOF_COLLECTION(SymbolPartP); // ----------------------------------------------------------------------------- : Simple rendering -Image render_symbol(const SymbolP& symbol, double border_radius, int size) { +Image render_symbol(const SymbolP& symbol, double border_radius, int size, bool editing_hints) { SymbolViewer viewer(symbol, size, border_radius); Bitmap bmp(size, size); wxMemoryDC dc; @@ -28,11 +28,12 @@ Image render_symbol(const SymbolP& symbol, double border_radius, int size) { // ----------------------------------------------------------------------------- : Constructor -SymbolViewer::SymbolViewer(const SymbolP& symbol, double size, double border_radius) - : border_radius(border_radius) +SymbolViewer::SymbolViewer(const SymbolP& symbol, bool editing_hints, double size, double border_radius) + : border_radius(border_radius), editing_hints(editing_hints) , rotation(0, RealRect(0,0,size,size), size) , multiply(size,0,0,size) , origin(0,0) + , in_symmetry(0) { setSymbol(symbol); } @@ -42,7 +43,7 @@ void SymbolViewer::setZoom(double zoom) { multiply = Matrix2D(zoom,0, 0,zoom); } -// ----------------------------------------------------------------------------- : Drawing +// ----------------------------------------------------------------------------- : Drawing : Combining typedef shared_ptr MemoryDCP; @@ -52,7 +53,7 @@ MemoryDCP getTempDC(DC& dc) { Bitmap buffer(s.GetWidth(), s.GetHeight(), 1); MemoryDCP newDC(new wxMemoryDC); newDC->SelectObject(buffer); - clearDC_black(*newDC); + clearDC(*newDC, *wxBLACK_BRUSH); return newDC; } @@ -66,6 +67,7 @@ void combineBuffers(DC& dc, DC* borders, DC* interior) { void SymbolViewer::draw(DC& dc) { bool paintedSomething = false; bool buffersFilled = false; + in_symmetry = 0; // Temporary dcs MemoryDCP borderDC; MemoryDCP interiorDC; @@ -80,12 +82,15 @@ void SymbolViewer::draw(DC& dc) { } } // Draw all parts - combineSymbolPart(dc, *symbol, paintedSomething, buffersFilled, true, borderDC, interiorDC); - + combineSymbolPart(dc, *symbol, paintedSomething, buffersFilled, false, borderDC, interiorDC); // Output the final parts from the buffer if (buffersFilled) { combineBuffers(dc, borderDC.get(), interiorDC.get()); } + // Editing hints? + if (editing_hints) { + drawEditingHints(dc); + } } void SymbolViewer::combineSymbolPart(DC& dc, const SymbolPart& part, bool& paintedSomething, bool& buffersFilled, bool allow_overlap, MemoryDCP& borderDC, MemoryDCP& interiorDC) { if (const SymbolShape* s = part.isSymbolShape()) { @@ -124,8 +129,10 @@ void SymbolViewer::combineSymbolPart(DC& dc, const SymbolPart& part, bool& paint Matrix2D old_m = multiply; Vector2D old_o = origin; int copies = s->kind == SYMMETRY_REFLECTION ? s->copies / 2 * 2 : s->copies; + if (copies > 1) ++in_symmetry; FOR_EACH_CONST_REVERSE(p, s->parts) { - for (int i = 0 ; i < copies ; ++i) { + for (int i = copies - 1 ; i >= 0 ; --i) { + if (i == 0) --in_symmetry; if (s->clip) { // todo: clip } @@ -164,11 +171,14 @@ void SymbolViewer::combineSymbolPart(DC& dc, const SymbolPart& part, bool& paint origin = old_o + (s->center - s->center * rot) * old_m; } // draw rotated copy - combineSymbolPart(dc, *p, paintedSomething, buffersFilled, allow_overlap && i == 0, borderDC, interiorDC); + combineSymbolPart(dc, *p, paintedSomething, buffersFilled, allow_overlap && i == copies - 1, borderDC, interiorDC); } } multiply = old_m; origin = old_o; + if (editing_hints) { + highlightPart(dc, *s, HIGHLIGHT_LESS); + } } else if (const SymbolGroup* g = part.isSymbolGroup()) { // Draw all parts, in reverse order (bottom to top) FOR_EACH_CONST_REVERSE(p, g->parts) { @@ -177,83 +187,14 @@ void SymbolViewer::combineSymbolPart(DC& dc, const SymbolPart& part, bool& paint } } -void SymbolViewer::highlightPart(DC& dc, const SymbolPart& part, HighlightStyle style) { - if (const SymbolShape* s = part.isSymbolShape()) { - highlightPart(dc, *s, style); - } else if (const SymbolSymmetry* s = part.isSymbolSymmetry()) { - highlightPart(dc, *s); - } else if (const SymbolGroup* g = part.isSymbolGroup()) { - highlightPart(dc, *g, style); - } else { - throw InternalError(_("Invalid symbol part type")); - } -} -void SymbolViewer::highlightPart(DC& dc, const SymbolShape& shape, HighlightStyle style) { - // create point list - vector points; - size_t size = shape.points.size(); - for(size_t i = 0 ; i < size ; ++i) { - segment_subdivide(*shape.getPoint((int)i), *shape.getPoint((int)i+1), origin, multiply, points); - } - // draw - if (style == HIGHLIGHT_BORDER) { - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.SetPen (wxPen(Color(255,0,0), 2)); - dc.DrawPolygon((int)points.size(), &points[0]); - } else if (style == HIGHLIGHT_BORDER_DOT) { - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.SetPen (wxPen(Color(255,0,0), 1, wxDOT)); - dc.DrawPolygon((int)points.size(), &points[0]); - } else { - dc.SetLogicalFunction(wxOR); - dc.SetBrush(Color(0,0,64)); - dc.SetPen (*wxTRANSPARENT_PEN); - dc.DrawPolygon((int)points.size(), &points[0]); - if (shape.combine == SYMBOL_COMBINE_SUBTRACT || shape.combine == SYMBOL_COMBINE_BORDER) { - dc.SetLogicalFunction(wxAND); - dc.SetBrush(Color(191,191,255)); - dc.DrawPolygon((int)points.size(), &points[0]); - } - dc.SetLogicalFunction(wxCOPY); - } -} -void SymbolViewer::highlightPart(DC& dc, const SymbolSymmetry& sym) { - // center - RealPoint center = rotation.tr(sym.center); - // draw 'spokes' - double angle = atan2(sym.handle.y, sym.handle.x); - dc.SetPen(wxPen(Color(255,200,0),3)); - for (int i = 0; i < sym.copies ; ++i) { - double a = angle + (i + 0.5) * 2 * M_PI / sym.copies; - Vector2D dir(cos(a), sin(a)); - Vector2D dir2 = rotation.tr(sym.center + 2 * dir); - dc.DrawLine(center.x, center.y, dir2.x, dir2.y); - } - // draw center - dc.SetPen(*wxBLACK_PEN); - dc.SetBrush(Color(255,200,0)); - dc.DrawCircle(center.x, center.y, 5); - // draw handle - Vector2D dir2 = rotation.tr(sym.center + sym.handle); - dc.SetPen(wxPen(Color(255,200,0),1,wxDOT)); - dc.DrawLine(center.x, center.y, dir2.x, dir2.y); -} -void SymbolViewer::highlightPart(DC& dc, const SymbolGroup& group, HighlightStyle style) { - if (style == HIGHLIGHT_BORDER) { - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.SetPen (wxPen(Color(255,0,0), 2)); - dc.DrawRectangle(rotation.tr(RealRect(group.min_pos, RealSize(group.max_pos - group.min_pos)))); - } - FOR_EACH_CONST(part, group.parts) { - highlightPart(dc, *part, (HighlightStyle)(style | HIGHLIGHT_LESS)); - } -} - void SymbolViewer::combineSymbolShape(const SymbolShape& shape, DC& border, DC& interior, bool directB, bool directI) { // what color should the interior be? // use black when drawing to the screen Byte interiorCol = directI ? 0 : 255; + if (editing_hints && in_symmetry) { + interiorCol = directI ? 16 : 240; + } // how to draw depends on combining mode switch(shape.combine) { case SYMBOL_COMBINE_OVERLAP: @@ -287,6 +228,10 @@ void SymbolViewer::combineSymbolShape(const SymbolShape& shape, DC& border, DC& } } + +// ----------------------------------------------------------------------------- : Drawing : Basic + + void SymbolViewer::drawSymbolShape(const SymbolShape& shape, DC* border, DC* interior, Byte borderCol, Byte interiorCol, bool directB, bool clear) { // create point list vector points; @@ -318,3 +263,90 @@ void SymbolViewer::drawSymbolShape(const SymbolShape& shape, DC* border, DC* int interior->DrawPolygon((int)points.size(), &points[0]); } } + +// ----------------------------------------------------------------------------- : Drawing : Highlighting + +void SymbolViewer::highlightPart(DC& dc, const SymbolPart& part, HighlightStyle style) { + if (const SymbolShape* s = part.isSymbolShape()) { + highlightPart(dc, *s, style); + } else if (const SymbolSymmetry* s = part.isSymbolSymmetry()) { + highlightPart(dc, *s, style); + } else if (const SymbolGroup* g = part.isSymbolGroup()) { + highlightPart(dc, *g, style); + } else { + throw InternalError(_("Invalid symbol part type")); + } +} + +void SymbolViewer::highlightPart(DC& dc, const SymbolShape& shape, HighlightStyle style) { + if (style == HIGHLIGHT_LESS) return; + // create point list + vector points; + size_t size = shape.points.size(); + for(size_t i = 0 ; i < size ; ++i) { + segment_subdivide(*shape.getPoint((int)i), *shape.getPoint((int)i+1), origin, multiply, points); + } + // draw + if (style == HIGHLIGHT_BORDER) { + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen (wxPen(Color(255,0,0), 2)); + dc.DrawPolygon((int)points.size(), &points[0]); + } else if (style == HIGHLIGHT_BORDER_DOT) { + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen (wxPen(Color(255,0,0), 1, wxDOT)); + dc.DrawPolygon((int)points.size(), &points[0]); + } else { + dc.SetLogicalFunction(wxOR); + dc.SetBrush(Color(0,0,64)); + dc.SetPen (*wxTRANSPARENT_PEN); + dc.DrawPolygon((int)points.size(), &points[0]); + if (shape.combine == SYMBOL_COMBINE_SUBTRACT || shape.combine == SYMBOL_COMBINE_BORDER) { + dc.SetLogicalFunction(wxAND); + dc.SetBrush(Color(191,191,255)); + dc.DrawPolygon((int)points.size(), &points[0]); + } + dc.SetLogicalFunction(wxCOPY); + } +} + +void SymbolViewer::highlightPart(DC& dc, const SymbolSymmetry& sym, HighlightStyle style) { + // highlight parts? + FOR_EACH_CONST(part, sym.parts) { + highlightPart(dc, *part, (HighlightStyle)(style | HIGHLIGHT_LESS)); + } + // Color? + Color color = style & HIGHLIGHT_BORDER ? Color(255,100,0) + : style & HIGHLIGHT_INTERIOR ? Color(255,200,0) + : Color(200,170,0); + // center + RealPoint center = rotation.tr(sym.center); + // draw 'spokes' + double angle = atan2(sym.handle.y, sym.handle.x); + dc.SetPen(wxPen(color, sym.kind == SYMMETRY_ROTATION ? 1 : 3)); + for (int i = 0; i < sym.copies ; ++i) { + double a = angle + (i + 0.5) * 2 * M_PI / sym.copies; + Vector2D dir(cos(a), sin(a)); + Vector2D dir2 = rotation.tr(sym.center + 2 * dir); + dc.DrawLine(center.x, center.y, dir2.x, dir2.y); + } + // draw center + dc.SetPen(*wxBLACK_PEN); + dc.SetBrush(color); + dc.DrawCircle(center.x, center.y, sym.kind == SYMMETRY_ROTATION ? 7 : 5); +} + +void SymbolViewer::highlightPart(DC& dc, const SymbolGroup& group, HighlightStyle style) { + if (style == HIGHLIGHT_BORDER) { + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen (wxPen(Color(255,0,0), 2)); + dc.DrawRectangle(rotation.tr(RealRect(group.min_pos, RealSize(group.max_pos - group.min_pos)))); + } + FOR_EACH_CONST(part, group.parts) { + highlightPart(dc, *part, (HighlightStyle)(style | HIGHLIGHT_LESS)); + } +} + + +void SymbolViewer::drawEditingHints(DC& dc) { + // TODO? +} diff --git a/src/render/symbol/viewer.hpp b/src/render/symbol/viewer.hpp index 32d99122..ee40dc9c 100644 --- a/src/render/symbol/viewer.hpp +++ b/src/render/symbol/viewer.hpp @@ -17,7 +17,7 @@ // ----------------------------------------------------------------------------- : Simple rendering /// Render a Symbol to an Image -Image render_symbol(const SymbolP& symbol, double border_radius = 0.05, int size = 100); +Image render_symbol(const SymbolP& symbol, double border_radius = 0.05, int size = 100, bool editing_hints = false); // ----------------------------------------------------------------------------- : Symbol Viewer @@ -32,11 +32,12 @@ enum HighlightStyle class SymbolViewer : public SymbolView { public: // --------------------------------------------------- : Data - SymbolViewer(const SymbolP& symbol, double size = 500, double border_radius = 0.05); + SymbolViewer(const SymbolP& symbol, bool editing_hints, double size = 500, double border_radius = 0.05); // drawing double border_radius; - + bool editing_hints; + // --------------------------------------------------- : Point translation void setZoom(double zoom); @@ -50,15 +51,20 @@ class SymbolViewer : public SymbolView { /// Draw the symbol to a dc void draw(DC& dc); - void highlightPart(DC& dc, const SymbolPart& part, HighlightStyle style); - void highlightPart(DC& dc, const SymbolShape& shape, HighlightStyle style); - void highlightPart(DC& dc, const SymbolSymmetry& sym); - void highlightPart(DC& dc, const SymbolGroup& group, HighlightStyle style); + void highlightPart(DC& dc, const SymbolPart& part, HighlightStyle style); + void highlightPart(DC& dc, const SymbolShape& shape, HighlightStyle style); + void highlightPart(DC& dc, const SymbolSymmetry& sym, HighlightStyle style); + void highlightPart(DC& dc, const SymbolGroup& group, HighlightStyle style); + + void drawEditingHints(DC& dc); void onAction(const Action&, bool) {} + private: typedef shared_ptr MemoryDCP; + /// Inside a reflection? + int in_symmetry; /// Combine a symbol part with the dc void SymbolViewer::combineSymbolPart(DC& dc, const SymbolPart& part, bool& paintedSomething, bool& buffersFilled, bool allow_overlap, MemoryDCP& borderDC, MemoryDCP& interiorDC); diff --git a/src/resource/common/icon_symmetry_reflection.png b/src/resource/common/icon_symmetry_reflection.png index a4ecafd4d0863db3585a6d01de40b6a39cf7c2d4..48503ea5d18f33963e47dd3083c12df90e9bca68 100644 GIT binary patch delta 89 zcmXS|n4l8E6W|l#dgja-1_p-zKyZekfz_lD$YCrA@(X5gcy=QV$dU4NaSW-rm7K8m p&;f=+lgtGzK2KwoXpbvpV5m#s_!;>1+%cdk22WQ%mvv4FO#lEqA$kA+ delta 86 zcmb;@ouCrP8Q>G*`v3ob1_p-zXBd{Zv3>%w7)yfuf*Bm1-ADs+q&!_5Ln>}1C(J!` nfZgTe~DWM4fG*`v3ob1_p-zXBd{Zv3>%w7)yfuf*Bm1-ADs+6g*uVLn>}1C(M0y u;DFV^Mn{1bb)$xj${hZQ2^@0|F)*aru%#MEM=SuUW$<+Mb6Mw<&;$T9`ydzq diff --git a/src/resource/msw/mse.rc b/src/resource/msw/mse.rc index 07fb720c..cbeae38f 100644 --- a/src/resource/msw/mse.rc +++ b/src/resource/msw/mse.rc @@ -95,6 +95,8 @@ tool/grid_snap IMAGE "tool/grid_snap.png" tool/group IMAGE "tool/group.png" tool/ungroup IMAGE "tool/ungroup.png" +tool/symmetry_add IMAGE "tool/symmetry_add.png" + combine_or IMAGE "../common/combine_or.png" combine_sub IMAGE "../common/combine_sub.png" combine_sub_dark IMAGE "../common/combine_sub_dark.png" diff --git a/src/resource/msw/tool/symmetry_add.png b/src/resource/msw/tool/symmetry_add.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e663c5a3ef63d8a2a48d6fe8f9b9dde7b5d4b8 GIT binary patch literal 330 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!6#K6G#d|Jc;Acw6a$S;`TKM0yfNY?^II14-? ziy0X7ltGxWVyS%@NU+2;qQp5rH#aq}gn^+`tCtNZc-7OzF{ENn@1%`MF|5_}$p5?ed35>fq@J3r zb-a8#lRU3qk322vK};fH|Jgjq*Y7W7TX<5;<#DJtV|$U3I&(JMHtQvM3vJa#ZV zC2`5~xu;TMhg4&t~wGKBT!<(*4zuf#n-5~UUUOc1K zCf0ME59ay(?2uWpW`{AO-5sxMli810GdHu#B-@pH{hTb5Ajz6zaapzO-X;5wZ1o-P VOzfVIvw$9C@O1TaS?83{1OOqiee(bS literal 0 HcmV?d00001 diff --git a/src/util/window_id.hpp b/src/util/window_id.hpp index f59b95a7..8f21d1e9 100644 --- a/src/util/window_id.hpp +++ b/src/util/window_id.hpp @@ -153,10 +153,11 @@ enum ChildMenuID { , ID_SIDES // SymbolSymmetryEditor toolbar/menu -, ID_SYMMETRY = 2201 -, ID_SYMMETRY_ROTATION = ID_SHAPE +, ID_SYMMETRY = 2301 +, ID_SYMMETRY_ROTATION = ID_SYMMETRY , ID_SYMMETRY_REFLECTION , ID_SYMMETRY_MAX +, ID_ADD_SYMMETRY , ID_COPIES // On cards panel