mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): organize konva state and files
This commit is contained in:
parent
0b5d20c9f0
commit
47b94d563c
@ -1,12 +1,16 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { logger } from 'app/logging/logger';
|
||||||
|
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
||||||
import { useAppStore } from 'app/store/storeHooks';
|
import { useAppStore } from 'app/store/storeHooks';
|
||||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||||
import { initializeRenderer } from 'features/controlLayers/konva/renderers/renderer';
|
import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const log = logger('konva');
|
||||||
|
|
||||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||||
Konva.showWarnings = false;
|
Konva.showWarnings = false;
|
||||||
|
|
||||||
@ -15,7 +19,25 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
|||||||
const dpr = useDevicePixelRatio({ round: false });
|
const dpr = useDevicePixelRatio({ round: false });
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const cleanup = initializeRenderer(store, stage, container);
|
/**
|
||||||
|
* Logs a message to the console if debugging is enabled.
|
||||||
|
*/
|
||||||
|
const logIfDebugging = (message: string) => {
|
||||||
|
if ($isDebugging.get()) {
|
||||||
|
log.debug(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logIfDebugging('Initializing renderer');
|
||||||
|
if (!container) {
|
||||||
|
// Nothing to clean up
|
||||||
|
logIfDebugging('No stage container, skipping initialization');
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new KonvaNodeManager(stage, container, store, logIfDebugging);
|
||||||
|
setNodeManager(manager);
|
||||||
|
const cleanup = manager.initialize();
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [asPreview, container, stage, store]);
|
}, [asPreview, container, stage, store]);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||||
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
const baseGridLineColor = getArbitraryBaseColor(27);
|
const baseGridLineColor = getArbitraryBaseColor(27);
|
||||||
@ -30,19 +31,21 @@ const getGridSpacing = (scale: number): number => {
|
|||||||
|
|
||||||
export class CanvasBackground {
|
export class CanvasBackground {
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
|
manager: KonvaNodeManager;
|
||||||
|
|
||||||
constructor() {
|
constructor(manager: KonvaNodeManager) {
|
||||||
|
this.manager = manager;
|
||||||
this.layer = new Konva.Layer({ listening: false });
|
this.layer = new Konva.Layer({ listening: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBackground(stage: Konva.Stage): void {
|
renderBackground() {
|
||||||
this.layer.zIndex(0);
|
this.layer.zIndex(0);
|
||||||
const scale = stage.scaleX();
|
const scale = this.manager.stage.scaleX();
|
||||||
const gridSpacing = getGridSpacing(scale);
|
const gridSpacing = getGridSpacing(scale);
|
||||||
const x = stage.x();
|
const x = this.manager.stage.x();
|
||||||
const y = stage.y();
|
const y = this.manager.stage.y();
|
||||||
const width = stage.width();
|
const width = this.manager.stage.width();
|
||||||
const height = stage.height();
|
const height = this.manager.stage.height();
|
||||||
const stageRect = {
|
const stageRect = {
|
||||||
x1: 0,
|
x1: 0,
|
||||||
y1: 0,
|
y1: 0,
|
@ -2,19 +2,19 @@ import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMult
|
|||||||
import {
|
import {
|
||||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||||
PREVIEW_GENERATION_BBOX_GROUP,
|
PREVIEW_GENERATION_BBOX_GROUP,
|
||||||
PREVIEW_GENERATION_BBOX_TRANSFORMER
|
PREVIEW_GENERATION_BBOX_TRANSFORMER,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
|
||||||
export class CanvasBbox {
|
export class CanvasBbox {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
rect: Konva.Rect;
|
rect: Konva.Rect;
|
||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
|
manager: KonvaNodeManager;
|
||||||
|
|
||||||
ALL_ANCHORS: string[] = [
|
ALL_ANCHORS: string[] = [
|
||||||
'top-left',
|
'top-left',
|
||||||
@ -29,17 +29,11 @@ export class CanvasBbox {
|
|||||||
CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||||
NO_ANCHORS: string[] = [];
|
NO_ANCHORS: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(manager: KonvaNodeManager) {
|
||||||
getBbox: () => IRect,
|
this.manager = manager;
|
||||||
onBboxTransformed: (bbox: IRect) => void,
|
|
||||||
getShiftKey: () => boolean,
|
|
||||||
getCtrlKey: () => boolean,
|
|
||||||
getMetaKey: () => boolean,
|
|
||||||
getAltKey: () => boolean
|
|
||||||
) {
|
|
||||||
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
|
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
|
||||||
// transforming the bbox.
|
// transforming the bbox.
|
||||||
const bbox = getBbox();
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
const $aspectRatioBuffer = atom(bbox.width / bbox.height);
|
const $aspectRatioBuffer = atom(bbox.width / bbox.height);
|
||||||
|
|
||||||
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
||||||
@ -50,11 +44,11 @@ export class CanvasBbox {
|
|||||||
listening: false,
|
listening: false,
|
||||||
strokeEnabled: false,
|
strokeEnabled: false,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
...getBbox(),
|
...this.manager.stateApi.getBbox(),
|
||||||
});
|
});
|
||||||
this.rect.on('dragmove', () => {
|
this.rect.on('dragmove', () => {
|
||||||
const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64;
|
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
||||||
const oldBbox = getBbox();
|
const oldBbox = this.manager.stateApi.getBbox();
|
||||||
const newBbox: IRect = {
|
const newBbox: IRect = {
|
||||||
...oldBbox,
|
...oldBbox,
|
||||||
x: roundToMultiple(this.rect.x(), gridSize),
|
x: roundToMultiple(this.rect.x(), gridSize),
|
||||||
@ -62,7 +56,7 @@ export class CanvasBbox {
|
|||||||
};
|
};
|
||||||
this.rect.setAttrs(newBbox);
|
this.rect.setAttrs(newBbox);
|
||||||
if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) {
|
if (oldBbox.x !== newBbox.x || oldBbox.y !== newBbox.y) {
|
||||||
onBboxTransformed(newBbox);
|
this.manager.stateApi.onBboxTransformed(newBbox);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,7 +98,7 @@ export class CanvasBbox {
|
|||||||
assert(stage, 'Stage must exist');
|
assert(stage, 'Stage must exist');
|
||||||
|
|
||||||
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
||||||
const gridSize = getCtrlKey() || getMetaKey() ? 8 : 64;
|
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
||||||
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
||||||
const scaledGridSize = gridSize * stage.scaleX();
|
const scaledGridSize = gridSize * stage.scaleX();
|
||||||
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
||||||
@ -129,10 +123,10 @@ export class CanvasBbox {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alt = getAltKey();
|
const alt = this.manager.stateApi.getAltKey();
|
||||||
const ctrl = getCtrlKey();
|
const ctrl = this.manager.stateApi.getCtrlKey();
|
||||||
const meta = getMetaKey();
|
const meta = this.manager.stateApi.getMetaKey();
|
||||||
const shift = getShiftKey();
|
const shift = this.manager.stateApi.getShiftKey();
|
||||||
|
|
||||||
// Grid size depends on the modifier keys
|
// Grid size depends on the modifier keys
|
||||||
let gridSize = ctrl || meta ? 8 : 64;
|
let gridSize = ctrl || meta ? 8 : 64;
|
||||||
@ -141,7 +135,7 @@ export class CanvasBbox {
|
|||||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||||
if (getAltKey()) {
|
if (this.manager.stateApi.getAltKey()) {
|
||||||
gridSize = gridSize * 2;
|
gridSize = gridSize * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +190,7 @@ export class CanvasBbox {
|
|||||||
this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 });
|
this.rect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 });
|
||||||
|
|
||||||
// Update the bbox in internal state.
|
// Update the bbox in internal state.
|
||||||
onBboxTransformed(bbox);
|
this.manager.stateApi.onBboxTransformed(bbox);
|
||||||
|
|
||||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||||
@ -217,7 +211,10 @@ export class CanvasBbox {
|
|||||||
this.group.add(this.transformer);
|
this.group.add(this.transformer);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(bbox: CanvasV2State['bbox'], toolState: CanvasV2State['tool']) {
|
render() {
|
||||||
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
|
||||||
this.group.listening(toolState.selected === 'bbox');
|
this.group.listening(toolState.selected === 'bbox');
|
||||||
this.rect.setAttrs({
|
this.rect.setAttrs({
|
||||||
x: bbox.x,
|
x: bbox.x,
|
@ -1,6 +1,6 @@
|
|||||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||||
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
|
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
|
||||||
import type { CanvasV2State, StageAttrs } from 'features/controlLayers/store/types';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
export class CanvasDocumentSizeOverlay {
|
export class CanvasDocumentSizeOverlay {
|
||||||
@ -8,8 +8,10 @@ export class CanvasDocumentSizeOverlay {
|
|||||||
outerRect: Konva.Rect;
|
outerRect: Konva.Rect;
|
||||||
innerRect: Konva.Rect;
|
innerRect: Konva.Rect;
|
||||||
padding: number;
|
padding: number;
|
||||||
|
manager: KonvaNodeManager;
|
||||||
|
|
||||||
constructor(padding?: number) {
|
constructor(manager: KonvaNodeManager, padding?: number) {
|
||||||
|
this.manager = manager;
|
||||||
this.padding = padding ?? DOCUMENT_FIT_PADDING_PX;
|
this.padding = padding ?? DOCUMENT_FIT_PADDING_PX;
|
||||||
this.group = new Konva.Group({ id: 'document_overlay_group', listening: false });
|
this.group = new Konva.Group({ id: 'document_overlay_group', listening: false });
|
||||||
this.outerRect = new Konva.Rect({
|
this.outerRect = new Konva.Rect({
|
||||||
@ -28,14 +30,15 @@ export class CanvasDocumentSizeOverlay {
|
|||||||
this.group.add(this.innerRect);
|
this.group.add(this.innerRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(stage: Konva.Stage, document: CanvasV2State['document']) {
|
render() {
|
||||||
|
const document = this.manager.stateApi.getDocument();
|
||||||
this.group.zIndex(0);
|
this.group.zIndex(0);
|
||||||
|
|
||||||
const x = stage.x();
|
const x = this.manager.stage.x();
|
||||||
const y = stage.y();
|
const y = this.manager.stage.y();
|
||||||
const width = stage.width();
|
const width = this.manager.stage.width();
|
||||||
const height = stage.height();
|
const height = this.manager.stage.height();
|
||||||
const scale = stage.scaleX();
|
const scale = this.manager.stage.scaleX();
|
||||||
|
|
||||||
this.outerRect.setAttrs({
|
this.outerRect.setAttrs({
|
||||||
offsetX: x / scale,
|
offsetX: x / scale,
|
||||||
@ -52,16 +55,18 @@ export class CanvasDocumentSizeOverlay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fitToStage(stage: Konva.Stage, document: CanvasV2State['document'], setStageAttrs: (attrs: StageAttrs) => void) {
|
fitToStage() {
|
||||||
|
const document = this.manager.stateApi.getDocument();
|
||||||
|
|
||||||
// Fit & center the document on the stage
|
// Fit & center the document on the stage
|
||||||
const width = stage.width();
|
const width = this.manager.stage.width();
|
||||||
const height = stage.height();
|
const height = this.manager.stage.height();
|
||||||
const docWidthWithBuffer = document.width + this.padding * 2;
|
const docWidthWithBuffer = document.width + this.padding * 2;
|
||||||
const docHeightWithBuffer = document.height + this.padding * 2;
|
const docHeightWithBuffer = document.height + this.padding * 2;
|
||||||
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1);
|
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1);
|
||||||
const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale;
|
const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale;
|
||||||
const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale;
|
const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale;
|
||||||
stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
|
this.manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
|
||||||
setStageAttrs({ x, y, width, height, scale });
|
this.manager.stateApi.setStageAttrs({ x, y, width, height, scale });
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ import {
|
|||||||
RASTER_LAYER_OBJECT_GROUP_NAME,
|
RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||||
RG_LAYER_OBJECT_GROUP_NAME,
|
RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import { createBboxRect } from 'features/controlLayers/konva/renderers/objects';
|
import { createBboxRect } from 'features/controlLayers/konva/objects';
|
||||||
import { imageDataToDataURL } from 'features/controlLayers/konva/util';
|
import { imageDataToDataURL } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
BboxChangedArg,
|
BboxChangedArg,
|
@ -128,7 +128,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
|
|
||||||
//#region mouseenter
|
//#region mouseenter
|
||||||
stage.on('mouseenter', () => {
|
stage.on('mouseenter', () => {
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mousedown
|
//#region mousedown
|
||||||
@ -249,7 +249,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setLastAddedPoint(pos);
|
setLastAddedPoint(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mouseup
|
//#region mouseup
|
||||||
@ -288,7 +288,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setLastMouseDownPos(null);
|
setLastMouseDownPos(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mousemove
|
//#region mousemove
|
||||||
@ -394,7 +394,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mouseleave
|
//#region mouseleave
|
||||||
@ -423,7 +423,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region wheel
|
//#region wheel
|
||||||
@ -464,11 +464,11 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
stage.scaleY(newScale);
|
stage.scaleY(newScale);
|
||||||
stage.position(newPos);
|
stage.position(newPos);
|
||||||
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
|
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
|
||||||
manager.renderBackground();
|
manager.preview.tool.render();
|
||||||
manager.renderDocumentSizeOverlay();
|
manager.preview.documentSizeOverlay.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region dragmove
|
//#region dragmove
|
||||||
@ -480,9 +480,9 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.renderBackground();
|
manager.preview.tool.render();
|
||||||
manager.renderDocumentSizeOverlay();
|
manager.preview.documentSizeOverlay.render();
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region dragend
|
//#region dragend
|
||||||
@ -495,7 +495,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region key
|
//#region key
|
||||||
@ -520,11 +520,12 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
} else if (e.key === 'r') {
|
} else if (e.key === 'r') {
|
||||||
setLastCursorPos(null);
|
setLastCursorPos(null);
|
||||||
setLastMouseDownPos(null);
|
setLastMouseDownPos(null);
|
||||||
manager.fitDocument();
|
manager.preview.documentSizeOverlay.fitToStage();
|
||||||
manager.renderBackground();
|
manager.preview.tool.render();
|
||||||
manager.renderDocumentSizeOverlay();
|
|
||||||
|
manager.preview.documentSizeOverlay.render();
|
||||||
}
|
}
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
@ -542,7 +543,7 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
|
|||||||
setToolBuffer(null);
|
setToolBuffer(null);
|
||||||
setSpaceKey(false);
|
setSpaceKey(false);
|
||||||
}
|
}
|
||||||
manager.renderToolPreview();
|
manager.preview.tool.render();
|
||||||
};
|
};
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||||
|
import { getObjectGroupId,INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
|
||||||
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 InpaintMaskEntity, isDrawingTool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
@ -19,10 +19,10 @@ export class CanvasInpaintMask {
|
|||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect>;
|
||||||
|
|
||||||
constructor(entity: InpaintMaskEntity, manager: KonvaNodeManager) {
|
constructor(manager: KonvaNodeManager) {
|
||||||
this.id = entity.id;
|
this.id = INPAINT_MASK_LAYER_ID;
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.layer = new Konva.Layer({ id: entity.id });
|
this.layer = new Konva.Layer({ id: INPAINT_MASK_LAYER_ID });
|
||||||
|
|
||||||
this.group = new Konva.Group({
|
this.group = new Konva.Group({
|
||||||
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
@ -1,6 +1,6 @@
|
|||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types';
|
import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
@ -39,11 +39,11 @@ export const RASTER_LAYER_ERASER_LINE_NAME = `${RASTER_LAYER_NAME}.eraser_line`;
|
|||||||
export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`;
|
export const RASTER_LAYER_RECT_SHAPE_NAME = `${RASTER_LAYER_NAME}.rect_shape`;
|
||||||
export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`;
|
export const RASTER_LAYER_IMAGE_NAME = `${RASTER_LAYER_NAME}.image`;
|
||||||
|
|
||||||
export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer';
|
export const INPAINT_MASK_LAYER_ID = 'inpaint_mask_layer';
|
||||||
export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_NAME}.object_group`;
|
export const INPAINT_MASK_LAYER_OBJECT_GROUP_NAME = `${INPAINT_MASK_LAYER_ID}.object_group`;
|
||||||
export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.brush_line`;
|
export const INPAINT_MASK_LAYER_BRUSH_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.brush_line`;
|
||||||
export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_NAME}.eraser_line`;
|
export const INPAINT_MASK_LAYER_ERASER_LINE_NAME = `${INPAINT_MASK_LAYER_ID}.eraser_line`;
|
||||||
export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_NAME}.rect_shape`;
|
export const INPAINT_MASK_LAYER_RECT_SHAPE_NAME = `${INPAINT_MASK_LAYER_ID}.rect_shape`;
|
||||||
|
|
||||||
export const BACKGROUND_LAYER_ID = 'background_layer';
|
export const BACKGROUND_LAYER_ID = 'background_layer';
|
||||||
|
|
||||||
|
@ -1,89 +1,28 @@
|
|||||||
|
import type { Store } from '@reduxjs/toolkit';
|
||||||
|
import type { RootState } from 'app/store/store';
|
||||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||||
import { CanvasBackground } from 'features/controlLayers/konva/renderers/background';
|
import { CanvasBackground } from 'features/controlLayers/konva/background';
|
||||||
import { CanvasPreview } from 'features/controlLayers/konva/renderers/preview';
|
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||||
|
import { CanvasPreview } from 'features/controlLayers/konva/preview';
|
||||||
import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
import { konvaNodeToBlob, konvaNodeToImageData, previewBlob } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
BrushLineAddedArg,
|
import type { CanvasV2State, GenerationMode, Rect } from 'features/controlLayers/store/types';
|
||||||
CanvasEntity,
|
|
||||||
CanvasV2State,
|
|
||||||
EraserLineAddedArg,
|
|
||||||
GenerationMode,
|
|
||||||
PointAddedToLineArg,
|
|
||||||
PosChangedArg,
|
|
||||||
Rect,
|
|
||||||
RectShapeAddedArg,
|
|
||||||
RgbaColor,
|
|
||||||
ScaleChangedArg,
|
|
||||||
StageAttrs,
|
|
||||||
Tool,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
||||||
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
||||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
import { CanvasBbox } from './renderers/bbox';
|
import { CanvasBbox } from './bbox';
|
||||||
import { CanvasControlAdapter } from './renderers/controlAdapters';
|
import { CanvasControlAdapter } from './controlAdapters';
|
||||||
import { CanvasDocumentSizeOverlay } from './renderers/documentSizeOverlay';
|
import { CanvasDocumentSizeOverlay } from './documentSizeOverlay';
|
||||||
import { CanvasInpaintMask } from './renderers/inpaintMask';
|
import { CanvasInpaintMask } from './inpaintMask';
|
||||||
import { CanvasLayer } from './renderers/layers';
|
import { CanvasLayer } from './layers';
|
||||||
import { CanvasRegion } from './renderers/regions';
|
import { CanvasRegion } from './regions';
|
||||||
import { CanvasStagingArea } from './renderers/stagingArea';
|
import { CanvasStagingArea } from './stagingArea';
|
||||||
import { CanvasTool } from './renderers/tool';
|
import { StateApi } from './stateApi';
|
||||||
|
import { CanvasTool } from './tool';
|
||||||
export type StateApi = {
|
|
||||||
getToolState: () => CanvasV2State['tool'];
|
|
||||||
getCurrentFill: () => RgbaColor;
|
|
||||||
setTool: (tool: Tool) => void;
|
|
||||||
setToolBuffer: (tool: Tool | null) => void;
|
|
||||||
getIsDrawing: () => boolean;
|
|
||||||
setIsDrawing: (isDrawing: boolean) => void;
|
|
||||||
getIsMouseDown: () => boolean;
|
|
||||||
setIsMouseDown: (isMouseDown: boolean) => void;
|
|
||||||
getLastMouseDownPos: () => Vector2d | null;
|
|
||||||
setLastMouseDownPos: (pos: Vector2d | null) => void;
|
|
||||||
getLastCursorPos: () => Vector2d | null;
|
|
||||||
setLastCursorPos: (pos: Vector2d | null) => void;
|
|
||||||
getLastAddedPoint: () => Vector2d | null;
|
|
||||||
setLastAddedPoint: (pos: Vector2d | null) => void;
|
|
||||||
setStageAttrs: (attrs: StageAttrs) => void;
|
|
||||||
getSelectedEntity: () => CanvasEntity | null;
|
|
||||||
getSpaceKey: () => boolean;
|
|
||||||
setSpaceKey: (val: boolean) => void;
|
|
||||||
getShouldShowStagedImage: () => boolean;
|
|
||||||
getBbox: () => CanvasV2State['bbox'];
|
|
||||||
getSettings: () => CanvasV2State['settings'];
|
|
||||||
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onBrushWidthChanged: (size: number) => void;
|
|
||||||
onEraserWidthChanged: (size: number) => void;
|
|
||||||
getMaskOpacity: () => number;
|
|
||||||
getIsSelected: (id: string) => boolean;
|
|
||||||
onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
|
|
||||||
onBboxTransformed: (bbox: Rect) => void;
|
|
||||||
getShiftKey: () => boolean;
|
|
||||||
getCtrlKey: () => boolean;
|
|
||||||
getMetaKey: () => boolean;
|
|
||||||
getAltKey: () => boolean;
|
|
||||||
getDocument: () => CanvasV2State['document'];
|
|
||||||
getLayersState: () => CanvasV2State['layers'];
|
|
||||||
getControlAdaptersState: () => CanvasV2State['controlAdapters'];
|
|
||||||
getRegionsState: () => CanvasV2State['regions'];
|
|
||||||
getInpaintMaskState: () => CanvasV2State['inpaintMask'];
|
|
||||||
getStagingAreaState: () => CanvasV2State['stagingArea'];
|
|
||||||
getLastProgressEvent: () => InvocationDenoiseProgressEvent | null;
|
|
||||||
resetLastProgressEvent: () => void;
|
|
||||||
onInpaintMaskImageCached: (imageDTO: ImageDTO) => void;
|
|
||||||
onRegionMaskImageCached: (id: string, imageDTO: ImageDTO) => void;
|
|
||||||
onLayerImageCached: (imageDTO: ImageDTO) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Util = {
|
type Util = {
|
||||||
getImageDTO: (imageName: string) => Promise<ImageDTO | null>;
|
getImageDTO: (imageName: string) => Promise<ImageDTO | null>;
|
||||||
@ -116,41 +55,44 @@ export class KonvaNodeManager {
|
|||||||
stateApi: StateApi;
|
stateApi: StateApi;
|
||||||
preview: CanvasPreview;
|
preview: CanvasPreview;
|
||||||
background: CanvasBackground;
|
background: CanvasBackground;
|
||||||
|
private store: Store<RootState>;
|
||||||
|
private isFirstRender: boolean;
|
||||||
|
private prevState: CanvasV2State;
|
||||||
|
private log: (message: string) => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
container: HTMLDivElement,
|
container: HTMLDivElement,
|
||||||
stateApi: StateApi,
|
store: Store<RootState>,
|
||||||
|
log: (message: string) => void,
|
||||||
getImageDTO: Util['getImageDTO'] = defaultGetImageDTO,
|
getImageDTO: Util['getImageDTO'] = defaultGetImageDTO,
|
||||||
uploadImage: Util['uploadImage'] = defaultUploadImage
|
uploadImage: Util['uploadImage'] = defaultUploadImage
|
||||||
) {
|
) {
|
||||||
|
this.log = log;
|
||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.stateApi = stateApi;
|
this.store = store;
|
||||||
|
this.stateApi = new StateApi(this.store, this.log);
|
||||||
|
this.prevState = this.stateApi.getState();
|
||||||
|
this.isFirstRender = true;
|
||||||
|
|
||||||
this.util = {
|
this.util = {
|
||||||
getImageDTO,
|
getImageDTO,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.preview = new CanvasPreview(
|
this.preview = new CanvasPreview(
|
||||||
new CanvasBbox(
|
new CanvasBbox(this),
|
||||||
this.stateApi.getBbox,
|
new CanvasTool(this),
|
||||||
this.stateApi.onBboxTransformed,
|
new CanvasDocumentSizeOverlay(this),
|
||||||
this.stateApi.getShiftKey,
|
new CanvasStagingArea(this)
|
||||||
this.stateApi.getCtrlKey,
|
|
||||||
this.stateApi.getMetaKey,
|
|
||||||
this.stateApi.getAltKey
|
|
||||||
),
|
|
||||||
new CanvasTool(),
|
|
||||||
new CanvasDocumentSizeOverlay(),
|
|
||||||
new CanvasStagingArea()
|
|
||||||
);
|
);
|
||||||
this.stage.add(this.preview.layer);
|
this.stage.add(this.preview.layer);
|
||||||
|
|
||||||
this.background = new CanvasBackground();
|
this.background = new CanvasBackground(this);
|
||||||
this.stage.add(this.background.layer);
|
this.stage.add(this.background.layer);
|
||||||
|
|
||||||
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
|
this.inpaintMask = new CanvasInpaintMask(this);
|
||||||
this.stage.add(this.inpaintMask.layer);
|
this.stage.add(this.inpaintMask.layer);
|
||||||
|
|
||||||
this.layers = new Map();
|
this.layers = new Map();
|
||||||
@ -247,46 +189,6 @@ export class KonvaNodeManager {
|
|||||||
this.preview.layer.zIndex(++zIndex);
|
this.preview.layer.zIndex(++zIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDocumentSizeOverlay() {
|
|
||||||
this.preview.documentSizeOverlay.render(this.stage, this.stateApi.getDocument());
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBbox() {
|
|
||||||
this.preview.bbox.render(this.stateApi.getBbox(), this.stateApi.getToolState());
|
|
||||||
}
|
|
||||||
|
|
||||||
renderToolPreview() {
|
|
||||||
this.preview.tool.render(
|
|
||||||
this.stage,
|
|
||||||
1, // TODO(psyche): this should be renderable entity count
|
|
||||||
this.stateApi.getToolState(),
|
|
||||||
this.stateApi.getCurrentFill(),
|
|
||||||
this.stateApi.getSelectedEntity(),
|
|
||||||
this.stateApi.getLastCursorPos(),
|
|
||||||
this.stateApi.getLastMouseDownPos(),
|
|
||||||
this.stateApi.getIsDrawing(),
|
|
||||||
this.stateApi.getIsMouseDown()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBackground() {
|
|
||||||
this.background.renderBackground(this.stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderStagingArea() {
|
|
||||||
this.preview.stagingArea.render(
|
|
||||||
this.stateApi.getStagingAreaState(),
|
|
||||||
this.stateApi.getBbox(),
|
|
||||||
this.stateApi.getShouldShowStagedImage(),
|
|
||||||
this.stateApi.getLastProgressEvent(),
|
|
||||||
this.stateApi.resetLastProgressEvent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fitDocument() {
|
|
||||||
this.preview.documentSizeOverlay.fitToStage(this.stage, this.stateApi.getDocument(), this.stateApi.setStageAttrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
fitStageToContainer() {
|
fitStageToContainer() {
|
||||||
this.stage.width(this.container.offsetWidth);
|
this.stage.width(this.container.offsetWidth);
|
||||||
this.stage.height(this.container.offsetHeight);
|
this.stage.height(this.container.offsetHeight);
|
||||||
@ -297,10 +199,150 @@ export class KonvaNodeManager {
|
|||||||
height: this.stage.height(),
|
height: this.stage.height(),
|
||||||
scale: this.stage.scaleX(),
|
scale: this.stage.scaleX(),
|
||||||
});
|
});
|
||||||
this.renderBackground();
|
this.background.renderBackground();
|
||||||
this.renderDocumentSizeOverlay();
|
this.preview.documentSizeOverlay.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render = async () => {
|
||||||
|
const state = this.stateApi.getState();
|
||||||
|
|
||||||
|
if (this.prevState === state && !this.isFirstRender) {
|
||||||
|
this.log('No changes detected, skipping render');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.layers.entities !== this.prevState.layers.entities ||
|
||||||
|
state.tool.selected !== this.prevState.tool.selected ||
|
||||||
|
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||||
|
) {
|
||||||
|
this.log('Rendering layers');
|
||||||
|
this.renderLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.regions.entities !== this.prevState.regions.entities ||
|
||||||
|
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||||
|
state.tool.selected !== this.prevState.tool.selected ||
|
||||||
|
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||||
|
) {
|
||||||
|
this.log('Rendering regions');
|
||||||
|
this.renderRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||||
|
state.settings.maskOpacity !== this.prevState.settings.maskOpacity ||
|
||||||
|
state.tool.selected !== this.prevState.tool.selected ||
|
||||||
|
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||||
|
) {
|
||||||
|
this.log('Rendering inpaint mask');
|
||||||
|
this.renderInpaintMask();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||||
|
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||||
|
) {
|
||||||
|
this.log('Rendering control adapters');
|
||||||
|
this.renderControlAdapters();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFirstRender || state.document !== this.prevState.document) {
|
||||||
|
this.log('Rendering document bounds overlay');
|
||||||
|
this.preview.documentSizeOverlay.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.bbox !== this.prevState.bbox ||
|
||||||
|
state.tool.selected !== this.prevState.tool.selected
|
||||||
|
) {
|
||||||
|
this.log('Rendering generation bbox');
|
||||||
|
this.preview.bbox.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.layers !== this.prevState.layers ||
|
||||||
|
state.controlAdapters !== this.prevState.controlAdapters ||
|
||||||
|
state.regions !== this.prevState.regions
|
||||||
|
) {
|
||||||
|
// this.log('Updating entity bboxes');
|
||||||
|
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isFirstRender || state.stagingArea !== this.prevState.stagingArea) {
|
||||||
|
this.log('Rendering staging area');
|
||||||
|
this.preview.stagingArea.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isFirstRender ||
|
||||||
|
state.layers.entities !== this.prevState.layers.entities ||
|
||||||
|
state.controlAdapters.entities !== this.prevState.controlAdapters.entities ||
|
||||||
|
state.regions.entities !== this.prevState.regions.entities ||
|
||||||
|
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||||
|
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||||
|
) {
|
||||||
|
this.log('Arranging entities');
|
||||||
|
this.arrangeEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prevState = state;
|
||||||
|
|
||||||
|
if (this.isFirstRender) {
|
||||||
|
this.isFirstRender = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize = () => {
|
||||||
|
this.log('Initializing renderer');
|
||||||
|
this.stage.container(this.container);
|
||||||
|
|
||||||
|
const cleanupListeners = setStageEventHandlers(this);
|
||||||
|
|
||||||
|
// We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and
|
||||||
|
// document bounds overlay when the stage is resized.
|
||||||
|
const resizeObserver = new ResizeObserver(this.fitStageToContainer.bind(this));
|
||||||
|
resizeObserver.observe(this.container);
|
||||||
|
this.fitStageToContainer();
|
||||||
|
|
||||||
|
const unsubscribeRenderer = this.store.subscribe(this.render);
|
||||||
|
|
||||||
|
// When we this flag, we need to render the staging area
|
||||||
|
$shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => {
|
||||||
|
this.log('Rendering staging area');
|
||||||
|
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
|
||||||
|
this.preview.stagingArea.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$lastProgressEvent.subscribe(() => {
|
||||||
|
this.log('Rendering staging area');
|
||||||
|
this.preview.stagingArea.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('First render of konva stage');
|
||||||
|
// On first render, the document should be fit to the stage.
|
||||||
|
this.preview.documentSizeOverlay.render();
|
||||||
|
this.preview.documentSizeOverlay.fitToStage();
|
||||||
|
this.preview.tool.render();
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.log('Cleaning up konva renderer');
|
||||||
|
unsubscribeRenderer();
|
||||||
|
cleanupListeners();
|
||||||
|
$shouldShowStagedImage.off();
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
getInpaintMaskLayerClone(): Konva.Layer {
|
getInpaintMaskLayerClone(): Konva.Layer {
|
||||||
const layerClone = this.inpaintMask.layer.clone();
|
const layerClone = this.inpaintMask.layer.clone();
|
||||||
const objectGroupClone = this.inpaintMask.group.clone();
|
const objectGroupClone = this.inpaintMask.group.clone();
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import { getNodeBboxFast } from 'features/controlLayers/konva/renderers/entityBbox';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||||
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
import { KonvaBrushLine, KonvaEraserLine, KonvaRect } from 'features/controlLayers/konva/objects';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import {
|
import { isDrawingTool, type RegionEntity } from 'features/controlLayers/store/types';
|
||||||
isDrawingTool,
|
|
||||||
type RegionEntity,
|
|
||||||
} 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';
|
@ -1,499 +0,0 @@
|
|||||||
import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library';
|
|
||||||
import type { Store } from '@reduxjs/toolkit';
|
|
||||||
import { logger } from 'app/logging/logger';
|
|
||||||
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
|
||||||
import type { RootState } from 'app/store/store';
|
|
||||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
|
||||||
import { KonvaNodeManager, setNodeManager } from 'features/controlLayers/konva/nodeManager';
|
|
||||||
import { updateBboxes } from 'features/controlLayers/konva/renderers/entityBbox';
|
|
||||||
import {
|
|
||||||
$lastProgressEvent,
|
|
||||||
$shouldShowStagedImage,
|
|
||||||
$stageAttrs,
|
|
||||||
bboxChanged,
|
|
||||||
brushWidthChanged,
|
|
||||||
caBboxChanged,
|
|
||||||
caTranslated,
|
|
||||||
eraserWidthChanged,
|
|
||||||
imBboxChanged,
|
|
||||||
imBrushLineAdded,
|
|
||||||
imEraserLineAdded,
|
|
||||||
imImageCacheChanged,
|
|
||||||
imLinePointAdded,
|
|
||||||
imRectAdded,
|
|
||||||
imScaled,
|
|
||||||
imTranslated,
|
|
||||||
layerBboxChanged,
|
|
||||||
layerBrushLineAdded,
|
|
||||||
layerEraserLineAdded,
|
|
||||||
layerImageCacheChanged,
|
|
||||||
layerLinePointAdded,
|
|
||||||
layerRectAdded,
|
|
||||||
layerScaled,
|
|
||||||
layerTranslated,
|
|
||||||
rgBboxChanged,
|
|
||||||
rgBrushLineAdded,
|
|
||||||
rgEraserLineAdded,
|
|
||||||
rgImageCacheChanged,
|
|
||||||
rgLinePointAdded,
|
|
||||||
rgRectAdded,
|
|
||||||
rgScaled,
|
|
||||||
rgTranslated,
|
|
||||||
toolBufferChanged,
|
|
||||||
toolChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasV2Slice';
|
|
||||||
import type {
|
|
||||||
BboxChangedArg,
|
|
||||||
BrushLineAddedArg,
|
|
||||||
CanvasEntity,
|
|
||||||
CanvasV2State,
|
|
||||||
EraserLineAddedArg,
|
|
||||||
PointAddedToLineArg,
|
|
||||||
PosChangedArg,
|
|
||||||
RectShapeAddedArg,
|
|
||||||
ScaleChangedArg,
|
|
||||||
Tool,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import type Konva from 'konva';
|
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import type { RgbaColor } from 'react-colorful';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the canvas renderer. It subscribes to the redux store and listens for changes directly, bypassing the
|
|
||||||
* react rendering cycle entirely, improving canvas performance.
|
|
||||||
* @param store The redux store
|
|
||||||
* @param stage The konva stage
|
|
||||||
* @param container The stage's target container element
|
|
||||||
* @returns A cleanup function
|
|
||||||
*/
|
|
||||||
export const initializeRenderer = (
|
|
||||||
store: Store<RootState>,
|
|
||||||
stage: Konva.Stage,
|
|
||||||
container: HTMLDivElement | null
|
|
||||||
): (() => void) => {
|
|
||||||
const _log = logger('konva');
|
|
||||||
/**
|
|
||||||
* Logs a message to the console if debugging is enabled.
|
|
||||||
*/
|
|
||||||
const logIfDebugging = (message: string) => {
|
|
||||||
if ($isDebugging.get()) {
|
|
||||||
_log.debug(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logIfDebugging('Initializing renderer');
|
|
||||||
if (!container) {
|
|
||||||
// Nothing to clean up
|
|
||||||
logIfDebugging('No stage container, skipping initialization');
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.container(container);
|
|
||||||
|
|
||||||
// Set up callbacks for various events
|
|
||||||
const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('onPosChanged');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerTranslated(arg));
|
|
||||||
} else if (entityType === 'control_adapter') {
|
|
||||||
dispatch(caTranslated(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgTranslated(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imTranslated(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('onScaleChanged');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerScaled(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imScaled(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgScaled(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('Entity bbox changed');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerBboxChanged(arg));
|
|
||||||
} else if (entityType === 'control_adapter') {
|
|
||||||
dispatch(caBboxChanged(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgBboxChanged(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imBboxChanged(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('Brush line added');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerBrushLineAdded(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgBrushLineAdded(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imBrushLineAdded(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('Eraser line added');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerEraserLineAdded(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgEraserLineAdded(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imEraserLineAdded(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('Point added to line');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerLinePointAdded(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgLinePointAdded(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imLinePointAdded(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => {
|
|
||||||
logIfDebugging('Rect shape added');
|
|
||||||
if (entityType === 'layer') {
|
|
||||||
dispatch(layerRectAdded(arg));
|
|
||||||
} else if (entityType === 'regional_guidance') {
|
|
||||||
dispatch(rgRectAdded(arg));
|
|
||||||
} else if (entityType === 'inpaint_mask') {
|
|
||||||
dispatch(imRectAdded(arg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onBboxTransformed = (bbox: IRect) => {
|
|
||||||
logIfDebugging('Generation bbox transformed');
|
|
||||||
dispatch(bboxChanged(bbox));
|
|
||||||
};
|
|
||||||
const onBrushWidthChanged = (width: number) => {
|
|
||||||
logIfDebugging('Brush width changed');
|
|
||||||
dispatch(brushWidthChanged(width));
|
|
||||||
};
|
|
||||||
const onEraserWidthChanged = (width: number) => {
|
|
||||||
logIfDebugging('Eraser width changed');
|
|
||||||
dispatch(eraserWidthChanged(width));
|
|
||||||
};
|
|
||||||
const onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
|
||||||
logIfDebugging('Region mask image cached');
|
|
||||||
dispatch(rgImageCacheChanged({ id, imageDTO }));
|
|
||||||
};
|
|
||||||
const onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
|
||||||
logIfDebugging('Inpaint mask image cached');
|
|
||||||
dispatch(imImageCacheChanged({ imageDTO }));
|
|
||||||
};
|
|
||||||
const onLayerImageCached = (imageDTO: ImageDTO) => {
|
|
||||||
logIfDebugging('Layer image cached');
|
|
||||||
dispatch(layerImageCacheChanged({ imageDTO }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTool = (tool: Tool) => {
|
|
||||||
logIfDebugging('Tool selection changed');
|
|
||||||
dispatch(toolChanged(tool));
|
|
||||||
};
|
|
||||||
const setToolBuffer = (toolBuffer: Tool | null) => {
|
|
||||||
logIfDebugging('Tool buffer changed');
|
|
||||||
dispatch(toolBufferChanged(toolBuffer));
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectSelectedEntity = (canvasV2: CanvasV2State): CanvasEntity | null => {
|
|
||||||
const identifier = canvasV2.selectedEntityIdentifier;
|
|
||||||
let selectedEntity: CanvasEntity | null = null;
|
|
||||||
if (!identifier) {
|
|
||||||
selectedEntity = null;
|
|
||||||
} else if (identifier.type === 'layer') {
|
|
||||||
selectedEntity = canvasV2.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'control_adapter') {
|
|
||||||
selectedEntity = canvasV2.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'ip_adapter') {
|
|
||||||
selectedEntity = canvasV2.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'regional_guidance') {
|
|
||||||
selectedEntity = canvasV2.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'inpaint_mask') {
|
|
||||||
selectedEntity = canvasV2.inpaintMask;
|
|
||||||
} else {
|
|
||||||
selectedEntity = null;
|
|
||||||
}
|
|
||||||
return selectedEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectCurrentFill = (canvasV2: CanvasV2State, selectedEntity: CanvasEntity | null) => {
|
|
||||||
let currentFill: RgbaColor = canvasV2.tool.fill;
|
|
||||||
if (selectedEntity) {
|
|
||||||
if (selectedEntity.type === 'regional_guidance') {
|
|
||||||
currentFill = { ...selectedEntity.fill, a: canvasV2.settings.maskOpacity };
|
|
||||||
} else if (selectedEntity.type === 'inpaint_mask') {
|
|
||||||
currentFill = { ...canvasV2.inpaintMask.fill, a: canvasV2.settings.maskOpacity };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentFill = canvasV2.tool.fill;
|
|
||||||
}
|
|
||||||
return currentFill;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getState, subscribe, dispatch } = store;
|
|
||||||
|
|
||||||
// On the first render, we need to render everything.
|
|
||||||
let isFirstRender = true;
|
|
||||||
|
|
||||||
// Stage interaction listeners need helpers to get and update current state. Some of the state is read-only, like
|
|
||||||
// bbox, document and tool state, while interaction state is read-write.
|
|
||||||
|
|
||||||
// Read-only state, derived from redux
|
|
||||||
let prevCanvasV2 = getState().canvasV2;
|
|
||||||
let canvasV2 = getState().canvasV2;
|
|
||||||
const getSelectedEntity = () => selectSelectedEntity(canvasV2);
|
|
||||||
const getCurrentFill = () => selectCurrentFill(canvasV2, getSelectedEntity());
|
|
||||||
const getBbox = () => canvasV2.bbox;
|
|
||||||
const getDocument = () => canvasV2.document;
|
|
||||||
const getToolState = () => canvasV2.tool;
|
|
||||||
const getSettings = () => canvasV2.settings;
|
|
||||||
const getRegionsState = () => canvasV2.regions;
|
|
||||||
const getLayersState = () => canvasV2.layers;
|
|
||||||
const getControlAdaptersState = () => canvasV2.controlAdapters;
|
|
||||||
const getInpaintMaskState = () => canvasV2.inpaintMask;
|
|
||||||
const getMaskOpacity = () => canvasV2.settings.maskOpacity;
|
|
||||||
const getStagingAreaState = () => canvasV2.stagingArea;
|
|
||||||
const getIsSelected = (id: string) => getSelectedEntity()?.id === id;
|
|
||||||
|
|
||||||
// Read-only state, derived from nanostores
|
|
||||||
const resetLastProgressEvent = () => {
|
|
||||||
$lastProgressEvent.set(null);
|
|
||||||
};
|
|
||||||
// Read-write state, ephemeral interaction state
|
|
||||||
let isDrawing = false;
|
|
||||||
const getIsDrawing = () => isDrawing;
|
|
||||||
const setIsDrawing = (val: boolean) => {
|
|
||||||
isDrawing = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
let isMouseDown = false;
|
|
||||||
const getIsMouseDown = () => isMouseDown;
|
|
||||||
const setIsMouseDown = (val: boolean) => {
|
|
||||||
isMouseDown = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastAddedPoint: Vector2d | null = null;
|
|
||||||
const getLastAddedPoint = () => lastAddedPoint;
|
|
||||||
const setLastAddedPoint = (val: Vector2d | null) => {
|
|
||||||
lastAddedPoint = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastMouseDownPos: Vector2d | null = null;
|
|
||||||
const getLastMouseDownPos = () => lastMouseDownPos;
|
|
||||||
const setLastMouseDownPos = (val: Vector2d | null) => {
|
|
||||||
lastMouseDownPos = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastCursorPos: Vector2d | null = null;
|
|
||||||
const getLastCursorPos = () => lastCursorPos;
|
|
||||||
const setLastCursorPos = (val: Vector2d | null) => {
|
|
||||||
lastCursorPos = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
let spaceKey = false;
|
|
||||||
const getSpaceKey = () => spaceKey;
|
|
||||||
const setSpaceKey = (val: boolean) => {
|
|
||||||
spaceKey = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stateApi: KonvaNodeManager['stateApi'] = {
|
|
||||||
// Read-only state
|
|
||||||
getToolState,
|
|
||||||
getSelectedEntity,
|
|
||||||
getBbox,
|
|
||||||
getSettings,
|
|
||||||
getCurrentFill,
|
|
||||||
getAltKey: $alt.get,
|
|
||||||
getCtrlKey: $ctrl.get,
|
|
||||||
getMetaKey: $meta.get,
|
|
||||||
getShiftKey: $shift.get,
|
|
||||||
getControlAdaptersState,
|
|
||||||
getDocument,
|
|
||||||
getLayersState,
|
|
||||||
getRegionsState,
|
|
||||||
getMaskOpacity,
|
|
||||||
getInpaintMaskState,
|
|
||||||
getStagingAreaState,
|
|
||||||
getShouldShowStagedImage: $shouldShowStagedImage.get,
|
|
||||||
getLastProgressEvent: $lastProgressEvent.get,
|
|
||||||
resetLastProgressEvent,
|
|
||||||
getIsSelected,
|
|
||||||
|
|
||||||
// Read-write state
|
|
||||||
setTool,
|
|
||||||
setToolBuffer,
|
|
||||||
getIsDrawing,
|
|
||||||
setIsDrawing,
|
|
||||||
getIsMouseDown,
|
|
||||||
setIsMouseDown,
|
|
||||||
getLastAddedPoint,
|
|
||||||
setLastAddedPoint,
|
|
||||||
getLastCursorPos,
|
|
||||||
setLastCursorPos,
|
|
||||||
getLastMouseDownPos,
|
|
||||||
setLastMouseDownPos,
|
|
||||||
getSpaceKey,
|
|
||||||
setSpaceKey,
|
|
||||||
setStageAttrs: $stageAttrs.set,
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
onBrushLineAdded,
|
|
||||||
onEraserLineAdded,
|
|
||||||
onPointAddedToLine,
|
|
||||||
onRectShapeAdded,
|
|
||||||
onBrushWidthChanged,
|
|
||||||
onEraserWidthChanged,
|
|
||||||
onPosChanged,
|
|
||||||
onBboxTransformed,
|
|
||||||
onRegionMaskImageCached,
|
|
||||||
onInpaintMaskImageCached,
|
|
||||||
onLayerImageCached,
|
|
||||||
onScaleChanged,
|
|
||||||
};
|
|
||||||
|
|
||||||
const manager = new KonvaNodeManager(stage, container, stateApi);
|
|
||||||
setNodeManager(manager);
|
|
||||||
console.log(manager);
|
|
||||||
|
|
||||||
const cleanupListeners = setStageEventHandlers(manager);
|
|
||||||
|
|
||||||
// Calculating bounding boxes is expensive, must be debounced to not block the UI thread during a user interaction.
|
|
||||||
// TODO(psyche): Figure out how to do this in a worker. Probably means running the renderer in a worker and sending
|
|
||||||
// the entire state over when needed.
|
|
||||||
const debouncedUpdateBboxes = debounce(updateBboxes, 300);
|
|
||||||
|
|
||||||
const renderCanvas = async () => {
|
|
||||||
canvasV2 = store.getState().canvasV2;
|
|
||||||
|
|
||||||
if (prevCanvasV2 === canvasV2 && !isFirstRender) {
|
|
||||||
logIfDebugging('No changes detected, skipping render');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
|
||||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
|
||||||
) {
|
|
||||||
logIfDebugging('Rendering layers');
|
|
||||||
manager.renderLayers();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
|
||||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
|
||||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
|
||||||
) {
|
|
||||||
logIfDebugging('Rendering regions');
|
|
||||||
manager.renderRegions();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
|
||||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
|
||||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
|
||||||
) {
|
|
||||||
logIfDebugging('Rendering inpaint mask');
|
|
||||||
manager.renderInpaintMask();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
|
||||||
) {
|
|
||||||
logIfDebugging('Rendering control adapters');
|
|
||||||
manager.renderControlAdapters();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstRender || canvasV2.document !== prevCanvasV2.document) {
|
|
||||||
logIfDebugging('Rendering document bounds overlay');
|
|
||||||
manager.renderDocumentSizeOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstRender || canvasV2.bbox !== prevCanvasV2.bbox || canvasV2.tool.selected !== prevCanvasV2.tool.selected) {
|
|
||||||
logIfDebugging('Rendering generation bbox');
|
|
||||||
manager.renderBbox();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.layers !== prevCanvasV2.layers ||
|
|
||||||
canvasV2.controlAdapters !== prevCanvasV2.controlAdapters ||
|
|
||||||
canvasV2.regions !== prevCanvasV2.regions
|
|
||||||
) {
|
|
||||||
// logIfDebugging('Updating entity bboxes');
|
|
||||||
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstRender || canvasV2.stagingArea !== prevCanvasV2.stagingArea) {
|
|
||||||
logIfDebugging('Rendering staging area');
|
|
||||||
manager.renderStagingArea();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFirstRender ||
|
|
||||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
|
||||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
|
||||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
|
||||||
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
|
||||||
canvasV2.selectedEntityIdentifier?.id !== prevCanvasV2.selectedEntityIdentifier?.id
|
|
||||||
) {
|
|
||||||
logIfDebugging('Arranging entities');
|
|
||||||
manager.arrangeEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
prevCanvasV2 = canvasV2;
|
|
||||||
|
|
||||||
if (isFirstRender) {
|
|
||||||
isFirstRender = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// We can use a resize observer to ensure the stage always fits the container. We also need to re-render the bg and
|
|
||||||
// document bounds overlay when the stage is resized.
|
|
||||||
const resizeObserver = new ResizeObserver(manager.fitStageToContainer.bind(manager));
|
|
||||||
resizeObserver.observe(container);
|
|
||||||
manager.fitStageToContainer();
|
|
||||||
|
|
||||||
const unsubscribeRenderer = subscribe(renderCanvas);
|
|
||||||
|
|
||||||
// When we this flag, we need to render the staging area
|
|
||||||
$shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => {
|
|
||||||
logIfDebugging('Rendering staging area');
|
|
||||||
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
|
|
||||||
manager.renderStagingArea();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$lastProgressEvent.subscribe(() => {
|
|
||||||
logIfDebugging('Rendering staging area');
|
|
||||||
manager.renderStagingArea();
|
|
||||||
});
|
|
||||||
|
|
||||||
logIfDebugging('First render of konva stage');
|
|
||||||
// On first render, the document should be fit to the stage.
|
|
||||||
manager.renderDocumentSizeOverlay();
|
|
||||||
manager.fitDocument();
|
|
||||||
manager.renderToolPreview();
|
|
||||||
renderCanvas();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
logIfDebugging('Cleaning up konva renderer');
|
|
||||||
unsubscribeRenderer();
|
|
||||||
cleanupListeners();
|
|
||||||
$shouldShowStagedImage.off();
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,29 +1,29 @@
|
|||||||
import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/renderers/objects';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import type { CanvasV2State } from 'features/controlLayers/store/types';
|
import { KonvaImage, KonvaProgressImage } from 'features/controlLayers/konva/objects';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
|
||||||
|
|
||||||
export class CanvasStagingArea {
|
export class CanvasStagingArea {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
image: KonvaImage | null;
|
image: KonvaImage | null;
|
||||||
progressImage: KonvaProgressImage | null;
|
progressImage: KonvaProgressImage | null;
|
||||||
imageDTO: ImageDTO | null;
|
imageDTO: ImageDTO | null;
|
||||||
|
manager: KonvaNodeManager;
|
||||||
|
|
||||||
constructor() {
|
constructor(manager: KonvaNodeManager) {
|
||||||
|
this.manager = manager;
|
||||||
this.group = new Konva.Group({ listening: false });
|
this.group = new Konva.Group({ listening: false });
|
||||||
this.image = null;
|
this.image = null;
|
||||||
this.progressImage = null;
|
this.progressImage = null;
|
||||||
this.imageDTO = null;
|
this.imageDTO = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(
|
async render() {
|
||||||
stagingArea: CanvasV2State['stagingArea'],
|
const stagingArea = this.manager.stateApi.getStagingAreaState();
|
||||||
bbox: CanvasV2State['bbox'],
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
shouldShowStagedImage: boolean,
|
const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage();
|
||||||
lastProgressEvent: InvocationDenoiseProgressEvent | null,
|
const lastProgressEvent = this.manager.stateApi.getLastProgressEvent();
|
||||||
resetLastProgressEvent: () => void
|
|
||||||
) {
|
|
||||||
this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
|
this.imageDTO = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
|
||||||
|
|
||||||
if (this.imageDTO) {
|
if (this.imageDTO) {
|
||||||
@ -58,7 +58,7 @@ export class CanvasStagingArea {
|
|||||||
konvaImage.width(this.imageDTO.width);
|
konvaImage.width(this.imageDTO.width);
|
||||||
konvaImage.height(this.imageDTO.height);
|
konvaImage.height(this.imageDTO.height);
|
||||||
}
|
}
|
||||||
resetLastProgressEvent();
|
this.manager.stateApi.resetLastProgressEvent();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -100,7 +100,7 @@ export class CanvasStagingArea {
|
|||||||
if (this.progressImage) {
|
if (this.progressImage) {
|
||||||
this.progressImage.konvaImageGroup.visible(false);
|
this.progressImage.konvaImageGroup.visible(false);
|
||||||
}
|
}
|
||||||
resetLastProgressEvent();
|
this.manager.stateApi.resetLastProgressEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,237 @@
|
|||||||
|
import { $alt, $ctrl, $meta, $shift } from "@invoke-ai/ui-library";
|
||||||
|
import type { Store } from "@reduxjs/toolkit";
|
||||||
|
import type { RootState } from "app/store/store";
|
||||||
|
import { $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $lastProgressEvent, $shouldShowStagedImage, $spaceKey, $stageAttrs, bboxChanged, brushWidthChanged, caBboxChanged, caTranslated, eraserWidthChanged, imBboxChanged, imBrushLineAdded, imEraserLineAdded, imImageCacheChanged, imLinePointAdded, imRectAdded, imScaled, imTranslated, layerBboxChanged, layerBrushLineAdded, layerEraserLineAdded, layerImageCacheChanged, layerLinePointAdded, layerRectAdded, layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, rgEraserLineAdded, rgImageCacheChanged, rgLinePointAdded, rgRectAdded, rgScaled, rgTranslated, toolBufferChanged, toolChanged } from "features/controlLayers/store/canvasV2Slice";
|
||||||
|
import type { BboxChangedArg, BrushLineAddedArg, CanvasEntity, EraserLineAddedArg, PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, ScaleChangedArg, Tool } from "features/controlLayers/store/types";
|
||||||
|
import type { IRect } from "konva/lib/types";
|
||||||
|
import type { RgbaColor } from "react-colorful";
|
||||||
|
import type { ImageDTO } from "services/api/types";
|
||||||
|
|
||||||
|
|
||||||
|
export class StateApi {
|
||||||
|
private store: Store<RootState>;
|
||||||
|
private log: (message: string) => void;
|
||||||
|
|
||||||
|
constructor(store: Store<RootState>, log: (message: string) => void) {
|
||||||
|
this.store = store;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reminder - use arrow functions to avoid binding issues
|
||||||
|
getState = () => {
|
||||||
|
return this.store.getState().canvasV2;
|
||||||
|
};
|
||||||
|
|
||||||
|
onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('onPosChanged');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerTranslated(arg));
|
||||||
|
} else if (entityType === 'control_adapter') {
|
||||||
|
this.store.dispatch(caTranslated(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgTranslated(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imTranslated(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('onScaleChanged');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerScaled(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imScaled(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgScaled(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('Entity bbox changed');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerBboxChanged(arg));
|
||||||
|
} else if (entityType === 'control_adapter') {
|
||||||
|
this.store.dispatch(caBboxChanged(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgBboxChanged(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imBboxChanged(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onBrushLineAdded = (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('Brush line added');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerBrushLineAdded(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgBrushLineAdded(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imBrushLineAdded(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onEraserLineAdded = (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('Eraser line added');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerEraserLineAdded(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgEraserLineAdded(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imEraserLineAdded(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onPointAddedToLine = (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('Point added to line');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerLinePointAdded(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgLinePointAdded(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imLinePointAdded(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onRectShapeAdded = (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => {
|
||||||
|
this.log('Rect shape added');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerRectAdded(arg));
|
||||||
|
} else if (entityType === 'regional_guidance') {
|
||||||
|
this.store.dispatch(rgRectAdded(arg));
|
||||||
|
} else if (entityType === 'inpaint_mask') {
|
||||||
|
this.store.dispatch(imRectAdded(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onBboxTransformed = (bbox: IRect) => {
|
||||||
|
this.log('Generation bbox transformed');
|
||||||
|
this.store.dispatch(bboxChanged(bbox));
|
||||||
|
};
|
||||||
|
onBrushWidthChanged = (width: number) => {
|
||||||
|
this.log('Brush width changed');
|
||||||
|
this.store.dispatch(brushWidthChanged(width));
|
||||||
|
};
|
||||||
|
onEraserWidthChanged = (width: number) => {
|
||||||
|
this.log('Eraser width changed');
|
||||||
|
this.store.dispatch(eraserWidthChanged(width));
|
||||||
|
};
|
||||||
|
onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
||||||
|
this.log('Region mask image cached');
|
||||||
|
this.store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||||
|
};
|
||||||
|
onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
||||||
|
this.log('Inpaint mask image cached');
|
||||||
|
this.store.dispatch(imImageCacheChanged({ imageDTO }));
|
||||||
|
};
|
||||||
|
onLayerImageCached = (imageDTO: ImageDTO) => {
|
||||||
|
this.log('Layer image cached');
|
||||||
|
this.store.dispatch(layerImageCacheChanged({ imageDTO }));
|
||||||
|
};
|
||||||
|
setTool = (tool: Tool) => {
|
||||||
|
this.log('Tool selection changed');
|
||||||
|
this.store.dispatch(toolChanged(tool));
|
||||||
|
};
|
||||||
|
setToolBuffer = (toolBuffer: Tool | null) => {
|
||||||
|
this.log('Tool buffer changed');
|
||||||
|
this.store.dispatch(toolBufferChanged(toolBuffer));
|
||||||
|
};
|
||||||
|
|
||||||
|
getSelectedEntity = (): CanvasEntity | null => {
|
||||||
|
const state = this.getState();
|
||||||
|
const identifier = state.selectedEntityIdentifier;
|
||||||
|
let selectedEntity: CanvasEntity | null = null;
|
||||||
|
if (!identifier) {
|
||||||
|
selectedEntity = null;
|
||||||
|
} else if (identifier.type === 'layer') {
|
||||||
|
selectedEntity = state.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'control_adapter') {
|
||||||
|
selectedEntity = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'ip_adapter') {
|
||||||
|
selectedEntity = state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'regional_guidance') {
|
||||||
|
selectedEntity = state.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'inpaint_mask') {
|
||||||
|
selectedEntity = state.inpaintMask;
|
||||||
|
} else {
|
||||||
|
selectedEntity = null;
|
||||||
|
}
|
||||||
|
return selectedEntity;
|
||||||
|
};
|
||||||
|
|
||||||
|
getCurrentFill = () => {
|
||||||
|
const state = this.getState();
|
||||||
|
const selectedEntity = this.getSelectedEntity();
|
||||||
|
let currentFill: RgbaColor = state.tool.fill;
|
||||||
|
if (selectedEntity) {
|
||||||
|
if (selectedEntity.type === 'regional_guidance') {
|
||||||
|
currentFill = { ...selectedEntity.fill, a: state.settings.maskOpacity };
|
||||||
|
} else if (selectedEntity.type === 'inpaint_mask') {
|
||||||
|
currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentFill = state.tool.fill;
|
||||||
|
}
|
||||||
|
return currentFill;
|
||||||
|
};
|
||||||
|
getBbox = () => {
|
||||||
|
return this.getState().bbox;
|
||||||
|
};
|
||||||
|
getDocument = () => {
|
||||||
|
return this.getState().document;
|
||||||
|
};
|
||||||
|
getToolState = () => {
|
||||||
|
return this.getState().tool;
|
||||||
|
};
|
||||||
|
getSettings = () => {
|
||||||
|
return this.getState().settings;
|
||||||
|
};
|
||||||
|
getRegionsState = () => {
|
||||||
|
return this.getState().regions;
|
||||||
|
};
|
||||||
|
getLayersState = () => {
|
||||||
|
return this.getState().layers;
|
||||||
|
};
|
||||||
|
getControlAdaptersState = () => {
|
||||||
|
return this.getState().controlAdapters;
|
||||||
|
};
|
||||||
|
getInpaintMaskState = () => {
|
||||||
|
return this.getState().inpaintMask;
|
||||||
|
};
|
||||||
|
getMaskOpacity = () => {
|
||||||
|
return this.getState().settings.maskOpacity;
|
||||||
|
};
|
||||||
|
getStagingAreaState = () => {
|
||||||
|
return this.getState().stagingArea;
|
||||||
|
};
|
||||||
|
getIsSelected = (id: string) => {
|
||||||
|
return this.getSelectedEntity()?.id === id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read-only state, derived from nanostores
|
||||||
|
resetLastProgressEvent = () => {
|
||||||
|
$lastProgressEvent.set(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read-write state, ephemeral interaction state
|
||||||
|
getIsDrawing = $isDrawing.get;
|
||||||
|
setIsDrawing = $isDrawing.set;
|
||||||
|
|
||||||
|
getIsMouseDown = $isMouseDown.get;
|
||||||
|
setIsMouseDown = $isMouseDown.set;
|
||||||
|
|
||||||
|
getLastAddedPoint = $lastAddedPoint.get;
|
||||||
|
setLastAddedPoint = $lastAddedPoint.set;
|
||||||
|
|
||||||
|
getLastMouseDownPos = $lastMouseDownPos.get;
|
||||||
|
setLastMouseDownPos = $lastMouseDownPos.set;
|
||||||
|
|
||||||
|
getLastCursorPos = $lastCursorPos.get;
|
||||||
|
setLastCursorPos = $lastCursorPos.set;
|
||||||
|
|
||||||
|
getSpaceKey = $spaceKey.get;
|
||||||
|
setSpaceKey = $spaceKey.set;
|
||||||
|
|
||||||
|
getLastProgressEvent = $lastProgressEvent.get;
|
||||||
|
setLastProgressEvent = $lastProgressEvent.set;
|
||||||
|
|
||||||
|
getAltKey = $alt.get;
|
||||||
|
getCtrlKey = $ctrl.get;
|
||||||
|
getMetaKey = $meta.get;
|
||||||
|
getShiftKey = $shift.get;
|
||||||
|
|
||||||
|
getShouldShowStagedImage = $shouldShowStagedImage.get;
|
||||||
|
setStageAttrs = $stageAttrs.set;
|
||||||
|
}
|
@ -5,10 +5,11 @@ import {
|
|||||||
BRUSH_ERASER_BORDER_WIDTH,
|
BRUSH_ERASER_BORDER_WIDTH,
|
||||||
} from 'features/controlLayers/konva/constants';
|
} from 'features/controlLayers/konva/constants';
|
||||||
import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming';
|
import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming';
|
||||||
import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types';
|
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
export class CanvasTool {
|
export class CanvasTool {
|
||||||
|
manager: KonvaNodeManager;
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
brush: {
|
brush: {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
@ -27,7 +28,8 @@ export class CanvasTool {
|
|||||||
fillRect: Konva.Rect;
|
fillRect: Konva.Rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor(manager: KonvaNodeManager) {
|
||||||
|
this.manager = manager;
|
||||||
this.group = new Konva.Group();
|
this.group = new Konva.Group();
|
||||||
|
|
||||||
// Create the brush preview group & circles
|
// Create the brush preview group & circles
|
||||||
@ -94,8 +96,9 @@ export class CanvasTool {
|
|||||||
this.group.add(this.rect.group);
|
this.group.add(this.rect.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
scaleTool(stage: Konva.Stage, toolState: CanvasV2State['tool']) {
|
scaleTool = () => {
|
||||||
const scale = stage.scaleX();
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const scale = this.manager.stage.scaleX();
|
||||||
|
|
||||||
const brushRadius = toolState.brush.width / 2;
|
const brushRadius = toolState.brush.width / 2;
|
||||||
this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
|
this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale);
|
||||||
@ -110,19 +113,19 @@ export class CanvasTool {
|
|||||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
|
strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const stage = this.manager.stage;
|
||||||
|
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const currentFill = this.manager.stateApi.getCurrentFill();
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
const cursorPos = this.manager.stateApi.getLastCursorPos();
|
||||||
|
const lastMouseDownPos = this.manager.stateApi.getLastMouseDownPos();
|
||||||
|
const isDrawing = this.manager.stateApi.getIsDrawing();
|
||||||
|
const isMouseDown = this.manager.stateApi.getIsMouseDown();
|
||||||
|
|
||||||
render(
|
|
||||||
stage: Konva.Stage,
|
|
||||||
renderedEntityCount: number,
|
|
||||||
toolState: CanvasV2State['tool'],
|
|
||||||
currentFill: RgbaColor,
|
|
||||||
selectedEntity: CanvasEntity | null,
|
|
||||||
cursorPos: Position | null,
|
|
||||||
lastMouseDownPos: Position | null,
|
|
||||||
isDrawing: boolean,
|
|
||||||
isMouseDown: boolean
|
|
||||||
) {
|
|
||||||
const tool = toolState.selected;
|
const tool = toolState.selected;
|
||||||
const isDrawableEntity =
|
const isDrawableEntity =
|
||||||
selectedEntity?.type === 'regional_guidance' ||
|
selectedEntity?.type === 'regional_guidance' ||
|
||||||
@ -182,7 +185,7 @@ export class CanvasTool {
|
|||||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scaleTool(stage, toolState);
|
this.scaleTool();
|
||||||
|
|
||||||
this.brush.group.visible(true);
|
this.brush.group.visible(true);
|
||||||
this.eraser.group.visible(false);
|
this.eraser.group.visible(false);
|
||||||
@ -208,7 +211,7 @@ export class CanvasTool {
|
|||||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scaleTool(stage, toolState);
|
this.scaleTool();
|
||||||
|
|
||||||
this.brush.group.visible(false);
|
this.brush.group.visible(false);
|
||||||
this.eraser.group.visible(true);
|
this.eraser.group.visible(true);
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
CA_LAYER_NAME,
|
CA_LAYER_NAME,
|
||||||
INPAINT_MASK_LAYER_NAME,
|
INPAINT_MASK_LAYER_ID,
|
||||||
RASTER_LAYER_BRUSH_LINE_NAME,
|
RASTER_LAYER_BRUSH_LINE_NAME,
|
||||||
RASTER_LAYER_ERASER_LINE_NAME,
|
RASTER_LAYER_ERASER_LINE_NAME,
|
||||||
RASTER_LAYER_IMAGE_NAME,
|
RASTER_LAYER_IMAGE_NAME,
|
||||||
@ -102,7 +102,7 @@ export const selectRenderableLayers = (node: Konva.Node): boolean =>
|
|||||||
node.name() === RG_LAYER_NAME ||
|
node.name() === RG_LAYER_NAME ||
|
||||||
node.name() === CA_LAYER_NAME ||
|
node.name() === CA_LAYER_NAME ||
|
||||||
node.name() === RASTER_LAYER_NAME ||
|
node.name() === RASTER_LAYER_NAME ||
|
||||||
node.name() === INPAINT_MASK_LAYER_NAME;
|
node.name() === INPAINT_MASK_LAYER_ID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konva selection callback to select RG mask objects. This includes lines and rects.
|
* Konva selection callback to select RG mask objects. This includes lines and rects.
|
||||||
|
@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
import { INPAINT_MASK_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||||
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
||||||
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
||||||
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
|
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
|
||||||
@ -20,19 +21,19 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/
|
|||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
|
||||||
|
|
||||||
import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types';
|
import type { CanvasEntityIdentifier, CanvasV2State, Position, StageAttrs } from './types';
|
||||||
import { RGBA_RED } from './types';
|
import { RGBA_RED } from './types';
|
||||||
|
|
||||||
const initialState: CanvasV2State = {
|
const initialState: CanvasV2State = {
|
||||||
_version: 3,
|
_version: 3,
|
||||||
selectedEntityIdentifier: { type: 'inpaint_mask', id: 'inpaint_mask' },
|
selectedEntityIdentifier: { type: 'inpaint_mask', id: INPAINT_MASK_LAYER_ID },
|
||||||
layers: { entities: [], imageCache: null },
|
layers: { entities: [], imageCache: null },
|
||||||
controlAdapters: { entities: [] },
|
controlAdapters: { entities: [] },
|
||||||
ipAdapters: { entities: [] },
|
ipAdapters: { entities: [] },
|
||||||
regions: { entities: [] },
|
regions: { entities: [] },
|
||||||
loras: [],
|
loras: [],
|
||||||
inpaintMask: {
|
inpaintMask: {
|
||||||
id: 'inpaint_mask',
|
id: INPAINT_MASK_LAYER_ID,
|
||||||
type: 'inpaint_mask',
|
type: 'inpaint_mask',
|
||||||
bbox: null,
|
bbox: null,
|
||||||
bboxNeedsUpdate: false,
|
bboxNeedsUpdate: false,
|
||||||
@ -366,6 +367,12 @@ export const $stageAttrs = atom<StageAttrs>({
|
|||||||
});
|
});
|
||||||
export const $shouldShowStagedImage = atom(true);
|
export const $shouldShowStagedImage = atom(true);
|
||||||
export const $lastProgressEvent = atom<InvocationDenoiseProgressEvent | null>(null);
|
export const $lastProgressEvent = atom<InvocationDenoiseProgressEvent | null>(null);
|
||||||
|
export const $isDrawing = atom<boolean>(false);
|
||||||
|
export const $isMouseDown = atom<boolean>(false);
|
||||||
|
export const $lastAddedPoint = atom<Position | null>(null);
|
||||||
|
export const $lastMouseDownPos = atom<Position | null>(null);
|
||||||
|
export const $lastCursorPos = atom<Position | null>(null);
|
||||||
|
export const $spaceKey = atom<boolean>(false);
|
||||||
|
|
||||||
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
||||||
name: canvasV2Slice.name,
|
name: canvasV2Slice.name,
|
||||||
|
Loading…
Reference in New Issue
Block a user