diff --git a/src/app/snap_to_grid.cpp b/src/app/snap_to_grid.cpp index 7e9b86262..7ae0346e6 100644 --- a/src/app/snap_to_grid.cpp +++ b/src/app/snap_to_grid.cpp @@ -27,11 +27,40 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, const gfx::Point& point, const PreferSnapTo prefer) { + // Because we force unworkable grid sizes to share a pixel, + // we need to account for that here + auto guide = doc::Grid(grid).getIsometricLinePoints(); + const int width = guide[2].x; + int height = guide[2].y; + + if (ABS(grid.w - grid.h) > 1) { + const bool x_share = (guide[1].x & 1) != 0 && (grid.w & 1) == 0; + const bool y_share = ((guide[0].y & 1) == 0 || (grid.w & 1) == 0) && (grid.h & 1) != 0; + const bool y_undiv = ((grid.h / 2) & 1) != 0; + const bool y_uneven = (grid.w & 1) != 0 && (grid.h & 1) == 0; + const bool y_skip = !x_share && !y_undiv && !y_uneven && (grid.w & 1) != 0 && (grid.h & 1) != 0; + if (x_share) { + guide[1].x++; + } + if (y_share && !y_skip) { + guide[0].y--; + } + else { + if (y_undiv) { + height++; + } + if (y_uneven) { + guide[0].y++; + guide[1].x += int((grid.w & 1) == 0); + } + } + } + // Convert point to grid space const gfx::PointF newPoint(int((point.x - grid.x) / double(grid.w)) * grid.w, int((point.y - grid.y) / double(grid.h)) * grid.h); // And then make it relative to the center of a cell - const gfx::PointF vto((newPoint + grid.center()) - point); + const gfx::PointF vto((newPoint + gfx::Point(guide[1].x, guide[0].y)) - point); // The following happens here: // @@ -48,23 +77,24 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, // In order to snap to a position relative to the "in-between" diamonds, // we need to determine whether the cell coords are outside the // bounds of the current grid cell. - bool outside; - { + bool outside = false; + + if (prefer != PreferSnapTo::ClosestGridVertex) { // We use the pixel-precise grid for this bounds-check - const auto& line = doc::Grid(grid).getIsometricLinePoints(); + const auto& line = doc::Grid(grid).getIsometricLine(); const int index = int(ABS(vto.y) - int(vto.y > 0)) + 1; - const gfx::Point co(-vto.x + int(grid.w / 2), -vto.y + int(grid.h / 2)); + const gfx::Point co(-vto.x + guide[1].x, -vto.y + guide[0].y); const gfx::Point& p = line[index]; - outside = !(p.x <= co.x) || !(co.x < grid.w - p.x) || !(grid.h - p.y <= co.y) || !(co.y < p.y); + outside = !(p.x <= co.x) || !(co.x < width - p.x) || !(height - p.y <= co.y) || !(co.y < p.y); } // Find which of the four corners of the current diamond // should be picked gfx::Point near(0, 0); - const gfx::Point candidates[] = { gfx::Point(grid.w / 2, 0), - gfx::Point(grid.w / 2, grid.h), - gfx::Point(0, grid.h / 2), - gfx::Point(grid.w, grid.h / 2) }; + const gfx::Point candidates[] = { gfx::Point(guide[1].x, 0), + gfx::Point(guide[1].x, height), + gfx::Point(0, guide[0].y), + gfx::Point(width, guide[0].y) }; switch (prefer) { case PreferSnapTo::ClosestGridVertex: if (ABS(vto.x) > ABS(vto.y)) @@ -78,7 +108,7 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, case PreferSnapTo::BoxOrigin: if (outside) { near = (vto.x < 0 ? candidates[3] : candidates[2]); - near.y -= (vto.y > 0 ? grid.h : 0); + near.y -= (vto.y > 0 ? height : 0); } else { near = candidates[0]; @@ -90,7 +120,7 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, case PreferSnapTo::BoxEnd: if (outside) { near = (vto.x < 0 ? candidates[3] : candidates[2]); - near.y += (vto.y < 0 ? grid.h : 0); + near.y += (vto.y < 0 ? height : 0); } else { near = candidates[1]; diff --git a/src/app/tools/controllers.h b/src/app/tools/controllers.h index f2b6ba638..2b9bf934d 100644 --- a/src/app/tools/controllers.h +++ b/src/app/tools/controllers.h @@ -18,42 +18,70 @@ namespace app { namespace tools { using namespace gfx; // Adjustment for snap to isometric grid -static void snap_isometric_line(ToolLoop* loop, Stroke& stroke) +static void snap_isometric_line(ToolLoop* loop, Stroke& stroke, bool lineCtl) { + // Get last two points + Stroke::Pt& a = stroke[stroke.size() - 2]; + Stroke::Pt& b = stroke[stroke.size() - 1]; + + // Get function invoked by line tool + bool lineTool = (string_id_to_brush_type(loop->getTool()->getId()) == kLineBrushType); + + // TODO: rectangles and ellipses + if (lineCtl && !loop->getIntertwine()->snapByAngle()) + return; + // Get line angle - PointF vto(stroke[1].x - stroke[0].x, stroke[1].y - stroke[0].y); + PointF vto(b.x - a.x, b.y - a.y); double len = ABS(vto.x) + ABS(vto.y); vto /= len; - // Skip on single point - if (std::isnan(vto.x) && std::isnan(vto.y)) - return; - - // Offset vertical lines one pixel left for line tool. + // Offset vertical lines/single point one pixel left for line tool. // Because pressing the angle snap key will bypass this function, // this makes it so one can selectively apply the offset. - const gfx::Rect& grid = loop->getGridBounds(); - if (int(vto.x) == 0 && int(vto.y) != 0) { - bool lineTool = (string_id_to_brush_type(loop->getTool()->getId()) == kLineBrushType); - stroke[0].x -= lineTool; - stroke[1].x -= lineTool; + if ((std::isnan(vto.x) && std::isnan(vto.y)) || (int(vto.x) == 0 && int(vto.y) != 0)) { + a.x -= lineTool; + b.x -= lineTool; } - // Diagonal lines for width-to-height ratios greater than 1:1 - else if (grid.w / float(grid.h)) { + // Diagonal lines + else { // Skip horizontal or cross-cell diagonal lines - PointF normal(grid.w * 0.5, grid.h * 0.5); - normal /= normal.x + normal.y; - const double eps = 0.15; + const auto& line = loop->getGrid().getIsometricLinePoints(); + PointF normal(line[1].x, line[0].y); + normal /= ABS(normal.x) + ABS(normal.y); + const double eps = 0.05; if (ABS(vto.x) < normal.x - eps || ABS(vto.x) > normal.x + eps || ABS(vto.y) < normal.y - eps || ABS(vto.y) > normal.y + eps) return; - // Adjust line start/end point based on direction - stroke[0].y += (!(grid.h & 1) ? vto.y < 0 : 0); - Point delta(std::round(SGN(vto.x) * normal.x * len), std::round(SGN(vto.y) * normal.y * len)); - stroke[1].x = stroke[0].x + delta.x; - stroke[1].y = stroke[0].y + delta.y; - stroke[1].y += (!(grid.h & 1) ? SGN(vto.y) : 0); + // Adjust start/end point based on line direction and grid size + const gfx::Rect& grid = loop->getGridBounds(); + const bool x_even = (grid.w & 1) == 0 && ((grid.w / 2) & 1) == 0; + const bool y_even = (grid.h & 1) == 0 && ((grid.h / 2) & 1) == 0; + const bool stretch = (line[1].x & 1) != 0 && (grid.w & 1) == 0; + const bool square = ABS(grid.w - grid.h) <= 1; + + if (vto.x < 0) { + if (square && x_even && y_even) + b.y -= SGN(vto.y); + + a.x -= ((y_even || stretch) ? 1 : -1) * int(x_even); + b.x += 1 * int(x_even && !y_even && !stretch); + } + else { + if (square && x_even && y_even) { + b.x--; + b.y -= SGN(vto.y); + } + b.x -= int(int(y_even) * int(x_even) == 0); + } + + if (vto.y < 0) { + if (square && x_even && y_even) { + a.y--; + b.y--; + } + } } } @@ -110,6 +138,11 @@ public: { m_last = pt; stroke.addPoint(pt); + if (loop->getController()->canSnapToGrid() && loop->getSnapToGrid() && + loop->sprite()->gridType() == doc::Grid::Type::Isometric) { + snap_isometric_line(loop, stroke, false); + m_last = stroke[stroke.size() - 1]; + } } void getStrokeToInterwine(const Stroke& input, Stroke& output) override @@ -160,8 +193,17 @@ public: stroke.addPoint(pt); stroke.addPoint(pt); - if (loop->isSelectingTiles()) + if (loop->isSelectingTiles()) { snapPointsToGridTiles(loop, stroke); + } + else if ( + // 'Angle Snap' key not pressed... + !(int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect)) && + + // And snapping to isometric grid + (loop->getSnapToGrid() && loop->sprite()->gridType() == doc::Grid::Type::Isometric)) { + snap_isometric_line(loop, stroke, true); + } } bool releaseButton(Stroke& stroke, const Stroke::Pt& pt) override { return false; } @@ -193,6 +235,8 @@ public: stroke[1] = pt; bool isoAngle = false; + bool isoMode = loop->getController()->canSnapToGrid() && loop->getSnapToGrid() && + loop->sprite()->gridType() == doc::Grid::Type::Isometric; if ((int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect))) { int dx = stroke[1].x - m_first.x; @@ -237,8 +281,8 @@ public: stroke[1].y = m_first.y + SGN(dy) * minsize; } } - else if (loop->getSnapToGrid() && loop->sprite()->gridType() == doc::Grid::Type::Isometric) { - snap_isometric_line(loop, stroke); + else if (isoMode) { + snap_isometric_line(loop, stroke, true); } if (hasAngle()) { @@ -265,7 +309,7 @@ public: if (loop->isSelectingTiles()) { snapPointsToGridTiles(loop, stroke); } - else { + else if (!isoMode) { if (stroke[0].x < stroke[1].x) stroke[1].x -= bounds.w; else if (stroke[0].x > stroke[1].x) diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp index c13e2a1ef..8d858b695 100644 --- a/src/app/ui/editor/editor.cpp +++ b/src/app/ui/editor/editor.cpp @@ -1176,6 +1176,10 @@ void Editor::drawGrid(Graphics* g, int dx = std::round(grid.w * pix.w); int dy = std::round(grid.h * pix.h); + // Diamonds share a side when their size is uneven + dx -= pix.w * (grid.w & 1); + dy -= pix.h * (grid.h & 1); + if (dx < 2) dx = 2; if (dy < 2) @@ -1209,11 +1213,11 @@ void Editor::drawGrid(Graphics* g, // Draw straight isometric line grid else { // Single side of diamond is line (a, b) - Point a(0, std::round(grid.h * 0.5 * pix.h)); - Point b(std::round(grid.w * 0.5 * pix.w), dy); + PointF a(0, dy * 0.5); + PointF b(dx * 0.5, dy); // Get length and direction of line (a, b) - Point vto = b - a; + Point vto = Point(b - a); Point ivto = Point(-vto.x, vto.y); const double lenF = sqrt(vto.x * vto.x + vto.y * vto.y); @@ -1277,7 +1281,7 @@ gfx::Path& Editor::getIsometricGridPath(Rect& grid) // Prepare bitmap from points of pixel precise line. // A single grid cell is calculated from these im->clear(0x00); - for (const auto& p : getSite().grid().getIsometricLinePoints()) + for (const auto& p : getSite().grid().getIsometricLine()) im->fillRect(std::round(p.x * pix.w), std::round((grid.h - p.y) * pix.h), std::floor((grid.w - p.x) * pix.w), diff --git a/src/doc/grid.cpp b/src/doc/grid.cpp index 10322ef47..bea0a9d3b 100644 --- a/src/doc/grid.cpp +++ b/src/doc/grid.cpp @@ -20,6 +20,7 @@ #include "gfx/size.h" #include +#include #include #include #include @@ -185,17 +186,36 @@ static void push_isometric_line_point(int x, int y, std::vector* dat } }; -std::vector Grid::getIsometricLinePoints(void) const +std::array Grid::getIsometricLinePoints() const +{ + int x = 0; + int y = std::round(m_tileSize.h * 0.5); + int dx = m_tileSize.w / 2; + const int dy = m_tileSize.h; + + const bool x_uneven = (m_tileSize.w & 1) != 0 || (dx & 1) != 0; + const bool y_uneven = (m_tileSize.h & 1) != 0 || (y & 1) != 0; + + dx -= int(x_uneven ^ y_uneven); + y -= m_tileSize.w & 1; + x -= m_tileSize.w & 1; + + return { gfx::Point(x, y), + gfx::Point(dx, dy), + gfx::Point(m_tileSize.w - int(x_uneven), m_tileSize.h - int(y_uneven)) }; +} + +std::vector Grid::getIsometricLine(void) const { std::vector result; - const gfx::Point a(0, std::round(m_tileSize.h * 0.5)); - const gfx::Point b(std::floor(m_tileSize.w * 0.5), m_tileSize.h); + const auto pts = getIsometricLinePoints(); + // We use the line drawing algorithm to find the points // for a single pixel-precise line - doc::algo_line_continuous_with_fix_for_line_brush(a.x, - a.y, - b.x, - b.y, + doc::algo_line_continuous_with_fix_for_line_brush(pts[0].x, + pts[0].y, + pts[1].x, + pts[1].y, &result, (doc::AlgoPixel)&push_isometric_line_point); diff --git a/src/doc/grid.h b/src/doc/grid.h index 58962cdf8..217f743e1 100644 --- a/src/doc/grid.h +++ b/src/doc/grid.h @@ -86,7 +86,8 @@ public: // Returns an array of coordinates used for calculating the // pixel-precise bounds of an isometric grid cell - std::vector getIsometricLinePoints(void) const; + std::array getIsometricLinePoints() const; + std::vector getIsometricLine() const; private: gfx::Size m_tileSize;