Merge 9cc740bc20f08feedc006b598e5a4c94418fcd7e into c949f4a5a6ca6adb411674583a57551ff99b8e88

This commit is contained in:
دانتي باولا 2025-03-07 19:30:31 +08:00 committed by GitHub
commit edf5e8f7d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 654 additions and 20 deletions

View File

@ -526,6 +526,7 @@
<section id="grid" canforce="true">
<option id="snap" type="bool" default="false" />
<option id="bounds" type="gfx::Rect" default="doc::Sprite::DefaultGridBounds()" />
<option id="type" type="doc::Grid::Type" default="doc::Sprite::DefaultGridType()" />
<option id="color" type="app::Color" default="app::Color::fromRgb(0, 0, 255)" />
<option id="opacity" type="int" default="160" />
<option id="auto_opacity" type="bool" default="true" />

View File

@ -837,6 +837,8 @@ x = X:
y = Y:
width = Width:
height = Height:
type_orthogonal = Orthogonal
type_isometric = Isometric
[home_view]
title = Home
@ -1462,6 +1464,8 @@ grid_x = X:
grid_y = Y:
grid_width = Width:
grid_height = Height:
grid_type_orthogonal = Orthogonal
grid_type_isometric = Isometric
grid_color = Color:
grid_opacity = Opacity:
grid_auto = Auto

View File

@ -15,6 +15,10 @@
<label text="@.height" />
<expr id="grid_h" text="" />
<combobox id="grid_type" editable="true" expansive="true">
<listitem text="@.type_orthogonal" />
<listitem text="@.type_isometric" />
</combobox>
<separator horizontal="true" cell_hspan="4" />

View File

@ -423,6 +423,10 @@
<expr id="grid_w" text="" />
<label text="@.grid_height" />
<expr id="grid_h" text="" />
<combobox id="grid_type" editable="true" expansive="true">
<listitem text="@.grid_type_orthogonal" />
<listitem text="@.grid_type_isometric" />
</combobox>
<hbox />
<label text="@.grid_color" />

View File

@ -321,6 +321,7 @@ target_sources(app-lib PRIVATE
cmd/set_cel_zindex.cpp
cmd/set_frame_duration.cpp
cmd/set_grid_bounds.cpp
cmd/set_grid_type.cpp
cmd/set_last_point.cpp
cmd/set_layer_blend_mode.cpp
cmd/set_layer_flags.cpp

View File

@ -0,0 +1,49 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/cmd/set_grid_type.h"
#include "app/doc.h"
#include "app/pref/preferences.h"
#include "doc/sprite.h"
namespace app { namespace cmd {
using namespace doc;
SetGridType::SetGridType(Sprite* sprite, const doc::Grid::Type type)
: WithSprite(sprite)
, m_oldType(sprite->gridType())
, m_newType(type)
{
}
void SetGridType::onExecute()
{
setGrid(m_newType);
}
void SetGridType::onUndo()
{
setGrid(m_oldType);
}
void SetGridType::setGrid(const doc::Grid::Type type)
{
Sprite* spr = sprite();
spr->setGridType(type);
Doc* doc = static_cast<Doc*>(spr->document());
auto& docPref = Preferences::instance().document(doc);
docPref.grid.type(type);
spr->incrementVersion();
}
}} // namespace app::cmd

View File

@ -0,0 +1,40 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_CMD_SET_GRID_TYPE_H_INCLUDED
#define APP_CMD_SET_GRID_TYPE_H_INCLUDED
#pragma once
#include "app/cmd.h"
#include "app/cmd/with_sprite.h"
#include "doc/grid.h"
namespace doc {
class Sprite;
}
namespace app { namespace cmd {
class SetGridType : public Cmd,
public WithSprite {
public:
SetGridType(doc::Sprite* sprite, doc::Grid::Type type);
protected:
void onExecute() override;
void onUndo() override;
size_t onMemSize() const override { return sizeof(*this); }
private:
void setGrid(doc::Grid::Type type);
doc::Grid::Type m_oldType;
doc::Grid::Type m_newType;
};
}} // namespace app::cmd
#endif

View File

@ -11,11 +11,13 @@
#include "app/app.h"
#include "app/cmd/set_grid_bounds.h"
#include "app/cmd/set_grid_type.h"
#include "app/commands/command.h"
#include "app/context.h"
#include "app/context_access.h"
#include "app/doc.h"
#include "app/find_widget.h"
#include "app/i18n/strings.h"
#include "app/load_widget.h"
#include "app/pref/preferences.h"
#include "app/tx.h"
@ -107,6 +109,10 @@ void GridSettingsCommand::onExecute(Context* context)
Site site = context->activeSite();
Rect bounds = site.gridBounds();
doc::Grid::Type type = site.gridType();
std::string typestr = (type == doc::Grid::Type::Isometric ?
app::Strings::grid_settings_type_isometric() :
app::Strings::grid_settings_type_orthogonal());
window.gridX()->setTextf("%d", bounds.x);
window.gridY()->setTextf("%d", bounds.y);
@ -122,6 +128,7 @@ void GridSettingsCommand::onExecute(Context* context)
if (window.gridH()->textInt() <= 0)
window.gridH()->setText("1");
});
window.gridType()->getEntryWidget()->setText(typestr);
window.openWindowInForeground();
if (window.closer() == window.ok()) {
@ -132,9 +139,15 @@ void GridSettingsCommand::onExecute(Context* context)
bounds.w = std::max(bounds.w, 1);
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);
ContextWriter writer(context);
Tx tx(writer, friendlyName(), ModifyDocument);
tx(new cmd::SetGridBounds(site.sprite(), bounds));
tx(new cmd::SetGridType(site.sprite(), type));
tx.commit();
auto& docPref = Preferences::instance().document(site.document());

