perf(ui): buffered drawing (wip)

This commit is contained in:
psychedelicious 2024-07-05 00:41:26 +10:00
parent be5b474f1e
commit 5c15458e15
12 changed files with 605 additions and 256 deletions

View File

@ -27,6 +27,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
const manager = new CanvasManager(stage, container, store); const manager = new CanvasManager(stage, container, store);
setCanvasManager(manager); setCanvasManager(manager);
console.log(manager);
const cleanup = manager.initialize(); const cleanup = manager.initialize();
return cleanup; return cleanup;
}, [asPreview, container, stage, store]); }, [asPreview, container, stage, store]);

View File

@ -6,7 +6,8 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming'; import { getObjectGroupId, INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { type InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types'; import type { BrushLine, EraserLine, InpaintMaskEntity } from 'features/controlLayers/store/types';
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -20,8 +21,10 @@ export class CanvasInpaintMask {
compositingRect: Konva.Rect; compositingRect: Konva.Rect;
transformer: Konva.Transformer; transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
private drawingBuffer: BrushLine | EraserLine | null;
private prevInpaintMaskState: InpaintMaskEntity;
constructor(manager: CanvasManager) { constructor(entity: InpaintMaskEntity, manager: CanvasManager) {
this.id = INPAINT_MASK_LAYER_ID; this.id = INPAINT_MASK_LAYER_ID;
this.manager = manager; this.manager = manager;
this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID }); this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID });
@ -56,12 +59,42 @@ export class CanvasInpaintMask {
this.compositingRect = new Konva.Rect({ listening: false }); this.compositingRect = new Konva.Rect({ listening: false });
this.group.add(this.compositingRect); this.group.add(this.compositingRect);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null;
this.prevInpaintMaskState = entity;
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.layer.destroy();
} }
getDrawingBuffer() {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
this.drawingBuffer = obj;
if (this.drawingBuffer) {
if (this.drawingBuffer.type === 'brush_line') {
this.drawingBuffer.color = RGBA_RED;
}
await this.renderObject(this.drawingBuffer, true);
this.updateGroup(true, this.prevInpaintMaskState);
}
}
finalizeDrawingBuffer() {
if (!this.drawingBuffer) {
return;
}
if (this.drawingBuffer.type === 'brush_line') {
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');
}
this.setDrawingBuffer(null);
}
async render(inpaintMaskState: InpaintMaskEntity) { async render(inpaintMaskState: InpaintMaskEntity) {
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.group.setAttrs({
@ -84,51 +117,62 @@ export class CanvasInpaintMask {
} }
for (const obj of inpaintMaskState.objects) { for (const obj of inpaintMaskState.objects) {
if (obj.type === 'brush_line') { didDraw = await this.renderObject(obj);
let brushLine = this.objects.get(obj.id); }
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
if (!brushLine) { this.updateGroup(didDraw, inpaintMaskState);
brushLine = new CanvasBrushLine(obj); this.prevInpaintMaskState = inpaintMaskState;
this.objects.set(brushLine.id, brushLine); }
this.objectsGroup.add(brushLine.konvaLineGroup);
didDraw = true; private async renderObject(obj: InpaintMaskEntity['objects'][number], force = false): Promise<boolean> {
} else { if (obj.type === 'brush_line') {
if (brushLine.update(obj)) { let brushLine = this.objects.get(obj.id);
didDraw = true; assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
}
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
return true;
} else {
if (brushLine.update(obj, force)) {
return true;
} }
} else if (obj.type === 'eraser_line') { }
let eraserLine = this.objects.get(obj.id); } else if (obj.type === 'eraser_line') {
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
if (!eraserLine) { if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj); eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup); this.objectsGroup.add(eraserLine.konvaLineGroup);
didDraw = true; return true;
} else { } else {
if (eraserLine.update(obj)) { if (eraserLine.update(obj, force)) {
didDraw = true; return true;
}
} }
} else if (obj.type === 'rect_shape') { }
let rect = this.objects.get(obj.id); } else if (obj.type === 'rect_shape') {
assert(rect instanceof CanvasRect || rect === undefined); let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
if (!rect) { if (!rect) {
rect = new CanvasRect(obj); rect = new CanvasRect(obj);
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect); this.objectsGroup.add(rect.konvaRect);
didDraw = true; return true;
} else { } else {
if (rect.update(obj)) { if (rect.update(obj, force)) {
didDraw = true; return true;
}
} }
} }
} }
return false;
}
updateGroup(didDraw: boolean, inpaintMaskState: InpaintMaskEntity) {
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
if (this.layer.visible() !== inpaintMaskState.isEnabled) { if (this.layer.visible() !== inpaintMaskState.isEnabled) {
this.layer.visible(inpaintMaskState.isEnabled); this.layer.visible(inpaintMaskState.isEnabled);
@ -155,10 +199,6 @@ export class CanvasInpaintMask {
}); });
} }
this.updateGroup(didDraw);
}
updateGroup(didDraw: boolean) {
const isSelected = this.manager.stateApi.getIsSelected(this.id); const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected; const selectedTool = this.manager.stateApi.getToolState().selected;

View File

@ -5,7 +5,8 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect'; import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; import type { BrushLine, EraserLine, LayerEntity } from 'features/controlLayers/store/types';
import { isDrawingTool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -15,8 +16,11 @@ export class CanvasLayer {
manager: CanvasManager; manager: CanvasManager;
layer: Konva.Layer; layer: Konva.Layer;
group: Konva.Group; group: Konva.Group;
objectsGroup: Konva.Group;
transformer: Konva.Transformer; transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
private drawingBuffer: BrushLine | EraserLine | null;
private prevLayerState: LayerEntity;
constructor(entity: LayerEntity, manager: CanvasManager) { constructor(entity: LayerEntity, manager: CanvasManager) {
this.id = entity.id; this.id = entity.id;
@ -30,6 +34,8 @@ export class CanvasLayer {
id: getObjectGroupId(this.layer.id(), uuidv4()), id: getObjectGroupId(this.layer.id(), uuidv4()),
listening: false, listening: false,
}); });
this.objectsGroup = new Konva.Group({});
this.group.add(this.objectsGroup);
this.layer.add(this.group); this.layer.add(this.group);
this.transformer = new Konva.Transformer({ this.transformer = new Konva.Transformer({
@ -52,12 +58,40 @@ export class CanvasLayer {
this.layer.add(this.transformer); this.layer.add(this.transformer);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null;
this.prevLayerState = entity;
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.layer.destroy();
} }
getDrawingBuffer() {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
if (obj) {
this.drawingBuffer = obj;
await this.renderObject(this.drawingBuffer, true);
this.updateGroup(true, this.prevLayerState);
} else {
this.drawingBuffer = null;
}
}
finalizeDrawingBuffer() {
if (!this.drawingBuffer) {
return;
}
if (this.drawingBuffer.type === 'brush_line') {
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');
}
this.setDrawingBuffer(null);
}
async render(layerState: LayerEntity) { async render(layerState: LayerEntity) {
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.group.setAttrs({
@ -72,7 +106,7 @@ export class CanvasLayer {
const objectIds = layerState.objects.map(mapId); const objectIds = layerState.objects.map(mapId);
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const object of this.objects.values()) { for (const object of this.objects.values()) {
if (!objectIds.includes(object.id)) { if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
this.objects.delete(object.id); this.objects.delete(object.id);
object.destroy(); object.destroy();
didDraw = true; didDraw = true;
@ -80,67 +114,11 @@ export class CanvasLayer {
} }
for (const obj of layerState.objects) { for (const obj of layerState.objects) {
if (obj.type === 'brush_line') { didDraw = await this.renderObject(obj);
let brushLine = this.objects.get(obj.id); }
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
if (!brushLine) { if (this.drawingBuffer) {
brushLine = new CanvasBrushLine(obj); didDraw = await this.renderObject(this.drawingBuffer);
this.objects.set(brushLine.id, brushLine);
this.group.add(brushLine.konvaLineGroup);
didDraw = true;
} else {
if (brushLine.update(obj)) {
didDraw = true;
}
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine);
this.group.add(eraserLine.konvaLineGroup);
didDraw = true;
} else {
if (eraserLine.update(obj)) {
didDraw = true;
}
}
} else if (obj.type === 'rect_shape') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
if (!rect) {
rect = new CanvasRect(obj);
this.objects.set(rect.id, rect);
this.group.add(rect.konvaRect);
didDraw = true;
} else {
if (rect.update(obj)) {
didDraw = true;
}
}
} else if (obj.type === 'image') {
let image = this.objects.get(obj.id);
assert(image instanceof CanvasImage || image === undefined);
if (!image) {
image = await new CanvasImage(obj, {
onLoad: () => {
this.updateGroup(true);
},
});
this.objects.set(image.id, image);
this.group.add(image.konvaImageGroup);
await image.updateImageSource(obj.image.name);
} else {
if (await image.update(obj)) {
didDraw = true;
}
}
}
} }
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
@ -151,10 +129,78 @@ export class CanvasLayer {
this.group.opacity(layerState.opacity); this.group.opacity(layerState.opacity);
// The layer only listens when using the move tool - otherwise the stage is handling mouse events // The layer only listens when using the move tool - otherwise the stage is handling mouse events
this.updateGroup(didDraw); this.updateGroup(didDraw, this.prevLayerState);
this.prevLayerState = layerState;
} }
updateGroup(didDraw: boolean) { private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise<boolean> {
if (obj.type === 'brush_line') {
let brushLine = this.objects.get(obj.id);
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
return true;
} else {
if (brushLine.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup);
return true;
} else {
if (eraserLine.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'rect_shape') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
if (!rect) {
rect = new CanvasRect(obj);
this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect);
return true;
} else {
if (rect.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'image') {
let image = this.objects.get(obj.id);
assert(image instanceof CanvasImage || image === undefined);
if (!image) {
image = await new CanvasImage(obj, {
onLoad: () => {
this.updateGroup(true, this.prevLayerState);
},
});
this.objects.set(image.id, image);
this.objectsGroup.add(image.konvaImageGroup);
await image.updateImageSource(obj.image.name);
} else {
if (await image.update(obj, force)) {
return true;
}
}
}
return false;
}
updateGroup(didDraw: boolean, _: LayerEntity) {
const isSelected = this.manager.stateApi.getIsSelected(this.id); const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected; const selectedTool = this.manager.stateApi.getToolState().selected;

View File

@ -95,7 +95,7 @@ export class CanvasManager {
this.background = new CanvasBackground(this); this.background = new CanvasBackground(this);
this.stage.add(this.background.layer); this.stage.add(this.background.layer);
this.inpaintMask = new CanvasInpaintMask(this); this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
this.stage.add(this.inpaintMask.layer); this.stage.add(this.inpaintMask.layer);
this.layers = new Map(); this.layers = new Map();
@ -346,6 +346,24 @@ export class CanvasManager {
}; };
}; };
getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => {
const state = this.stateApi.getState();
const identifier = state.selectedEntityIdentifier;
if (!identifier) {
return null;
} else if (identifier.type === 'layer') {
return this.layers.get(identifier.id) ?? null;
} else if (identifier.type === 'control_adapter') {
return this.controlAdapters.get(identifier.id) ?? null;
} else if (identifier.type === 'regional_guidance') {
return this.regions.get(identifier.id) ?? null;
} else if (identifier.type === 'inpaint_mask') {
return this.inpaintMask;
} else {
return null;
}
};
getGenerationMode() { getGenerationMode() {
return getGenerationMode({ manager: this }); return getGenerationMode({ manager: this });
} }

View File

@ -6,7 +6,8 @@ import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox'; import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import { getObjectGroupId } from 'features/controlLayers/konva/naming';
import { mapId } from 'features/controlLayers/konva/util'; import { mapId } from 'features/controlLayers/konva/util';
import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types'; import type { BrushLine, EraserLine, RegionEntity } from 'features/controlLayers/store/types';
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -20,6 +21,8 @@ export class CanvasRegion {
compositingRect: Konva.Rect; compositingRect: Konva.Rect;
transformer: Konva.Transformer; transformer: Konva.Transformer;
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>; objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect>;
private drawingBuffer: BrushLine | EraserLine | null;
private prevRegionState: RegionEntity;
constructor(entity: RegionEntity, manager: CanvasManager) { constructor(entity: RegionEntity, manager: CanvasManager) {
this.id = entity.id; this.id = entity.id;
@ -56,12 +59,41 @@ export class CanvasRegion {
this.compositingRect = new Konva.Rect({ listening: false }); this.compositingRect = new Konva.Rect({ listening: false });
this.group.add(this.compositingRect); this.group.add(this.compositingRect);
this.objects = new Map(); this.objects = new Map();
this.drawingBuffer = null;
this.prevRegionState = entity;
} }
destroy(): void { destroy(): void {
this.layer.destroy(); this.layer.destroy();
} }
getDrawingBuffer() {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: BrushLine | EraserLine | null) {
this.drawingBuffer = obj;
if (this.drawingBuffer) {
if (this.drawingBuffer.type === 'brush_line') {
this.drawingBuffer.color = RGBA_RED;
}
await this.renderObject(this.drawingBuffer, true);
this.updateGroup(true, this.prevRegionState);
}
}
finalizeDrawingBuffer() {
if (!this.drawingBuffer) {
return;
}
if (this.drawingBuffer.type === 'brush_line') {
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');
}
this.setDrawingBuffer(null);
}
async render(regionState: RegionEntity) { async render(regionState: RegionEntity) {
// Update the layer's position and listening state // Update the layer's position and listening state
this.group.setAttrs({ this.group.setAttrs({
@ -84,51 +116,62 @@ export class CanvasRegion {
} }
for (const obj of regionState.objects) { for (const obj of regionState.objects) {
if (obj.type === 'brush_line') { didDraw = await this.renderObject(obj);
let brushLine = this.objects.get(obj.id); }
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
if (!brushLine) { this.updateGroup(didDraw, regionState);
brushLine = new CanvasBrushLine(obj); this.prevRegionState = regionState;
this.objects.set(brushLine.id, brushLine); }
this.objectsGroup.add(brushLine.konvaLineGroup);
didDraw = true; private async renderObject(obj: RegionEntity['objects'][number], force = false): Promise<boolean> {
} else { if (obj.type === 'brush_line') {
if (brushLine.update(obj)) { let brushLine = this.objects.get(obj.id);
didDraw = true; assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
}
if (!brushLine) {
brushLine = new CanvasBrushLine(obj);
this.objects.set(brushLine.id, brushLine);
this.objectsGroup.add(brushLine.konvaLineGroup);
return true;
} else {
if (brushLine.update(obj, force)) {
return true;
} }
} else if (obj.type === 'eraser_line') { }
let eraserLine = this.objects.get(obj.id); } else if (obj.type === 'eraser_line') {
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined); let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
if (!eraserLine) { if (!eraserLine) {
eraserLine = new CanvasEraserLine(obj); eraserLine = new CanvasEraserLine(obj);
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.objectsGroup.add(eraserLine.konvaLineGroup); this.objectsGroup.add(eraserLine.konvaLineGroup);
didDraw = true; return true;
} else { } else {
if (eraserLine.update(obj)) { if (eraserLine.update(obj, force)) {
didDraw = true; return true;
}
} }
} else if (obj.type === 'rect_shape') { }
let rect = this.objects.get(obj.id); } else if (obj.type === 'rect_shape') {
assert(rect instanceof CanvasRect || rect === undefined); let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRect || rect === undefined);
if (!rect) { if (!rect) {
rect = new CanvasRect(obj); rect = new CanvasRect(obj);
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.objectsGroup.add(rect.konvaRect); this.objectsGroup.add(rect.konvaRect);
didDraw = true; return true;
} else { } else {
if (rect.update(obj)) { if (rect.update(obj, force)) {
didDraw = true; return true;
}
} }
} }
} }
return false;
}
updateGroup(didDraw: boolean, regionState: RegionEntity) {
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
if (this.layer.visible() !== regionState.isEnabled) { if (this.layer.visible() !== regionState.isEnabled) {
this.layer.visible(regionState.isEnabled); this.layer.visible(regionState.isEnabled);
@ -141,7 +184,6 @@ export class CanvasRegion {
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(regionState.fill); const rgbColor = rgbColorToString(regionState.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity(); const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.compositingRect.setAttrs({ this.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.objectsGroup), ...getNodeBboxFast(this.objectsGroup),
@ -149,16 +191,11 @@ export class CanvasRegion {
opacity: maskOpacity, opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
globalCompositeOperation: 'source-in', globalCompositeOperation: 'source-in',
visible: true,
// This rect must always be on top of all other shapes // This rect must always be on top of all other shapes
zIndex: this.objects.size + 1, zIndex: this.objects.size + 1,
}); });
} }
this.updateGroup(didDraw);
}
updateGroup(didDraw: boolean) {
const isSelected = this.manager.stateApi.getIsSelected(this.id); const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected; const selectedTool = this.manager.stateApi.getToolState().selected;

View File

@ -19,7 +19,9 @@ import {
eraserWidthChanged, eraserWidthChanged,
imBboxChanged, imBboxChanged,
imBrushLineAdded, imBrushLineAdded,
imBrushLineAdded2,
imEraserLineAdded, imEraserLineAdded,
imEraserLineAdded2,
imImageCacheChanged, imImageCacheChanged,
imLinePointAdded, imLinePointAdded,
imRectAdded, imRectAdded,
@ -27,7 +29,9 @@ import {
imTranslated, imTranslated,
layerBboxChanged, layerBboxChanged,
layerBrushLineAdded, layerBrushLineAdded,
layerBrushLineAdded2,
layerEraserLineAdded, layerEraserLineAdded,
layerEraserLineAdded2,
layerImageCacheChanged, layerImageCacheChanged,
layerLinePointAdded, layerLinePointAdded,
layerRectAdded, layerRectAdded,
@ -35,7 +39,9 @@ import {
layerTranslated, layerTranslated,
rgBboxChanged, rgBboxChanged,
rgBrushLineAdded, rgBrushLineAdded,
rgBrushLineAdded2,
rgEraserLineAdded, rgEraserLineAdded,
rgEraserLineAdded2,
rgImageCacheChanged, rgImageCacheChanged,
rgLinePointAdded, rgLinePointAdded,
rgRectAdded, rgRectAdded,
@ -46,8 +52,10 @@ import {
} from 'features/controlLayers/store/canvasV2Slice'; } from 'features/controlLayers/store/canvasV2Slice';
import type { import type {
BboxChangedArg, BboxChangedArg,
BrushLine,
BrushLineAddedArg, BrushLineAddedArg,
CanvasEntity, CanvasEntity,
EraserLine,
EraserLineAddedArg, EraserLineAddedArg,
PointAddedToLineArg, PointAddedToLineArg,
PosChangedArg, PosChangedArg,
@ -127,6 +135,26 @@ export class CanvasStateApi {
this.store.dispatch(imEraserLineAdded(arg)); this.store.dispatch(imEraserLineAdded(arg));
} }
}; };
onBrushLineAdded2 = (arg: { id: string; brushLine: BrushLine }, entityType: CanvasEntity['type']) => {
log.debug('Brush line added');
if (entityType === 'layer') {
this.store.dispatch(layerBrushLineAdded2(arg));
} else if (entityType === 'regional_guidance') {
this.store.dispatch(rgBrushLineAdded2(arg));
} else if (entityType === 'inpaint_mask') {
this.store.dispatch(imBrushLineAdded2(arg));
}
};
onEraserLineAdded2 = (arg: { id: string; eraserLine: EraserLine }, entityType: CanvasEntity['type']) => {
log.debug('Eraser line added');
if (entityType === 'layer') {
this.store.dispatch(layerEraserLineAdded2(arg));
} else if (entityType === 'regional_guidance') {
this.store.dispatch(rgEraserLineAdded2(arg));
} else if (entityType === 'inpaint_mask') {
this.store.dispatch(imEraserLineAdded2(arg));
}
};
onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => { onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
log.debug('Point added to line'); log.debug('Point added to line');
if (entityType === 'layer') { if (entityType === 'layer') {
@ -183,23 +211,21 @@ export class CanvasStateApi {
getSelectedEntity = (): CanvasEntity | null => { getSelectedEntity = (): CanvasEntity | null => {
const state = this.getState(); const state = this.getState();
const identifier = state.selectedEntityIdentifier; const identifier = state.selectedEntityIdentifier;
let selectedEntity: CanvasEntity | null = null;
if (!identifier) { if (!identifier) {
selectedEntity = null; return null;
} else if (identifier.type === 'layer') { } else if (identifier.type === 'layer') {
selectedEntity = state.layers.entities.find((i) => i.id === identifier.id) ?? null; return state.layers.entities.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'control_adapter') { } else if (identifier.type === 'control_adapter') {
selectedEntity = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null; return state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'ip_adapter') { } else if (identifier.type === 'ip_adapter') {
selectedEntity = state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null; return state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'regional_guidance') { } else if (identifier.type === 'regional_guidance') {
selectedEntity = state.regions.entities.find((i) => i.id === identifier.id) ?? null; return state.regions.entities.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'inpaint_mask') { } else if (identifier.type === 'inpaint_mask') {
selectedEntity = state.inpaintMask; return state.inpaintMask;
} else { } else {
selectedEntity = null; return null;
} }
return selectedEntity;
}; };
getCurrentFill = () => { getCurrentFill = () => {

View File

@ -1,19 +1,14 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getScaledCursorPosition } from 'features/controlLayers/konva/util'; import { getScaledCursorPosition } from 'features/controlLayers/konva/util';
import type { CanvasEntity } from 'features/controlLayers/store/types'; import type { CanvasEntity, CanvasV2State, Position } from 'features/controlLayers/store/types';
import { isDrawableEntity, isDrawableEntityAdapter } from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid';
import { import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from './constants';
BRUSH_SPACING_TARGET_SCALE, import { getBrushLineId, PREVIEW_TOOL_GROUP_ID } from './naming';
CANVAS_SCALE_BY,
MAX_BRUSH_SPACING_PX,
MAX_CANVAS_SCALE,
MIN_BRUSH_SPACING_PX,
MIN_CANVAS_SCALE,
} from './constants';
import { PREVIEW_TOOL_GROUP_ID } from './naming';
/** /**
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
@ -21,10 +16,7 @@ import { PREVIEW_TOOL_GROUP_ID } from './naming';
* @param stage The konva stage * @param stage The konva stage
* @param setLastCursorPos The callback to store the cursor pos * @param setLastCursorPos The callback to store the cursor pos
*/ */
const updateLastCursorPos = ( const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => {
stage: Konva.Stage,
setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']
) => {
const pos = getScaledCursorPosition(stage); const pos = getScaledCursorPosition(stage);
if (!pos) { if (!pos) {
return null; return null;
@ -61,24 +53,18 @@ const maybeAddNextPoint = (
setLastAddedPoint: CanvasManager['stateApi']['setLastAddedPoint'], setLastAddedPoint: CanvasManager['stateApi']['setLastAddedPoint'],
onPointAddedToLine: CanvasManager['stateApi']['onPointAddedToLine'] onPointAddedToLine: CanvasManager['stateApi']['onPointAddedToLine']
) => { ) => {
const isDrawableEntity = if (!isDrawableEntity(selectedEntity)) {
selectedEntity?.type === 'regional_guidance' ||
selectedEntity?.type === 'layer' ||
selectedEntity?.type === 'inpaint_mask';
if (!isDrawableEntity) {
return; return;
} }
// Continue the last line // Continue the last line
const lastAddedPoint = getLastAddedPoint(); const lastAddedPoint = getLastAddedPoint();
const toolState = getToolState(); const toolState = getToolState();
const minSpacingPx = clamp( const minSpacingPx =
toolState.selected === 'brush' toolState.selected === 'brush'
? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE ? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE
: toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE, : toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE;
MIN_BRUSH_SPACING_PX,
MAX_BRUSH_SPACING_PX
);
if (lastAddedPoint) { if (lastAddedPoint) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) { if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) {
@ -95,8 +81,29 @@ const maybeAddNextPoint = (
); );
}; };
const getNextPoint = (
currentPos: Position,
toolState: CanvasV2State['tool'],
lastAddedPoint: Position | null
): Position | null => {
// Continue the last line
const minSpacingPx =
toolState.selected === 'brush'
? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE
: toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE;
if (lastAddedPoint) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) {
return null;
}
}
return currentPos;
};
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => { export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const { stage, stateApi } = manager; const { stage, stateApi, getSelectedEntityAdapter } = manager;
const { const {
getToolState, getToolState,
getCurrentFill, getCurrentFill,
@ -132,17 +139,21 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}); });
//#region mousedown //#region mousedown
stage.on('mousedown', (e) => { stage.on('mousedown', async (e) => {
setIsMouseDown(true); setIsMouseDown(true);
const toolState = getToolState(); const toolState = getToolState();
const pos = updateLastCursorPos(stage, setLastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedEntity = getSelectedEntity(); const selectedEntity = getSelectedEntity();
const isDrawableEntity = const selectedEntityAdapter = getSelectedEntityAdapter();
selectedEntity?.type === 'regional_guidance' ||
selectedEntity?.type === 'layer' ||
selectedEntity?.type === 'inpaint_mask';
if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { if (
pos &&
selectedEntity &&
isDrawableEntity(selectedEntity) &&
selectedEntityAdapter &&
isDrawableEntityAdapter(selectedEntityAdapter) &&
!getSpaceKey()
) {
setIsDrawing(true); setIsDrawing(true);
setLastMouseDownPos(pos); setLastMouseDownPos(pos);
@ -180,21 +191,37 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
); );
} }
} else { } else {
onBrushLineAdded( if (selectedEntityAdapter.getDrawingBuffer()) {
{ selectedEntityAdapter.finalizeDrawingBuffer();
id: selectedEntity.id, }
points: [ await selectedEntityAdapter.setDrawingBuffer({
pos.x - selectedEntity.x, id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
pos.y - selectedEntity.y, type: 'brush_line',
pos.x - selectedEntity.x, points: [
pos.y - selectedEntity.y, pos.x - selectedEntity.x,
], pos.y - selectedEntity.y,
color: getCurrentFill(), pos.x - selectedEntity.x,
width: toolState.brush.width, pos.y - selectedEntity.y,
clip, ],
}, strokeWidth: toolState.brush.width,
selectedEntity.type color: getCurrentFill(),
); clip,
});
// onBrushLineAdded(
// {
// id: selectedEntity.id,
// points: [
// pos.x - selectedEntity.x,
// pos.y - selectedEntity.y,
// pos.x - selectedEntity.x,
// pos.y - selectedEntity.y,
// ],
// color: getCurrentFill(),
// width: toolState.brush.width,
// clip,
// },
// selectedEntity.type
// );
} }
setLastAddedPoint(pos); setLastAddedPoint(pos);
} }
@ -231,20 +258,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
); );
} }
} else { } else {
onEraserLineAdded( if (selectedEntityAdapter.getDrawingBuffer()) {
{ selectedEntityAdapter.finalizeDrawingBuffer();
id: selectedEntity.id, }
points: [ await selectedEntityAdapter.setDrawingBuffer({
pos.x - selectedEntity.x, id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
pos.y - selectedEntity.y, type: 'eraser_line',
pos.x - selectedEntity.x, points: [
pos.y - selectedEntity.y, pos.x - selectedEntity.x,
], pos.y - selectedEntity.y,
width: toolState.eraser.width, pos.x - selectedEntity.x,
clip, pos.y - selectedEntity.y,
}, ],
selectedEntity.type strokeWidth: toolState.eraser.width,
); clip,
});
// onEraserLineAdded(
// {
// id: selectedEntity.id,
// points: [
// pos.x - selectedEntity.x,
// pos.y - selectedEntity.y,
// pos.x - selectedEntity.x,
// pos.y - selectedEntity.y,
// ],
// width: toolState.eraser.width,
// clip,
// },
// selectedEntity.type
// );
} }
setLastAddedPoint(pos); setLastAddedPoint(pos);
} }
@ -253,18 +296,40 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}); });
//#region mouseup //#region mouseup
stage.on('mouseup', () => { stage.on('mouseup', async () => {
setIsMouseDown(false); setIsMouseDown(false);
const pos = getLastCursorPos(); const pos = getLastCursorPos();
const selectedEntity = getSelectedEntity(); const selectedEntity = getSelectedEntity();
const isDrawableEntity = const selectedEntityAdapter = getSelectedEntityAdapter();
selectedEntity?.type === 'regional_guidance' ||
selectedEntity?.type === 'layer' ||
selectedEntity?.type === 'inpaint_mask';
if (pos && selectedEntity && isDrawableEntity && !getSpaceKey()) { if (
pos &&
selectedEntity &&
isDrawableEntity(selectedEntity) &&
selectedEntityAdapter &&
isDrawableEntityAdapter(selectedEntityAdapter) &&
!getSpaceKey()
) {
const toolState = getToolState(); const toolState = getToolState();
if (toolState.selected === 'brush') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer?.type === 'brush_line') {
selectedEntityAdapter.finalizeDrawingBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
}
if (toolState.selected === 'eraser') {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer?.type === 'eraser_line') {
selectedEntityAdapter.finalizeDrawingBuffer();
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
}
if (toolState.selected === 'rect') { if (toolState.selected === 'rect') {
const lastMouseDownPos = getLastMouseDownPos(); const lastMouseDownPos = getLastMouseDownPos();
if (lastMouseDownPos) { if (lastMouseDownPos) {
@ -292,32 +357,48 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
}); });
//#region mousemove //#region mousemove
stage.on('mousemove', () => { stage.on('mousemove', async () => {
const toolState = getToolState(); const toolState = getToolState();
const pos = updateLastCursorPos(stage, setLastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedEntity = getSelectedEntity(); const selectedEntity = getSelectedEntity();
const selectedEntityAdapter = getSelectedEntityAdapter();
stage stage
.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`) .findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)
?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser'); ?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser');
const isDrawableEntity = if (
selectedEntity?.type === 'regional_guidance' || pos &&
selectedEntity?.type === 'layer' || selectedEntity &&
selectedEntity?.type === 'inpaint_mask'; isDrawableEntity(selectedEntity) &&
selectedEntityAdapter &&
if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { isDrawableEntityAdapter(selectedEntityAdapter) &&
!getSpaceKey() &&
getIsMouseDown()
) {
if (toolState.selected === 'brush') { if (toolState.selected === 'brush') {
if (getIsDrawing()) { if (getIsDrawing()) {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer?.type === 'brush_line') {
const lastAddedPoint = getLastAddedPoint();
const nextPoint = getNextPoint(pos, toolState, lastAddedPoint);
if (nextPoint) {
drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
setLastAddedPoint(nextPoint);
}
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
// Continue the last line // Continue the last line
maybeAddNextPoint( // maybeAddNextPoint(
selectedEntity, // selectedEntity,
pos, // pos,
getToolState, // getToolState,
getLastAddedPoint, // getLastAddedPoint,
setLastAddedPoint, // setLastAddedPoint,
onPointAddedToLine // onPointAddedToLine
); // );
} else { } else {
const bbox = getBbox(); const bbox = getBbox();
const settings = getSettings(); const settings = getSettings();
@ -353,15 +434,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
if (toolState.selected === 'eraser') { if (toolState.selected === 'eraser') {
if (getIsDrawing()) { if (getIsDrawing()) {
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
if (drawingBuffer?.type === 'eraser_line') {
const lastAddedPoint = getLastAddedPoint();
const nextPoint = getNextPoint(pos, toolState, lastAddedPoint);
if (nextPoint) {
drawingBuffer.points.push(nextPoint.x - selectedEntity.x, nextPoint.y - selectedEntity.y);
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
setLastAddedPoint(nextPoint);
}
} else {
await selectedEntityAdapter.setDrawingBuffer(null);
}
// Continue the last line // Continue the last line
maybeAddNextPoint( // maybeAddNextPoint(
selectedEntity, // selectedEntity,
pos, // pos,
getToolState, // getToolState,
getLastAddedPoint, // getLastAddedPoint,
setLastAddedPoint, // setLastAddedPoint,
onPointAddedToLine // onPointAddedToLine
); // );
} else { } else {
const bbox = getBbox(); const bbox = getBbox();
const settings = getSettings(); const settings = getSettings();
@ -407,12 +501,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
const toolState = getToolState(); const toolState = getToolState();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
const isDrawableEntity =
selectedEntity?.type === 'regional_guidance' ||
selectedEntity?.type === 'layer' ||
selectedEntity?.type === 'inpaint_mask';
if (pos && selectedEntity && isDrawableEntity && !getSpaceKey() && getIsMouseDown()) { if (pos && selectedEntity && isDrawableEntity(selectedEntity) && !getSpaceKey() && getIsMouseDown()) {
if (getIsMouseDown()) { if (getIsMouseDown()) {
if (toolState.selected === 'brush') { if (toolState.selected === 'brush') {
onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type); onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type);

View File

@ -347,6 +347,12 @@ export const {
stagingAreaCanceledStaging, stagingAreaCanceledStaging,
stagingAreaNextImageSelected, stagingAreaNextImageSelected,
stagingAreaPreviousImageSelected, stagingAreaPreviousImageSelected,
layerBrushLineAdded2,
layerEraserLineAdded2,
rgBrushLineAdded2,
rgEraserLineAdded2,
imBrushLineAdded2,
imEraserLineAdded2,
} = canvasV2Slice.actions; } = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;

View File

@ -1,6 +1,12 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CanvasV2State, InpaintMaskEntity, ScaleChangedArg } from 'features/controlLayers/store/types'; import type {
BrushLine,
CanvasV2State,
EraserLine,
InpaintMaskEntity,
ScaleChangedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
@ -81,6 +87,18 @@ export const inpaintMaskReducers = {
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
imBrushLineAdded2: (state, action: PayloadAction<{ brushLine: BrushLine }>) => {
const { brushLine } = action.payload;
state.inpaintMask.objects.push(brushLine);
state.inpaintMask.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
imEraserLineAdded2: (state, action: PayloadAction<{ eraserLine: EraserLine }>) => {
const { eraserLine } = action.payload;
state.inpaintMask.objects.push(eraserLine);
state.inpaintMask.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
imEraserLineAdded: { imEraserLineAdded: {
reducer: (state, action: PayloadAction<Omit<EraserLineAddedArg, 'id'> & { lineId: string }>) => { reducer: (state, action: PayloadAction<Omit<EraserLineAddedArg, 'id'> & { lineId: string }>) => {
const { points, lineId, width, clip } = action.payload; const { points, lineId, width, clip } = action.payload;

View File

@ -7,8 +7,10 @@ import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type {
BrushLine,
BrushLineAddedArg, BrushLineAddedArg,
CanvasV2State, CanvasV2State,
EraserLine,
EraserLineAddedArg, EraserLineAddedArg,
ImageObjectAddedArg, ImageObjectAddedArg,
LayerEntity, LayerEntity,
@ -152,6 +154,28 @@ export const layersReducers = {
moveToStart(state.layers.entities, layer); moveToStart(state.layers.entities, layer);
state.layers.imageCache = null; state.layers.imageCache = null;
}, },
layerBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => {
const { id, brushLine } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(brushLine);
layer.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
layerEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => {
const { id, eraserLine } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push(eraserLine);
layer.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
layerBrushLineAdded: { layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width, clip } = action.payload; const { id, points, lineId, color, width, clip } = action.payload;

View File

@ -1,7 +1,14 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2, ScaleChangedArg } from 'features/controlLayers/store/types'; import type {
BrushLine,
CanvasV2State,
CLIPVisionModelV2,
EraserLine,
IPMethodV2,
ScaleChangedArg,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims, RGBA_RED } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
@ -354,6 +361,28 @@ export const regionsReducers = {
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
rgBrushLineAdded2: (state, action: PayloadAction<{ id: string; brushLine: BrushLine }>) => {
const { id, brushLine } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(brushLine);
rg.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
rgEraserLineAdded2: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => {
const { id, eraserLine } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push(eraserLine);
rg.bboxNeedsUpdate = true;
state.layers.imageCache = null;
},
rgEraserLineAdded: { rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width, clip } = action.payload; const { id, points, lineId, width, clip } = action.payload;

View File

@ -1,3 +1,7 @@
import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter';
import { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask';
import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion';
import { getImageObjectId } from 'features/controlLayers/konva/naming'; import { getImageObjectId } from 'features/controlLayers/konva/naming';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
@ -924,3 +928,13 @@ export type RemoveIndexString<T> = {
}; };
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
export function isDrawableEntity(entity: CanvasEntity): entity is LayerEntity | RegionEntity | InpaintMaskEntity {
return entity.type === 'layer' || entity.type === 'regional_guidance' || entity.type === 'inpaint_mask';
}
export function isDrawableEntityAdapter(
adapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask
): adapter is CanvasLayer | CanvasRegion | CanvasInpaintMask {
return adapter instanceof CanvasLayer || adapter instanceof CanvasRegion || adapter instanceof CanvasInpaintMask;
}