mirror of https://github.com/aseprite/aseprite.git
Revise implementation of isometric 'Snap To'
This commit is contained in:
parent
2322cd02ee
commit
f612e48966
|
@ -140,8 +140,7 @@ void GridSettingsCommand::onExecute(Context* context)
|
||||||
bounds.h = std::max(bounds.h, 1);
|
bounds.h = std::max(bounds.h, 1);
|
||||||
|
|
||||||
typestr = window.gridType()->getEntryWidget()->text();
|
typestr = window.gridType()->getEntryWidget()->text();
|
||||||
type = (typestr == app::Strings::grid_settings_type_isometric() ?
|
type = (typestr == app::Strings::grid_settings_type_isometric() ? doc::Grid::Type::Isometric :
|
||||||
doc::Grid::Type::Isometric :
|
|
||||||
doc::Grid::Type::Orthogonal);
|
doc::Grid::Type::Orthogonal);
|
||||||
|
|
||||||
ContextWriter writer(context);
|
ContextWriter writer(context);
|
||||||
|
|
|
@ -29,38 +29,15 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid,
|
||||||
{
|
{
|
||||||
// Because we force unworkable grid sizes to share a pixel,
|
// Because we force unworkable grid sizes to share a pixel,
|
||||||
// we need to account for that here
|
// we need to account for that here
|
||||||
auto guide = doc::Grid(grid).getIsometricLinePoints();
|
auto guide = doc::Grid::IsometricGuide(grid.size());
|
||||||
const int width = guide[2].x;
|
const int width = grid.w - int(!guide.evenWidth);
|
||||||
int height = guide[2].y;
|
const int height = grid.h - int(!guide.evenHeight);
|
||||||
|
|
||||||
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
|
// Convert point to grid space
|
||||||
const gfx::PointF newPoint(int((point.x - grid.x) / double(grid.w)) * grid.w,
|
const gfx::PointF newPoint(int((point.x - grid.x) / width) * width,
|
||||||
int((point.y - grid.y) / double(grid.h)) * grid.h);
|
int((point.y - grid.y) / height) * height);
|
||||||
// And then make it relative to the center of a cell
|
// 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:
|
// The following happens here:
|
||||||
//
|
//
|
||||||
|
@ -81,9 +58,9 @@ gfx::Point snap_to_isometric_grid(const gfx::Rect& grid,
|
||||||
|
|
||||||
if (prefer != PreferSnapTo::ClosestGridVertex) {
|
if (prefer != PreferSnapTo::ClosestGridVertex) {
|
||||||
// We use the pixel-precise grid for this bounds-check
|
// 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 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];
|
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);
|
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
|
// Find which of the four corners of the current diamond
|
||||||
// should be picked
|
// should be picked
|
||||||
gfx::Point near(0, 0);
|
gfx::Point near(0, 0);
|
||||||
const gfx::Point candidates[] = { gfx::Point(guide[1].x, 0),
|
const int offsetEvenX = (!guide.squareRatio ? int(guide.evenWidth) : 0);
|
||||||
gfx::Point(guide[1].x, height),
|
const int offsetOddY = (!guide.squareRatio ? int(!guide.shareEdges || !guide.evenHeight) :
|
||||||
gfx::Point(0, guide[0].y),
|
int(!guide.evenHeight));
|
||||||
gfx::Point(width, guide[0].y) };
|
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) {
|
switch (prefer) {
|
||||||
case PreferSnapTo::ClosestGridVertex:
|
case PreferSnapTo::ClosestGridVertex:
|
||||||
if (ABS(vto.x) > ABS(vto.y))
|
if (ABS(vto.x) > ABS(vto.y))
|
||||||
|
|
|
@ -36,51 +36,71 @@ static void snap_isometric_line(ToolLoop* loop, Stroke& stroke, bool lineCtl)
|
||||||
double len = ABS(vto.x) + ABS(vto.y);
|
double len = ABS(vto.x) + ABS(vto.y);
|
||||||
vto /= len;
|
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,
|
// Because pressing the angle snap key will bypass this function,
|
||||||
// this makes it so one can selectively apply the offset.
|
// 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)) {
|
if ((std::isnan(vto.x) && std::isnan(vto.y)) || (int(vto.x) == 0 && int(vto.y) != 0)) {
|
||||||
a.x -= lineTool;
|
const int step = 1 + (line.oddSize * int(!line.squareRatio));
|
||||||
b.x -= lineTool;
|
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
|
// Diagonal lines
|
||||||
else {
|
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
|
// Adjust start/end point based on line direction and grid size
|
||||||
const gfx::Rect& grid = loop->getGridBounds();
|
if (!line.squareRatio) {
|
||||||
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 (vto.x < 0) {
|
||||||
if (square && x_even && y_even)
|
a.x -= line.evenWidth;
|
||||||
b.y -= SGN(vto.y);
|
b.x -= 2 * line.oddSize;
|
||||||
|
|
||||||
a.x -= ((y_even || stretch) ? 1 : -1) * int(x_even);
|
|
||||||
b.x += 1 * int(x_even && !y_even && !stretch);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (square && x_even && y_even) {
|
a.x -= 2 * line.oddSize;
|
||||||
b.x--;
|
b.x -= line.evenWidth;
|
||||||
b.y -= SGN(vto.y);
|
|
||||||
}
|
|
||||||
b.x -= int(int(y_even) * int(x_even) == 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vto.y < 0) {
|
// Unticking 'share borders' adds one pixel of distance between edges
|
||||||
if (square && x_even && y_even) {
|
if (!line.shareEdges) {
|
||||||
|
if (vto.y < 0)
|
||||||
a.y--;
|
a.y--;
|
||||||
|
else
|
||||||
b.y--;
|
b.y--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (vto.x < 0) {
|
||||||
|
a.x -= line.evenWidth;
|
||||||
|
b.y -= SGN(vto.y) * line.evenHeight;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
b.x -= line.evenWidth;
|
||||||
|
b.y -= SGN(vto.y) * line.evenHeight;
|
||||||
|
}
|
||||||
|
if (vto.y < 0) {
|
||||||
|
a.y -= line.evenHeight;
|
||||||
|
b.y -= line.evenHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1176,9 +1176,11 @@ void Editor::drawGrid(Graphics* g,
|
||||||
int dx = std::round(grid.w * pix.w);
|
int dx = std::round(grid.w * pix.w);
|
||||||
int dy = std::round(grid.h * pix.h);
|
int dy = std::round(grid.h * pix.h);
|
||||||
|
|
||||||
|
auto guide = doc::Grid::IsometricGuide(grid.size());
|
||||||
|
|
||||||
// Diamonds share a side when their size is uneven
|
// Diamonds share a side when their size is uneven
|
||||||
dx -= pix.w * (grid.w & 1);
|
dx -= pix.w * int(!guide.evenWidth);
|
||||||
dy -= pix.h * (grid.h & 1);
|
dy -= pix.h * int(!guide.evenHeight);
|
||||||
|
|
||||||
if (dx < 2)
|
if (dx < 2)
|
||||||
dx = 2;
|
dx = 2;
|
||||||
|
@ -1218,7 +1220,7 @@ void Editor::drawGrid(Graphics* g,
|
||||||
|
|
||||||
// Get length and direction of line (a, b)
|
// Get length and direction of line (a, b)
|
||||||
Point vto = Point(b - a);
|
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);
|
const double lenF = sqrt(vto.x * vto.x + vto.y * vto.y);
|
||||||
|
|
||||||
// Now displace point (b) to right edge of canvas
|
// 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
|
// Calculate how much we need to stretch
|
||||||
// line (a, b) to cover the whole canvas
|
// 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) +
|
const double len = (x2 - x1) + (y2 - y1);
|
||||||
1 * int(grid.y > 0) + 2;
|
|
||||||
|
|
||||||
vto.x = std::round(vto.x * len);
|
vto.x = std::round(vto.x * len);
|
||||||
vto.y = std::round(vto.y * len);
|
vto.y = std::round(vto.y * len);
|
||||||
ivto.x = std::round(ivto.x * 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.
|
// Prepare bitmap from points of pixel precise line.
|
||||||
// A single grid cell is calculated from these
|
// A single grid cell is calculated from these
|
||||||
im->clear(0x00);
|
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),
|
im->fillRect(std::round(p.x * pix.w),
|
||||||
std::round((grid.h - p.y) * pix.h),
|
std::round((grid.h - p.y) * pix.h),
|
||||||
std::floor((grid.w - p.x) * pix.w),
|
std::floor((grid.w - p.x) * pix.w),
|
||||||
|
|
|
@ -179,43 +179,56 @@ std::vector<gfx::Point> Grid::tilesInCanvasRegion(const gfx::Region& rgn) const
|
||||||
return result;
|
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<gfx::Point>* data)
|
static void push_isometric_line_point(int x, int y, std::vector<gfx::Point>* data)
|
||||||
{
|
{
|
||||||
if (data->empty() || data->back().y != y) {
|
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<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> Grid::getIsometricLine(const gfx::Size& sz)
|
||||||
{
|
{
|
||||||
std::vector<gfx::Point> result;
|
std::vector<gfx::Point> result;
|
||||||
const auto pts = getIsometricLinePoints();
|
const auto guide = IsometricGuide(sz);
|
||||||
|
|
||||||
// We use the line drawing algorithm to find the points
|
// We use the line drawing algorithm to find the points
|
||||||
// for a single pixel-precise line
|
// for a single pixel-precise line
|
||||||
doc::algo_line_continuous_with_fix_for_line_brush(pts[0].x,
|
doc::algo_line_continuous_with_fix_for_line_brush(guide.start.x,
|
||||||
pts[0].y,
|
guide.start.y,
|
||||||
pts[1].x,
|
guide.end.x,
|
||||||
pts[1].y,
|
guide.end.y,
|
||||||
&result,
|
&result,
|
||||||
(doc::AlgoPixel)&push_isometric_line_point);
|
(doc::AlgoPixel)&push_isometric_line_point);
|
||||||
|
|
||||||
|
|
|
@ -84,10 +84,31 @@ public:
|
||||||
// Returns an array of tile positions that are touching the given region in the canvas
|
// 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;
|
std::vector<gfx::Point> 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
|
// Returns an array of coordinates used for calculating the
|
||||||
// pixel-precise bounds of an isometric grid cell
|
// pixel-precise bounds of an isometric grid cell
|
||||||
std::array<gfx::Point, 3> getIsometricLinePoints() const;
|
static std::vector<gfx::Point> getIsometricLine(const gfx::Size& sz);
|
||||||
std::vector<gfx::Point> getIsometricLine() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
gfx::Size m_tileSize;
|
gfx::Size m_tileSize;
|
||||||
|
|
Loading…
Reference in New Issue