/** @file A set of methods to render schematic and to convert between viewport, contents and model coordinate systems. @c Schematic object has @e contents and @e viewport –– they are inherited from Q3ScrollView. @e Contents is basically a canvas to draw something on it. Its size is not fixed and may be changed anytime (@ref Q3ScrollView::resizeContents) @e Viewport is the visible part of the contents and at the same time it's the widget you actually see. Contents may be larger than the display and viewport allows us to scroll the contents and to observe parts of it. @e Model is not inhereted from Q3ScrollView and is not represented by any widget, it is actually entirely abstract/logical entity. It describes some abstract space where an "idea" of the schematic lives. All dimensions and sizes in model are @e reference dimensions and sizes. For example, when schematic has to be drawn in double scale (twice as large), model size multiplied by two is the contents size for double-scale schematic. Want to draw schematic in half-scale? Take model size and divide it by two, that would be the contents size for half-scale schematic. In other words contents is the @em projection of model and there is a connection between them: @code * = @endcode Because of this connection between model and contents every point on contents has a counterpart in model and vise-versa. Contents coordinates originate at top-left corner and grow to the right and downwards. Model coordinates also originate at top-left corner, but while contents top-left is always (0,0), model's top-left is not fixed and changes as user scrolls or occupies space with schematic elements. Think about model as of a rectangle somewhere in cartesian coordinate system. There is also viewport coordinates; they are relative to top left corner of the viewport widget. When you click on schematic, position of the click may be described in - viewport coordinates (relative to top left corner of what you see) - contents coordinates (relative to top left corner of contents, which may not be visible at them moment) - model coordinates There is a set of methods to convert between types of coordinates. The core of drawing a schematic –– or @e rendering –– is @ref Schematic::renderModel method. Use it when you need to show display changes applied to schematic, like: - schematic should be drawn in another scale - model size has changed because the view has been scrolled See method description for example usage. */ #include "mouseactions.h" #include "qucsdoc.h" #include "qucs.h" #include "schematic.h" QRect Schematic::modelRect() { return QRect{a_ViewX1, a_ViewY1, a_ViewX2 - a_ViewX1, a_ViewY2 - a_ViewY1}; } QRect Schematic::viewportRect() { return QRect{0, 0, viewport()->width(), viewport()->height()}; } QPoint Schematic::modelToViewport(const QPoint& modelCoordinates) { return modelToContents(modelCoordinates) - QPoint{ contentsX(), contentsY() }; } QPoint Schematic::viewportToModel(const QPoint& viewportCoordinates) { return contentsToModel(QPoint{ contentsX(), contentsY() } + viewportCoordinates); } QPoint Schematic::contentsToModel(const QPoint& coordinates) { // Sizes in the model and contents are interconnected and obey the rule: // * = // // Contents is a rectangle with (0, 0) at its top-left corner. Model plane // is rectangular area of abstract infinite plane, so model plane's top-left // corner may have any coordinates. // // To transform coordinates of a point on the contents to coordinates // of corresponding point on model plane: // 1. Adjust "contents" coordinates so that they become having the same scale // the model plane has // 2. Adjust resulting coordinates so they become absolute coordinates // in model plane // QPoint overrides operator /. It divides both coordinates on given value QPoint modelCoords = coordinates / a_Scale; modelCoords.setX(a_ViewX1 + modelCoords.x()); modelCoords.setY(a_ViewY1 + modelCoords.y()); return modelCoords; } QPoint Schematic::modelToContents(const QPoint& coordinates) { // Model and contents sizes are interconnected and obey the rule: // * = // // Contents is a rectangle with (0, 0) at its top-left corner. Model plane // is rectangular area of abstract infinite plane, so model plane's top-left // corner may have any coordinates. // // To transform coordinates of a point on the model plane to coordinates // of corresponding point on the contents: // 1. Adjust coordinates so that they become relative to model planes' // top-left corner // 2. Adjust resulting coordinates so thay they become having the same scale // as contents QPoint contentsCoords{coordinates.x() - a_ViewX1, coordinates.y() - a_ViewY1}; contentsCoords *= a_Scale; return contentsCoords; } double Schematic::clipScale(double offeredScale) { if (offeredScale > a_maxScale) { return a_maxScale; } else if (offeredScale < a_minScale) { return a_minScale; } else { return offeredScale; } } bool Schematic::shouldRender(const double& newScale, const QRect& newModelBounds, const QPoint& toBeDisplayed, const QPoint& viewportCoords) { const QRect currentModelBounds = modelRect(); // This point currently displayed at "viewportCoords" of the viewport const QPoint currenlyDisplayed = viewportToModel(viewportCoords); return a_Scale != newScale || toBeDisplayed != currenlyDisplayed || currentModelBounds != newModelBounds; } double Schematic::renderModel(const double offeredScale, QRect newModel, const QPoint modelPoint, const QPoint viewportPoint) { // DO NOT alter model bounds or scale and DO NOT call resizeContens() outside // of this method. It may break the state and lead to hard-to-find bugs. // Pass the desired model bounds or scale as the argument to this method. assert(modelPoint.x() >= newModel.left() && modelPoint.x() <= newModel.right()); assert(modelPoint.y() >= newModel.top() && modelPoint.y() <= newModel.bottom()); assert(viewportPoint.x() >= 0 && viewportPoint.x() < viewport()->width()); assert(viewportPoint.y() >= 0 && viewportPoint.y() < viewport()->height()); // Maybe there is no need to do anything const double newScale = clipScale(offeredScale); if (!shouldRender(newScale, newModel, modelPoint, viewportPoint)) { return a_Scale; } // The part below is quite tricky: while working at the model plane scale, // we construct a "viewport" rectangle and position it so that it contains the // area of the model, which should be displayed in the real viewport at the // end. We do this because the "should-be-displayed" area might go beyond // the model plane bounds, in which case the model plane size would have to // be adjusted to include this area. // // Remember that * = QSize viewportSizeOnModelPlane = viewportRect().size() / newScale; QPoint vpTopLeftOnModelPlane{ modelPoint.x() - static_cast(std::round(viewportPoint.x() / newScale)), modelPoint.y() - static_cast(std::round(viewportPoint.y() / newScale)) }; QRect viewportOnModelPlane{vpTopLeftOnModelPlane, viewportSizeOnModelPlane}; newModel |= viewportOnModelPlane; // At this point everything is ready for rendering and positioning // Set new model size a_ViewX1 = newModel.left(); a_ViewY1 = newModel.top(); a_ViewX2 = newModel.left() + newModel.width(); a_ViewY2 = newModel.top() + newModel.height(); a_Scale = newScale; resizeContents(static_cast(std::round(newModel.width() * a_Scale)), static_cast(std::round(newModel.height() * a_Scale))); auto contentTopLeft = modelToContents(vpTopLeftOnModelPlane); setContentsPos(contentTopLeft.x(), contentTopLeft.y()); viewport()->update(); return a_Scale; }