View File

@ -194,10 +194,12 @@ void OpenFileCommand::onExecute(Context* context)
// interact with old versions of Aseprite saving the grid
// bounds in the aseprite.ini file)
docPref.grid.bounds(doc->sprite()->gridBounds());
docPref.grid.type(doc->sprite()->gridType());
}
else {
// Get grid bounds from preferences
doc->sprite()->setGridBounds(docPref.grid.bounds());
doc->sprite()->setGridType(docPref.grid.type());
}
}

View File

@ -11,6 +11,7 @@
#include "app/app.h"
#include "app/cmd/set_grid_bounds.h"
#include "app/cmd/set_grid_type.h"
#include "app/commands/command.h"
#include "app/console.h"
#include "app/context.h"
@ -837,6 +838,20 @@ public:
}
}
// Change sprite grid type
if (m_context && m_context->activeDocument() && m_context->activeDocument()->sprite() &&
m_context->activeDocument()->sprite()->gridType() != gridTypeInt()) {
try {
ContextWriter writer(m_context, 1000);
Tx tx(writer, Strings::commands_GridSettings(), ModifyDocument);
tx(new cmd::SetGridType(writer.sprite(), gridTypeInt()));
tx.commit();
}
catch (const std::exception& ex) {
Console::showException(ex);
}
}
m_curPref->show.grid(gridVisible()->isSelected());
m_curPref->grid.bounds(gridBounds());
m_curPref->grid.color(gridColor()->getColor());
@ -1516,6 +1531,12 @@ private:
gridW()->setTextf("%d", m_curPref->grid.bounds().w);
gridH()->setTextf("%d", m_curPref->grid.bounds().h);
doc::Grid::Type type = m_curPref->grid.type();
std::string typestr = (type == doc::Grid::Type::Isometric ?
app::Strings::grid_settings_type_isometric() :
app::Strings::grid_settings_type_orthogonal());
gridType()->getEntryWidget()->setText(typestr);
gridColor()->setColor(m_curPref->grid.color());
gridOpacity()->setValue(m_curPref->grid.opacity());
gridAutoOpacity()->setSelected(m_curPref->grid.autoOpacity());
@ -1560,6 +1581,12 @@ private:
gridW()->setTextf("%d", pref.grid.bounds.defaultValue().w);
gridH()->setTextf("%d", pref.grid.bounds.defaultValue().h);
doc::Grid::Type type = pref.grid.type.defaultValue();
std::string typestr = (type == doc::Grid::Type::Isometric ?
app::Strings::grid_settings_type_isometric() :
app::Strings::grid_settings_type_orthogonal());
gridType()->getEntryWidget()->setText(typestr);
gridColor()->setColor(pref.grid.color.defaultValue());
gridOpacity()->setValue(pref.grid.opacity.defaultValue());
gridAutoOpacity()->setSelected(pref.grid.autoOpacity.defaultValue());
@ -1577,6 +1604,12 @@ private:
gridW()->setTextf("%d", pref.grid.bounds().w);
gridH()->setTextf("%d", pref.grid.bounds().h);
doc::Grid::Type type = pref.grid.type();
std::string typestr = (type == doc::Grid::Type::Isometric ?
app::Strings::grid_settings_type_isometric() :
app::Strings::grid_settings_type_orthogonal());
gridType()->getEntryWidget()->setText(typestr);
gridColor()->setColor(pref.grid.color());
gridOpacity()->setValue(pref.grid.opacity());
gridAutoOpacity()->setSelected(pref.grid.autoOpacity());
@ -2045,6 +2078,15 @@ private:
return gfx::Rect(gridX()->textInt(), gridY()->textInt(), gridW()->textInt(), gridH()->textInt());
}
doc::Grid::Type gridTypeInt() const
{
std::string typestr = gridType()->getEntryWidget()->text();
if (typestr == app::Strings::grid_settings_type_isometric())
return doc::Grid::Type::Isometric;
return doc::Grid::Type::Orthogonal;
}
static std::string userThemeFolder()
{
ResourceFinder rf;

View File

@ -559,6 +559,7 @@ static void ase_file_prepare_header(FILE* f,
header->grid_y = sprite->gridBounds().y;
header->grid_width = sprite->gridBounds().w;
header->grid_height = sprite->gridBounds().h;
header->grid_type = uint16_t(sprite->gridType());
}
static void ase_file_write_header(FILE* f, dio::AsepriteHeader* header)
@ -586,6 +587,7 @@ static void ase_file_write_header(FILE* f, dio::AsepriteHeader* header)
fputw(header->grid_y, f);
fputw(header->grid_width, f);
fputw(header->grid_height, f);
fputw(header->grid_type, f);
fseek(f, header->pos + 128, SEEK_SET);
}

