mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): align all tools to 1px grid
- Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary - Round the rect tool also
This commit is contained in:
parent
0d4b91afe0
commit
2bb74abf31
@ -5,6 +5,7 @@ import {
|
||||
BRUSH_BORDER_OUTER_COLOR,
|
||||
BRUSH_ERASER_BORDER_WIDTH,
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import { alignCoordForTool } from 'features/controlLayers/konva/util';
|
||||
import Konva from 'konva';
|
||||
|
||||
export class CanvasTool {
|
||||
@ -183,12 +184,14 @@ export class CanvasTool {
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
|
||||
const scale = stage.scaleX();
|
||||
// Update the fill circle
|
||||
const radius = toolState.brush.width / 2;
|
||||
|
||||
this.konva.brush.fillCircle.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: isDrawing ? '' : rgbaColorToString(currentFill),
|
||||
});
|
||||
@ -209,12 +212,14 @@ export class CanvasTool {
|
||||
this.konva.eraser.group.visible(false);
|
||||
// this.rect.group.visible(false);
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
|
||||
const scale = stage.scaleX();
|
||||
// Update the fill circle
|
||||
const radius = toolState.eraser.width / 2;
|
||||
this.konva.eraser.fillCircle.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: 'white',
|
||||
});
|
||||
|
@ -1,5 +1,10 @@
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getObjectId, getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
alignCoordForTool,
|
||||
getObjectId,
|
||||
getScaledCursorPosition,
|
||||
offsetCoord,
|
||||
} from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
@ -22,7 +27,7 @@ import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANV
|
||||
* @param setLastCursorPos The callback to store the cursor pos
|
||||
*/
|
||||
const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => {
|
||||
const pos = getScaledFlooredCursorPosition(stage);
|
||||
const pos = getScaledCursorPosition(stage);
|
||||
if (!pos) {
|
||||
return null;
|
||||
}
|
||||
@ -177,14 +182,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
getIsPrimaryMouseDown(e)
|
||||
) {
|
||||
setLastMouseDownPos(pos);
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
|
||||
if (toolState.selected === 'brush') {
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
@ -192,8 +200,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
pos.x - selectedEntity.position.x,
|
||||
pos.y - selectedEntity.position.y,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
@ -206,17 +214,18 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
clip: getClip(selectedEntity),
|
||||
});
|
||||
}
|
||||
setLastAddedPoint(pos);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
|
||||
if (toolState.selected === 'eraser') {
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
@ -229,8 +238,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
pos.x - selectedEntity.position.x,
|
||||
pos.y - selectedEntity.position.y,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
@ -242,12 +251,12 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('eraser_line', true),
|
||||
type: 'eraser_line',
|
||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
});
|
||||
}
|
||||
setLastAddedPoint(pos);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
|
||||
if (toolState.selected === 'rect') {
|
||||
@ -257,8 +266,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('rect_shape', true),
|
||||
type: 'rect_shape',
|
||||
x: pos.x - selectedEntity.position.x,
|
||||
y: pos.y - selectedEntity.position.y,
|
||||
x: Math.round(normalizedPoint.x),
|
||||
y: Math.round(normalizedPoint.y),
|
||||
width: 0,
|
||||
height: 0,
|
||||
color: getCurrentFill(),
|
||||
@ -340,12 +349,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (drawingBuffer?.type === 'brush_line') {
|
||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||
if (nextPoint) {
|
||||
drawingBuffer.points.push(
|
||||
nextPoint.x - selectedEntity.position.x,
|
||||
nextPoint.y - selectedEntity.position.y
|
||||
);
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
setLastAddedPoint(nextPoint);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
@ -354,15 +362,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
clip: getClip(selectedEntity),
|
||||
});
|
||||
setLastAddedPoint(pos);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,12 +382,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (drawingBuffer.type === 'eraser_line') {
|
||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||
if (nextPoint) {
|
||||
drawingBuffer.points.push(
|
||||
nextPoint.x - selectedEntity.position.x,
|
||||
nextPoint.y - selectedEntity.position.y
|
||||
);
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
setLastAddedPoint(nextPoint);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
@ -386,14 +395,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getObjectId('eraser_line', true),
|
||||
type: 'eraser_line',
|
||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
});
|
||||
setLastAddedPoint(pos);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,8 +412,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
|
||||
if (drawingBuffer) {
|
||||
if (drawingBuffer.type === 'rect_shape') {
|
||||
drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x;
|
||||
drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y;
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
||||
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
@ -432,17 +444,20 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
getIsPrimaryMouseDown(e)
|
||||
) {
|
||||
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
||||
drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
||||
drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') {
|
||||
drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x;
|
||||
drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y;
|
||||
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
||||
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types';
|
||||
import type { Coordinate, GenerationMode, Rect, RenderableObject, RgbaColor } from 'features/controlLayers/store/types';
|
||||
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
@ -41,6 +41,39 @@ export const getScaledCursorPosition = (stage: Konva.Stage): Vector2d | null =>
|
||||
return stageTransform.invert().point(pointerPosition);
|
||||
};
|
||||
|
||||
/**
|
||||
* Aligns a coordinate to the nearest integer. When the tool width is odd, an offset is added to align the edges
|
||||
* of the tool to the grid. Without this alignment, the edges of the tool will be 0.5px off.
|
||||
* @param coord The coordinate to align
|
||||
* @param toolWidth The width of the tool
|
||||
* @returns The aligned coordinate
|
||||
*/
|
||||
export const alignCoordForTool = (coord: Coordinate, toolWidth: number): Coordinate => {
|
||||
const roundedX = Math.round(coord.x);
|
||||
const roundedY = Math.round(coord.y);
|
||||
const deltaX = coord.x - roundedX;
|
||||
const deltaY = coord.y - roundedY;
|
||||
const offset = (toolWidth / 2) % 1;
|
||||
const point = {
|
||||
x: roundedX + Math.sign(deltaX) * offset,
|
||||
y: roundedY + Math.sign(deltaY) * offset,
|
||||
};
|
||||
return point;
|
||||
};
|
||||
|
||||
/**
|
||||
* Offsets a point by the given offset. The offset is subtracted from the point.
|
||||
* @param coord The coordinate to offset
|
||||
* @param offset The offset to apply
|
||||
* @returns
|
||||
*/
|
||||
export const offsetCoord = (coord: Coordinate, offset: Coordinate): Coordinate => {
|
||||
return {
|
||||
x: coord.x - offset.x,
|
||||
y: coord.y - offset.y,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Snaps a position to the edge of the stage if within a threshold of the edge
|
||||
* @param pos The position to snap
|
||||
|
Loading…
Reference in New Issue
Block a user