Fix isometric 'snap to' for odd grid sizes

This commit is contained in:
Liebranca 2025-02-19 16:42:52 -03:00
parent 4d239f089f
commit 2322cd02ee
5 changed files with 150 additions and 51 deletions

View File

@ -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];

View File

@ -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)

View File

@ -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),

View File

@ -20,6 +20,7 @@
#include "gfx/size.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <vector>
@ -185,17 +186,36 @@ static void push_isometric_line_point(int x, int y, std::vector<gfx::Point>* dat
}
};
std::vector<gfx::Point> Grid::getIsometricLinePoints(void) const
std::array<gfx::Point, 3> 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<gfx::Point> Grid::getIsometricLine(void) const
{
std::vector<gfx::Point> 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);

View File

@ -86,7 +86,8 @@ public:
// Returns an array of coordinates used for calculating the
// pixel-precise bounds of an isometric grid cell
std::vector<gfx::Point> getIsometricLinePoints(void) const;
std::array<gfx::Point, 3> getIsometricLinePoints() const;
std::vector<gfx::Point> getIsometricLine() const;
private:
gfx::Size m_tileSize;