View File

@ -397,6 +397,7 @@ FOR_ENUM(doc::BrushPattern)
FOR_ENUM(doc::ColorMode)
FOR_ENUM(doc::FitCriteria)
FOR_ENUM(doc::RgbMapAlgorithm)
FOR_ENUM(doc::Grid::Type)
FOR_ENUM(filters::HueSaturationFilter::Mode)
FOR_ENUM(filters::TiledMode)
FOR_ENUM(render::OnionskinPosition)

View File

@ -129,6 +129,14 @@ gfx::Rect Site::gridBounds() const
return doc::Sprite::DefaultGridBounds();
}
doc::Grid::Type Site::gridType() const
{
if (m_sprite)
return m_sprite->gridType();
return doc::Sprite::DefaultGridType();
}
bool Site::shouldTrimCel(Cel* cel) const
{
return (cel && cel->layer() && cel->layer()->isTransparent() &&

View File

@ -13,6 +13,7 @@
#include "app/tileset_mode.h"
#include "doc/cel_list.h"
#include "doc/frame.h"
#include "doc/grid.h"
#include "doc/palette_picks.h"
#include "doc/selected_objects.h"
#include "gfx/fwd.h"
@ -99,6 +100,7 @@ public:
doc::Tileset* tileset() const;
doc::Grid grid() const;
gfx::Rect gridBounds() const;
doc::Grid::Type gridType() const;
void tilemapMode(const TilemapMode mode) { m_tilemapMode = mode; }
void tilesetMode(const TilesetMode mode) { m_tilesetMode = mode; }

View File

@ -11,6 +11,11 @@
#include "app/snap_to_grid.h"
#include "app/app.h"
#include "app/context.h"
#include "app/doc.h"
#include "app/pref/preferences.h"
#include "doc/grid.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -18,11 +23,106 @@
namespace app {
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::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) / 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.end.x, guide.start.y)) - point);
// 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 = false;
if (prefer != PreferSnapTo::ClosestGridVertex) {
// We use the pixel-precise grid for this bounds-check
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.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);
}
// Find which of the four corners of the current diamond
// should be picked
gfx::Point near(0, 0);
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))
near = (vto.x < 0 ? candidates[3] : candidates[2]);
else
near = (vto.y < 0 ? candidates[1] : candidates[0]);
break;
// Pick topmost corner
case PreferSnapTo::FloorGrid:
case PreferSnapTo::BoxOrigin:
if (outside) {
near = (vto.x < 0 ? candidates[3] : candidates[2]);
near.y -= (vto.y > 0 ? height : 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 ? height : 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)
{
if (grid.isEmpty())
return point;
// Use different logic for isometric grid
const doc::Grid::Type gridType =
App::instance()->preferences().document(App::instance()->context()->documents()[0]).grid.type();
if (gridType == doc::Grid::Type::Isometric)
return snap_to_isometric_grid(grid, point, prefer);
div_t d, dx, dy;
dx = std::div(grid.x, grid.w);
dy = std::div(grid.y, grid.h);

View File

@ -17,6 +17,94 @@ namespace app { namespace tools {
using namespace gfx;
// Adjustment for snap to isometric grid
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(b.x - a.x, b.y - a.y);
double len = ABS(vto.x) + ABS(vto.y);
vto /= len;
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)) {
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 {
// Adjust start/end point based on line direction and grid size
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;
}
// Unticking 'share borders' adds one pixel of distance between edges
if (!line.shareEdges) {
if (vto.y < 0)
a.y--;
else
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;
}
}
}
}
// Shared logic between controllers that can move/displace all points
// using the space bar.
class MoveOriginCapability : public Controller {
@ -70,6 +158,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
@ -120,8 +213,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; }
@ -153,6 +255,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;
@ -197,6 +301,9 @@ public:
stroke[1].y = m_first.y + SGN(dy) * minsize;
}
}
else if (isoMode) {
snap_isometric_line(loop, stroke, true);
}
if (hasAngle()) {
int rx = stroke[1].x - m_center.x;
@ -222,7 +329,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

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

@ -57,6 +57,8 @@
#include "app/util/tile_flags_utils.h"
#include "base/chrono.h"
#include "base/convert_to.h"
#include "base/pi.h"
#include "doc/algo.h"
#include "doc/doc.h"
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
@ -805,7 +807,7 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g,
alpha = std::clamp(alpha, 0, 255);
}
drawGrid(g, enclosingRect, Rect(0, 0, 1, 1), m_docPref.pixelGrid.color(), alpha);
drawGrid(g, enclosingRect, Rect(0, 0, 1, 1), m_docPref.pixelGrid.color(), alpha, true);
// Save all pixel grid settings that are unset
m_docPref.pixelGrid.forceSection();
@ -828,7 +830,7 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g,
}
if (alpha > 8) {
drawGrid(g, enclosingRect, gridrc, m_docPref.grid.color(), alpha);
drawGrid(g, enclosingRect, gridrc, m_docPref.grid.color(), alpha, false);
}
}
@ -1103,7 +1105,8 @@ void Editor::drawGrid(Graphics* g,
const gfx::Rect& spriteBounds,
const Rect& gridBounds,
const app::Color& color,
int alpha)
int alpha,
bool isPixelGrid)
{
if ((m_flags & kShowGrid) == 0)
return;
@ -1146,21 +1149,156 @@ void Editor::drawGrid(Graphics* g,
grid_color =
gfx::rgba(gfx::getr(grid_color), gfx::getg(grid_color), gfx::getb(grid_color), alpha);
// Draw horizontal lines
int x1 = spriteBounds.x;
int y1 = gridF.y;
int x2 = spriteBounds.x + spriteBounds.w;
int y2 = spriteBounds.y + spriteBounds.h;
// Orthogonal grid
if (isPixelGrid || getSite().sprite()->gridType() == doc::Grid::Type::Orthogonal) {
// Draw horizontal lines
int x1 = spriteBounds.x;
int y1 = gridF.y;
int x2 = spriteBounds.x + spriteBounds.w;
int y2 = spriteBounds.y + spriteBounds.h;
for (double c = y1; c <= y2; c += gridF.h)
g->drawHLine(grid_color, x1, c, spriteBounds.w);
for (double c = y1; c <= y2; c += gridF.h)
g->drawHLine(grid_color, x1, c, spriteBounds.w);
// Draw vertical lines
x1 = gridF.x;
y1 = spriteBounds.y;
// Draw vertical lines
x1 = gridF.x;
y1 = spriteBounds.y;
for (double c = x1; c <= x2; c += gridF.w)
g->drawVLine(grid_color, c, y1, spriteBounds.h);
for (double c = x1; c <= x2; c += gridF.w)
g->drawVLine(grid_color, c, y1, spriteBounds.h);
}
// Isometric grid
else {
const RectF pix(editorToScreenF(RectF(0, 0, 1, 1)));
int x1 = gridF.x;
int y1 = gridF.y;
const int x2 = spriteBounds.x + spriteBounds.w;
const int y2 = spriteBounds.y + spriteBounds.h;
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 * int(!guide.evenWidth);
dy -= pix.h * int(!guide.evenHeight);
if (dx < 2)
dx = 2;
if (dy < 2)
dy = 2;
while (x1 > spriteBounds.x)
x1 -= dx;
while (y1 > spriteBounds.y)
y1 -= dy;
// Draw pixel-precise isometric grid when zoomed in
if (m_proj.zoom().scale() > 6.00) {
ui::Paint paint;
paint.style(ui::Paint::Stroke);
paint.antialias(false);
paint.color(grid_color);
// Move single cell across the screen
// to draw entire grid
Path& cell = getIsometricGridPath(grid);
for (int y = y1; y < y2; y += dy) {
for (int x = x1; x < x2; x += dx) {
cell.offset(x, y);
g->drawPath(cell, paint);
// Restore original position for later use
cell.offset(-x, -y);
}
}
}
// Draw straight isometric line grid
else {
// Single side of diamond is line (a, b)
PointF a(0, dy * 0.5);
PointF b(dx * 0.5, dy);
// Get length and direction of line (a, b)
Point vto = Point(b - a);
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
b = a + PointF(dx * 0.5, -dy * 0.5);
// Offset line (a, b) by screen coords
a += Point(x1, y1);
b += Point(x1, y1);
// Calculate number of diamonds required to
// fill canvas horizontally
Point left(std::round(((x2 - x1) / dx) * dx), 0);
while (left.x < (x2 - x1))
left.x += dx;
// Calculate how much we need to stretch
// line (a, b) to cover the whole canvas
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);
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, 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, Point(b), Point(b + vto));
g->drawLine(grid_color, Point(b), Point(b + ivto));
b.x += dx;
}
}
}
}
gfx::Path& Editor::getIsometricGridPath(Rect& grid)
{
static Path path;
static Size prevSize(0, 0);
static double prevScale = 0.00;
// Regenerate bitmap on zoom or grid size change
if (prevScale != m_proj.zoom().scale() || prevSize != grid.size()) {
const RectF pix(editorToScreenF(RectF(0, 0, 1, 1)));
doc::ImageRef imref(doc::Image::create(doc::PixelFormat::IMAGE_BITMAP,
std::round(grid.w * pix.w),
std::round((grid.h + 1) * pix.h)));
// Return previous path if image generation fails
doc::Image* im = imref.get();
if (!im)
return path;
// Prepare bitmap from points of pixel precise line.
// A single grid cell is calculated from these
im->clear(0x00);
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),
std::floor(p.y * pix.h),
0x01);
doc::MaskBoundaries immask;
immask.regen(im);
immask.createPathIfNeeeded();
path = immask.path();
}
// Remember scale used to generate the current path
prevScale = m_proj.zoom().scale();
prevSize.w = grid.w;
prevSize.h = grid.h;
return path;
}
void Editor::drawSlices(ui::Graphics* g)

