mirror of https://github.com/aseprite/aseprite.git
879 lines
28 KiB
C++
879 lines
28 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2025 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#define BP_TRACE(...) // TRACEARGS(__VA_ARGS__)
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/ui/editor/brush_preview.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/color.h"
|
|
#include "app/color_utils.h"
|
|
#include "app/doc.h"
|
|
#include "app/snap_to_grid.h"
|
|
#include "app/tools/controller.h"
|
|
#include "app/tools/ink.h"
|
|
#include "app/tools/intertwine.h"
|
|
#include "app/tools/point_shape.h"
|
|
#include "app/tools/tool.h"
|
|
#include "app/tools/tool_loop.h"
|
|
#include "app/ui/context_bar.h"
|
|
#include "app/ui/editor/editor.h"
|
|
#include "app/ui/editor/tool_loop_impl.h"
|
|
#include "app/ui_context.h"
|
|
#include "app/util/shader_helpers.h"
|
|
#include "app/util/wrap_value.h"
|
|
#include "base/debug.h"
|
|
#include "base/scoped_value.h"
|
|
#include "doc/algo.h"
|
|
#include "doc/blend_internals.h"
|
|
#include "doc/brush.h"
|
|
#include "doc/cel.h"
|
|
#include "doc/image.h"
|
|
#include "doc/layer.h"
|
|
#include "doc/primitives.h"
|
|
#include "os/surface.h"
|
|
#include "os/system.h"
|
|
#include "os/window.h"
|
|
#include "render/render.h"
|
|
#include "ui/layer.h"
|
|
#include "ui/manager.h"
|
|
#include "ui/system.h"
|
|
|
|
#include <array>
|
|
|
|
namespace app {
|
|
|
|
using namespace doc;
|
|
|
|
static int g_crosshair_pattern[7 * 7] = {
|
|
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
|
|
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
|
|
};
|
|
|
|
// We're going to keep a cache of native mouse cursor for each
|
|
// possibility of the following crosshair:
|
|
//
|
|
// 2
|
|
// 2
|
|
// 22 3 22
|
|
// 2
|
|
// 2
|
|
//
|
|
// Number of crosshair cursors 2^8 * 3 + 2 = 770
|
|
// Here the center can be black, white or hidden. When the center is
|
|
// black or white, the crosshair can be empty.
|
|
//
|
|
// The index/key of this array is calculated in
|
|
// BrushPreview::createCrosshairCursor().
|
|
//
|
|
// Win32: This is needed to avoid converting from a os::Surface ->
|
|
// HCURSOR calling CreateIconIndirect() as many times as possible for
|
|
// each mouse movement (because it's a slow function).
|
|
//
|
|
static std::array<os::CursorRef, 770> g_bwCursors;
|
|
static int g_cacheCursorScale = 0;
|
|
|
|
// 3 cached cursors when we use a solid cursor (1 dot, crosshair
|
|
// without dot at the center, crosshair with dot in the center)
|
|
static std::array<os::CursorRef, 3> g_solidCursors;
|
|
static gfx::Color g_solidCursorColor = gfx::ColorNone;
|
|
|
|
// Shader used as a blender for the UILayer to draw the Eraser or
|
|
// selection cursors. Based on color_utils::blackandwhite_neg()
|
|
// function.
|
|
static const char* kNegativeBlackAndWhiteShaderCode = R"(
|
|
half4 main(half4 src, half4 dst) {
|
|
if (src.a > 0) {
|
|
return ((dst.r*0.30 + dst.g*0.59 + dst.b*0.11) < 0.5*dst.a) ?
|
|
half4(1): half4(0, 0, 0, 1);
|
|
}
|
|
return dst;
|
|
}
|
|
)";
|
|
|
|
// static
|
|
void BrushPreview::destroyInternals()
|
|
{
|
|
g_bwCursors.fill(nullptr);
|
|
g_solidCursors.fill(nullptr);
|
|
}
|
|
|
|
BrushPreview::BrushPreview(Editor* editor) : m_editor(editor)
|
|
{
|
|
}
|
|
|
|
BrushPreview::~BrushPreview()
|
|
{
|
|
if (m_onScreen) {
|
|
// Called mainly to remove the UILayer from the Display. If we
|
|
// don't explicitly call Display::removeLayer(), the m_uiLayer
|
|
// instance will be referenced by the Display until it's destroyed
|
|
// (keeping the UILayer alive and visible for the whole program).
|
|
hide();
|
|
}
|
|
}
|
|
|
|
BrushRef BrushPreview::getCurrentBrush()
|
|
{
|
|
return App::instance()->contextBar()->activeBrush(m_editor->getCurrentEditorTool());
|
|
}
|
|
|
|
// static
|
|
color_t BrushPreview::getBrushColor(Sprite* sprite, Layer* layer)
|
|
{
|
|
app::Color c = Preferences::instance().colorBar.fgColor();
|
|
ASSERT(sprite != NULL);
|
|
|
|
// Avoid using invalid colors
|
|
if (!c.isValid())
|
|
return 0;
|
|
|
|
if (layer != NULL)
|
|
return color_utils::color_for_layer(c, layer);
|
|
else
|
|
return color_utils::color_for_image(c, sprite->pixelFormat());
|
|
}
|
|
|
|
// Draws the brush cursor in the specified absolute mouse position
|
|
// given in 'pos' param. Warning: You should clean the cursor before
|
|
// to use this routine with other editor.
|
|
//
|
|
// TODO the logic in this function is really complex, we should think
|
|
// a way to simplify all possibilities
|
|
void BrushPreview::show(const gfx::Point& screenPos)
|
|
{
|
|
if (m_onScreen)
|
|
hide();
|
|
|
|
Doc* document = m_editor->document();
|
|
Sprite* sprite = m_editor->sprite();
|
|
Site site = m_editor->getSite();
|
|
Layer* layer = (m_editor->layer() && m_editor->layer()->isImage() ? m_editor->layer() : nullptr);
|
|
ASSERT(sprite);
|
|
|
|
// Get drawable region
|
|
m_editor->getDrawableRegion(m_clippingRegion, ui::Widget::kCutTopWindows);
|
|
|
|
// Remove the invalidated region in the editor.
|
|
m_clippingRegion.createSubtraction(m_clippingRegion, m_editor->getUpdateRegion());
|
|
|
|
// Get cursor color
|
|
const auto& pref = Preferences::instance();
|
|
app::Color appCursorColor = pref.cursor.cursorColor();
|
|
m_blackAndWhiteNegative = (appCursorColor.getType() == app::Color::MaskType);
|
|
|
|
BrushRef brush = getCurrentBrush();
|
|
|
|
tools::Tool* tool = m_editor->getCurrentEditorTool();
|
|
const bool isFloodfill = tool->getPointShape(0)->isFloodFill();
|
|
|
|
// Get current tilemap mode
|
|
const TilemapMode tilemapMode = site.tilemapMode();
|
|
|
|
gfx::Rect origBrushBounds = ((isFloodfill && brush->type() != BrushType::kImageBrushType) ||
|
|
tilemapMode == TilemapMode::Tiles ?
|
|
gfx::Rect(0, 0, 1, 1) :
|
|
brush->bounds());
|
|
gfx::Rect brushBounds = origBrushBounds;
|
|
|
|
// Cursor in the screen (view)
|
|
m_screenPosition = screenPos;
|
|
|
|
// Get cursor position in the editor
|
|
gfx::Point spritePos = m_editor->screenToEditor(screenPos);
|
|
if (pref.cursor.snapToGrid() && m_editor->docPref().grid.snap()) {
|
|
spritePos =
|
|
snap_to_grid(m_editor->docPref().grid.bounds(), spritePos, PreferSnapTo::ClosestGridVertex) +
|
|
gfx::Point(brushBounds.w / 2, brushBounds.h / 2);
|
|
}
|
|
|
|
// Get the current tool
|
|
tools::Ink* ink = m_editor->getCurrentEditorInk();
|
|
|
|
// Setup the cursor type depending on several factors (current tool,
|
|
// foreground color, layer transparency, brush size, etc.).
|
|
color_t brush_color = getBrushColor(sprite, layer);
|
|
color_t mask_index = sprite->transparentColor();
|
|
|
|
// Check dynamics option for freehand tools
|
|
if (tool && tool->getController(0)->isFreehand() &&
|
|
// TODO add support for dynamics to contour tool
|
|
tool->getFill(0) == tools::FillNone) {
|
|
const auto& dynamics = App::instance()->contextBar()->getDynamics();
|
|
if (brush->type() != doc::kImageBrushType && (dynamics.size != tools::DynamicSensor::Static ||
|
|
dynamics.angle != tools::DynamicSensor::Static)) {
|
|
brush.reset(new Brush(
|
|
brush->type(),
|
|
(dynamics.size != tools::DynamicSensor::Static ? dynamics.minSize : brush->size()),
|
|
(dynamics.angle != tools::DynamicSensor::Static ? dynamics.minAngle : brush->angle())));
|
|
}
|
|
}
|
|
|
|
if (ink->isSelection() || ink->isSlice()) {
|
|
m_type = SELECTION_CROSSHAIR;
|
|
}
|
|
else if ((tilemapMode == TilemapMode::Pixels) &&
|
|
(brush->type() == kImageBrushType ||
|
|
((isFloodfill ? 1 : brush->size()) > (1.0 / m_editor->zoom().scale()))) &&
|
|
( // Use cursor bounds for inks that are effects (eraser, blur, etc.)
|
|
(ink->isEffect()) ||
|
|
// or when the brush color is transparent and we are not in the background layer
|
|
(!ink->isShading() && (layer && layer->isTransparent()) &&
|
|
((sprite->pixelFormat() == IMAGE_INDEXED && brush_color == mask_index) ||
|
|
(sprite->pixelFormat() == IMAGE_RGB && rgba_geta(brush_color) == 0) ||
|
|
(sprite->pixelFormat() == IMAGE_GRAYSCALE && graya_geta(brush_color) == 0))))) {
|
|
m_type = BRUSH_BOUNDARIES;
|
|
}
|
|
else {
|
|
m_type = CROSSHAIR;
|
|
}
|
|
|
|
bool showPreview = false;
|
|
bool showPreviewWithEdges = false;
|
|
bool cancelEdges = false;
|
|
auto brushPreview = pref.cursor.brushPreview();
|
|
if (!m_editor->docPref().show.brushPreview())
|
|
brushPreview = app::gen::BrushPreview::NONE;
|
|
|
|
switch (brushPreview) {
|
|
case app::gen::BrushPreview::NONE: m_type = CROSSHAIR; break;
|
|
case app::gen::BrushPreview::EDGES: m_type = BRUSH_BOUNDARIES; break;
|
|
case app::gen::BrushPreview::FULL:
|
|
case app::gen::BrushPreview::FULLALL:
|
|
case app::gen::BrushPreview::FULLNEDGES:
|
|
showPreview = m_editor->getState()->requireBrushPreview();
|
|
switch (brushPreview) {
|
|
case app::gen::BrushPreview::FULLALL:
|
|
if (showPreview)
|
|
m_type = CROSSHAIR;
|
|
cancelEdges = true;
|
|
break;
|
|
case app::gen::BrushPreview::FULLNEDGES:
|
|
if (showPreview)
|
|
showPreviewWithEdges = true;
|
|
m_type = BRUSH_BOUNDARIES;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// For tiles as we show the edges of the tile, we add the crosshair
|
|
// in the mouse position as a helper.
|
|
if (m_type & BRUSH_BOUNDARIES && tilemapMode == TilemapMode::Tiles)
|
|
m_type |= CROSSHAIR;
|
|
|
|
if (m_type & SELECTION_CROSSHAIR)
|
|
showPreview = false;
|
|
|
|
// When the extra cel is locked (e.g. we are flashing the active
|
|
// layer) we don't show the brush preview temporally.
|
|
if (showPreview && m_editor->isExtraCelLocked()) {
|
|
showPreview = false;
|
|
showPreviewWithEdges = false;
|
|
cancelEdges = false;
|
|
m_type |= BRUSH_BOUNDARIES;
|
|
}
|
|
|
|
// Use a simple cross
|
|
if (pref.cursor.paintingCursorType() == gen::PaintingCursorType::SIMPLE_CROSSHAIR) {
|
|
m_type &= ~(CROSSHAIR | SELECTION_CROSSHAIR);
|
|
m_type |= NATIVE_CROSSHAIR;
|
|
}
|
|
|
|
// For cursor type 'bounds' we have to generate cursor boundaries
|
|
if (m_type & BRUSH_BOUNDARIES) {
|
|
if (brush->type() != kImageBrushType)
|
|
showPreview = showPreviewWithEdges;
|
|
if (cancelEdges)
|
|
m_type &= ~BRUSH_BOUNDARIES;
|
|
}
|
|
if (m_type & BRUSH_BOUNDARIES)
|
|
createBoundaries(site, spritePos);
|
|
|
|
// Draw pixel/brush preview
|
|
if (showPreview) {
|
|
brushBounds.offset(spritePos);
|
|
gfx::Rect extraCelBoundsInCanvas = brushBounds;
|
|
|
|
// Tiled mode might require a bigger extra cel (to show the tiled)
|
|
if (int(m_editor->docPref().tiled.mode()) & int(filters::TiledMode::X_AXIS)) {
|
|
brushBounds.x = wrap_value(brushBounds.x, sprite->width());
|
|
extraCelBoundsInCanvas.x = brushBounds.x;
|
|
if ((extraCelBoundsInCanvas.x < 0 && extraCelBoundsInCanvas.x2() > 0) ||
|
|
(extraCelBoundsInCanvas.x < sprite->width() &&
|
|
extraCelBoundsInCanvas.x2() > sprite->width())) {
|
|
extraCelBoundsInCanvas.x = 0;
|
|
extraCelBoundsInCanvas.w = sprite->width();
|
|
}
|
|
}
|
|
if (int(m_editor->docPref().tiled.mode()) & int(filters::TiledMode::Y_AXIS)) {
|
|
brushBounds.y = wrap_value(brushBounds.y, sprite->height());
|
|
extraCelBoundsInCanvas.y = brushBounds.y;
|
|
if ((extraCelBoundsInCanvas.y < 0 && extraCelBoundsInCanvas.y2() > 0) ||
|
|
(extraCelBoundsInCanvas.y < sprite->height() &&
|
|
extraCelBoundsInCanvas.y2() > sprite->height())) {
|
|
extraCelBoundsInCanvas.y = 0;
|
|
extraCelBoundsInCanvas.h = sprite->height();
|
|
}
|
|
}
|
|
|
|
gfx::Rect extraCelBounds;
|
|
if (tilemapMode == TilemapMode::Tiles) {
|
|
ASSERT(layer->isTilemap());
|
|
doc::Grid grid = site.grid();
|
|
extraCelBounds = grid.canvasToTile(extraCelBoundsInCanvas);
|
|
extraCelBoundsInCanvas = grid.tileToCanvas(extraCelBounds);
|
|
}
|
|
else {
|
|
extraCelBounds = extraCelBoundsInCanvas;
|
|
}
|
|
|
|
BP_TRACE("BrushPreview:",
|
|
"brushBounds",
|
|
brushBounds,
|
|
"extraCelBounds",
|
|
extraCelBounds,
|
|
"extraCelBoundsInCanvas",
|
|
extraCelBoundsInCanvas);
|
|
|
|
// Create the extra cel to show the brush preview
|
|
Cel* cel = site.cel();
|
|
|
|
int t, opacity = 255;
|
|
if (cel)
|
|
opacity = MUL_UN8(opacity, cel->opacity(), t);
|
|
if (layer)
|
|
opacity = MUL_UN8(opacity, static_cast<LayerImage*>(layer)->opacity(), t);
|
|
|
|
if (!m_extraCel)
|
|
m_extraCel.reset(new ExtraCel);
|
|
|
|
m_extraCel->create(ExtraCel::Purpose::BrushPreview,
|
|
tilemapMode,
|
|
document->sprite(),
|
|
extraCelBoundsInCanvas,
|
|
extraCelBounds.size(),
|
|
site.frame(),
|
|
opacity);
|
|
m_extraCel->setType(render::ExtraType::NONE);
|
|
m_extraCel->setBlendMode(
|
|
(layer ? static_cast<LayerImage*>(layer)->blendMode() : BlendMode::NORMAL));
|
|
|
|
document->setExtraCel(m_extraCel);
|
|
|
|
Image* extraImage = m_extraCel->image();
|
|
if (extraImage->pixelFormat() == IMAGE_TILEMAP) {
|
|
extraImage->setMaskColor(notile);
|
|
clear_image(extraImage, notile);
|
|
}
|
|
else {
|
|
extraImage->setMaskColor(mask_index);
|
|
clear_image(extraImage, (extraImage->pixelFormat() == IMAGE_INDEXED ? mask_index : 0));
|
|
}
|
|
|
|
if (layer) {
|
|
render::Render().renderLayer(extraImage,
|
|
layer,
|
|
site.frame(),
|
|
gfx::Clip(0, 0, extraCelBoundsInCanvas),
|
|
BlendMode::SRC);
|
|
|
|
// This extra cel is a patch for the current layer/frame
|
|
m_extraCel->setType(render::ExtraType::PATCH);
|
|
}
|
|
|
|
{
|
|
std::unique_ptr<tools::ToolLoop> loop(
|
|
create_tool_loop_preview(m_editor, brush, extraImage, extraCelBounds.origin()));
|
|
if (loop) {
|
|
loop->getInk()->prepareInk(loop.get());
|
|
loop->getController()->prepareController(loop.get());
|
|
loop->getIntertwine()->prepareIntertwine(loop.get());
|
|
loop->getPointShape()->preparePointShape(loop.get());
|
|
|
|
tools::Stroke::Pt pt(brushBounds.x - origBrushBounds.x, brushBounds.y - origBrushBounds.y);
|
|
pt.size = brush->size();
|
|
pt.angle = brush->angle();
|
|
loop->getPointShape()->transformPoint(loop.get(), pt);
|
|
}
|
|
}
|
|
|
|
document->notifySpritePixelsModified(sprite,
|
|
gfx::Region(m_lastBounds = extraCelBoundsInCanvas),
|
|
m_lastFrame = site.frame());
|
|
|
|
m_withRealPreview = true;
|
|
}
|
|
|
|
// Save area and draw the cursor
|
|
if (!(m_type & NATIVE_CROSSHAIR) || (m_type & BRUSH_BOUNDARIES)) {
|
|
ui::Display* display = m_editor->display();
|
|
gfx::Color uiCursorColor = color_utils::color_for_ui(appCursorColor);
|
|
|
|
if (!(m_type & NATIVE_CROSSHAIR)) {
|
|
// We use the Display back layer to get pixels in createCrosshairCursor().
|
|
ui::Graphics g(display, display->backLayer()->surface(), 0, 0);
|
|
createCrosshairCursor(&g, uiCursorColor);
|
|
}
|
|
|
|
// Create (or re-use) the UILayer
|
|
if ((m_type & SELECTION_CROSSHAIR) || (m_type & BRUSH_BOUNDARIES)) {
|
|
bool cached = createUILayer(brushBounds);
|
|
const render::Projection& proj = m_editor->projection();
|
|
|
|
if (m_uiLayer->surface()) {
|
|
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
|
|
|
if (m_type & SELECTION_CROSSHAIR) {
|
|
layerBounds.setOrigin(m_editor->editorToScreen(spritePos) -
|
|
gfx::Point(layerBounds.size() / 2) +
|
|
gfx::Point(proj.applyX(1) / 2, proj.applyY(1) / 2));
|
|
}
|
|
else if (m_type & BRUSH_BOUNDARIES) {
|
|
layerBounds.setOrigin(m_editor->editorToScreen(spritePos) -
|
|
gfx::Point(layerBounds.w / 2 - proj.applyX(layerBounds.w % 2),
|
|
layerBounds.h / 2 - proj.applyY(layerBounds.h % 2)));
|
|
}
|
|
|
|
m_uiLayer->setPosition(layerBounds.origin());
|
|
m_uiLayer->setClipRegion(m_clippingRegion);
|
|
}
|
|
|
|
display->addLayer(m_uiLayer);
|
|
m_layerAdded = true;
|
|
|
|
// Check if the cached brush painted in the m_uiLayer is still valid
|
|
if (m_cachedType != m_type || m_cachedBrushGen != m_brushGen) {
|
|
cached = false;
|
|
}
|
|
|
|
// Here we re-use the cached surface
|
|
if (!cached && m_uiLayer->surface()) {
|
|
m_uiLayer->surface()->clear();
|
|
|
|
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
|
|
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);
|
|
|
|
os::Paint paint;
|
|
paint.style(os::Paint::Stroke);
|
|
if (m_blackAndWhiteNegative)
|
|
paint.color(gfx::rgba(255, 255, 255, 255));
|
|
else
|
|
paint.color(uiCursorColor);
|
|
|
|
if (m_type & SELECTION_CROSSHAIR) {
|
|
gfx::Point pos(3, 3);
|
|
strokeSelectionCrossPixels(&g, pos, paint, 1);
|
|
}
|
|
else if (m_type & BRUSH_BOUNDARIES) {
|
|
gfx::Point pos(layerBounds.w / 2 - proj.applyX(layerBounds.w % 2),
|
|
layerBounds.h / 2 - proj.applyY(layerBounds.h % 2));
|
|
strokeBrushBoundaries(&g, pos, paint);
|
|
}
|
|
|
|
#if LAF_SKIA
|
|
if (m_blackAndWhiteNegative) {
|
|
static sk_sp<SkBlender> blender;
|
|
if (!blender) {
|
|
SkRuntimeBlendBuilder builder(make_blender(kNegativeBlackAndWhiteShaderCode));
|
|
blender = builder.makeBlender();
|
|
}
|
|
m_uiLayer->paint().skPaint().setBlender(blender);
|
|
}
|
|
else {
|
|
m_uiLayer->paint().skPaint().setBlender(nullptr);
|
|
}
|
|
#endif // LAF_SKIA
|
|
|
|
m_cachedType = m_type;
|
|
m_cachedBrushGen = m_brushGen;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cursor in the editor (model)
|
|
m_onScreen = true;
|
|
m_editorPosition = spritePos;
|
|
|
|
if (m_type & NATIVE_CROSSHAIR)
|
|
ui::set_mouse_cursor(ui::kCrosshairCursor);
|
|
|
|
m_lastLayer = site.layer();
|
|
m_lastTilemapMode = tilemapMode;
|
|
}
|
|
|
|
// Cleans the brush cursor from the specified editor.
|
|
//
|
|
// The mouse position is got from the last call to showBrushPreview()
|
|
// (m_cursorEditor). So you must to use this routine only if you
|
|
// called showBrushPreview() before.
|
|
void BrushPreview::hide()
|
|
{
|
|
if (!m_onScreen)
|
|
return;
|
|
|
|
// Don't hide the cursor to avoid flickering, the native mouse
|
|
// cursor will be changed anyway after the hide() by the caller.
|
|
//
|
|
// if (m_cursor)
|
|
// m_editor->display()->nativeWindow()->setCursor(os::NativeCursor::Hidden);
|
|
|
|
if (m_layerAdded) {
|
|
ui::Display* display = m_editor->display();
|
|
|
|
// Remove the UI layer to hide the brush preview
|
|
display->removeLayer(m_uiLayer);
|
|
m_layerAdded = false;
|
|
}
|
|
|
|
// Clean pixel/brush preview
|
|
if (m_withRealPreview) {
|
|
Doc* document = m_editor->document();
|
|
doc::Sprite* sprite = m_editor->sprite();
|
|
|
|
ASSERT(document);
|
|
ASSERT(sprite);
|
|
|
|
if (document && sprite) {
|
|
document->setExtraCel(ExtraCelRef(nullptr));
|
|
document->notifySpritePixelsModified(sprite, gfx::Region(m_lastBounds), m_lastFrame);
|
|
}
|
|
|
|
m_withRealPreview = false;
|
|
}
|
|
|
|
m_onScreen = false;
|
|
m_clippingRegion.clear();
|
|
}
|
|
|
|
void BrushPreview::discardBrushPreview()
|
|
{
|
|
Doc* document = m_editor->document();
|
|
ASSERT(document);
|
|
|
|
if (document && m_onScreen && m_withRealPreview) {
|
|
document->setExtraCel(ExtraCelRef(nullptr));
|
|
}
|
|
}
|
|
|
|
void BrushPreview::redraw()
|
|
{
|
|
if (m_onScreen) {
|
|
gfx::Point screenPos = m_screenPosition;
|
|
hide();
|
|
show(screenPos);
|
|
}
|
|
}
|
|
|
|
void BrushPreview::invalidateRegion(const gfx::Region& region)
|
|
{
|
|
m_clippingRegion.createSubtraction(m_clippingRegion, region);
|
|
}
|
|
|
|
void BrushPreview::calculateTileBoundariesOrigin(const doc::Grid& grid, const gfx::Point& spritePos)
|
|
{
|
|
m_brushBoundaries.offset(-m_brushBoundaries.begin()[0].bounds().x,
|
|
-m_brushBoundaries.begin()[0].bounds().y);
|
|
gfx::Point canvasPos = grid.tileToCanvas(grid.canvasToTile(spritePos));
|
|
m_brushBoundaries.offset(canvasPos.x - spritePos.x, canvasPos.y - spritePos.y);
|
|
}
|
|
|
|
bool BrushPreview::createUILayer(const gfx::Rect& brushBounds)
|
|
{
|
|
if (!m_uiLayer)
|
|
m_uiLayer = ui::UILayer::Make();
|
|
|
|
gfx::Size sizeHint(0, 0);
|
|
const render::Projection& proj = m_editor->projection();
|
|
|
|
if (m_type & SELECTION_CROSSHAIR) {
|
|
gfx::Size pixelSize(6 + std::max(1, proj.applyX(1)), 6 + std::max(1, proj.applyY(1)));
|
|
sizeHint = pixelSize;
|
|
}
|
|
else if (m_type & BRUSH_BOUNDARIES) {
|
|
gfx::Size brushSize(proj.applyX(brushBounds.w + 2), proj.applyY(brushBounds.h + 2));
|
|
sizeHint = brushSize;
|
|
}
|
|
|
|
if (sizeHint.w == 0 || sizeHint.h == 0)
|
|
return false;
|
|
|
|
os::SurfaceRef surface = m_uiLayer->surface();
|
|
if (!surface || surface->width() != sizeHint.w || surface->height() != sizeHint.h) {
|
|
surface = os::System::instance()->makeRgbaSurface(sizeHint.w, sizeHint.h);
|
|
m_uiLayer->setSurface(surface);
|
|
}
|
|
else
|
|
return true; // We can use the cached UILayer
|
|
|
|
return false;
|
|
}
|
|
|
|
void BrushPreview::createBoundaries(const Site& site, const gfx::Point& spritePos)
|
|
{
|
|
BrushRef brush = getCurrentBrush();
|
|
Layer* currentLayer = site.layer();
|
|
TilemapMode tilemapMode = site.tilemapMode();
|
|
|
|
if (tilemapMode == TilemapMode::Pixels && tilemapMode == m_lastTilemapMode &&
|
|
!m_brushBoundaries.isEmpty() && m_brushGen == brush->gen()) {
|
|
return;
|
|
}
|
|
else if (tilemapMode == TilemapMode::Tiles && tilemapMode == m_lastTilemapMode &&
|
|
m_lastLayer == currentLayer) {
|
|
// When tilemapMode is 'Tiles' is needed an offset
|
|
// re-calculation of the brush boundaries, even
|
|
// if it's no need to update the mask.
|
|
calculateTileBoundariesOrigin(site.grid(), spritePos);
|
|
return;
|
|
}
|
|
|
|
const bool isOnePixel = (m_editor->getCurrentEditorTool()->getPointShape(0)->isPixel() ||
|
|
m_editor->getCurrentEditorTool()->getPointShape(0)->isFloodFill());
|
|
Image* brushImage = brush->image();
|
|
m_brushGen = brush->gen();
|
|
|
|
Image* mask = nullptr;
|
|
bool deleteMask = true;
|
|
if (tilemapMode == TilemapMode::Tiles) {
|
|
mask = Image::create(IMAGE_BITMAP, site.grid().tileSize().w, site.grid().tileSize().h);
|
|
mask->clear((color_t)1);
|
|
}
|
|
else if (isOnePixel) {
|
|
mask = Image::create(IMAGE_BITMAP, 1, 1);
|
|
mask->putPixel(0, 0, (color_t)1);
|
|
}
|
|
else if (brushImage->pixelFormat() != IMAGE_BITMAP) {
|
|
ASSERT(brush->maskBitmap());
|
|
deleteMask = false;
|
|
mask = brush->maskBitmap();
|
|
}
|
|
|
|
m_brushBoundaries.regen(mask ? mask : brushImage);
|
|
if (tilemapMode == TilemapMode::Pixels) {
|
|
if (!isOnePixel)
|
|
m_brushBoundaries.offset(-brush->center().x, -brush->center().y);
|
|
}
|
|
else
|
|
calculateTileBoundariesOrigin(site.grid(), spritePos);
|
|
|
|
if (deleteMask)
|
|
delete mask;
|
|
}
|
|
|
|
void BrushPreview::createCrosshairCursor(ui::Graphics* g, const gfx::Color cursorColor)
|
|
{
|
|
ASSERT(!(m_type & NATIVE_CROSSHAIR));
|
|
|
|
gfx::Rect cursorBounds;
|
|
gfx::Point cursorCenter;
|
|
|
|
// Depending on the editor zoom, maybe we need subpixel movement (a
|
|
// little dot inside the active pixel)
|
|
const bool requireLittleCenterDot = (m_editor->zoom().scale() >= 4.0);
|
|
|
|
if (m_type & CROSSHAIR) {
|
|
// Regular crosshair of 7x7
|
|
cursorBounds |= gfx::Rect(-3, -3, 7, 7);
|
|
cursorCenter = -cursorBounds.origin();
|
|
}
|
|
else if (requireLittleCenterDot) {
|
|
// Special case of a cursor for one pixel
|
|
cursorBounds = gfx::Rect(0, 0, 1, 1);
|
|
cursorCenter = gfx::Point(0, 0);
|
|
}
|
|
else {
|
|
// TODO should we use ui::set_mouse_cursor()?
|
|
ui::set_mouse_cursor_reset_info();
|
|
m_editor->display()->nativeWindow()->setCursor(os::NativeCursor::Hidden);
|
|
return;
|
|
}
|
|
|
|
os::Window* window = m_editor->display()->nativeWindow();
|
|
const int scale = window->scale();
|
|
os::CursorRef cursor = nullptr;
|
|
|
|
// Invalidate the entire cache if the scale has changed
|
|
if (g_cacheCursorScale != scale) {
|
|
g_cacheCursorScale = scale;
|
|
g_bwCursors.fill(nullptr);
|
|
g_solidCursors.fill(nullptr);
|
|
}
|
|
|
|
// Cursor with black/white colors (we create a key/index for
|
|
// g_cachedCursors depending on the colors on the screen)
|
|
if (m_blackAndWhiteNegative) {
|
|
int k = 0;
|
|
if (m_type & CROSSHAIR) {
|
|
int bit = 0;
|
|
for (int v = 0; v < 7; v++) {
|
|
for (int u = 0; u < 7; u++) {
|
|
if (g_crosshair_pattern[v * 7 + u]) {
|
|
color_t c = g->getPixel(m_screenPosition.x - 3 + u, m_screenPosition.y - 3 + v);
|
|
c = color_utils::blackandwhite_neg(c);
|
|
if (rgba_getr(c) == 255) { // White
|
|
k |= (1 << bit);
|
|
}
|
|
++bit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (requireLittleCenterDot) {
|
|
color_t c = g->getPixel(m_screenPosition.x, m_screenPosition.y);
|
|
c = color_utils::blackandwhite_neg(c);
|
|
if (rgba_getr(c) == 255) { // White
|
|
k |= (m_type & CROSSHAIR ? 0x200 : 0x301);
|
|
}
|
|
else { // Black
|
|
k |= (m_type & CROSSHAIR ? 0x100 : 0x300);
|
|
}
|
|
}
|
|
|
|
ASSERT(k < int(g_bwCursors.size()));
|
|
if (k >= int(g_bwCursors.size())) // Unexpected key value in release mode
|
|
return;
|
|
|
|
// Use cached cursor
|
|
if (g_bwCursors[k]) {
|
|
cursor = g_bwCursors[k];
|
|
}
|
|
else {
|
|
const gfx::Color black = gfx::rgba(0, 0, 0);
|
|
const gfx::Color white = gfx::rgba(255, 255, 255);
|
|
os::SurfaceRef cursorSurface = os::System::instance()->makeRgbaSurface(cursorBounds.w,
|
|
cursorBounds.h);
|
|
cursorSurface->clear();
|
|
int bit = 0;
|
|
if (m_type & CROSSHAIR) {
|
|
for (int v = 0; v < 7; v++) {
|
|
for (int u = 0; u < 7; u++) {
|
|
if (g_crosshair_pattern[v * 7 + u]) {
|
|
cursorSurface->putPixel((k & (1 << bit) ? white : black), u, v);
|
|
++bit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (requireLittleCenterDot) {
|
|
if (m_type & CROSSHAIR) {
|
|
if (k & (0x100 | 0x200)) { // 0x100 or 0x200
|
|
cursorSurface->putPixel((k & 0x100 ? black : white),
|
|
cursorBounds.w / 2,
|
|
cursorBounds.h / 2);
|
|
}
|
|
}
|
|
else { // 0x300 or 0x301
|
|
cursorSurface->putPixel((k == 0x300 ? black : white),
|
|
cursorBounds.w / 2,
|
|
cursorBounds.h / 2);
|
|
}
|
|
}
|
|
|
|
cursor = g_bwCursors[k] =
|
|
os::System::instance()->makeCursor(cursorSurface.get(), cursorCenter, scale);
|
|
}
|
|
}
|
|
// Cursor with solid color (easiest case, we don't have to check the
|
|
// colors in the screen to create the crosshair)
|
|
else {
|
|
// We have to recreate all cursors if the color has changed.
|
|
if (g_solidCursorColor != cursorColor) {
|
|
g_solidCursors.fill(nullptr);
|
|
g_solidCursorColor = cursorColor;
|
|
}
|
|
|
|
int k = 0;
|
|
if (m_type & CROSSHAIR) {
|
|
if (requireLittleCenterDot)
|
|
k = 2;
|
|
else
|
|
k = 1;
|
|
}
|
|
|
|
// Use cached cursor
|
|
if (g_solidCursors[k]) {
|
|
cursor = g_solidCursors[k];
|
|
}
|
|
else {
|
|
os::SurfaceRef cursorSurface = os::System::instance()->makeRgbaSurface(cursorBounds.w,
|
|
cursorBounds.h);
|
|
cursorSurface->clear();
|
|
if (m_type & CROSSHAIR) {
|
|
for (int v = 0; v < 7; v++)
|
|
for (int u = 0; u < 7; u++)
|
|
if (g_crosshair_pattern[v * 7 + u])
|
|
cursorSurface->putPixel(cursorColor, u, v);
|
|
}
|
|
if (requireLittleCenterDot)
|
|
cursorSurface->putPixel(cursorColor, cursorBounds.w / 2, cursorBounds.h / 2);
|
|
|
|
cursor = g_solidCursors[k] =
|
|
os::System::instance()->makeCursor(cursorSurface.get(), cursorCenter, scale);
|
|
}
|
|
}
|
|
|
|
if (cursor) {
|
|
// TODO should we use ui::set_mouse_cursor()?
|
|
ui::set_mouse_cursor_reset_info();
|
|
window->setCursor(cursor);
|
|
}
|
|
}
|
|
|
|
// Thick crosshair used for selection tools
|
|
void BrushPreview::strokeSelectionCrossPixels(ui::Graphics* g,
|
|
gfx::Point pos,
|
|
const ui::Paint& paint,
|
|
const int thickness)
|
|
{
|
|
const render::Projection& proj = m_editor->projection();
|
|
gfx::Size size(proj.applyX(thickness / 2), proj.applyY(thickness / 2));
|
|
gfx::Size size2(proj.applyX(thickness), proj.applyY(thickness));
|
|
if (size2.w == 0)
|
|
size2.w = 1;
|
|
if (size2.h == 0)
|
|
size2.h = 1;
|
|
|
|
int u0 = pos.x - size.w - 3;
|
|
int v0 = pos.y - size.h - 1;
|
|
int u1 = pos.x - size.w + size2.w;
|
|
int v1 = pos.y - size.h + size2.h;
|
|
g->drawHLine(u0, v0, 3, paint);
|
|
g->drawHLine(u1, v0, 3, paint);
|
|
g->drawHLine(u0, v1, 3, paint);
|
|
g->drawHLine(u1, v1, 3, paint);
|
|
|
|
u0 = pos.x - size.w - 1;
|
|
v0 = pos.y - size.h - 3;
|
|
g->drawVLine(u0, v0, 3, paint);
|
|
g->drawVLine(u1, v0, 3, paint);
|
|
g->drawVLine(u0, v1, 3, paint);
|
|
g->drawVLine(u1, v1, 3, paint);
|
|
}
|
|
|
|
// Brush edges used for Eraser tool
|
|
void BrushPreview::strokeBrushBoundaries(ui::Graphics* g, gfx::Point pos, const os::Paint& paint)
|
|
{
|
|
auto& segs = m_brushBoundaries;
|
|
segs.createPathIfNeeeded();
|
|
|
|
const render::Projection& proj = m_editor->projection();
|
|
|
|
gfx::Path path;
|
|
segs.path().transform(proj.scaleMatrix(), &path);
|
|
|
|
path.offset(pos.x, pos.y);
|
|
|
|
g->drawPath(path, paint);
|
|
}
|
|
|
|
} // namespace app
|