Adjust line tool for isometric 'snap to'

This commit is contained in:
Liebranca 2025-02-19 14:57:31 -03:00
parent 156cf0cfcf
commit 4d239f089f
6 changed files with 162 additions and 62 deletions

View File

@ -28,39 +28,77 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid,
const PreferSnapTo prefer)
{
// Convert point to grid space
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) / 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);
// Substract this from original point (also in grid space)
// to obtain newPoint as an offset within the first grid cell
const gfx::PointF diff((point - grid.origin()) - newPoint);
// We now find the closest corner to that offset
const gfx::PointF candidates[] = { gfx::PointF(grid.w * 0.5, 0),
gfx::PointF(grid.w * 0.5, grid.h),
gfx::PointF(0, grid.h * 0.5),
gfx::PointF(grid.w, grid.h * 0.5) };
gfx::PointF near(grid.origin());
double dist = (grid.w + grid.h) * 2;
for (const auto& c : candidates) {
gfx::PointF vto = diff - c;
if (vto.x < 0)
vto.x = -vto.x;
if (vto.y < 0)
vto.y = -vto.y;
const double newDist = vto.x + vto.y;
if (newDist < dist) {
near = c;
dist = newDist;
}
// The following happens here:
//
// /\ /\
// /A \/B \
// \ /\ /
// \/ \/
// /\ /\
// /C \/D \
//
// Only the origin for diamonds (A,B,C,D) can be found by dividing
// the original point by grid size.
//
// 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;
{
// We use the pixel-precise grid for this bounds-check
const auto& line = doc::Grid(grid).getIsometricLinePoints();
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& p = line[index];
outside = !(p.x <= co.x) || !(co.x < grid.w - p.x) || !(grid.h - p.y <= co.y) || !(co.y < p.y);
}
// TODO: translate the use of the 'prefer' argument from
// the orthogonal logic to this function
// 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) };
switch (prefer) {
case PreferSnapTo::ClosestGridVertex:
if (ABS(vto.x) > ABS(vto.y))
near = (vto.x < 0 ? candidates[3] : candidates[2]);
else
near = (vto.y < 0 ? candidates[1] : candidates[0]);
break;
// Convert cell offset to pixel space
newPoint += near + grid.origin();
return gfx::Point(std::round(newPoint.x), std::round(newPoint.y));
// Pick topmost corner
case PreferSnapTo::FloorGrid:
case PreferSnapTo::BoxOrigin:
if (outside) {
near = (vto.x < 0 ? candidates[3] : candidates[2]);
near.y -= (vto.y > 0 ? grid.h : 0);
}
else {
near = candidates[0];
}
break;
// Pick bottom-most corner
case PreferSnapTo::CeilGrid:
case PreferSnapTo::BoxEnd:
if (outside) {
near = (vto.x < 0 ? candidates[3] : candidates[2]);
near.y += (vto.y < 0 ? grid.h : 0);
}
else {
near = candidates[1];
}
break;
}
// Convert offset back to pixel space
return gfx::Point(newPoint + near + grid.origin());
}
gfx::Point snap_to_grid(const gfx::Rect& grid, const gfx::Point& point, const PreferSnapTo prefer)

View File