View File

@ -366,7 +366,9 @@ private:
const gfx::Rect& spriteBounds,
const gfx::Rect& gridBounds,
const app::Color& color,
int alpha);
int alpha,
bool isPixelGrid);
gfx::Path& getIsometricGridPath(gfx::Rect& grid);
void drawSlices(ui::Graphics* g);
void drawTileNumbers(ui::Graphics* g, const Cel* cel);
void drawCelBounds(ui::Graphics* g, const Cel* cel, const gfx::Color color);

View File

@ -101,6 +101,7 @@ struct AsepriteHeader {
int16_t grid_y;
uint16_t grid_width;
uint16_t grid_height;
uint16_t grid_type;
};
struct AsepriteFrameHeader {

View File

@ -74,6 +74,7 @@ bool AsepriteDecoder::decode()
// Set grid bounds
sprite->setGridBounds(
gfx::Rect(header.grid_x, header.grid_y, header.grid_width, header.grid_height));
sprite->setGridType(static_cast<doc::Grid::Type>(header.grid_type));
sprite->setUuidsForLayers((header.flags & ASE_FILE_FLAG_LAYER_WITH_UUID) ==
ASE_FILE_FLAG_LAYER_WITH_UUID);
@ -339,6 +340,7 @@ bool AsepriteDecoder::readHeader(AsepriteHeader* header)
header->grid_y = (int16_t)read16();
header->grid_width = read16();
header->grid_height = read16();
header->grid_type = read16();
if (header->depth != 8) // Transparent index only valid for indexed images
header->transparent_index = 0;

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"
@ -19,6 +20,7 @@
#include "gfx/size.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <vector>
@ -177,4 +179,60 @@ std::vector<gfx::Point> 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<gfx::Point>* data)
{
if (data->empty() || data->back().y != y) {
data->push_back(gfx::Point(x * int(x >= 0), y));
}
}
std::vector<gfx::Point> Grid::getIsometricLine(const gfx::Size& sz)
{
std::vector<gfx::Point> result;
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(guide.start.x,
guide.start.y,
guide.end.x,
guide.end.y,
&result,
(doc::AlgoPixel)&push_isometric_line_point);
return result;
}
} // namespace doc

