Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic support for line-based drawing in tileset projects #95

Merged
merged 6 commits into from
Nov 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/qml/ui/ToolBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ ToolBar {
objectName: "toolShapeButton"
hoverEnabled: true
focusPolicy: Qt.NoFocus
visible: projectType === Project.ImageType || projectType === Project.LayeredImageType

readonly property bool squareShape: canvas && canvas.toolShape === ImageCanvas.SquareToolShape
icon.source: squareShape ? "qrc:/images/square-tool-shape.png" : "qrc:/images/circle-tool-shape.png"
Expand Down
46 changes: 34 additions & 12 deletions lib/applypixellinecommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,38 @@ Q_LOGGING_CATEGORY(lcApplyPixelLineCommand, "app.undo.applyPixelLineCommand")
// The undo command for lines needs the project image before and after
// the line was drawn on it.
ApplyPixelLineCommand::ApplyPixelLineCommand(ImageCanvas *canvas, int layerIndex, QImage &currentProjectImage, const QPointF point1, const QPointF point2,
const QPoint &newLastPixelPenReleaseScenePos, const QPoint &oldLastPixelPenReleaseScenePos, const QPainter::CompositionMode mode, QUndoCommand *parent) :
const QPointF &newLastPixelPenReleaseScenePos, const QPointF &oldLastPixelPenReleaseScenePos,
const QPainter::CompositionMode mode, QUndoCommand *parent) :
QUndoCommand(parent),
mCanvas(canvas),
mLayerIndex(layerIndex),
mImageWithLine(currentProjectImage),
mLineRect(mCanvas->normalisedLineRect(point1, point2)),
mImageWithoutLine(currentProjectImage.copy(mLineRect)),
mNewLastPixelPenReleaseScenePos(newLastPixelPenReleaseScenePos),
mOldLastPixelPenReleaseScenePos(oldLastPixelPenReleaseScenePos)
mOldLastPixelPenReleaseScenePos(oldLastPixelPenReleaseScenePos),
subImageDatas()
{
QPainter painter(&mImageWithLine);
mCanvas->drawLine(&painter, point1, point2, mode);
painter.end();
mImageWithLine = mImageWithLine.copy(mLineRect);
const QRect lineRect = mCanvas->normalisedLineRect(point1, point2);
const QList<ImageCanvas::SubImage> subImages = canvas->subImagesInBounds(lineRect);
for (auto const &subImage : subImages) {
// subimage-space to scene-space offset
const QPoint offset = subImage.bounds.topLeft() - subImage.offset;
// line rect offset to scene space and clipped to subimage bounds
const QRect subImageLineRect = subImage.bounds.intersected(lineRect.translated(offset));

SubImageData subImageData;
subImageData.subImage = subImage;
subImageData.lineRect = subImageLineRect;
subImageData.imageWithoutLine = currentProjectImage.copy(subImageLineRect);

QPainter painter(&currentProjectImage);
// Clip drawing to subimage
painter.setClipRect(subImageLineRect);
// Draw line with offset to subimage
mCanvas->drawLine(&painter, point1 + offset, point2 + offset, mode);
painter.end();

subImageData.imageWithLine = currentProjectImage.copy(subImageLineRect);
subImageDatas.append(subImageData);
}

qCDebug(lcApplyPixelLineCommand) << "constructed" << this;
}
Expand All @@ -56,13 +74,17 @@ ApplyPixelLineCommand::~ApplyPixelLineCommand()
void ApplyPixelLineCommand::undo()
{
qCDebug(lcApplyPixelLineCommand) << "undoing" << this;
mCanvas->applyPixelLineTool(mLayerIndex, mImageWithoutLine, mLineRect, mOldLastPixelPenReleaseScenePos);
for (auto const &subImageData : subImageDatas) {
mCanvas->applyPixelLineTool(mLayerIndex, subImageData.imageWithoutLine, subImageData.lineRect, mOldLastPixelPenReleaseScenePos);
}
}

void ApplyPixelLineCommand::redo()
{
qCDebug(lcApplyPixelLineCommand) << "redoing" << this;
mCanvas->applyPixelLineTool(mLayerIndex, mImageWithLine, mLineRect, mNewLastPixelPenReleaseScenePos);
for (auto const &subImageData : subImageDatas) {
mCanvas->applyPixelLineTool(mLayerIndex, subImageData.imageWithLine, subImageData.lineRect, mNewLastPixelPenReleaseScenePos);
}
}

