From f612e48966e02247a1fa20d19c8e4a54c9c66122 Mon Sep 17 00:00:00 2001 From: Liebranca Date: Wed, 19 Feb 2025 17:50:50 -0300 Subject: [PATCH] Revise implementation of isometric 'Snap To' --- src/app/commands/cmd_grid.cpp | 5 +-- src/app/snap_to_grid.cpp | 51 +++++++-------------- src/app/tools/controllers.h | 84 ++++++++++++++++++++++------------- src/app/ui/editor/editor.cpp | 14 +++--- src/doc/grid.cpp | 65 ++++++++++++++++----------- src/doc/grid.h | 25 ++++++++++- 6 files changed, 139 insertions(+), 105 deletions(-) diff --git a/src/app/commands/cmd_grid.cpp b/src/app/commands/cmd_grid.cpp index 032006b88..60be5274e 100644 --- a/src/app/commands/cmd_grid.cpp +++ b/src/app/commands/cmd_grid.cpp @@ -140,9 +140,8 @@ void GridSettingsCommand::onExecute(Context* context) bounds.h = std::max(bounds.h, 1); typestr = window.gridType()->getEntryWidget()->text(); - type = (typestr == app::Strings::grid_settings_type_isometric() ? - doc::Grid::Type::Isometric : - doc::Grid::Type::Orthogonal); + type = (typestr == app::Strings::grid_settings_type_isometric() ? doc::Grid::Type::Isometric : + doc::Grid::Type::Orthogonal); ContextWriter writer(context); Tx tx(writer, friendlyName(), ModifyDocument); diff --git a/src/app/snap_to_grid.cpp b/src/app/snap_to_grid.cpp index 7ae0346e6..b298e07d9 100644 --- a/src/app/snap_to_grid.cpp +++ b/src/app/snap_to_grid.cpp @@ -29,38 +29,15 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, { // 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); - } - } - } + auto guide = doc::Grid::IsometricGuide(grid.size()); + const int width = grid.w - int(!guide.evenWidth); + const int height = grid.h - int(!guide.evenHeight); // 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); + const gfx::PointF newPoint(int((point.x - grid.x) / width) * width, + int((point.y - grid.y) / height) * height); // And then make it relative to the center of a cell - const gfx::PointF vto((newPoint + gfx::Point(guide[1].x, guide[0].y)) - point); + const gfx::PointF vto((newPoint + gfx::Point(guide.end.x, guide.start.y)) - point); // The following happens here: // @@ -81,9 +58,9 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, if (prefer != PreferSnapTo::ClosestGridVertex) { // We use the pixel-precise grid for this bounds-check - const auto& line = doc::Grid(grid).getIsometricLine(); + const auto line = doc::Grid::getIsometricLine(grid.size()); const int index = int(ABS(vto.y) - int(vto.y > 0)) + 1; - const gfx::Point co(-vto.x + guide[1].x, -vto.y + guide[0].y); + const gfx::Point co(-vto.x + guide.end.x, -vto.y + guide.start.y); const gfx::Point& p = line[index]; outside = !(p.x <= co.x) || !(co.x < width - p.x) || !(height - p.y <= co.y) || !(co.y < p.y); } @@ -91,10 +68,14 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid, // Find which of the four corners of the current diamond // should be picked gfx::Point near(0, 0); - 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) }; + const int offsetEvenX = (!guide.squareRatio ? int(guide.evenWidth) : 0); + const int offsetOddY = (!guide.squareRatio ? int(!guide.shareEdges || !guide.evenHeight) : + int(!guide.evenHeight)); + const int offsetOddX = (!guide.squareRatio ? int(guide.oddSize) : 0); + const gfx::Point candidates[] = { gfx::Point(guide.end.x + offsetEvenX, 0), + gfx::Point(guide.end.x + offsetEvenX, height), + gfx::Point(offsetOddX, guide.start.y - offsetOddY), + gfx::Point(width + offsetOddX, guide.start.y - offsetOddY) }; switch (prefer) { case PreferSnapTo::ClosestGridVertex: if (ABS(vto.x) > ABS(vto.y)) diff --git a/src/app/tools/controllers.h b/src/app/tools/controllers.h index 2b9bf934d..c2d4bc3d6 100644 --- a/src/app/tools/controllers.h +++ b/src/app/tools/controllers.h @@ -36,50 +36,70 @@ static void snap_isometric_line(ToolLoop* loop, Stroke& stroke, bool lineCtl) double len = ABS(vto.x) + ABS(vto.y); vto /= len; - // Offset vertical lines/single point one pixel left for line tool. + const gfx::Rect& grid = loop->getGridBounds(); + const auto line = doc::Grid::IsometricGuide(grid.size()); + + // Offset vertical lines/single point to the left for line tool. // Because pressing the angle snap key will bypass this function, // this makes it so one can selectively apply the offset. if ((std::isnan(vto.x) && std::isnan(vto.y)) || (int(vto.x) == 0 && int(vto.y) != 0)) { - a.x -= lineTool; - b.x -= lineTool; + const int step = 1 + (line.oddSize * int(!line.squareRatio)); + a.x -= step * int(lineTool); + b.x -= step * int(lineTool); + } + // Horizontal lines + else if (int(vto.y) == 0 && int(vto.x) != 0) { + if (vto.x > 0) + b.x--; + else + a.x--; } // Diagonal lines else { - // Skip horizontal or cross-cell diagonal lines - 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 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 (!line.squareRatio) { + if (vto.x < 0) { + a.x -= line.evenWidth; + b.x -= 2 * line.oddSize; + } + else { + a.x -= 2 * line.oddSize; + b.x -= line.evenWidth; + } - if (vto.x < 0) { - if (square && x_even && y_even) - b.y -= SGN(vto.y); + // Unticking 'share borders' adds one pixel of distance between edges + if (!line.shareEdges) { + if (vto.y < 0) + a.y--; + else + b.y--; + } - a.x -= ((y_even || stretch) ? 1 : -1) * int(x_even); - b.x += 1 * int(x_even && !y_even && !stretch); + // Some line angles do not intertwine in the exact same way + // when the order of the two points is inverted, so we try to + // detect this edge case and flip the points. + // + // TODO: this fix only works for two-point lines. Support + // for freehand strokes would require changes to intertwiners, + // not just the freehand controller itself. + if (lineTool && vto.x < 0 && a.x % (grid.w - !line.evenWidth)) { + auto tmp = a; + a = b; + b = tmp; + } } else { - if (square && x_even && y_even) { - b.x--; - b.y -= SGN(vto.y); + if (vto.x < 0) { + a.x -= line.evenWidth; + b.y -= SGN(vto.y) * line.evenHeight; } - b.x -= int(int(y_even) * int(x_even) == 0); - } - - if (vto.y < 0) { - if (square && x_even && y_even) { - a.y--; - b.y--; + else { + b.x -= line.evenWidth; + b.y -= SGN(vto.y) * line.evenHeight; + } + if (vto.y < 0) { + a.y -= line.evenHeight; + b.y -= line.evenHeight; } } } diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp index 8d858b695..8679a3a04 100644 --- a/src/app/ui/editor/editor.cpp +++ b/src/app/ui/editor/editor.cpp @@ -1176,9 +1176,11 @@ void Editor::drawGrid(Graphics* g, int dx = std::round(grid.w * pix.w); int dy = std::round(grid.h * pix.h); + auto guide = doc::Grid::IsometricGuide(grid.size()); + // Diamonds share a side when their size is uneven - dx -= pix.w * (grid.w & 1); - dy -= pix.h * (grid.h & 1); + dx -= pix.w * int(!guide.evenWidth); + dy -= pix.h * int(!guide.evenHeight); if (dx < 2) dx = 2; @@ -1218,7 +1220,7 @@ void Editor::drawGrid(Graphics* g, // Get length and direction of line (a, b) Point vto = Point(b - a); - Point ivto = Point(-vto.x, vto.y); + PointF ivto = PointF(-vto.x, vto.y); const double lenF = sqrt(vto.x * vto.x + vto.y * vto.y); // Now displace point (b) to right edge of canvas @@ -1236,9 +1238,7 @@ void Editor::drawGrid(Graphics* g, // Calculate how much we need to stretch // line (a, b) to cover the whole canvas - const double len = std::round(left.x / lenF) + std::round(dx / lenF) + 1 * int(grid.x > 0) + - 1 * int(grid.y > 0) + 2; - + const double len = (x2 - x1) + (y2 - y1); vto.x = std::round(vto.x * len); vto.y = std::round(vto.y * len); ivto.x = std::round(ivto.x * len); @@ -1281,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().getIsometricLine()) + for (const auto p : doc::Grid::getIsometricLine(grid.size())) 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 bea0a9d3b..8c0298e63 100644 --- a/src/doc/grid.cpp +++ b/src/doc/grid.cpp @@ -179,43 +179,56 @@ std::vector Grid::tilesInCanvasRegion(const gfx::Region& rgn) const return result; } +Grid::IsometricGuide::IsometricGuide(const gfx::Size& sz) +{ + evenWidth = sz.w % 2 == 0; + evenHeight = sz.h % 2 == 0; + evenHalfWidth = ((sz.w / 2) % 2) == 0; + evenHalfHeight = ((sz.h / 2) % 2) == 0; + squareRatio = ABS(sz.w - sz.h) <= 1; + oddSize = !evenWidth && evenHalfWidth && !evenHeight && evenHalfHeight; + + // TODO: add 'share edges' checkbox to UI. + // For testing the option, set this 'false' to 'true' + shareEdges = !(false && !squareRatio && evenHeight); + + start.x = 0; + start.y = std::round(sz.h * 0.5); + end.x = sz.w / 2; + end.y = sz.h; + + if (!squareRatio) { + if (evenWidth) { + end.x--; + } + else if (oddSize) { + start.x--; + end.x++; + } + } + if (!shareEdges) { + start.y++; + } +} + static void push_isometric_line_point(int x, int y, std::vector* data) { if (data->empty() || data->back().y != y) { - data->push_back(gfx::Point(x, y)); + data->push_back(gfx::Point(x * int(x >= 0), y)); } -}; - -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 Grid::getIsometricLine(const gfx::Size& sz) { std::vector result; - const auto pts = getIsometricLinePoints(); + const auto guide = IsometricGuide(sz); // 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(pts[0].x, - pts[0].y, - pts[1].x, - pts[1].y, + doc::algo_line_continuous_with_fix_for_line_brush(guide.start.x, + guide.start.y, + guide.end.x, + guide.end.y, &result, (doc::AlgoPixel)&push_isometric_line_point); diff --git a/src/doc/grid.h b/src/doc/grid.h index 217f743e1..1bc4e52b8 100644 --- a/src/doc/grid.h +++ b/src/doc/grid.h @@ -84,10 +84,31 @@ public: // Returns an array of tile positions that are touching the given region in the canvas std::vector tilesInCanvasRegion(const gfx::Region& rgn) const; + // Helper structure for calculating both isometric grid cells + // as well as point snapping + struct IsometricGuide { + gfx::Point start; + gfx::Point end; + + union { + struct { + bool evenWidth : 1; + bool evenHeight : 1; + bool evenHalfWidth : 1; + bool evenHalfHeight : 1; + bool squareRatio : 1; + bool oddSize : 1; + bool shareEdges : 1; + }; + int flags; + }; + + IsometricGuide(const gfx::Size& sz); + }; + // Returns an array of coordinates used for calculating the // pixel-precise bounds of an isometric grid cell - std::array getIsometricLinePoints() const; - std::vector getIsometricLine() const; + static std::vector getIsometricLine(const gfx::Size& sz); private: gfx::Size m_tileSize;