@ -17,6 +17,46 @@ namespace app { namespace tools {
using namespace gfx;
// Adjustment for snap to isometric grid
static void snap_isometric_line(ToolLoop* loop, Stroke& stroke)
{
// Get line angle
PointF vto(stroke[1].x - stroke[0].x, stroke[1].y - stroke[0].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.
// 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;
}
// Diagonal lines for width-to-height ratios greater than 1:1
else if (grid.w / float(grid.h)) {
// 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;
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);
}
}
// Shared logic between controllers that can move/displace all points
// using the space bar.
class MoveOriginCapability : public Controller {
@ -197,6 +237,9 @@ 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);
}
if (hasAngle()) {
int rx = stroke[1].x - m_center.x;

View File

@ -145,8 +145,10 @@ doc::AlgoLineWithAlgoPixel Intertwine::getLineAlgo(ToolLoop* loop,
if ( // When "Snap Angle" in being used or...
(int(loop->getModifiers()) & int(ToolLoopModifiers::kSquareAspect)) ||
// "Snap to Grid" is enabled
(loop->getController()->canSnapToGrid() && loop->getSnapToGrid())) {
// "Snap to Grid" is enabled...
(loop->getController()->canSnapToGrid() && loop->getSnapToGrid() &&
// And we are not in isometric mode
loop->sprite()->gridType() != doc::Grid::Type::Isometric)) {
// We prefer the perfect pixel lines that matches grid tiles
return (needsFixForLineBrush ? algo_line_perfect_with_fix_for_line_brush : algo_line_perfect);
}

View File

@ -1213,12 +1213,12 @@ void Editor::drawGrid(Graphics* g,
Point b(std::round(grid.w * 0.5 * pix.w), dy);
// Get length and direction of line (a, b)
const Point vto = b - a;
const Point ivto = Point(-vto.x, vto.y);
Point vto = b - a;
Point ivto = Point(-vto.x, vto.y);
const double lenF = sqrt(vto.x * vto.x + vto.y * vto.y);
// Now displace point (b) to upper edge of canvas
b = a + Point(std::round(dx * 0.5), std::round(-dy * 0.5));
// Now displace point (b) to right edge of canvas
b = a + PointF(dx * 0.5, -dy * 0.5);
// Offset line (a, b) by screen coords
a += Point(x1, y1);
@ -1232,29 +1232,30 @@ void Editor::drawGrid(Graphics* g,
// Calculate how much we need to stretch
// line (a, b) to cover the whole canvas
const int len = int(std::round(left.x / lenF)) + 1 + int(grid.x > 0) + int(grid.y > 0);
const double len = std::round(left.x / lenF) + std::round(dx / lenF) + 1 * int(grid.x > 0) +
1 * int(grid.y > 0) + 2;
vto.x = std::round(vto.x * len);
vto.y = std::round(vto.y * len);
ivto.x = std::round(ivto.x * len);
ivto.y = std::round(ivto.y * len);
// Move these two points across the screen in
// cell-sized steps to draw the entire grid
for (int y = y1; y < y2; y += dy) {
g->drawLine(grid_color, a, a + vto * len);
g->drawLine(grid_color, a + left, (a + left) + ivto * len);
g->drawLine(grid_color, Point(a), Point(a + vto));
g->drawLine(grid_color, Point(a + left), Point((a + left) + ivto));
a.y += dy;
}
for (int x = x1; x < x2; x += dx) {
g->drawLine(grid_color, b, b + vto * len);
g->drawLine(grid_color, b, b + ivto * len);
g->drawLine(grid_color, Point(b), Point(b + vto));
g->drawLine(grid_color, Point(b), Point(b + ivto));
b.x += dx;
}
}
}
}
static void push_line_pixel(const int x, const int y, std::vector<Point>* const data)
{
data->push_back(Point(x, y));
};
gfx::Path& Editor::getIsometricGridPath(Rect& grid)
{
static Path path;
@ -1273,24 +1274,10 @@ gfx::Path& Editor::getIsometricGridPath(Rect& grid)
if (!im)
return path;
// Prepare bitmap
// Prepare bitmap from points of pixel precise line.
// A single grid cell is calculated from these
im->clear(0x00);
const Point a(0, std::round(grid.h * 0.5));
const Point b(std::floor(grid.w * 0.5), grid.h);
std::vector<Point> line;
// 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,
&line,
(doc::AlgoPixel)&push_line_pixel);
// Iterating on said points, we fill in the bitmap
// for a single cell grid
for (auto p : line)
for (const auto& p : getSite().grid().getIsometricLinePoints())
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

@ -10,6 +10,7 @@
#include "doc/grid.h"
#include "doc/algo.h"
#include "doc/image.h"
#include "doc/image_ref.h"
#include "doc/primitives.h"
@ -177,4 +178,28 @@ std::vector<gfx::Point> Grid::tilesInCanvasRegion(const gfx::Region& rgn) const
return result;
}
static void push_isometric_line_point(int x, int y, std::vector<gfx::Point>* data)
{
if (data->empty() || data->back().y != y) {
data->push_back(gfx::Point(x, y));
}
};
std::vector<gfx::Point> Grid::getIsometricLinePoints(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);
// 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,
&result,
(doc::AlgoPixel)&push_isometric_line_point);
return result;
}
} // namespace doc

View File

@ -20,6 +20,7 @@ public:
enum class Type {
Orthogonal = 0x00,
Isometric = 0x01,
};
explicit Grid(const gfx::Size& sz = gfx::Size(16, 16))
: m_tileSize(sz)
@ -83,6 +84,10 @@ public:
// Returns an array of tile positions that are touching the given region in the canvas
std::vector<gfx::Point> tilesInCanvasRegion(const gfx::Region& rgn) const;
// Returns an array of coordinates used for calculating the
// pixel-precise bounds of an isometric grid cell
std::vector<gfx::Point> getIsometricLinePoints(void) const;
private:
gfx::Size m_tileSize;
gfx::Point m_origin;