View File

@ -17,6 +17,11 @@ namespace doc {
class Grid {
public:
enum class Type {
Orthogonal = 0x00,
Isometric = 0x01,
};
explicit Grid(const gfx::Size& sz = gfx::Size(16, 16))
: m_tileSize(sz)
, m_origin(0, 0)
@ -79,6 +84,32 @@ 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;
// 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
static std::vector<gfx::Point> getIsometricLine(const gfx::Size& sz);
private:
gfx::Size m_tileSize;
gfx::Point m_origin;

View File

@ -37,6 +37,7 @@
namespace doc {
static gfx::Rect g_defaultGridBounds(0, 0, 16, 16);
static Grid::Type g_defaultGridType = Grid::Type::Orthogonal;
// static
gfx::Rect Sprite::DefaultGridBounds()
@ -55,6 +56,18 @@ void Sprite::SetDefaultGridBounds(const gfx::Rect& defGridBounds)
g_defaultGridBounds.h = 1;
}
// static
Grid::Type Sprite::DefaultGridType()
{
return g_defaultGridType;
}
// static
void Sprite::SetDefaultGridType(const Grid::Type type)
{
g_defaultGridType = type;
}
//////////////////////////////////////////////////////////////////////
// Constructors/Destructor

View File

@ -15,6 +15,7 @@
#include "doc/color.h"
#include "doc/fit_criteria.h"
#include "doc/frame.h"
#include "doc/grid.h"
#include "doc/image_buffer.h"
#include "doc/image_ref.h"
#include "doc/image_spec.h"
@ -122,6 +123,8 @@ public:
// Defaults
static gfx::Rect DefaultGridBounds();
static void SetDefaultGridBounds(const gfx::Rect& defGridBounds);
static Grid::Type DefaultGridType();
static void SetDefaultGridType(Grid::Type type);
static RgbMapAlgorithm DefaultRgbMapAlgorithm();
static void SetDefaultRgbMapAlgorithm(const RgbMapAlgorithm mapAlgo);
@ -136,6 +139,9 @@ public:
m_gridBounds.h = 1;
}
void setGridType(const Grid::Type type) { m_gridType = type; }
Grid::Type gridType() const { return m_gridType; }
virtual int getMemSize() const override;
////////////////////////////////////////
@ -256,6 +262,7 @@ private:
PalettesList m_palettes; // list of palettes
LayerGroup* m_root; // main group of layers
gfx::Rect m_gridBounds; // grid settings
Grid::Type m_gridType;
// Current rgb map
mutable std::unique_ptr<RgbMap> m_rgbMap;