From 022bb8649cec8c2a59a56e25a6869d547e19c4bb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:04:38 +1000 Subject: [PATCH] feat(ui): move tool now only moves --- .../components/StageComponent.tsx | 4 +- .../controlLayers/konva/CanvasLayer.ts | 81 +++++++++++++------ .../controlLayers/konva/CanvasManager.ts | 5 +- .../controlLayers/konva/CanvasStateApi.ts | 5 ++ .../src/features/controlLayers/konva/util.ts | 9 ++- .../src/features/controlLayers/store/types.ts | 2 +- 6 files changed, 78 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 1db97cce03..31072b0fa1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -10,6 +10,8 @@ import { v4 as uuidv4 } from 'uuid'; const log = logger('canvas'); +const showHud = false; + // This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead? Konva.showWarnings = false; @@ -83,7 +85,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { /> {!asPreview && ( - + {showHud && } )} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index b9ced230e5..df8392c4f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -16,6 +16,7 @@ export class CanvasLayer { static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`; static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`; static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`; + static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`; private drawingBuffer: BrushLine | EraserLine | RectShape | null; private state: LayerEntity; @@ -39,21 +40,26 @@ export class CanvasLayer { this.id = state.id; this.manager = manager; this.konva = { - layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), - group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true }), + layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }), bbox: new Konva.Rect({ - listening: true, + listening: false, + name: CanvasLayer.BBOX_NAME, stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400 + fill: '', + perfectDrawEnabled: false, + strokeHitEnabled: false, }), objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), transformer: new Konva.Transformer({ name: CanvasLayer.TRANSFORMER_NAME, shouldOverdrawWholeArea: true, - draggable: true, + draggable: false, dragDistance: 0, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], rotateEnabled: false, flipEnabled: false, + listening: false, }), }; @@ -71,7 +77,7 @@ export class CanvasLayer { 'layer' ); }); - this.konva.transformer.on('dragend', () => { + this.konva.group.on('dragend', () => { this.manager.stateApi.onPosChanged( { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'layer' @@ -152,6 +158,7 @@ export class CanvasLayer { } } + this.renderBbox(); this.updateGroup(didDraw); } @@ -225,7 +232,12 @@ export class CanvasLayer { } if (didDraw) { - this.getBbox(); + if (this.objects.size > 0) { + this.getBbox(); + } else { + this.bbox = null; + this.renderBbox(); + } } this.konva.layer.visible(true); @@ -233,26 +245,42 @@ export class CanvasLayer { const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; + const transformerListening = selectedTool === 'transform' && isSelected; + const bboxListening = selectedTool === 'move' && isSelected; + + this.konva.layer.listening(transformerListening || bboxListening); + this.konva.transformer.listening(transformerListening); + this.konva.group.listening(bboxListening); + this.konva.bbox.listening(bboxListening); + 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(); } - } else if (isSelected && selectedTool === 'move') { + } else if (isSelected && selectedTool === 'transform') { // 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(); - } else if (isSelected && selectedTool !== 'move') { + this.konva.transformer.visible(true); + } else if (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.transformer.nodes([]); + this.konva.transformer.forceUpdate(); + this.konva.transformer.visible(false); + } else if (isSelected) { // 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)) { @@ -270,7 +298,6 @@ export class CanvasLayer { } } else 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. @@ -281,16 +308,23 @@ export class CanvasLayer { } renderBbox() { - if (!this.bbox) { - this.konva.bbox.visible(false); - return; - } - this.konva.bbox.visible(true); - this.konva.bbox.strokeWidth(1 / this.manager.stage.scaleX()); - this.konva.bbox.setAttrs(this.bbox); + const isSelected = this.manager.stateApi.getIsSelected(this.id); + const selectedTool = this.manager.stateApi.getToolState().selected; + + this.konva.bbox.setAttrs({ + ...this.bbox, + strokeWidth: 1 / this.manager.stage.scaleX(), + visible: this.bbox !== null && selectedTool === 'move' && isSelected, + }); } private _getBbox() { + if (this.objects.size === 0) { + this.bbox = null; + this.renderBbox(); + return; + } + let needsPixelBbox = false; const rect = this.konva.objectGroup.getClientRect({ skipTransform: true }); // console.log('rect', rect); @@ -334,11 +368,12 @@ export class CanvasLayer { (extents) => { // console.log('extents', extents); if (extents) { + const { minX, minY, maxX, maxY } = extents; this.bbox = { - x: extents.minX + rect.x - Math.floor(this.konva.layer.x()), - y: extents.minY + rect.y - Math.floor(this.konva.layer.y()), - width: extents.maxX - extents.minX, - height: extents.maxY - extents.minY, + x: minX + rect.x - Math.floor(this.konva.layer.x()), + y: minY + rect.y - Math.floor(this.konva.layer.y()), + width: maxX - minX, + height: maxY - minY, }; } else { this.bbox = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 7c56b21a7e..a294f71506 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -33,6 +33,7 @@ import { CanvasTool } from './CanvasTool'; import { setStageEventHandlers } from './events'; const log = logger('canvas'); +const workerLog = logger('worker'); // type Extents = { // minX: number; @@ -137,9 +138,9 @@ export class CanvasManager { const { type, data } = event.data; if (type === 'log') { if (data.ctx) { - log[data.level](data.ctx, data.message); + workerLog[data.level](data.ctx, data.message); } else { - log[data.level](data.message); + workerLog[data.level](data.message); } } else if (type === 'extents') { const task = this.tasks.get(data.id); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 162789bd2e..f16fd45ec5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -17,6 +17,7 @@ import { caBboxChanged, caScaled, caTranslated, + entitySelected, eraserWidthChanged, imBboxChanged, imBrushLineAdded, @@ -136,6 +137,10 @@ export class CanvasStateApi { this.store.dispatch(imRectShapeAdded(arg)); } }; + onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => { + log.debug('Entity selected'); + this.store.dispatch(entitySelected(arg)); + }; onBboxTransformed = (bbox: IRect) => { log.debug('Generation bbox transformed'); this.store.dispatch(bboxChanged(bbox)); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index ba6064d53b..fc764afe04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,4 +1,5 @@ import { getImageDataTransparency } from 'common/util/arrayBuffer'; +import { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types'; import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers'; @@ -366,7 +367,7 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko stageClone.y(0); const validLayers = layersState.entities.filter(isValidLayer); - + console.log(validLayers); // Konva bug (?) - when iterating over the array returned from `stage.getLayers()`, if you destroy a layer, the array // is mutated in-place and the next iteration will skip the next layer. To avoid this, we first collect the layers // to delete in a separate array and then destroy them. @@ -376,7 +377,10 @@ export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Ko for (const konvaLayer of stageClone.getLayers()) { const layer = validLayers.find((l) => l.id === konvaLayer.id()); if (!layer) { + console.log('deleting', konvaLayer); toDelete.push(konvaLayer); + } else { + konvaLayer.findOne(`.${CanvasLayer.GROUP_NAME}`)?.findOne(`.${CanvasLayer.BBOX_NAME}`)?.destroy(); } } @@ -395,6 +399,9 @@ export function getGenerationMode(arg: { manager: CanvasManager }): GenerationMo const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); const compositeLayer = getCompositeLayerStageClone(arg); const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); + imageDataToBlob(compositeLayerImageData).then((blob) => { + previewBlob(blob, 'composite layer'); + }); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); if (compositeLayerTransparency.isPartiallyTransparent) { if (compositeLayerTransparency.isFullyTransparent) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index be4c3e1e9d..eaa2e32c64 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -464,7 +464,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); +const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'transform']); export type Tool = z.infer; export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { return tool === 'brush' || tool === 'eraser' || tool === 'rect';