int ApplyPixelLineCommand::id() const
Expand All @@ -79,7 +101,7 @@ QDebug operator<<(QDebug debug, const ApplyPixelLineCommand *command)
{
debug.nospace() << "(ApplyPixelLineCommand"
<< " layerIndex=" << command->mLayerIndex
<< ", lineRect" << command->mLineRect
// << ", lineRect" << command->mLineRect
<< ", newLastPixelPenReleaseScenePos=" << command->mNewLastPixelPenReleaseScenePos
<< ", oldLastPixelPenReleaseScenePos=" << command->mOldLastPixelPenReleaseScenePos
<< ")";
Expand Down
20 changes: 13 additions & 7 deletions lib/applypixellinecommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
class SLATE_EXPORT ApplyPixelLineCommand : public QUndoCommand
{
public:
ApplyPixelLineCommand(ImageCanvas *canvas, int layerIndex, QImage &currentProjectImage, const QPointF point1, const QPointF point2, const QPoint &newLastPixelPenReleaseScenePos,
const QPoint &oldLastPixelPenReleaseScenePos, const QPainter::CompositionMode mode, QUndoCommand *parent = nullptr);
ApplyPixelLineCommand(ImageCanvas *canvas, int layerIndex, QImage &currentProjectImage, const QPointF point1, const QPointF point2,
const QPointF &newLastPixelPenReleaseScenePos, const QPointF &oldLastPixelPenReleaseScenePos,
const QPainter::CompositionMode mode, QUndoCommand *parent = nullptr);
~ApplyPixelLineCommand();

void undo() override;
Expand All @@ -45,11 +46,16 @@ class SLATE_EXPORT ApplyPixelLineCommand : public QUndoCommand

ImageCanvas *mCanvas;
int mLayerIndex;
QImage mImageWithLine;
QRect mLineRect;
QImage mImageWithoutLine;
QPoint mNewLastPixelPenReleaseScenePos;
QPoint mOldLastPixelPenReleaseScenePos;
QPointF mNewLastPixelPenReleaseScenePos;
QPointF mOldLastPixelPenReleaseScenePos;

struct SubImageData {
ImageCanvas::SubImage subImage;
QRect lineRect;
QImage imageWithoutLine;
QImage imageWithLine;
};
QList<SubImageData> subImageDatas;
};


Expand Down
60 changes: 44 additions & 16 deletions lib/imagecanvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,15 @@ qreal ImageCanvas::lineAngle() const
return line.angle();
}

QList<ImageCanvas::SubImage> ImageCanvas::subImagesInBounds(const QRect &bounds) const
{
QList<SubImage> subImages;
if (bounds.intersects(mProject->bounds())) {
subImages.append(SubImage{mProject->bounds(), {}});
}
return subImages;
}

void ImageCanvas::setAltPressed(bool altPressed)
{
if (altPressed == mAltPressed)
Expand Down Expand Up @@ -982,7 +991,7 @@ QImage ImageCanvas::getContentImage()
return image;
}

