mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip inpaint mask uses new API
This commit is contained in:
parent
31ac02cd93
commit
5dcef6fa0d
@ -1,294 +1,128 @@
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
|
||||||
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
|
||||||
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import type { CanvasInpaintMaskState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types';
|
||||||
import type {
|
|
||||||
CanvasBrushLineState,
|
|
||||||
CanvasEraserLineState,
|
|
||||||
CanvasInpaintMaskState,
|
|
||||||
CanvasRectState,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
|
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
import type { Logger } from 'roarr';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export class CanvasInpaintMask {
|
export class CanvasInpaintMask {
|
||||||
static NAME_PREFIX = 'inpaint-mask';
|
|
||||||
static LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`;
|
|
||||||
static TRANSFORMER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_transformer`;
|
|
||||||
static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`;
|
|
||||||
static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`;
|
|
||||||
static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`;
|
|
||||||
static TYPE = 'inpaint_mask' as const;
|
static TYPE = 'inpaint_mask' as const;
|
||||||
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
static NAME_PREFIX = 'inpaint-mask';
|
||||||
private state: CanvasInpaintMaskState;
|
static KONVA_LAYER_NAME = `${CanvasInpaintMask.NAME_PREFIX}_layer`;
|
||||||
|
static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`;
|
||||||
|
|
||||||
id = CanvasInpaintMask.TYPE;
|
id = CanvasInpaintMask.TYPE;
|
||||||
type = CanvasInpaintMask.TYPE;
|
type = CanvasInpaintMask.TYPE;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
|
log: Logger;
|
||||||
|
getLoggingContext: GetLoggingContext;
|
||||||
|
|
||||||
|
state: CanvasInpaintMaskState;
|
||||||
|
|
||||||
|
transformer: CanvasTransformer;
|
||||||
|
renderer: CanvasObjectRenderer;
|
||||||
|
|
||||||
|
isFirstRender: boolean = true;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
group: Konva.Group;
|
|
||||||
objectGroup: Konva.Group;
|
objectGroup: Konva.Group;
|
||||||
transformer: Konva.Transformer;
|
|
||||||
compositingRect: Konva.Rect;
|
|
||||||
};
|
};
|
||||||
objects: Map<string, CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer>;
|
|
||||||
|
|
||||||
constructor(state: CanvasInpaintMaskState, manager: CanvasManager) {
|
constructor(state: CanvasInpaintMaskState, manager: CanvasManager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
|
||||||
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
|
this.log.debug({ state }, 'Creating inpaint mask');
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }),
|
layer: new Konva.Layer({
|
||||||
group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }),
|
name: CanvasInpaintMask.KONVA_LAYER_NAME,
|
||||||
objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }),
|
listening: false,
|
||||||
transformer: new Konva.Transformer({
|
imageSmoothingEnabled: false,
|
||||||
name: CanvasInpaintMask.TRANSFORMER_NAME,
|
|
||||||
shouldOverdrawWholeArea: true,
|
|
||||||
draggable: true,
|
|
||||||
dragDistance: 0,
|
|
||||||
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
|
||||||
rotateEnabled: false,
|
|
||||||
flipEnabled: false,
|
|
||||||
}),
|
}),
|
||||||
compositingRect: new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }),
|
objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.konva.group.add(this.konva.objectGroup);
|
this.transformer = new CanvasTransformer(this);
|
||||||
this.konva.layer.add(this.konva.group);
|
this.renderer = new CanvasObjectRenderer(this, true);
|
||||||
|
assert(this.renderer.konva.compositingRect, 'Compositing rect must be set');
|
||||||
|
|
||||||
this.konva.transformer.on('transformend', () => {
|
this.konva.layer.add(this.konva.objectGroup);
|
||||||
this.manager.stateApi.onScaleChanged(
|
this.konva.layer.add(this.renderer.konva.compositingRect);
|
||||||
{
|
this.konva.layer.add(...this.transformer.getNodes());
|
||||||
id: this.id,
|
|
||||||
scale: this.konva.group.scaleX(),
|
|
||||||
position: { x: this.konva.group.x(), y: this.konva.group.y() },
|
|
||||||
},
|
|
||||||
'inpaint_mask'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.konva.transformer.on('dragend', () => {
|
|
||||||
this.manager.stateApi.setEntityPosition(
|
|
||||||
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
|
|
||||||
'inpaint_mask'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.konva.layer.add(this.konva.transformer);
|
|
||||||
|
|
||||||
this.konva.group.add(this.konva.compositingRect);
|
|
||||||
this.objects = new Map();
|
|
||||||
this.drawingBuffer = null;
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy = (): void => {
|
||||||
|
this.log.debug('Destroying inpaint mask');
|
||||||
|
// We need to call the destroy method on all children so they can do their own cleanup.
|
||||||
|
this.transformer.destroy();
|
||||||
|
this.renderer.destroy();
|
||||||
this.konva.layer.destroy();
|
this.konva.layer.destroy();
|
||||||
}
|
};
|
||||||
|
|
||||||
getDrawingBuffer() {
|
update = async (arg?: { state: CanvasInpaintMaskState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
||||||
return this.drawingBuffer;
|
const state = get(arg, 'state', this.state);
|
||||||
}
|
|
||||||
|
|
||||||
async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) {
|
if (!this.isFirstRender && state === this.state) {
|
||||||
this.drawingBuffer = obj;
|
this.log.trace('State unchanged, skipping update');
|
||||||
if (this.drawingBuffer) {
|
|
||||||
if (this.drawingBuffer.type === 'brush_line') {
|
|
||||||
this.drawingBuffer.color = RGBA_RED;
|
|
||||||
} else if (this.drawingBuffer.type === 'rect') {
|
|
||||||
this.drawingBuffer.color = RGBA_RED;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.renderObject(this.drawingBuffer, true);
|
|
||||||
this.updateGroup(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalizeDrawingBuffer() {
|
|
||||||
if (!this.drawingBuffer) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.drawingBuffer.type === 'brush_line') {
|
|
||||||
this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'inpaint_mask');
|
|
||||||
} else if (this.drawingBuffer.type === 'eraser_line') {
|
|
||||||
this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'inpaint_mask');
|
|
||||||
} else if (this.drawingBuffer.type === 'rect') {
|
|
||||||
this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'inpaint_mask');
|
|
||||||
}
|
|
||||||
this.setDrawingBuffer(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async render(state: CanvasInpaintMaskState) {
|
// const maskOpacity = this.manager.stateApi.getMaskOpacity()
|
||||||
|
|
||||||
|
this.log.debug('Updating');
|
||||||
|
const { position, objects, isEnabled } = state;
|
||||||
|
|
||||||
|
if (this.isFirstRender || objects !== this.state.objects) {
|
||||||
|
await this.updateObjects({ objects });
|
||||||
|
}
|
||||||
|
if (this.isFirstRender || position !== this.state.position) {
|
||||||
|
await this.transformer.updatePosition({ position });
|
||||||
|
}
|
||||||
|
// if (this.isFirstRender || opacity !== this.state.opacity) {
|
||||||
|
// await this.updateOpacity({ opacity });
|
||||||
|
// }
|
||||||
|
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||||
|
await this.updateVisibility({ isEnabled });
|
||||||
|
}
|
||||||
|
// this.transformer.syncInteractionState();
|
||||||
|
|
||||||
|
if (this.isFirstRender) {
|
||||||
|
await this.transformer.updateBbox();
|
||||||
|
}
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
this.isFirstRender = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Update the layer's position and listening state
|
updateObjects = async (arg?: { objects: CanvasInpaintMaskState['objects'] }) => {
|
||||||
this.konva.group.setAttrs({
|
this.log.trace('Updating objects');
|
||||||
x: state.position.x,
|
|
||||||
y: state.position.y,
|
|
||||||
scaleX: 1,
|
|
||||||
scaleY: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let didDraw = false;
|
const objects = get(arg, 'objects', this.state.objects);
|
||||||
|
|
||||||
const objectIds = state.objects.map(mapId);
|
const didUpdate = await this.renderer.render(objects);
|
||||||
// Destroy any objects that are no longer in state
|
|
||||||
for (const object of this.objects.values()) {
|
if (didUpdate) {
|
||||||
if (!objectIds.includes(object.id)) {
|
this.transformer.requestRectCalculation();
|
||||||
this.objects.delete(object.id);
|
|
||||||
object.destroy();
|
|
||||||
didDraw = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const obj of state.objects) {
|
this.isFirstRender = false;
|
||||||
if (await this.renderObject(obj)) {
|
};
|
||||||
didDraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.drawingBuffer) {
|
// updateOpacity = (arg?: { opacity: number }) => {
|
||||||
if (await this.renderObject(this.drawingBuffer)) {
|
// this.log.trace('Updating opacity');
|
||||||
didDraw = true;
|
// const opacity = get(arg, 'opacity', this.state.opacity);
|
||||||
}
|
// this.konva.objectGroup.opacity(opacity);
|
||||||
}
|
// };
|
||||||
|
|
||||||
this.updateGroup(didDraw);
|
updateVisibility = (arg?: { isEnabled: boolean }) => {
|
||||||
}
|
this.log.trace('Updating visibility');
|
||||||
|
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
|
||||||
private async renderObject(obj: CanvasInpaintMaskState['objects'][number], force = false): Promise<boolean> {
|
this.konva.layer.visible(isEnabled && this.renderer.hasObjects());
|
||||||
if (obj.type === 'brush_line') {
|
};
|
||||||
let brushLine = this.objects.get(obj.id);
|
|
||||||
assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined);
|
|
||||||
|
|
||||||
if (!brushLine) {
|
|
||||||
brushLine = new CanvasBrushLineRenderer(obj);
|
|
||||||
this.objects.set(brushLine.id, brushLine);
|
|
||||||
this.konva.objectGroup.add(brushLine.konva.group);
|
|
||||||
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 CanvasEraserLineRenderer || eraserLine === undefined);
|
|
||||||
|
|
||||||
if (!eraserLine) {
|
|
||||||
eraserLine = new CanvasEraserLineRenderer(obj);
|
|
||||||
this.objects.set(eraserLine.id, eraserLine);
|
|
||||||
this.konva.objectGroup.add(eraserLine.konva.group);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (eraserLine.update(obj, force)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (obj.type === 'rect') {
|
|
||||||
let rect = this.objects.get(obj.id);
|
|
||||||
assert(rect instanceof CanvasRectRenderer || rect === undefined);
|
|
||||||
|
|
||||||
if (!rect) {
|
|
||||||
rect = new CanvasRectRenderer(obj);
|
|
||||||
this.objects.set(rect.id, rect);
|
|
||||||
this.konva.objectGroup.add(rect.konva.group);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (rect.update(obj, force)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGroup(didDraw: boolean) {
|
|
||||||
this.konva.layer.visible(this.state.isEnabled);
|
|
||||||
|
|
||||||
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
|
|
||||||
this.konva.group.opacity(1);
|
|
||||||
|
|
||||||
if (didDraw) {
|
|
||||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
|
||||||
const rgbColor = rgbColorToString(this.state.fill);
|
|
||||||
const maskOpacity = this.manager.stateApi.getMaskOpacity();
|
|
||||||
|
|
||||||
this.konva.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
|
|
||||||
...getNodeBboxFast(this.konva.objectGroup),
|
|
||||||
fill: rgbColor,
|
|
||||||
opacity: maskOpacity,
|
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
|
||||||
globalCompositeOperation: 'source-in',
|
|
||||||
visible: true,
|
|
||||||
// This rect must always be on top of all other shapes
|
|
||||||
zIndex: this.objects.size + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
|
||||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
|
||||||
|
|
||||||
if (this.objects.size === 0) {
|
|
||||||
// If the layer is totally empty, reset the cache and bail out.
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
this.konva.transformer.nodes([]);
|
|
||||||
if (this.konva.group.isCached()) {
|
|
||||||
this.konva.group.clearCache();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected && selectedTool === 'move') {
|
|
||||||
// When the layer is selected and being moved, we should always cache it.
|
|
||||||
// We should update the cache if we drew to the layer.
|
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
|
||||||
// this.konva.group.cache();
|
|
||||||
}
|
|
||||||
// Activate the transformer
|
|
||||||
this.konva.layer.listening(true);
|
|
||||||
this.konva.transformer.nodes([this.konva.group]);
|
|
||||||
this.konva.transformer.forceUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected && selectedTool !== 'move') {
|
|
||||||
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
// The transformer also does not need to be active.
|
|
||||||
this.konva.transformer.nodes([]);
|
|
||||||
if (isDrawingTool(selectedTool)) {
|
|
||||||
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
|
||||||
// should never be cached.
|
|
||||||
if (this.konva.group.isCached()) {
|
|
||||||
this.konva.group.clearCache();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
|
|
||||||
// We should update the cache if we drew to the layer.
|
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
|
||||||
// this.konva.group.cache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSelected) {
|
|
||||||
// Unselected layers should not be listening
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
// The transformer also does not need to be active.
|
|
||||||
this.konva.transformer.nodes([]);
|
|
||||||
// Update the layer's cache if it's not already cached or we drew to it.
|
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
|
||||||
// this.konva.group.cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -177,9 +177,6 @@ export class CanvasManager {
|
|||||||
this.background = new CanvasBackground(this);
|
this.background = new CanvasBackground(this);
|
||||||
this.stage.add(this.background.konva.layer);
|
this.stage.add(this.background.konva.layer);
|
||||||
|
|
||||||
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
|
|
||||||
this.stage.add(this.inpaintMask.konva.layer);
|
|
||||||
|
|
||||||
this.layers = new Map();
|
this.layers = new Map();
|
||||||
this.regions = new Map();
|
this.regions = new Map();
|
||||||
this.controlAdapters = new Map();
|
this.controlAdapters = new Map();
|
||||||
@ -222,6 +219,9 @@ export class CanvasManager {
|
|||||||
this.getSelectedEntity(),
|
this.getSelectedEntity(),
|
||||||
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
|
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this);
|
||||||
|
this.stage.add(this.inpaintMask.konva.layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
enableDebugging() {
|
enableDebugging() {
|
||||||
@ -273,11 +273,6 @@ export class CanvasManager {
|
|||||||
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
|
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderInpaintMask() {
|
|
||||||
const inpaintMaskState = this.stateApi.getInpaintMaskState();
|
|
||||||
await this.inpaintMask.render(inpaintMaskState);
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderControlAdapters() {
|
async renderControlAdapters() {
|
||||||
const { entities } = this.stateApi.getControlAdaptersState();
|
const { entities } = this.stateApi.getControlAdaptersState();
|
||||||
|
|
||||||
@ -372,9 +367,9 @@ export class CanvasManager {
|
|||||||
const selectedEntity = this.getSelectedEntity();
|
const selectedEntity = this.getSelectedEntity();
|
||||||
if (selectedEntity) {
|
if (selectedEntity) {
|
||||||
if (selectedEntity.state.type === 'regional_guidance') {
|
if (selectedEntity.state.type === 'regional_guidance') {
|
||||||
currentFill = { ...selectedEntity.state.fill, a: state.settings.maskOpacity };
|
currentFill = { ...selectedEntity.state.fill, a: 1 };
|
||||||
} else if (selectedEntity.state.type === 'inpaint_mask') {
|
} else if (selectedEntity.state.type === 'inpaint_mask') {
|
||||||
currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity };
|
currentFill = { ...state.inpaintMask.fill, a: 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return currentFill;
|
return currentFill;
|
||||||
@ -394,7 +389,10 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
const layer = this.getSelectedEntity();
|
const layer = this.getSelectedEntity();
|
||||||
// TODO(psyche): Support other entity types
|
// TODO(psyche): Support other entity types
|
||||||
assert(layer?.adapter instanceof CanvasLayer, 'No selected layer');
|
assert(
|
||||||
|
layer && (layer.adapter instanceof CanvasLayer || layer.adapter instanceof CanvasInpaintMask),
|
||||||
|
'No selected layer'
|
||||||
|
);
|
||||||
layer.adapter.transformer.startTransform();
|
layer.adapter.transformer.startTransform();
|
||||||
this.isTransforming.publish(true);
|
this.isTransforming.publish(true);
|
||||||
}
|
}
|
||||||
@ -472,13 +470,16 @@ export class CanvasManager {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this._isFirstRender ||
|
this._isFirstRender ||
|
||||||
state.inpaintMask !== this._prevState.inpaintMask ||
|
|
||||||
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
state.settings.maskOpacity !== this._prevState.settings.maskOpacity ||
|
||||||
state.tool.selected !== this._prevState.tool.selected ||
|
state.tool.selected !== this._prevState.tool.selected ||
|
||||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||||
) {
|
) {
|
||||||
this.log.debug('Rendering inpaint mask');
|
this.log.debug('Rendering inpaint mask');
|
||||||
await this.renderInpaintMask();
|
await this.inpaintMask.update({
|
||||||
|
state: state.inpaintMask,
|
||||||
|
toolState: state.tool,
|
||||||
|
isSelected: state.selectedEntityIdentifier?.id === state.inpaintMask.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -670,9 +671,14 @@ export class CanvasManager {
|
|||||||
| CanvasTransformer
|
| CanvasTransformer
|
||||||
| CanvasObjectRenderer
|
| CanvasObjectRenderer
|
||||||
| CanvasLayer
|
| CanvasLayer
|
||||||
|
| CanvasInpaintMask
|
||||||
| CanvasStagingArea
|
| CanvasStagingArea
|
||||||
): GetLoggingContext => {
|
): GetLoggingContext => {
|
||||||
if (instance instanceof CanvasLayer || instance instanceof CanvasStagingArea) {
|
if (
|
||||||
|
instance instanceof CanvasLayer ||
|
||||||
|
instance instanceof CanvasStagingArea ||
|
||||||
|
instance instanceof CanvasInpaintMask
|
||||||
|
) {
|
||||||
return (extra?: JSONObject): JSONObject => {
|
return (extra?: JSONObject): JSONObject => {
|
||||||
return {
|
return {
|
||||||
...instance.manager.getLoggingContext(),
|
...instance.manager.getLoggingContext(),
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { JSONObject } from 'common/types';
|
import type { JSONObject } from 'common/types';
|
||||||
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
||||||
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
||||||
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
|
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
|
||||||
|
import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask';
|
||||||
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
||||||
@ -13,6 +15,7 @@ import type {
|
|||||||
CanvasImageState,
|
CanvasImageState,
|
||||||
CanvasRectState,
|
CanvasRectState,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
|
import Konva from 'konva';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -30,9 +33,10 @@ type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImage
|
|||||||
*/
|
*/
|
||||||
export class CanvasObjectRenderer {
|
export class CanvasObjectRenderer {
|
||||||
static TYPE = 'object_renderer';
|
static TYPE = 'object_renderer';
|
||||||
|
static KONVA_COMPOSITING_RECT_NAME = 'compositing-rect';
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
parent: CanvasLayer;
|
parent: CanvasLayer | CanvasInpaintMask;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
getLoggingContext: (extra?: JSONObject) => JSONObject;
|
getLoggingContext: (extra?: JSONObject) => JSONObject;
|
||||||
@ -54,7 +58,11 @@ export class CanvasObjectRenderer {
|
|||||||
*/
|
*/
|
||||||
renderers: Map<string, AnyObjectRenderer> = new Map();
|
renderers: Map<string, AnyObjectRenderer> = new Map();
|
||||||
|
|
||||||
constructor(parent: CanvasLayer) {
|
konva: {
|
||||||
|
compositingRect: Konva.Rect | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(parent: CanvasLayer | CanvasInpaintMask, withCompositingRect: boolean = false) {
|
||||||
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
|
this.id = getPrefixedId(CanvasObjectRenderer.TYPE);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.manager = parent.manager;
|
this.manager = parent.manager;
|
||||||
@ -62,6 +70,18 @@ export class CanvasObjectRenderer {
|
|||||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
this.log.trace('Creating object renderer');
|
this.log.trace('Creating object renderer');
|
||||||
|
|
||||||
|
this.konva = {
|
||||||
|
compositingRect: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (withCompositingRect) {
|
||||||
|
this.konva.compositingRect = new Konva.Rect({
|
||||||
|
name: CanvasObjectRenderer.KONVA_COMPOSITING_RECT_NAME,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
this.parent.konva.objectGroup.add(this.konva.compositingRect);
|
||||||
|
}
|
||||||
|
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.manager.toolState.subscribe((newVal, oldVal) => {
|
this.manager.toolState.subscribe((newVal, oldVal) => {
|
||||||
if (newVal.selected !== oldVal.selected) {
|
if (newVal.selected !== oldVal.selected) {
|
||||||
@ -69,6 +89,21 @@ export class CanvasObjectRenderer {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The compositing rect must cover the whole stage at all times. When the stage is scaled, moved or resized, we
|
||||||
|
// need to update the compositing rect to match the stage.
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.manager.stateApi.$stageAttrs.listen((attrs) => {
|
||||||
|
if (this.konva.compositingRect) {
|
||||||
|
this.konva.compositingRect.setAttrs({
|
||||||
|
x: -attrs.position.x / attrs.scale,
|
||||||
|
y: -attrs.position.y / attrs.scale,
|
||||||
|
width: attrs.dimensions.width / attrs.scale,
|
||||||
|
height: attrs.dimensions.height / attrs.scale,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,6 +131,24 @@ export class CanvasObjectRenderer {
|
|||||||
didRender = (await this.renderObject(this.buffer)) || didRender;
|
didRender = (await this.renderObject(this.buffer)) || didRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (didRender && this.parent.type === 'inpaint_mask') {
|
||||||
|
assert(this.konva.compositingRect, 'Compositing rect must exist for inpaint mask');
|
||||||
|
|
||||||
|
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||||
|
const rgbColor = rgbColorToString(this.parent.state.fill);
|
||||||
|
const maskOpacity = this.manager.stateApi.getMaskOpacity();
|
||||||
|
|
||||||
|
this.konva.compositingRect.setAttrs({
|
||||||
|
fill: rgbColor,
|
||||||
|
opacity: maskOpacity,
|
||||||
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
|
globalCompositeOperation: 'source-in',
|
||||||
|
visible: true,
|
||||||
|
// This rect must always be on top of all other shapes
|
||||||
|
// zIndex: this.renderers.size + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return didRender;
|
return didRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,6 +230,12 @@ export class CanvasObjectRenderer {
|
|||||||
|
|
||||||
this.buffer = objectState;
|
this.buffer = objectState;
|
||||||
return await this.renderObject(this.buffer, true);
|
return await this.renderObject(this.buffer, true);
|
||||||
|
|
||||||
|
// const didDraw = await this.renderObject(this.buffer, true);
|
||||||
|
// if (didDraw && this.konva.compositingRect) {
|
||||||
|
// this.konva.compositingRect.zIndex(this.renderers.size + 1);
|
||||||
|
// }
|
||||||
|
// return didDraw;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -204,11 +263,11 @@ export class CanvasObjectRenderer {
|
|||||||
this.buffer.id = getPrefixedId(this.buffer.type);
|
this.buffer.id = getPrefixedId(this.buffer.type);
|
||||||
|
|
||||||
if (this.buffer.type === 'brush_line') {
|
if (this.buffer.type === 'brush_line') {
|
||||||
this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, 'layer');
|
this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type);
|
||||||
} else if (this.buffer.type === 'eraser_line') {
|
} else if (this.buffer.type === 'eraser_line') {
|
||||||
this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, 'layer');
|
this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type);
|
||||||
} else if (this.buffer.type === 'rect') {
|
} else if (this.buffer.type === 'rect') {
|
||||||
this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, 'layer');
|
this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type);
|
||||||
} else {
|
} else {
|
||||||
this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type');
|
this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type');
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { CanvasInpaintMask } from 'features/controlLayers/konva/CanvasInpaintMask';
|
||||||
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getEmptyRect, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
@ -31,7 +32,7 @@ export class CanvasTransformer {
|
|||||||
static ANCHOR_HIT_PADDING = 10;
|
static ANCHOR_HIT_PADDING = 10;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
parent: CanvasLayer;
|
parent: CanvasLayer | CanvasInpaintMask;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
getLoggingContext: GetLoggingContext;
|
getLoggingContext: GetLoggingContext;
|
||||||
@ -89,7 +90,7 @@ export class CanvasTransformer {
|
|||||||
bboxOutline: Konva.Rect;
|
bboxOutline: Konva.Rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(parent: CanvasLayer) {
|
constructor(parent: CanvasLayer | CanvasInpaintMask) {
|
||||||
this.id = getPrefixedId(CanvasTransformer.TYPE);
|
this.id = getPrefixedId(CanvasTransformer.TYPE);
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.manager = parent.manager;
|
this.manager = parent.manager;
|
||||||
@ -354,7 +355,7 @@ export class CanvasTransformer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.log.trace({ position }, 'Position changed');
|
this.log.trace({ position }, 'Position changed');
|
||||||
this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, 'layer');
|
this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, this.parent.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
|
@ -128,8 +128,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
$spaceKey,
|
$spaceKey,
|
||||||
getBbox,
|
getBbox,
|
||||||
getSettings,
|
getSettings,
|
||||||
setBrushWidth: onBrushWidthChanged,
|
setBrushWidth,
|
||||||
setEraserWidth: onEraserWidthChanged,
|
setEraserWidth,
|
||||||
} = stateApi;
|
} = stateApi;
|
||||||
|
|
||||||
function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
||||||
@ -461,9 +461,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
}
|
}
|
||||||
// Holding ctrl or meta while scrolling changes the brush size
|
// Holding ctrl or meta while scrolling changes the brush size
|
||||||
if (toolState.selected === 'brush') {
|
if (toolState.selected === 'brush') {
|
||||||
onBrushWidthChanged(calculateNewBrushSize(toolState.brush.width, delta));
|
setBrushWidth(calculateNewBrushSize(toolState.brush.width, delta));
|
||||||
} else if (toolState.selected === 'eraser') {
|
} else if (toolState.selected === 'eraser') {
|
||||||
onEraserWidthChanged(calculateNewBrushSize(toolState.eraser.width, delta));
|
setEraserWidth(calculateNewBrushSize(toolState.eraser.width, delta));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We need the absolute cursor position - not the scaled position
|
// We need the absolute cursor position - not the scaled position
|
||||||
|
Loading…
Reference in New Issue
Block a user