File indexing completed on 2025-03-16 13:13:03
0001 /******************************************************************************** 0002 * Copyright (C) 2011-2015 by Stephen Allewell * 0003 * steve.allewell@gmail.com * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify * 0006 * it under the terms of the GNU General Public License as published by * 0007 * the Free Software Foundation; either version 2 of the License, or * 0008 * (at your option) any later version. * 0009 ********************************************************************************/ 0010 0011 0012 /** 0013 * @file 0014 * Implement the Editor class. 0015 */ 0016 0017 0018 /** 0019 * @page editor_window Editor 0020 * The editor allows the user to create or modify the current symbol. It consists of a fixed size square grid 0021 * with several tools to create QPainterPath objects which will then be used to render the symbol. The QPainterPath 0022 * consists of one or more subpaths. Each subpath has a start point, which defaults to (0,0), but can be moved as 0023 * required. The various tools require one or more additional points to define the subpath. For each of the 0024 * tools used,the last point of the last tool becomes the first point of the next. Consequently all the individual 0025 * tools are joined together to create a single subpath. A new subpath can be started by using the move to option 0026 * to specify a new start point. Additionally the use of the rectangle and ellipse tools will create stand alone subpaths. 0027 * 0028 * The editor shows a green square which is the largest preferred size of the symbol. This allows some white space 0029 * around the symbol when it will be rendered in a pattern. 0030 * 0031 * When placing points, guide lines will be shown when the cursor is at a position that aligns with other 0032 * points either horizontally, vertically or at a range of predefined angles. The circle guides are created if the cursor 0033 * is at a position that aligns with another point on a circle where the center is at the center of the grid. 0034 * When a symbol shape is made up of a large number of points, the number of guide lines generated can be significant. 0035 * If these become a problem then the generation of the guide lines can be turned off completely or an option is 0036 * available in the configuration dialog to simplify the range of angles tested. 0037 * 0038 * @section editor_tools Editor Tools 0039 * 0040 * @subsection move_to Move To 0041 * Allows moving the start point of the current subpath. If the current subpath already has elements in 0042 * it, this effectively closes the current subpath and creates a new one. 0043 * 0044 * @subsection line_to Line To 0045 * Draws a line from the last point to the next selected point. 0046 * 0047 * @subsection cubic_to Cubic To 0048 * Draws a cubic spline consisting of a start point which will be the last point of the last command, two 0049 * control points and a finish point. The control points define the path of the curve between the start and end points. 0050 * 0051 * @subsection rectangle Rectangle 0052 * Draw a rectangle between two points. The points define the opposite corners of the rectangle. These can either 0053 * entered by clicking two points or clicking and dragging. When dragging, a rubber band rectangle will be shown 0054 * indicating the rectangle. 0055 * 0056 * @subsection ellipse Ellipse 0057 * Draw an ellipse between two points. The points define the opposite corners of a rectangle encompassing the 0058 * ellipse. As with the rectangle the points can be selected by clicking two points or clicking and dragging. 0059 * When dragging, a rubber band ellipse will be shown indicating the ellipse. 0060 * 0061 * @subsection character Insert Character 0062 * This will display a character selection dialog allowing selection of a character from any font to be inserted as a 0063 * symbol. Double click the required character which will then be converted to a symbol format in the editor. Further changes 0064 * can then be made to it as needed. This selection will replace any current symbol definition in the editor. The dialog 0065 * will remain open, allowing a select, save, repeat work flow rapidly building up a library of symbols. 0066 * 0067 * @subsection rotate_left Rotate Left 0068 * Rotate the symbol counter clockwise 90 degrees. 0069 * 0070 * @subsection rotate_right Rotate Right 0071 * Rotate the symbol clockwise 90 degrees. 0072 * 0073 * @subsection flip_horizontal Flip Horizontally 0074 * Flip the symbol horizontally about an axis passing vertically through the center of the grid. 0075 * 0076 * @subsection flip_vertical Flip Vertically 0077 * Flip the symbol vertically about an axis passing horizontally through the center of the grid. 0078 * 0079 * @subsection scale_preferred Scale to Preferred Size 0080 * Scale the existing symbol so that it fits within the preferred size square. 0081 * 0082 * @subsection snap_grid Snap to Grid 0083 * The selection of points can either be snapped to the grid or can be freely placed depending on if 0084 * the snap option is toggled on or off. 0085 * 0086 * @subsection guide_lines Enable Guide Lines 0087 * The generation of guide lines can be toggled on or off. As the number of points in a symbol increases 0088 * the number of guide lines that can be created can become significant at which point they become less 0089 * useful. 0090 * 0091 * @section path_rendering Path Rendering 0092 * 0093 * @subsection fill_mode Fill Mode 0094 * The defined path can either be set to filled or outline depending on if the fill mode is toggled 0095 * on or off. 0096 * 0097 * @subsection fill_rule Fill Rule 0098 * When filling is enabled the path will be filled. The rule defining the fill will be either Winding of 0099 * Odd-Even. 0100 * 0101 * @par Winding 0102 * The winding rule will completely fill the objects boundary. 0103 * 0104 * @par Odd Even 0105 * The odd even rule will fill produce a checker board style fill where adjacent areas will be different 0106 * to each other. 0107 * 0108 * @subsection line_cap Line End Cap Style 0109 * The start and end of a path line can have different visual appearance depending on the following 0110 * attributes. 0111 * 0112 * @par Flat Cap 0113 * The line is stopped at the end point and the line end is cut square to the direction. 0114 * 0115 * @par Square Cap 0116 * The line is projected beyond the end point by half the line width and the line end is cut square to 0117 * the direction. 0118 * 0119 * @par Round Cap 0120 * The line is projected beyond the end point by half the line width and the line end is rounded. 0121 * 0122 * @subsection line_join Line Join Style 0123 * Where two lines join, the way the join is shown is determined by the join style as follows. 0124 * 0125 * @par Bevel Join 0126 * The lines are beveled at the intersection. 0127 * 0128 * @par Miter Join 0129 * The lines are extended and mitered to provide a corner. 0130 * 0131 * @par Round Join 0132 * The lines are joined with a rounded corner. 0133 * 0134 * @subsection line_width Line Width 0135 * The line width can be increased and decreased. The width is rendered equally about the line between 0136 * the two points. This attribute is mostly relevant to unfilled shapes. 0137 * 0138 * @section points Points 0139 * The points can either be snapped to the grid intersections or placed freely depending on the snap option. 0140 * All points can be moved by hovering over them until the cursor changes to a move cursor, then hold down 0141 * the left mouse button and drag the point to its new location. 0142 * 0143 * @section editing_symbols Editing Symbols 0144 * Editing a symbol in the library can be done by clicking the symbol in the library view. This will 0145 * transfer the symbol into the editor allowing it to be changed and saved. In this scenario there is a link 0146 * between the symbol being edited and the library, so saving does update the correct symbol. If you wish to 0147 * use this symbol as a basis for other symbols using rotation and flipping, use the File->Save Symbol as New 0148 * command which will save the symbol to the library as a new one. Further changes to the symbol which are 0149 * saved will also become new symbols. 0150 */ 0151 0152 0153 #include "Editor.h" 0154 0155 #include <QAction> 0156 #include <QMouseEvent> 0157 #include <QPainter> 0158 #include <QString> 0159 0160 #include <KCharSelect> 0161 #include <KLocalizedString> 0162 0163 #include <math.h> 0164 0165 #include "SymbolEditor.h" 0166 0167 0168 const int pointsRequired[] = {1, 1, 3, 2, 2}; /**< The number of points required for commands from Editor::ToolMode */ 0169 0170 const QString statusMessages[][3] = { 0171 { 0172 I18N_NOOP("Select a new starting position."), 0173 "", 0174 "" 0175 }, 0176 { 0177 I18N_NOOP("Select the line end point."), 0178 "", 0179 "" 0180 }, 0181 { 0182 I18N_NOOP("Select the first control point."), 0183 I18N_NOOP("Select the second control point."), 0184 I18N_NOOP("Select the end point"), 0185 }, 0186 { 0187 I18N_NOOP("Select the first corner."), 0188 I18N_NOOP("Select the second corner."), 0189 "" 0190 }, 0191 { 0192 I18N_NOOP("Select the first corner of the bounding rectangle."), 0193 I18N_NOOP("Select the second corner of the bounding rectangle."), 0194 "" 0195 }, 0196 { 0197 I18N_NOOP("Double click a character to insert into the editor."), 0198 "", 0199 "" 0200 } 0201 }; 0202 0203 0204 /** 0205 * Construct the Editor. 0206 * The editor is a sub class of a QWidget and is added to a layout widget. 0207 * The boundary edges of the symbol editor are defined in the range 0..1. These are used to intersect 0208 * the guidelines which are shown at the m_angles relative to existing points 0209 * 0210 * @param parent a pointer to the parent widget, this is passed to the base class object 0211 */ 0212 Editor::Editor(QWidget *parent) 0213 : QWidget(parent), 0214 m_index(0), 0215 m_charSelect(0) 0216 { 0217 readSettings(); 0218 0219 setMouseTracking(true); 0220 setFocusPolicy(Qt::StrongFocus); 0221 0222 m_topEdge = QLineF(0.0, 0.0, 1.0, 0.0); 0223 m_bottomEdge = QLineF(0.0, 1.0, 1.0, 1.0); 0224 m_leftEdge = QLineF(0.0, 0.0, 0.0, 1.0); 0225 m_rightEdge = QLineF(1.0, 0, 1.0, 1.0); 0226 } 0227 0228 0229 /** 0230 * Destructor for the Editor. 0231 * No additional destruction is required here. 0232 */ 0233 Editor::~Editor() 0234 { 0235 delete m_charSelect; 0236 } 0237 0238 0239 /** 0240 * Get the index and Symbol. 0241 * Update the Symbol with the path being edited. 0242 * 0243 * @return the edited Symbol and it's index in a QPair 0244 */ 0245 QPair<qint16, Symbol> Editor::symbol() 0246 { 0247 m_symbol.setPath(m_painterPath); 0248 return QPair<qint16, Symbol>(m_index, m_symbol); 0249 } 0250 0251 0252 /** 0253 * Set the symbol and index to be edited. 0254 * The undo stack will be cleared. 0255 * 0256 * @param pair a QPair consisting of the index and the Symbol 0257 */ 0258 void Editor::setSymbol(const QPair<qint16, Symbol> &pair) 0259 { 0260 m_undoStack.clear(); 0261 m_index = pair.first; 0262 m_symbol = pair.second; 0263 m_painterPath = m_symbol.path(); 0264 deconstructPainterPath(); 0265 update(); 0266 } 0267 0268 0269 /** 0270 * Move the start of the sub path. 0271 * The point is in symbol coordinates. 0272 * Returns the current painterPath for later undo. 0273 * 0274 * @param to a const reference to a QPointF representing the new start position 0275 * 0276 * @return a QPainterPath representing the original path 0277 */ 0278 QPainterPath Editor::moveTo(const QPointF &to) 0279 { 0280 QPainterPath path = m_painterPath; 0281 m_painterPath.moveTo(to); 0282 deconstructPainterPath(); 0283 update(); 0284 return path; 0285 } 0286 0287 0288 /** 0289 * Add a line to the sub path. 0290 * The point is in symbol coordinates. 0291 * Returns the current painterPath for later undo. 0292 * 0293 * @param to a const reference to a QPointF representing the end point of the line 0294 * 0295 * @return a QPainterPath representing the original path 0296 */ 0297 QPainterPath Editor::lineTo(const QPointF &to) 0298 { 0299 QPainterPath path = m_painterPath; 0300 m_painterPath.lineTo(to); 0301 deconstructPainterPath(); 0302 update(); 0303 return path; 0304 } 0305 0306 0307 /** 0308 * Add a cubic spline to the sub path. 0309 * The points are in symbol coordinates. 0310 * Returns the current painterPath for later undo. 0311 * 0312 * @param control1 a const reference to a QPointF representing the first control point 0313 * @param control2 a const reference to a QPointF representing the second control point 0314 * @param to a const reference to a QPointF representing the end point of the curve 0315 * 0316 * @return a QPainterPath representing the original path 0317 */ 0318 QPainterPath Editor::cubicTo(const QPointF &control1, const QPointF &control2, const QPointF &to) 0319 { 0320 QPainterPath path = m_painterPath; 0321 m_painterPath.cubicTo(control1, control2, to); 0322 deconstructPainterPath(); 0323 update(); 0324 return path; 0325 } 0326 0327 0328 /** 0329 * Add a rectangle as a new sub path. 0330 * The points are in symbol coordinates. 0331 * Returns the current painterPath for later undo. 0332 * 0333 * @param from a const reference to a QPointF representing the first corner 0334 * @param to a const reference to a QPointF representing the second corner 0335 * 0336 * @return a QPainterPath representing the original path 0337 */ 0338 QPainterPath Editor::addRectangle(const QPointF &from, const QPointF &to) 0339 { 0340 QPainterPath path = m_painterPath; 0341 m_painterPath.addRect(QRectF(from, to)); 0342 deconstructPainterPath(); 0343 update(); 0344 return path; 0345 } 0346 0347 0348 /** 0349 * Add an ellipse as a new sub path. 0350 * The points are in symbol coordinates. 0351 * Returns the current painterPath for later undo. 0352 * 0353 * @param from a const reference to a QPointF representing the first corner 0354 * @param to a const reference to a QPointF representing the second corner 0355 * 0356 * @return a QPainterPath representing the original path 0357 */ 0358 QPainterPath Editor::addEllipse(const QPointF &from, const QPointF &to) 0359 { 0360 QPainterPath path = m_painterPath; 0361 m_painterPath.addEllipse(QRectF(from, to)); 0362 deconstructPainterPath(); 0363 update(); 0364 return path; 0365 } 0366 0367 0368 /** 0369 * Remove the last added element by restoring the path that existed before the last command. 0370 * 0371 * @param path a const reference to a QPainterPath being restored 0372 */ 0373 void Editor::removeLast(const QPainterPath& path) 0374 { 0375 m_painterPath = path; 0376 deconstructPainterPath(); 0377 update(); 0378 } 0379 0380 0381 /** 0382 * Move a point node to a new position. 0383 * 0384 * @param index the index of the point to move 0385 * @param to a const reference to a QPointF representing the new position 0386 */ 0387 void Editor::movePoint(int index, const QPointF &to) 0388 { 0389 m_points[index] = to; 0390 constructPainterPath(); 0391 update(); 0392 } 0393 0394 0395 /** 0396 * Rotate all the points around the center of the symbol 90 degrees counter clockwise. 0397 */ 0398 void Editor::rotatePointsLeft() 0399 { 0400 for (int i = 0 ; i < m_points.count() ; ++i) { 0401 QPointF p = m_points[i]; 0402 m_points[i] = QPointF(p.y(), 1.0 - p.x()); 0403 } 0404 0405 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 0406 QPointF p = m_activePoints[i]; 0407 m_activePoints[i] = QPointF(p.y(), 1.0 - p.x()); 0408 } 0409 0410 constructPainterPath(); 0411 update(); 0412 } 0413 0414 0415 /** 0416 * Rotate all the points around the center of the symbol 90 degrees clockwise 0417 */ 0418 void Editor::rotatePointsRight() 0419 { 0420 for (int i = 0 ; i < m_points.count() ; ++i) { 0421 QPointF p = m_points[i]; 0422 m_points[i] = QPointF(1.0 - p.y(), p.x()); 0423 } 0424 0425 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 0426 QPointF p = m_activePoints[i]; 0427 m_activePoints[i] = QPointF(1.0 - p.y(), p.x()); 0428 } 0429 0430 constructPainterPath(); 0431 update(); 0432 } 0433 0434 0435 /** 0436 * Flip all the points about a vertical axis passing through the symbol center. 0437 */ 0438 void Editor::flipPointsHorizontal() 0439 { 0440 for (int i = 0 ; i < m_points.count() ; ++i) { 0441 QPointF p = m_points[i]; 0442 p.setX(1.0 - p.x()); 0443 m_points[i] = p; 0444 } 0445 0446 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 0447 QPointF p = m_activePoints[i]; 0448 p.setX(1.0 - p.x()); 0449 m_activePoints[i] = p; 0450 } 0451 0452 constructPainterPath(); 0453 update(); 0454 } 0455 0456 0457 /** 0458 * Flip all the points about a horizontal axis passing through the symbol center. 0459 */ 0460 void Editor::flipPointsVertical() 0461 { 0462 for (int i = 0 ; i < m_points.count() ; ++i) { 0463 QPointF p = m_points[i]; 0464 p.setY(1.0 - p.y()); 0465 m_points[i] = p; 0466 } 0467 0468 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 0469 QPointF p = m_activePoints[i]; 0470 p.setY(1.0 - p.y()); 0471 m_activePoints[i] = p; 0472 } 0473 0474 constructPainterPath(); 0475 update(); 0476 } 0477 0478 0479 /** 0480 * Set the fill state of the symbols path. 0481 * 0482 * @param filled @c true if the path is to be filled, @c false for outline paths 0483 */ 0484 void Editor::setFilled(bool filled) 0485 { 0486 m_symbol.setFilled(filled); 0487 update(); 0488 } 0489 0490 0491 /** 0492 * Set the fill rule of the symbols path. 0493 * 0494 * @param rule a Qt::FillRule value, see the QPainterPath documentation for details 0495 */ 0496 void Editor::setFillRule(Qt::FillRule rule) 0497 { 0498 m_painterPath.setFillRule(rule); 0499 update(); 0500 } 0501 0502 0503 /** 0504 * Set the pen cap style, see the QPen documentation for details on the styles. 0505 * 0506 * @param capStyle a Qt::PenCapStyle value 0507 */ 0508 void Editor::setCapStyle(Qt::PenCapStyle capStyle) 0509 { 0510 m_symbol.setCapStyle(capStyle); 0511 update(); 0512 } 0513 0514 0515 /** 0516 * Set the pen join style, see the QPen documentation for details on the styles. 0517 * 0518 * @param joinStyle a Qt::PenJoinStyle value 0519 */ 0520 void Editor::setJoinStyle(Qt::PenJoinStyle joinStyle) 0521 { 0522 m_symbol.setJoinStyle(joinStyle); 0523 update(); 0524 } 0525 0526 0527 /** 0528 * Set the line width. 0529 * The width parameter is rounded to 2 decimal places to avoid errors in the comparisons. 0530 * Signals are emitted to enable or disable the relevant actions depending on the values. 0531 * 0532 * @param width the new line width 0533 */ 0534 void Editor::setLineWidth(double width) 0535 { 0536 width = round(width * 100) / 100; 0537 m_symbol.setLineWidth(width); 0538 emit minLineWidth(width == 0.01); 0539 emit maxLineWidth(width == 1.00); 0540 update(); 0541 } 0542 0543 0544 /** 0545 * Set the painter path to the requested path updating the points lists and the view. 0546 * 0547 * @param path contains the new path 0548 * 0549 * @return the old path 0550 */ 0551 QPainterPath Editor::setPath(const QPainterPath &path) 0552 { 0553 QPainterPath originalPath = m_painterPath; 0554 m_painterPath = path; 0555 deconstructPainterPath(); 0556 update(); 0557 0558 return originalPath; 0559 } 0560 0561 0562 /** 0563 * Clear the editor to represent a blank symbol. 0564 * Delete the existing data and update the display. 0565 */ 0566 void Editor::clear() 0567 { 0568 m_undoStack.clear(); 0569 m_points.clear(); 0570 m_activePoints.clear(); 0571 m_elements.clear(); 0572 m_index = 0; 0573 m_symbol = Symbol(); 0574 m_painterPath = m_symbol.path(); 0575 update(); 0576 } 0577 0578 0579 /** 0580 * Get a pointer to the undo stack. 0581 * 0582 * @return a pointer to the QUndoStack 0583 */ 0584 QUndoStack *Editor::undoStack() 0585 { 0586 return &m_undoStack; 0587 } 0588 0589 0590 /** 0591 * Select the next tool. 0592 * Remove any points that may have been used in the last tool command but not yet committed. 0593 * Set the tool mode from the sending actions data. 0594 * If the character selector tool is selected, display a KCharSelect widget to get a character to be 0595 * inserted otherwise hide the KCharSelect widget. 0596 * 0597 * @param action a pointer to the action triggering the selection 0598 */ 0599 void Editor::selectTool(QAction *action) 0600 { 0601 m_activePoints.clear(); 0602 m_toolMode = static_cast<Editor::ToolMode>(action->data().toInt()); 0603 0604 if (m_toolMode == Editor::Character) { 0605 if (m_charSelect == 0) { 0606 m_charSelect = new KCharSelect(0, 0); 0607 connect(m_charSelect, SIGNAL(charSelected(QChar)), this, SLOT(charSelected(QChar))); 0608 } 0609 0610 m_charSelect->show(); 0611 } else if (m_charSelect) { 0612 m_charSelect->hide(); 0613 } 0614 0615 updateStatusMessage(); 0616 } 0617 0618 0619 /** 0620 * Called when a character is selected from the KCharSelect widget. Create a QPainterPath object and 0621 * make it available to the editor for insertion. 0622 * 0623 * @param char a QChar containing the character selected. 0624 */ 0625 void Editor::charSelected(const QChar &character) 0626 { 0627 QPainterPath path; 0628 QFont font = m_charSelect->currentFont(); 0629 path.addText(0.0, 0.0, font, QString(character)); 0630 0631 // scale the path to fit the bounding rectangle to fit in a rectangle 0,0-1,1 maintaining 0632 // aspect ratio. 0633 QRectF boundingRect = path.boundingRect(); 0634 0635 double scale = double(m_gridElements - m_borderSize - m_borderSize) / double(std::max(boundingRect.width(), boundingRect.height()) * m_gridElements); 0636 QTransform transform = QTransform::fromTranslate(-boundingRect.center().x(), -boundingRect.center().y()) * QTransform::fromScale(scale, scale) * QTransform::fromTranslate(0.5, 0.5); 0637 path = transform.map(path); 0638 0639 m_undoStack.push(new AddCharacterCommand(this, path)); 0640 } 0641 0642 0643 /** 0644 * Switch the snap mode on or off. 0645 * 0646 * @param enabled true if on, false otherwise 0647 */ 0648 void Editor::enableSnap(bool enabled) 0649 { 0650 m_snap = enabled; 0651 } 0652 0653 0654 /** 0655 * Switch the generation of guides on or off. 0656 * 0657 * @param enabled true if on, false otherwise 0658 */ 0659 void Editor::enableGuides(bool enabled) 0660 { 0661 m_guides = enabled; 0662 } 0663 0664 0665 /** 0666 * Switch the fill mode on or off. 0667 * 0668 * @param filled true if on, false otherwise 0669 */ 0670 void Editor::selectFilled(bool filled) 0671 { 0672 m_undoStack.push(new ChangeFilledCommand(this, m_symbol.filled(), filled)); 0673 } 0674 0675 0676 /** 0677 * Select the fill rule to be used when filling is enabled. 0678 * The specified rule is taken from the sending actions data. 0679 * 0680 * @param action a pointer to the action triggering the selection 0681 */ 0682 void Editor::selectFillRule(QAction *action) 0683 { 0684 Qt::FillRule fillRule = static_cast<Qt::FillRule>(action->data().toInt()); 0685 m_undoStack.push(new ChangeFillRuleCommand(this, m_symbol.path().fillRule(), fillRule)); 0686 } 0687 0688 0689 /** 0690 * Select the pen cap style. 0691 * The specified value is taken from the sending actions data. 0692 * 0693 * @param action a pointer to the action that triggered the selection 0694 */ 0695 void Editor::selectCapStyle(QAction *action) 0696 { 0697 Qt::PenCapStyle capStyle = static_cast<Qt::PenCapStyle>(action->data().toInt()); 0698 m_undoStack.push(new ChangeCapStyleCommand(this, m_symbol.capStyle(), capStyle)); 0699 } 0700 0701 0702 /** 0703 * Select the pen join style. 0704 * The specified value is taken from the sending actions data. 0705 * 0706 * @param action a pointer to the action that triggered the selection 0707 */ 0708 void Editor::selectJoinStyle(QAction *action) 0709 { 0710 Qt::PenJoinStyle joinStyle = static_cast<Qt::PenJoinStyle>(action->data().toInt()); 0711 m_undoStack.push(new ChangeJoinStyleCommand(this, m_symbol.joinStyle(), joinStyle)); 0712 } 0713 0714 0715 /** 0716 * Increase the thickness of the path. 0717 */ 0718 void Editor::increaseLineWidth() 0719 { 0720 m_undoStack.push(new IncreaseLineWidthCommand(this, m_symbol.lineWidth(), m_symbol.lineWidth() + 0.01)); 0721 } 0722 0723 0724 /** 0725 * Decrease the thickness of the path. 0726 */ 0727 void Editor::decreaseLineWidth() 0728 { 0729 m_undoStack.push(new DecreaseLineWidthCommand(this, m_symbol.lineWidth(), m_symbol.lineWidth() - 0.01)); 0730 } 0731 0732 0733 /** 0734 * Rotate the symbol left (counter clock wise) 0735 */ 0736 void Editor::rotateLeft() 0737 { 0738 m_undoStack.push(new RotateLeftCommand(this)); 0739 } 0740 0741 0742 /** 0743 * Rotate the symbol right (clock wise) 0744 */ 0745 void Editor::rotateRight() 0746 { 0747 m_undoStack.push(new RotateRightCommand(this)); 0748 } 0749 0750 0751 /** 0752 * Flip the symbol horizontally about the vertical center line. 0753 */ 0754 void Editor::flipHorizontal() 0755 { 0756 m_undoStack.push(new FlipHorizontalCommand(this)); 0757 } 0758 0759 0760 /** 0761 * Flip the symbol vertically about the horizontal center line. 0762 */ 0763 void Editor::flipVertical() 0764 { 0765 m_undoStack.push(new FlipVerticalCommand(this)); 0766 } 0767 0768 0769 /** 0770 * Scale the symbol to fit within the preferred size. 0771 */ 0772 void Editor::scalePreferred() 0773 { 0774 m_undoStack.push(new ScalePreferredCommand(this, m_painterPath, m_gridElements, m_borderSize)); 0775 } 0776 0777 0778 /** 0779 * Read the settings from the configuration file and apply them. 0780 * The widget is resized to accommodate a gridElements number of cells of width elementSize. An 0781 * additional 1 is added to allow for a right and bottom edge to be drawn. 0782 */ 0783 void Editor::readSettings() 0784 { 0785 m_gridElements = Configuration::editor_GridElements(); 0786 m_elementSize = Configuration::editor_ElementSize(); 0787 m_elementGrouping = Configuration::editor_ElementGrouping(); 0788 m_pointSize = static_cast<double>(Configuration::editor_PointSize()) / 500; 0789 m_snapThreshold = 1.0 / m_gridElements * Configuration::editor_SnapThreshold(); 0790 m_borderSize = Configuration::editor_BorderSize(); 0791 m_preferredSizeColor = Configuration::editor_PreferredSizeColor(); 0792 m_guideLineColor = Configuration::editor_GuideLineColor(); 0793 0794 m_angles.clear(); 0795 0796 if (Configuration::editor_SimplifiedGuideLines()) { 0797 m_angles << 0 << 45 << 90 << 135; 0798 } else { 0799 m_angles << 0 << 15 << 30 << 45 << 60 << 75 << 90 << 105 << 120 << 135 << 150 << 165; 0800 } 0801 0802 int minimumSize = m_elementSize * m_gridElements + 1; 0803 setMinimumSize(minimumSize, minimumSize); 0804 } 0805 0806 0807 /** 0808 * Process the mouse press event. Effective for the left mouse button pressed. 0809 * Test if the cursor is over an existing point in which case dragging of the 0810 * point is enabled. Otherwise a point is added to the active points. 0811 * 0812 * @param event a pointer to a QMouseEvent 0813 */ 0814 void Editor::mousePressEvent(QMouseEvent *event) 0815 { 0816 QPoint p = event->pos(); 0817 m_dragging = false; 0818 0819 if (event->buttons() & Qt::LeftButton) { 0820 m_start = m_tracking = snapPoint(p); 0821 m_rubberBand = QRectF(); 0822 0823 if (node(toSymbol(p))) { 0824 m_dragging = true; 0825 m_dragPointIndex = nodeUnderCursor(toSymbol(p)); 0826 } else { 0827 addPoint(m_start); 0828 } 0829 } 0830 0831 update(); 0832 } 0833 0834 0835 /** 0836 * Process the mouse move event. Effective for the left mouse button pressed. 0837 * Clear any currently defined guides. 0838 * If dragging a point, update the points position and update the view, otherwise 0839 * if the toolmode is for a rectangle or an ellipse initialize the rubber band 0840 * rectangle and update the view. 0841 * If the left button is not pressed, test if the cursor is over an existing point 0842 * and if it is change the cursor to a SizeAllCursor to indicate it can be moved, 0843 * otherwise reset it to an ArrowCursor. 0844 * Guide lines are constructed for each of the existing points to show if the new 0845 * position lies in line with or at a specific angle to or on a circular path to 0846 * that point. 0847 * 0848 * @param event a pointer to a QMouseEvent 0849 */ 0850 void Editor::mouseMoveEvent(QMouseEvent *event) 0851 { 0852 QPoint p = event->pos(); 0853 0854 if (event->buttons() & Qt::LeftButton) { 0855 if (m_tracking != snapPoint(p)) { 0856 m_tracking = snapPoint(p); 0857 0858 if (m_dragging) { 0859 if (m_dragPointIndex.first) { 0860 m_points[m_dragPointIndex.second] = m_tracking; 0861 constructPainterPath(); 0862 } else { 0863 m_activePoints[m_dragPointIndex.second] = m_tracking; 0864 } 0865 } else if (m_toolMode == Rectangle || m_toolMode == Ellipse) { 0866 m_rubberBand = QRectF(m_start, m_tracking).normalized(); 0867 } 0868 0869 update(); 0870 } 0871 } else { 0872 if (node(toSymbol(p))) { 0873 setCursor(Qt::SizeAllCursor); 0874 } else { 0875 setCursor(Qt::ArrowCursor); 0876 } 0877 } 0878 0879 constructGuides(toSymbol(p)); 0880 0881 update(); 0882 } 0883 0884 0885 /** 0886 * Process the mouse release event. 0887 * If dragging a point that is in the m_points list and it hasn't been dragged back to 0888 * its start position create a MovePointCommand and push it onto the editor undo stack. 0889 * Pushing commands onto the stack will execute it, although the point is already in its 0890 * new position, this will have no adverse effects, but will record the move for possible 0891 * undo/redo later. 0892 * If not dragging a point, clear the rubber band rectangle and add a point if the new 0893 * point is not the same as the starting point. 0894 * 0895 * @param event a pointer to a QMouseEvent 0896 */ 0897 void Editor::mouseReleaseEvent(QMouseEvent *event) 0898 { 0899 QPointF p = snapPoint(event->pos()); 0900 0901 if (m_dragging) { 0902 if ((p != m_start) && (m_dragPointIndex.first)) { 0903 m_undoStack.push(new MovePointCommand(this, m_dragPointIndex.second, m_start, p)); 0904 } 0905 0906 m_dragging = false; 0907 } else if (m_toolMode == Rectangle || m_toolMode == Ellipse) { 0908 m_rubberBand = QRectF(); 0909 0910 if (p != m_start) { 0911 addPoint(p); 0912 } 0913 } 0914 0915 update(); 0916 } 0917 0918 0919 /** 0920 * Add a point for the current tool. 0921 * Each tool requires a number of points to be defined to fully describe the shape the 0922 * tool represents. This function adds the points and keeps track of the number of points 0923 * required. When the number of points required have been selected, the tool command can 0924 * be implemented and added to the symbol undo stack. 0925 * The points are in symbol coordinates in the range 0..1 0926 * 0927 * @param point a const reference to a QPointF 0928 */ 0929 void Editor::addPoint(const QPointF &point) 0930 { 0931 m_activePoints.append(point); 0932 0933 if (m_activePoints.count() == pointsRequired[m_toolMode]) { 0934 switch (m_toolMode) { 0935 case MoveTo: 0936 m_undoStack.push(new MoveToCommand(this, m_activePoints.at(0))); 0937 break; 0938 0939 case LineTo: 0940 m_undoStack.push(new LineToCommand(this, m_activePoints.at(0))); 0941 break; 0942 0943 case CubicTo: 0944 m_undoStack.push(new CubicToCommand(this, m_activePoints.at(0), m_activePoints.at(1), m_activePoints.at(2))); 0945 break; 0946 0947 case Rectangle: 0948 m_undoStack.push(new RectangleCommand(this, m_activePoints.at(0), m_activePoints.at(1))); 0949 break; 0950 0951 case Ellipse: 0952 m_undoStack.push(new EllipseCommand(this, m_activePoints.at(0), m_activePoints.at(1))); 0953 break; 0954 0955 case Character: // Insertion of characters does not require points 0956 break; 0957 } 0958 0959 m_activePoints.clear(); 0960 } 0961 0962 update(); 0963 updateStatusMessage(); 0964 } 0965 0966 0967 /** 0968 * Update the status message dependent on the command in use and the number of active points 0969 * in the list. 0970 */ 0971 void Editor::updateStatusMessage() 0972 { 0973 emit message(statusMessages[m_toolMode][m_activePoints.count()]); 0974 } 0975 0976 0977 /** 0978 * Read settings and set the element size based on the size of the editor window 0979 * 0980 * @param event a pointer to the QResizeEvent 0981 */ 0982 void Editor::resizeEvent(QResizeEvent *event) 0983 { 0984 QSize requestedSize = event->size(); 0985 0986 int requestedWidth = requestedSize.width(); 0987 int requestedHeight = requestedSize.height(); 0988 int side = std::min(requestedWidth, requestedHeight); 0989 0990 m_elementSize = side / m_gridElements; 0991 m_size = m_elementSize * m_gridElements; 0992 0993 resize(m_size, m_size); 0994 move((requestedWidth - m_size) / 2, (requestedHeight - m_size) / 2); 0995 } 0996 0997 0998 /** 0999 * Paint the contents of the editor. 1000 * This will fill the background and draw a grid based on the elementSize and elementGroup. 1001 * For each element of the current path the control points are drawn with suitable lines 1002 * joining them, for example for a cubic curve, a curve is drawn, but the control points 1003 * are joined with dashed lines. 1004 * A complete path is constructed and painted in a light color with transparency to show 1005 * the current symbol shape. 1006 * 1007 * @param event a pointer to a QPaintEvent 1008 */ 1009 void Editor::paintEvent(QPaintEvent *event) 1010 { 1011 // initialise the painter 1012 QPainter p(this); 1013 p.setRenderHint(QPainter::Antialiasing, true); 1014 p.fillRect(event->rect(), Qt::white); 1015 1016 QPen lightGray(Qt::lightGray); 1017 lightGray.setCosmetic(true); 1018 1019 QPen darkGray(Qt::darkGray); 1020 darkGray.setCosmetic(true); 1021 1022 // scale the painter to suit the number of elements in the grid 1023 p.setWindow(0, 0, m_gridElements, m_gridElements); 1024 1025 // draw vertical grid 1026 for (int x = 0 ; x <= m_gridElements ; ++x) { 1027 if (x % m_elementGrouping) { 1028 p.setPen(lightGray); 1029 } else { 1030 p.setPen(darkGray); 1031 } 1032 1033 p.drawLine(x, 0, x, m_gridElements); 1034 } 1035 1036 // draw horizontal grid 1037 for (int y = 0 ; y <= m_gridElements ; ++y) { 1038 if (y % m_elementGrouping) { 1039 p.setPen(lightGray); 1040 } else { 1041 p.setPen(darkGray); 1042 } 1043 1044 p.drawLine(0, y, m_gridElements, y); 1045 } 1046 1047 // draw a rectangle representing the preferred symbol size allowing for some white space 1048 QRectF preferredSizeRect = QRectF(0, 0, m_gridElements, m_gridElements).adjusted(m_borderSize, m_borderSize, -m_borderSize, -m_borderSize); 1049 1050 QColor preferredSizeColor(m_preferredSizeColor); 1051 preferredSizeColor.setAlpha(128); 1052 1053 QPen preferredSizeColorPen(preferredSizeColor); 1054 preferredSizeColorPen.setCosmetic(true); 1055 1056 p.setPen(preferredSizeColorPen); 1057 p.setBrush(Qt::NoBrush); 1058 p.drawRect(preferredSizeRect); 1059 1060 // define a rectangle for the points 1061 QRectF dot(0, 0, m_pointSize, m_pointSize); 1062 1063 // create a dashed pen for use with curve references and a wide pen for the shape 1064 QPen dashedPen(Qt::DashLine); 1065 dashedPen.setCosmetic(true); 1066 1067 // draw all the points as a circle 1068 p.setBrush(Qt::SolidPattern); 1069 1070 // scale the painter to draw the points and symbols 1071 p.setWindow(0, 0, 1, 1); 1072 1073 for (int i = 0 ; i < m_points.count() ; ++i) { 1074 QPointF s = m_points.at(i); 1075 dot.moveCenter(s); 1076 p.drawEllipse(dot); 1077 } 1078 1079 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 1080 QPointF s = m_activePoints.at(i); 1081 dot.moveCenter(s); 1082 p.drawEllipse(dot); 1083 } 1084 1085 // iterate through the elements and for each curve element draw the reference lines with a dashed pen 1086 for (int i = 0, j = 0 ; i < m_elements.count() ; ++i) { 1087 QPainterPath::ElementType element = m_elements[i]; 1088 QPointF s; 1089 QPointF e; 1090 QPointF c1; 1091 QPointF c2; 1092 1093 switch (element) { 1094 case QPainterPath::MoveToElement: 1095 // increment to next pointer 1096 ++j; 1097 break; 1098 1099 case QPainterPath::LineToElement: 1100 // increment to next pointer 1101 j++; 1102 break; 1103 1104 case QPainterPath::CurveToElement: 1105 p.setPen(dashedPen); 1106 s = m_points.at(j - 1); 1107 c1 = m_points.at(j++); 1108 c2 = m_points.at(j++); 1109 e = m_points.at(j++); 1110 p.drawLine(s, c1); 1111 p.drawLine(c1, c2); 1112 p.drawLine(c2, e); 1113 break; 1114 1115 case QPainterPath::CurveToDataElement: // this is only used within the constructed QPainterPath 1116 break; 1117 } 1118 } 1119 1120 1121 // draw the rubber band rectangle or the active points for the current command 1122 if (m_rubberBand.isValid()) { 1123 QPen blackPen(Qt::black); 1124 blackPen.setCosmetic(true); 1125 p.setPen(blackPen); 1126 p.setBrush(Qt::NoBrush); 1127 1128 if (m_toolMode == Rectangle) { 1129 p.drawRect(m_rubberBand); 1130 } else { 1131 p.drawEllipse(m_rubberBand); 1132 } 1133 } else if (m_activePoints.count() && m_toolMode != Rectangle && m_toolMode != Ellipse) { 1134 p.setPen(dashedPen); 1135 QPointF s; 1136 1137 if (m_points.count()) { 1138 s = m_points.last(); 1139 } 1140 1141 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 1142 QPointF e = m_activePoints[i]; 1143 p.drawLine(s, e); 1144 s = e; 1145 } 1146 } 1147 1148 // scale the painter and draw the path 1149 QColor c(Qt::black); 1150 c.setAlpha(128); 1151 QPen pathPen(m_symbol.pen()); 1152 pathPen.setColor(c); 1153 p.setPen(pathPen); 1154 1155 QBrush pathFill(m_symbol.brush()); 1156 pathFill.setColor(c); 1157 p.setBrush(pathFill); 1158 1159 p.drawPath(m_painterPath); 1160 1161 // draw the guidelines 1162 QColor guideLineColor(m_guideLineColor); 1163 guideLineColor.setAlpha(128); 1164 1165 QPen guideLinePen(guideLineColor); 1166 guideLinePen.setCosmetic(true); 1167 1168 p.setPen(guideLinePen); 1169 p.setBrush(Qt::NoBrush); 1170 QRectF snapRect(0, 0, 0.03, 0.03); 1171 1172 foreach (const QPointF & snapPoint, m_snapPoints) { 1173 snapRect.moveCenter(snapPoint); 1174 p.drawRect(snapRect); 1175 } 1176 1177 foreach (const QLineF & guideLine, m_guideLines) { 1178 p.drawLine(guideLine); 1179 } 1180 1181 foreach (qreal guideCircle, m_guideCircles) { 1182 p.drawEllipse(QPointF(0.5, 0.5), guideCircle, guideCircle); 1183 } 1184 } 1185 1186 1187 /** 1188 * Process key presses to check for Escape to clear the current points being entered. 1189 * 1190 * @param event a pointer to a QKeyEvent 1191 */ 1192 void Editor::keyPressEvent(QKeyEvent *event) 1193 { 1194 switch (event->key()) { 1195 case Qt::Key_Escape: 1196 m_activePoints.clear(); 1197 update(); 1198 break; 1199 1200 default: 1201 QWidget::keyPressEvent(event); 1202 break; 1203 } 1204 } 1205 1206 1207 /** 1208 * Convert a point to a symbol point. 1209 * 1210 * The point will have come from the mouse events pos() value and will be converted to 1211 * a value representing a point in the symbol, not necessarily at a snap position. 1212 * 1213 * @param point a const reference to a QPoint 1214 * 1215 * @return a QPointF representing the point in symbol coordinates between 0..1 1216 */ 1217 QPointF Editor::toSymbol(const QPoint &point) const 1218 { 1219 double sx = static_cast<double>(point.x()) / m_size; 1220 double sy = static_cast<double>(point.y()) / m_size; 1221 return QPointF(sx, sy); 1222 } 1223 1224 1225 /** 1226 * Convert a point to a snap point. 1227 * 1228 * The point comes from the mouse events pos() value and will be converted to a value 1229 * in symbol coordinates. If snapping is enabled, the point will be snapped to a guide 1230 * or to a grid intersection. If snapping is disabled, the point is returned converted 1231 * to the symbol coordinates. 1232 * 1233 * @param point a const reference to a QPoint 1234 * 1235 * @return a QPointF representing the symbol coordinates either snapped or not. 1236 */ 1237 QPointF Editor::snapPoint(const QPoint &point) const 1238 { 1239 QPair<bool, QPointF> snap = snapToGuide(toSymbol(point)); 1240 1241 if (!snap.first) { 1242 snap = snapToGrid(point); 1243 } 1244 1245 return snap.second; 1246 } 1247 1248 1249 /** 1250 * Convert a point to a symbol snap point. 1251 * The point will have come from the mouse events pos() value and will be converted to 1252 * a value that represents an intersection of the grid lines. This will happen only if 1253 * snapping is enabled. 1254 * 1255 * @param point a const reference to a QPoint 1256 * 1257 * @return a QPair<bool, QPointF> the bool element determines if a snap point was found represented by the QPointF element 1258 */ 1259 QPair<bool, QPointF> Editor::snapToGrid(const QPoint &point) const 1260 { 1261 QPair<bool, QPointF> snap(false, toSymbol(point)); 1262 1263 if (m_snap) { 1264 double sx = round(static_cast<double>(point.x()) * m_gridElements / (m_size)) / m_gridElements; 1265 double sy = round(static_cast<double>(point.y()) * m_gridElements / (m_size)) / m_gridElements; 1266 snap.first = true; 1267 snap.second = QPointF(sx, sy); 1268 } 1269 1270 return snap; 1271 } 1272 1273 1274 /** 1275 * Convert a point to a guide snap point. 1276 * The point will have come from the mouse events pos() value and will be converted to 1277 * a value that is within the threshold of a calculated guide intersection. This will 1278 * happen only if the snapping is enabled. 1279 * 1280 * @param point a const reference to a QPointF 1281 * 1282 * @return a QPair<bool, QPointF> the bool element determines if a snap point was found represented by the QPointF element 1283 */ 1284 QPair<bool, QPointF> Editor::snapToGuide(const QPointF &point) const 1285 { 1286 QPair<bool, QPointF> snap(false, point); 1287 1288 if (m_snap) { 1289 foreach (const QPointF & p, m_snapPoints) { 1290 if ((point - p).manhattanLength() < m_snapThreshold) { 1291 snap.first = true; 1292 snap.second = p; 1293 break; 1294 } 1295 } 1296 } 1297 1298 return snap; 1299 } 1300 1301 1302 /** 1303 * Test if a control point is at the point specified. 1304 * This will test both the committed m_points and the uncommitted m_activePoints 1305 * 1306 * @param point a const reference to a QPointF representing the symbol coordinate 1307 * 1308 * @return true if a node exists here, false otherwise 1309 */ 1310 bool Editor::node(const QPointF &point) const 1311 { 1312 bool found = false; 1313 1314 for (int i = 0 ; i < m_points.count() ; ++i) { 1315 QPointF distance = point - m_points[i]; 1316 1317 if (distance.manhattanLength() < m_snapThreshold) { 1318 found = true; 1319 } 1320 } 1321 1322 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 1323 QPointF distance = point - m_activePoints[i]; 1324 1325 if (distance.manhattanLength() < m_snapThreshold) { 1326 found = true; 1327 } 1328 } 1329 1330 return found; 1331 } 1332 1333 1334 /** 1335 * Find the index of the node that is at the point specified. 1336 * This will test both the committed m_points and the uncommitted m_activePoints 1337 * 1338 * @param point a const reference to a QPointF representing the point to find 1339 * 1340 * @return a QPair<bool, int> representing the list and the index of the node, true if committed, false otherwise 1341 * -1 is returned as the index if the node was not found 1342 */ 1343 QPair<bool, int> Editor::nodeUnderCursor(const QPointF &point) const 1344 { 1345 for (int i = 0 ; i < m_points.count() ; ++i) { 1346 QPointF distance = point - m_points[i]; 1347 1348 if (distance.manhattanLength() < m_snapThreshold) { 1349 return QPair<bool, int>(true, i); 1350 } 1351 } 1352 1353 for (int i = 0 ; i < m_activePoints.count() ; ++i) { 1354 QPointF distance = point - m_activePoints[i]; 1355 1356 if (distance.manhattanLength() < m_snapThreshold) { 1357 return QPair<bool, int>(false, i); 1358 } 1359 } 1360 1361 return QPair<bool, int>(false, -1); 1362 } 1363 1364 1365 /** 1366 * Convert a QPainterPath to commands and points. 1367 * Clear the current lists of points. 1368 * Iterate all the elements of the QPainterPath adding commands and points as necessary. 1369 */ 1370 void Editor::deconstructPainterPath() 1371 { 1372 m_points.clear(); 1373 m_activePoints.clear(); 1374 m_elements.clear(); 1375 1376 for (int i = 0 ; i < m_painterPath.elementCount() ; ++i) { 1377 QPainterPath::Element e = m_painterPath.elementAt(i); 1378 1379 switch (e.type) { 1380 case QPainterPath::MoveToElement: 1381 m_elements.append(e.type); 1382 m_points.append(QPointF(e)); 1383 break; 1384 1385 case QPainterPath::LineToElement: 1386 m_elements.append(e.type); 1387 m_points.append(QPointF(e)); 1388 break; 1389 1390 case QPainterPath::CurveToElement: 1391 m_elements.append(e.type); 1392 m_points.append(QPointF(e)); 1393 break; 1394 1395 case QPainterPath::CurveToDataElement: 1396 m_points.append(QPointF(e)); 1397 break; 1398 } 1399 } 1400 } 1401 1402 1403 /** 1404 * Construct a QPainterPath from the commands and points. 1405 * Initialize an empty QPainterPath. 1406 * Iterate all elements stored in m_elements and add them to the path. 1407 */ 1408 void Editor::constructPainterPath() 1409 { 1410 QPainterPath path;; 1411 1412 for (int i = 0, j = 0 ; i < m_elements.count() ; ++i) { 1413 QPainterPath::ElementType e = m_elements[i]; 1414 1415 switch (e) { 1416 case QPainterPath::MoveToElement: 1417 path.moveTo(m_points[j++]); 1418 break; 1419 1420 case QPainterPath::LineToElement: 1421 path.lineTo(m_points[j++]); 1422 break; 1423 1424 case QPainterPath::CurveToElement: 1425 path.cubicTo(m_points[j], m_points[j + 1], m_points[j + 2]); 1426 j += 3; 1427 break; 1428 1429 case QPainterPath::CurveToDataElement: // this only appears within the QPainterPath 1430 break; 1431 } 1432 } 1433 1434 path.setFillRule(m_painterPath.fillRule()); 1435 m_painterPath = path; 1436 } 1437 1438 1439 /** 1440 * Construct guides for the cursor position point being tested relative to the other points. 1441 * Iterate through points in m_points and m_active points projecting them out in the directions 1442 * being tested to determine if they intersect any existing guide lines at a point near to the 1443 * cursor position. If found they are added to the list of relevant lines and the snap point of the 1444 * intersection is added to snap points. 1445 * A list of circles is created where the radius to the point is similar to the test point. Intersection 1446 * points are calculated for the projected lines and if these are close to the cursor position they 1447 * are added to the snap points along with the circles and lines. 1448 * If the generation of guide lines is turned off, the existing guides are cleared on no more are created. 1449 * 1450 * @param to a const reference to a QPointF representing the cursor position 1451 */ 1452 void Editor::constructGuides(const QPointF &to) 1453 { 1454 m_guideLines.clear(); 1455 m_guideCircles.clear(); 1456 m_snapPoints.clear(); 1457 1458 if (!m_guides) { 1459 return; 1460 } 1461 1462 QList<QPointF> points; 1463 points << m_points << m_activePoints; // construct list of all points on screen 1464 1465 QList<double> guideCircles; // local list of circles, used for filtering 1466 QList<QLineF> guideLines; // local list of lines, used for filtering 1467 1468 double radiusTo = sqrt(pow(0.5 - to.x(), 2) + pow(0.5 - to.y(), 2)); 1469 1470 foreach (const QPointF &from, points) { 1471 if ((from - to).manhattanLength() < m_snapThreshold * 2) { 1472 continue; // cursor close to existing point so ignore it 1473 } 1474 1475 double radiusFrom = sqrt(pow(0.5 - from.x(), 2) + pow(0.5 - from.y(), 2)); 1476 1477 if (fabs(radiusTo - radiusFrom) < m_snapThreshold) { 1478 guideCircles.append(radiusFrom); 1479 } 1480 1481 // construct line guides 1482 QLineF guideLine(from, from + QPointF(1.0, 0.0)); 1483 1484 foreach (double angle, m_angles) { 1485 guideLine.setAngle(angle); 1486 QLineF projectedGuideLine = projected(guideLine); 1487 1488 QPointF intersection; 1489 1490 foreach (const QLineF &line, guideLines) { 1491 if (projectedGuideLine.intersect(line, &intersection)) { 1492 if (((to - intersection).manhattanLength() < m_snapThreshold)) { 1493 addGuideLine(line); 1494 addGuideLine(projectedGuideLine); 1495 addSnapPoint(intersection); 1496 } 1497 } 1498 } 1499 1500 guideLines.append(projectedGuideLine); 1501 } 1502 1503 // construct circle guides 1504 foreach (double radius, guideCircles) { 1505 foreach (const QLineF &line, guideLines) { 1506 double ax = line.x1(); 1507 double ay = line.y1(); 1508 double bx = line.x2(); 1509 double by = line.y2(); 1510 double cx = 0.5; 1511 double cy = 0.5; 1512 1513 double lab = sqrt(pow(bx - ax, 2.0) + pow(by - ay, 2.0)); 1514 double dx = (bx - ax) / lab; 1515 double dy = (by - ay) / lab; 1516 1517 double t = dx * (cx - ax) + dy * (cy - ay); 1518 1519 double ex = t * dx + ax; 1520 double ey = t * dy + ay; 1521 1522 double lec = sqrt(pow(ex - cx, 2.0) + pow(ey - cy, 2.0)); 1523 1524 if (lec < radius) { 1525 double dt = sqrt(pow(radius, 2.0) - pow(lec, 2.0)); 1526 1527 // first intersection 1528 double fx = (t - dt) * dx + ax; 1529 double fy = (t - dt) * dy + ay; 1530 1531 if ((QPointF(fx, fy) - to).manhattanLength() < m_snapThreshold) { 1532 addGuideCircle(radius); 1533 addGuideLine(line); 1534 addSnapPoint(QPointF(fx, fy)); 1535 } 1536 1537 // second intersection 1538 double gx = (t + dt) * dx + ax; 1539 double gy = (t + dt) * dy + ay; 1540 1541 if ((QPointF(gx, gy) - to).manhattanLength() < m_snapThreshold) { 1542 addGuideCircle(radius); 1543 addGuideLine(line); 1544 addSnapPoint(QPointF(gx, gy)); 1545 } 1546 } else if (lec == radius) { 1547 // intersection is ex, ey 1548 if ((QPointF(ex, ey) - to).manhattanLength() < m_snapThreshold) { 1549 addGuideCircle(radius); 1550 addGuideLine(line); 1551 addSnapPoint(QPointF(ex, ey)); 1552 } 1553 } 1554 } 1555 } 1556 } 1557 } 1558 1559 1560 /** 1561 * Add a guide line to m_guideLines after checking it doesn't exist yet 1562 * 1563 * @param line a const reference to a QLineF representing the reference line 1564 */ 1565 void Editor::addGuideLine(const QLineF &line) 1566 { 1567 if (!m_guideLines.contains(line)) { 1568 m_guideLines.append(line); 1569 } 1570 } 1571 1572 1573 /** 1574 * Add a guide circle to m_guideCircles after checking it doesn't exist yet 1575 * 1576 * @param radius a double representing the circle at center (0.5, 0.5) 1577 */ 1578 void Editor::addGuideCircle(double radius) 1579 { 1580 if (!m_guideCircles.contains(radius)) { 1581 m_guideCircles.append(radius); 1582 } 1583 } 1584 1585 1586 /** 1587 * Add a snap point to m_snapPoints after checking it doesn't exist yet 1588 * 1589 * @param point a const reference to a QPointF representing the snap position 1590 */ 1591 void Editor::addSnapPoint(const QPointF &point) 1592 { 1593 if (!m_snapPoints.contains(point)) { 1594 m_snapPoints.append(point); 1595 } 1596 } 1597 1598 1599 /** 1600 * Project the line to the edges of the grid. 1601 * Calculate the points where the projected line would intersect the edges. 1602 * Check which edges get intersected within the coordinate 0..1 which will determine 1603 * which points are required to construct the projected line. 1604 * 1605 * @param line a const reference to a QLineF 1606 * 1607 * @return a QLineF representing the projected line 1608 */ 1609 QLineF Editor::projected(const QLineF &line) const 1610 { 1611 QPointF intersectTop; 1612 QPointF intersectBottom; 1613 QPointF intersectLeft; 1614 QPointF intersectRight; 1615 1616 QLineF::IntersectType t = line.intersect(m_topEdge, &intersectTop);; 1617 line.intersect(m_bottomEdge, &intersectBottom); 1618 line.intersect(m_leftEdge, &intersectLeft); 1619 line.intersect(m_rightEdge, &intersectRight); 1620 1621 if (t == QLineF::NoIntersection) { // horizontal line 1622 return QLineF(intersectLeft, intersectRight); 1623 } 1624 1625 return QLineF(intersectTop, intersectBottom); 1626 }