feat(ui): buffered rect drawing

This commit is contained in:
psychedelicious 2024-07-05 10:14:52 +10:00
parent 908e504a6f
commit 3f6ee1b7a4
10 changed files with 150 additions and 57 deletions

View File

@ -6,7 +6,7 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util';
import type { BrushLine, EraserLine, InpaintMaskEntity } from 'features/controlLayers/store/types';
import type { BrushLine, EraserLine, InpaintMaskEntity, RectShape } from 'features/controlLayers/store/types';
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
@ -21,7 +21,7 @@ export class CanvasInpaintMask {
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
private drawingBuffer: BrushLine | EraserLine | null;
private drawingBuffer: BrushLine | EraserLine | RectShape | null;
private inpaintMaskState: InpaintMaskEntity;
constructor(entity: InpaintMaskEntity, manager: CanvasManager) {
@ -71,7 +71,7 @@ export class CanvasInpaintMask {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
this.drawingBuffer = obj;
if (this.drawingBuffer) {
if (this.drawingBuffer.type === 'brush_line') {
@ -91,6 +91,8 @@ export class CanvasInpaintMask {
this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask');
} else if (this.drawingBuffer.type === 'eraser_line') {
this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask');
} else if (this.drawingBuffer.type === 'rect_shape') {
this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer');
}
this.setDrawingBuffer(null);
}

View File

@ -5,7 +5,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util';
import type { BrushLine, EraserLine, LayerEntity } from 'features/controlLayers/store/types';
import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types';
import { isDrawingTool } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
@ -19,7 +19,7 @@ export class CanvasLayer {
objectsGroup: Konva.Group;
transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
private drawingBuffer: BrushLine | EraserLine | null;
private drawingBuffer: BrushLine | EraserLine | RectShape | null;
private layerState: LayerEntity;
constructor(entity: LayerEntity, manager: CanvasManager) {
@ -70,7 +70,7 @@ export class CanvasLayer {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
if (obj) {
this.drawingBuffer = obj;
await this.renderObject(this.drawingBuffer, true);
@ -88,6 +88,8 @@ export class CanvasLayer {
this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'layer');
} else if (this.drawingBuffer.type === 'eraser_line') {
this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'layer');
} else if (this.drawingBuffer.type === 'rect_shape') {
this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer');
}
this.setDrawingBuffer(null);
}

View File

@ -6,7 +6,7 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util';
import type { BrushLine, EraserLine, RegionEntity } from 'features/controlLayers/store/types';
import type { BrushLine, EraserLine, RectShape, RegionEntity } from 'features/controlLayers/store/types';
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
@ -21,7 +21,7 @@ export class CanvasRegion {
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
private drawingBuffer: BrushLine | EraserLine | null;
private drawingBuffer: BrushLine | EraserLine | RectShape | null;
private regionState: RegionEntity;
constructor(entity: RegionEntity, manager: CanvasManager) {
@ -71,7 +71,7 @@ export class CanvasRegion {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
this.drawingBuffer = obj;
if (this.drawingBuffer) {
if (this.drawingBuffer.type === 'brush_line') {
@ -90,6 +90,8 @@ export class CanvasRegion {
this.manager.stateApi.onBrushLineAdded2({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance');
} else if (this.drawingBuffer.type === 'eraser_line') {
this.manager.stateApi.onEraserLineAdded2({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance');
} else if (this.drawingBuffer.type === 'rect_shape') {
this.manager.stateApi.onRectShapeAdded2({ id: this.id, rectShape: this.drawingBuffer }, 'layer');
}
this.setDrawingBuffer(null);
}

View File

@ -25,6 +25,7 @@ import {
imImageCacheChanged,
imLinePointAdded,
imRectAdded,
imRectShapeAdded2,
imScaled,
imTranslated,
layerBboxChanged,
@ -35,6 +36,7 @@ import {
layerImageCacheChanged,
layerLinePointAdded,
layerRectAdded,
layerRectShapeAdded2,
layerScaled,
layerTranslated,
rgBboxChanged,
@ -45,6 +47,7 @@ import {
rgImageCacheChanged,
rgLinePointAdded,
rgRectAdded,
rgRectShapeAdded2,
rgScaled,
rgTranslated,
toolBufferChanged,
@ -59,6 +62,7 @@ import type {
EraserLineAddedArg,
PointAddedToLineArg,
PosChangedArg,
RectShape,
RectShapeAddedArg,
ScaleChangedArg,
Tool,
@ -175,6 +179,16 @@ export class CanvasStateApi {
this.store.dispatch(imRectAdded(arg));
}
};
onRectShapeAdded2 = (arg: { id: string; rectShape: RectShape }, entityType: CanvasEntity['type']) => {
log.debug('Rect shape added');
if (entityType === 'layer') {
this.store.dispatch(layerRectShapeAdded2(arg));
} else if (entityType === 'regional_guidance') {
this.store.dispatch(rgRectShapeAdded2(arg));
} else if (entityType === 'inpaint_mask') {
this.store.dispatch(imRectShapeAdded2(arg));
}
};
onBboxTransformed = (bbox: IRect) => {
log.debug('Generation bbox transformed');
this.store.dispatch(bboxChanged(bbox));

View File

@ -5,7 +5,6 @@ import {
BRUSH_BORDER_OUTER_COLOR,
BRUSH_ERASER_BORDER_WIDTH,
} from 'features/controlLayers/konva/constants';
import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming';
import Konva from 'konva';
export class CanvasTool {
@ -23,10 +22,10 @@ export class CanvasTool {
innerBorderCircle: Konva.Circle;
outerBorderCircle: Konva.Circle;
};
rect: {
group: Konva.Group;
fillRect: Konva.Rect;
};
// rect: {
// group: Konva.Group;
// fillRect: Konva.Rect;
// };
constructor(manager: CanvasManager) {
this.manager = manager;
@ -83,17 +82,17 @@ export class CanvasTool {
this.eraser.group.add(this.eraser.outerBorderCircle);
this.group.add(this.eraser.group);
// Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
this.rect = {
group: new Konva.Group(),
fillRect: new Konva.Rect({
id: PREVIEW_RECT_ID,
listening: false,
strokeEnabled: false,
}),
};
this.rect.group.add(this.rect.fillRect);
this.group.add(this.rect.group);
// // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
// this.rect = {
// group: new Konva.Group(),
// fillRect: new Konva.Rect({
// id: PREVIEW_RECT_ID,
// listening: false,
// strokeEnabled: false,
// }),
// };
// this.rect.group.add(this.rect.fillRect);
// this.group.add(this.rect.group);
}
scaleTool = () => {
@ -189,7 +188,7 @@ export class CanvasTool {
this.brush.group.visible(true);
this.eraser.group.visible(false);
this.rect.group.visible(false);
// this.rect.group.visible(false);
} else if (cursorPos && tool === 'eraser') {
const scale = stage.scaleX();
// Update the fill circle
@ -215,23 +214,23 @@ export class CanvasTool {
this.brush.group.visible(false);
this.eraser.group.visible(true);
this.rect.group.visible(false);
} else if (cursorPos && lastMouseDownPos && tool === 'rect') {
this.rect.fillRect.setAttrs({
x: Math.min(cursorPos.x, lastMouseDownPos.x),
y: Math.min(cursorPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y),
fill: rgbaColorToString(currentFill),
visible: true,
});
this.brush.group.visible(false);
this.eraser.group.visible(false);
this.rect.group.visible(true);
// this.rect.group.visible(false);
// } else if (cursorPos && lastMouseDownPos && tool === 'rect') {
// this.rect.fillRect.setAttrs({
// x: Math.min(cursorPos.x, lastMouseDownPos.x),
// y: Math.min(cursorPos.y, lastMouseDownPos.y),
// width: Math.abs(cursorPos.x - lastMouseDownPos.x),
// height: Math.abs(cursorPos.y - lastMouseDownPos.y),
// fill: rgbaColorToString(currentFill),
// visible: true,
// });
// this.brush.group.visible(false);
// this.eraser.group.visible(false);
// this.rect.group.visible(true);
} else {
this.brush.group.visible(false);
this.eraser.group.visible(false);
this.rect.group.visible(false);
// this.rect.group.visible(false);
}
}
}

View File

@ -16,7 +16,7 @@ import { clamp } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid';
import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants';
import { getBrushLineId, getEraserLineId } from './naming';
import { getBrushLineId, getEraserLineId, getRectShapeId } from './naming';
/**
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
@ -267,6 +267,21 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}
setLastAddedPoint(pos);
}
if (toolState.selected === 'rect') {
if (selectedEntityAdapter.getDrawingBuffer()) {
selectedEntityAdapter.finalizeDrawingBuffer();
}
await selectedEntityAdapter.setDrawingBuffer({
id: getRectShapeId(selectedEntityAdapter.id, uuidv4()),
type: 'rect_shape',
x: pos.x - selectedEntity.x,
y: pos.y - selectedEntity.y,
width: 0,
height: 0,
color: getCurrentFill(),
});
}
}
manager.preview.tool.render();
});
@ -284,7 +299,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
isDrawableEntity(selectedEntity) &&
selectedEntityAdapter &&
isDrawableEntityAdapter(selectedEntityAdapter) &&
!getSpaceKey()
!getSpaceKey() &&
getIsPrimaryMouseDown(e)
) {
const toolState = getToolState();
@ -307,22 +323,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}
if (toolState.selected === 'rect') {
const lastMouseDownPos = getLastMouseDownPos();
if (lastMouseDownPos) {
onRectShapeAdded(
{
id: selectedEntity.id,
rect: {
x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x),
y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y),
width: Math.abs(pos.x - lastMouseDownPos.x),
height: Math.abs(pos.y - lastMouseDownPos.y),
},
color: getCurrentFill(),
},
selectedEntity.type
);
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer?.type === 'rect_shape') {
selectedEntityAdapter.finalizeDrawingBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
// const lastMouseDownPos = getLastMouseDownPos();
// if (lastMouseDownPos) {
// onRectShapeAdded(
// {
// id: selectedEntity.id,
// rect: {
// x: Math.min(pos.x - selectedEntity.x, lastMouseDownPos.x - selectedEntity.x),
// y: Math.min(pos.y - selectedEntity.y, lastMouseDownPos.y - selectedEntity.y),
// width: Math.abs(pos.x - lastMouseDownPos.x),
// height: Math.abs(pos.y - lastMouseDownPos.y),
// },
// color: getCurrentFill(),
// },
// selectedEntity.type
// );
// }
}
setLastMouseDownPos(null);
@ -415,6 +437,19 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
setLastAddedPoint(pos);
}
}
if (toolState.selected === 'rect') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer) {
if (drawingBuffer.type === 'rect_shape') {
drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x;
drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y;
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
}
}
}
manager.preview.tool.render();
});
@ -446,6 +481,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
drawingBuffer.points.push(pos.x - selectedEntity.x, pos.y - selectedEntity.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
selectedEntityAdapter.finalizeDrawingBuffer();
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') {
drawingBuffer.width = pos.x - selectedEntity.x - drawingBuffer.x;
drawingBuffer.height = pos.y - selectedEntity.y - drawingBuffer.y;
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
selectedEntityAdapter.finalizeDrawingBuffer();
}
}

View File

@ -349,10 +349,13 @@ export const {
stagingAreaPreviousImageSelected,
layerBrushLineAdded2,
layerEraserLineAdded2,
layerRectShapeAdded2,
rgBrushLineAdded2,
rgEraserLineAdded2,
rgRectShapeAdded2,
imBrushLineAdded2,
imEraserLineAdded2,
imRectShapeAdded2,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;

View File

@ -5,6 +5,7 @@ import type {
CanvasV2State,
EraserLine,
InpaintMaskEntity,
RectShape,
ScaleChangedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types';
@ -99,6 +100,12 @@ export const inpaintMaskReducers = {
state.inpaintMask.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
imRectShapeAdded2: (state, action: PayloadAction<{ rectShape: RectShape }>) => {
const { rectShape } = action.payload;
state.inpaintMask.objects.push(rectShape);
state.inpaintMask.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
imEraserLineAdded: {
reducer: (state, action: PayloadAction<Omit<EraserLineAddedArg, 'id'> & { lineId: string }>) => {
const { points, lineId, width, clip } = action.payload;

View File

@ -15,6 +15,7 @@ import type {
ImageObjectAddedArg,
LayerEntity,
PointAddedToLineArg,
RectShape,
RectShapeAddedArg,
ScaleChangedArg,
} from './types';
@ -176,6 +177,17 @@ export const layersReducers = {
layer.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
layerRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => {
const { id, rectShape } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(rectShape);
layer.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width, clip } = action.payload;

View File

@ -7,6 +7,7 @@ import type {
CLIPVisionModelV2,
EraserLine,
IPMethodV2,
RectShape,
ScaleChangedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types';
@ -383,6 +384,17 @@ export const regionsReducers = {
rg.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
rgRectShapeAdded2: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => {
const { id, rectShape } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(rectShape);
rg.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width, clip } = action.payload;