void ImageCanvas::drawLine(QPainter *painter, const QPointF point1, const QPointF point2, const QPainter::CompositionMode mode) const
void ImageCanvas::drawLine(QPainter *painter, QPointF point1, QPointF point2, const QPainter::CompositionMode mode) const
{
painter->save();

Expand All @@ -999,11 +1008,27 @@ void ImageCanvas::drawLine(QPainter *painter, const QPointF point1, const QPoint
}
painter->setPen(pen);

QLineF line(point1, point2);
// Offset odd sized pens to pixel centre to centre pen
const QPointF penOffset = (mToolSize % 2 == 1) ? QPointF(0.5, 0.5) : QPointF(0.0, 0.0);

// Snap points to points to pixel grid
if (mToolSize > 1) {
point1 = (point1 + penOffset).toPoint() - penOffset;
point2 = (point2 + penOffset).toPoint() - penOffset;
}
else {
// Handle inconsitant width 1 pen behaviour, off pixel centres so results in non-ideal asymetrical lines but
// would require either redrawing previous segment as part of stroke or custom line function to prevent spurs
point1 = QPointF(qFloor(point1.x()), qFloor(point1.y()));
point2 = QPointF(qFloor(point2.x()), qFloor(point2.y()));
}

const QLineF line(point1, point2);

painter->setCompositionMode(mode);
// Zero-length line doesn't draw so handle case with drawPoint
if (line.length() == 0.0) {
painter->drawPoint(point1);
// Zero-length line doesn't draw with round pen so handle case with drawPoint
if (line.p1() == line.p2()) {
painter->drawPoint(line.p1());
}
else {
painter->drawLine(line);
Expand Down Expand Up @@ -1648,6 +1673,7 @@ void ImageCanvas::reset()
mLastMouseButtonPressed = Qt::NoButton;
mPressPosition = QPoint(0, 0);
mPressScenePosition = QPoint(0, 0);
mPressScenePositionF = QPointF(0.0, 0.0);
mCurrentPaneOffsetBeforePress = QPoint(0, 0);
mFirstPaneVisibleSceneArea = QRect();
mSecondPaneVisibleSceneArea = QRect();
Expand Down Expand Up @@ -1938,7 +1964,7 @@ void ImageCanvas::applyCurrentTool()
// This ensures that e.g. a translucent red overwrites whatever pixels it
// lies on, rather than blending with them.
mProject->addChange(new ApplyPixelLineCommand(this, mProject->currentLayerIndex(), *currentProjectImage(), linePoint1(), linePoint2(),
mPressScenePosition, mLastPixelPenPressScenePosition, QPainter::CompositionMode_Source));
mPressScenePositionF, mLastPixelPenPressScenePositionF, QPainter::CompositionMode_Source));
break;
}
case EyeDropperTool: {
Expand All @@ -1952,7 +1978,7 @@ void ImageCanvas::applyCurrentTool()
mProject->beginMacro(QLatin1String("PixelEraserTool"));
// Draw the line on top of what has already been painted using a special composition mode to erase pixels.
mProject->addChange(new ApplyPixelLineCommand(this, mProject->currentLayerIndex(), *currentProjectImage(), linePoint1(), linePoint2(),
mPressScenePosition, mLastPixelPenPressScenePosition, QPainter::CompositionMode_Clear));
mPressScenePositionF, mLastPixelPenPressScenePositionF, QPainter::CompositionMode_Clear));
break;
}
case FillTool: {
Expand Down Expand Up @@ -2015,9 +2041,9 @@ void ImageCanvas::applyPixelPenTool(int layerIndex, const QPoint &scenePos, cons
}

void ImageCanvas::applyPixelLineTool(int layerIndex, const QImage &lineImage, const QRect &lineRect,
const QPoint &lastPixelPenReleaseScenePosition)
const QPointF &lastPixelPenReleaseScenePosition)
{
mLastPixelPenPressScenePosition = lastPixelPenReleaseScenePosition;
mLastPixelPenPressScenePositionF = lastPixelPenReleaseScenePosition;
QPainter painter(imageForLayerAt(layerIndex));
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.drawImage(lineRect, lineImage);
Expand Down Expand Up @@ -2076,12 +2102,12 @@ QRect ImageCanvas::doRotateSelection(int layerIndex, const QRect &area, int angl

QPointF ImageCanvas::linePoint1() const
{
return mLastPixelPenPressScenePosition;
return mLastPixelPenPressScenePositionF;
}

QPointF ImageCanvas::linePoint2() const
{
return QPointF(mCursorSceneX, mCursorSceneY);
return QPointF(mCursorSceneFX, mCursorSceneFY);
}

QRect ImageCanvas::normalisedLineRect(const QPointF point1, const QPointF point2) const
Expand All @@ -2090,7 +2116,7 @@ QRect ImageCanvas::normalisedLineRect(const QPointF point1, const QPointF point2
// a simplification of Pythagoras’ theorem.
// The bounds could be tighter by taking into account the specific rotation of the brush,
// but the sqrt(2) ensures it is big enough for any rotation.
const int margin = qCeil(M_SQRT2 * mToolSize / 2.0);
const int margin = qCeil(M_SQRT2 * mToolSize / 2.0) + 1;
return QRect(point1.toPoint(), point2.toPoint()).normalized()
.marginsAdded({margin, margin, margin, margin});
}
Expand Down Expand Up @@ -2483,6 +2509,7 @@ void ImageCanvas::mousePressEvent(QMouseEvent *event)
mLastMouseButtonPressed = mMouseButtonPressed;
mPressPosition = event->pos();
mPressScenePosition = QPoint(mCursorSceneX, mCursorSceneY);
mPressScenePositionF = QPointF(mCursorSceneFX, mCursorSceneFY);
mCurrentPaneOffsetBeforePress = mCurrentPane->integerOffset();
setContainsMouse(true);

Expand All @@ -2507,7 +2534,7 @@ void ImageCanvas::mousePressEvent(QMouseEvent *event)
}

if (!mShiftPressed) {
mLastPixelPenPressScenePosition = mPressScenePosition;
mLastPixelPenPressScenePositionF = mPressScenePositionF;
}

if (mTool != SelectionTool) {
Expand All @@ -2533,7 +2560,7 @@ void ImageCanvas::mouseMoveEvent(QMouseEvent *event)
{
QQuickItem::mouseMoveEvent(event);

const QPoint oldCursorScenePosition = QPoint(mCursorSceneX, mCursorSceneY);
const QPointF oldCursorScenePosition = QPointF(mCursorSceneFX, mCursorSceneFY);
updateCursorPos(event->pos());

if (!mProject->hasLoaded())
Expand All @@ -2551,11 +2578,12 @@ void ImageCanvas::mouseMoveEvent(QMouseEvent *event)
} else if (mPressedGuideIndex != -1) {
mGuidesItem->update();
} else {
if (!isPanning()) {
if (!isPanning()) {
if (mTool != SelectionTool) {
mPressScenePosition = QPoint(mCursorSceneX, mCursorSceneY);
mPressScenePositionF = QPointF(mCursorSceneFX, mCursorSceneFY);
if (!mShiftPressed) {
mLastPixelPenPressScenePosition = oldCursorScenePosition;
mLastPixelPenPressScenePositionF = oldCursorScenePosition;
}
applyCurrentTool();
} else {
Expand Down
29 changes: 27 additions & 2 deletions lib/imagecanvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ class SLATE_EXPORT ImageCanvas : public QQuickItem
int lineLength() const;
qreal lineAngle() const;

struct SubImage {
bool operator==(const SubImage &other) const {
return bounds == other.bounds && offset == other.offset;
}

QRect bounds;
QPoint offset;
};

virtual QList<SubImage> subImagesInBounds(const QRect &bounds) const;

// Essentially currentProjectImage() for regular image canvas, but may return a
// preview image if there is a selection active. For layered image canvases, this
// should return all layers flattened into one image, or the same flattened image
Expand Down Expand Up @@ -375,7 +386,7 @@ protected slots:

virtual void applyCurrentTool();
virtual void applyPixelPenTool(int layerIndex, const QPoint &scenePos, const QColor &colour, bool markAsLastRelease = false);
virtual void applyPixelLineTool(int layerIndex, const QImage &lineImage, const QRect &lineRect, const QPoint &lastPixelPenReleaseScenePosition);
virtual void applyPixelLineTool(int layerIndex, const QImage &lineImage, const QRect &lineRect, const QPointF &lastPixelPenReleaseScenePosition);
void paintImageOntoPortionOfImage(int layerIndex, const QRect &portion, const QImage &replacementImage);
void replacePortionOfImage(int layerIndex, const QRect &portion, const QImage &replacementImage);
void erasePortionOfImage(int layerIndex, const QRect &portion);
Expand Down Expand Up @@ -405,7 +416,7 @@ protected slots:
CanvasPane *hoveredPane(const QPoint &pos);
QPoint eventPosRelativeToCurrentPane(const QPoint &pos);
virtual QImage getContentImage();
void drawLine(QPainter *painter, const QPointF point1, const QPointF point2, const QPainter::CompositionMode mode) const;
void drawLine(QPainter *painter, QPointF point1, QPointF point2, const QPainter::CompositionMode mode) const;
void centrePanes(bool respectSceneCentred = true);
enum ResetPaneSizePolicy {
DontResetPaneSizes,
Expand Down Expand Up @@ -540,6 +551,7 @@ protected slots:
// The position at which the mouse is currently pressed.
QPoint mPressPosition;
QPoint mPressScenePosition;
QPointF mPressScenePositionF;
// The scene position at which the mouse was pressed before the most-recent press.
QPoint mCurrentPaneOffsetBeforePress;
QRect mFirstPaneVisibleSceneArea;
Expand All @@ -562,6 +574,7 @@ protected slots:
// It is set by the pixel tool as the last pixel in the command,
// and by the pixel line tool command.
QPoint mLastPixelPenPressScenePosition;
QPointF mLastPixelPenPressScenePositionF;
// An image as large as the rectangle that contains the line that is being previewed.
QImage mLinePreviewImage;

Expand Down Expand Up @@ -599,4 +612,16 @@ protected slots:
bool mHasBlankCursor;
};

inline uint qHash(const ImageCanvas::SubImage &key, const uint seed = 0) {
return qHashBits(&key, sizeof(ImageCanvas::SubImage), seed);
}

inline QDebug operator<<(QDebug debug, const ImageCanvas::SubImage &subImage)
{
QDebugStateSaver saver(debug);
debug.nospace() << "SubImage(" << subImage.bounds << ", " << subImage.offset << ')';

return debug;
}

#endif // IMAGECANVAS_H
2 changes: 1 addition & 1 deletion lib/imageproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class SLATE_EXPORT ImageProject : public Project
void setSize(const QSize &newSize) override;
int widthInPixels() const override;
int heightInPixels() const override;
QRect bounds() const override;
virtual QRect bounds() const override;

bool isUsingAnimation() const;
void setUsingAnimation(bool isUsingAnimation);
Expand Down
Loading