From 678d12fcd5bac99830f0836c3476d837c3d92f6a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:18:38 +1000 Subject: [PATCH] tidy(ui): update canvas classes, organise location of konva nodes --- .../controlLayers/konva/CanvasBackground.ts | 15 +- .../controlLayers/konva/CanvasBbox.ts | 178 +++++++++--------- .../controlLayers/konva/CanvasBrushLine.ts | 52 ++--- .../konva/CanvasControlAdapter.ts | 123 ++++++------ .../controlLayers/konva/CanvasEraserLine.ts | 52 ++--- .../controlLayers/konva/CanvasImage.ts | 115 +++++------ .../controlLayers/konva/CanvasInitialImage.ts | 52 ++--- .../controlLayers/konva/CanvasInpaintMask.ts | 121 ++++++------ .../controlLayers/konva/CanvasLayer.ts | 115 ++++++----- .../controlLayers/konva/CanvasManager.ts | 24 +-- .../controlLayers/konva/CanvasPreview.ts | 8 +- .../konva/CanvasProgressImage.ts | 22 ++- .../konva/CanvasProgressPreview.ts | 26 +-- .../controlLayers/konva/CanvasRect.ts | 35 ++-- .../controlLayers/konva/CanvasRegion.ts | 119 ++++++------ .../controlLayers/konva/CanvasStagingArea.ts | 22 ++- .../controlLayers/konva/CanvasTool.ts | 178 +++++++++--------- .../src/features/controlLayers/konva/util.ts | 16 +- 18 files changed, 675 insertions(+), 598 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 4ead021eb6..c5b78ea7bb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -33,16 +33,19 @@ export class CanvasBackground { static BASE_NAME = 'background'; static LAYER_NAME = `${CanvasBackground.BASE_NAME}_layer`; - layer: Konva.Layer; + konva: { + layer: Konva.Layer; + }; + manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }); + this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) }; } render() { - this.layer.zIndex(0); + this.konva.layer.zIndex(0); const scale = this.manager.stage.scaleX(); const gridSpacing = getGridSpacing(scale); const x = this.manager.stage.x(); @@ -86,11 +89,11 @@ export class CanvasBackground { let _x = 0; let _y = 0; - this.layer.destroyChildren(); + this.konva.layer.destroyChildren(); for (let i = 0; i < xSteps; i++) { _x = gridFullRect.x1 + i * gridSpacing; - this.layer.add( + this.konva.layer.add( new Konva.Line({ x: _x, y: gridFullRect.y1, @@ -103,7 +106,7 @@ export class CanvasBackground { } for (let i = 0; i < ySteps; i++) { _y = gridFullRect.y1 + i * gridSpacing; - this.layer.add( + this.konva.layer.add( new Konva.Line({ x: gridFullRect.x1, y: _y, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index cfa44e8bf6..78b47e4420 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -11,11 +11,10 @@ export class CanvasBbox { static RECT_NAME = `${CanvasBbox.BASE_NAME}_rect`; static TRANSFORMER_NAME = `${CanvasBbox.BASE_NAME}_transformer`; - group: Konva.Group; - rect: Konva.Rect; - transformer: Konva.Transformer; manager: CanvasManager; + konva: { group: Konva.Group; rect: Konva.Rect; transformer: Konva.Transformer }; + ALL_ANCHORS: string[] = [ 'top-left', 'top-center', @@ -36,88 +35,89 @@ export class CanvasBbox { const bbox = this.manager.stateApi.getBbox(); const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height); - // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully - // transparent rect for this purpose. - this.group = new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }); - this.rect = new Konva.Rect({ - name: CanvasBbox.RECT_NAME, - listening: false, - strokeEnabled: false, - draggable: true, - ...this.manager.stateApi.getBbox(), - }); - this.rect.on('dragmove', () => { + this.konva = { + group: new Konva.Group({ name: CanvasBbox.GROUP_NAME, listening: false }), + // Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully + // transparent rect for this purpose. + rect: new Konva.Rect({ + name: CanvasBbox.RECT_NAME, + listening: false, + strokeEnabled: false, + draggable: true, + ...this.manager.stateApi.getBbox(), + }), + transformer: new Konva.Transformer({ + name: CanvasBbox.TRANSFORMER_NAME, + borderDash: [5, 5], + borderStroke: 'rgba(212,216,234,1)', + borderEnabled: true, + rotateEnabled: false, + keepRatio: false, + ignoreStroke: true, + listening: false, + flipEnabled: false, + anchorFill: 'rgba(212,216,234,1)', + anchorStroke: 'rgb(42,42,42)', + anchorSize: 12, + anchorCornerRadius: 3, + shiftBehavior: 'none', // we will implement our own shift behavior + centeredScaling: false, + anchorStyleFunc: (anchor) => { + // Make the x/y resize anchors little bars + if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { + anchor.height(8); + anchor.offsetY(4); + anchor.width(30); + anchor.offsetX(15); + } + if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { + anchor.height(30); + anchor.offsetY(15); + anchor.width(8); + anchor.offsetX(4); + } + }, + anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { + // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed + // to konva's internal coordinate system. + const stage = this.konva.transformer.getStage(); + 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. + 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. + const scaledGridSize = gridSize * stage.scaleX(); + // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. + const stageAbsPos = stage.getAbsolutePosition(); + // The offset is the remainder of the stage's absolute position divided by the scaled grid size. + const offsetX = stageAbsPos.x % scaledGridSize; + const offsetY = stageAbsPos.y % scaledGridSize; + // Finally, calculate the position by rounding to the grid and adding the offset. + return { + x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, + y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, + }; + }, + }), + }; + this.konva.rect.on('dragmove', () => { const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64; const bbox = this.manager.stateApi.getBbox(); const bboxRect: Rect = { ...bbox.rect, - x: roundToMultiple(this.rect.x(), gridSize), - y: roundToMultiple(this.rect.y(), gridSize), + x: roundToMultiple(this.konva.rect.x(), gridSize), + y: roundToMultiple(this.konva.rect.y(), gridSize), }; - this.rect.setAttrs(bboxRect); + this.konva.rect.setAttrs(bboxRect); if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) { this.manager.stateApi.onBboxTransformed(bboxRect); } }); - this.transformer = new Konva.Transformer({ - name: CanvasBbox.TRANSFORMER_NAME, - borderDash: [5, 5], - borderStroke: 'rgba(212,216,234,1)', - borderEnabled: true, - rotateEnabled: false, - keepRatio: false, - ignoreStroke: true, - listening: false, - flipEnabled: false, - anchorFill: 'rgba(212,216,234,1)', - anchorStroke: 'rgb(42,42,42)', - anchorSize: 12, - anchorCornerRadius: 3, - shiftBehavior: 'none', // we will implement our own shift behavior - centeredScaling: false, - anchorStyleFunc: (anchor) => { - // Make the x/y resize anchors little bars - if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) { - anchor.height(8); - anchor.offsetY(4); - anchor.width(30); - anchor.offsetX(15); - } - if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) { - anchor.height(30); - anchor.offsetY(15); - anchor.width(8); - anchor.offsetX(4); - } - }, - anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => { - // This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed - // to konva's internal coordinate system. - const stage = this.transformer.getStage(); - 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. - 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. - const scaledGridSize = gridSize * stage.scaleX(); - // To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position. - const stageAbsPos = stage.getAbsolutePosition(); - // The offset is the remainder of the stage's absolute position divided by the scaled grid size. - const offsetX = stageAbsPos.x % scaledGridSize; - const offsetY = stageAbsPos.y % scaledGridSize; - // Finally, calculate the position by rounding to the grid and adding the offset. - return { - x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX, - y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY, - }; - }, - }); - - this.transformer.on('transform', () => { + this.konva.transformer.on('transform', () => { // In the transform callback, we calculate the bbox's new dims and pos and update the konva object. // Some special handling is needed depending on the anchor being dragged. - const anchor = this.transformer.getActiveAnchor(); + const anchor = this.konva.transformer.getActiveAnchor(); if (!anchor) { // Pretty sure we should always have an anchor here? return; @@ -140,14 +140,14 @@ export class CanvasBbox { } // The coords should be correct per the anchorDragBoundFunc. - let x = this.rect.x(); - let y = this.rect.y(); + let x = this.konva.rect.x(); + let y = this.konva.rect.y(); // Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height // *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap // them to the grid. - let width = roundToMultipleMin(this.rect.width() * this.rect.scaleX(), gridSize); - let height = roundToMultipleMin(this.rect.height() * this.rect.scaleY(), gridSize); + let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), gridSize); + let height = roundToMultipleMin(this.konva.rect.height() * this.konva.rect.scaleY(), gridSize); // If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this // if alt/opt is held - this requires math too big for my brain. @@ -187,7 +187,7 @@ export class CanvasBbox { // Update the bboxRect's attrs directly with the new transform, and reset its scale to 1. // TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly. // Gotta be a way to avoid setting it twice... - this.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); + this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 }); // Update the bbox in internal state. this.manager.stateApi.onBboxTransformed(bboxRect); @@ -199,16 +199,16 @@ export class CanvasBbox { } }); - this.transformer.on('transformend', () => { + this.konva.transformer.on('transformend', () => { // Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held, // we have the correct aspect ratio to start from. - $aspectRatioBuffer.set(this.rect.width() / this.rect.height()); + $aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height()); }); // The transformer will always be transforming the dummy rect - this.transformer.nodes([this.rect]); - this.group.add(this.rect); - this.group.add(this.transformer); + this.konva.transformer.nodes([this.konva.rect]); + this.konva.group.add(this.konva.rect); + this.konva.group.add(this.konva.transformer); } render() { @@ -217,14 +217,14 @@ export class CanvasBbox { const toolState = this.manager.stateApi.getToolState(); if (!session.isActive) { - this.group.listening(false); - this.group.visible(false); + this.konva.group.listening(false); + this.konva.group.visible(false); return; } - this.group.visible(true); - this.group.listening(toolState.selected === 'bbox'); - this.rect.setAttrs({ + this.konva.group.visible(true); + this.konva.group.listening(toolState.selected === 'bbox'); + this.konva.rect.setAttrs({ x: bbox.rect.x, y: bbox.rect.y, width: bbox.rect.width, @@ -233,7 +233,7 @@ export class CanvasBbox { scaleY: 1, listening: toolState.selected === 'bbox', }); - this.transformer.setAttrs({ + this.konva.transformer.setAttrs({ listening: toolState.selected === 'bbox', enabledAnchors: toolState.selected === 'bbox' ? this.ALL_ANCHORS : this.NO_ANCHORS, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts index e9cc950ef1..b5ac4ff1e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBrushLine.ts @@ -8,40 +8,44 @@ export class CanvasBrushLine { static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`; id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; + konva: { + group: Konva.Group; + line: Konva.Line; + }; lastBrushLine: BrushLine; constructor(brushLine: BrushLine) { const { id, strokeWidth, clip, color, points } = brushLine; this.id = id; - this.konvaLineGroup = new Konva.Group({ - name: CanvasBrushLine.GROUP_NAME, - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - name: CanvasBrushLine.LINE_NAME, - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'source-over', - stroke: rgbaColorToString(color), - // A line with only one point will not be rendered, so we duplicate the points to make it visible - points: points.length === 2 ? [...points, ...points] : points, - }); - this.konvaLineGroup.add(this.konvaLine); + this.konva = { + group: new Konva.Group({ + name: CanvasBrushLine.GROUP_NAME, + clip, + listening: false, + }), + line: new Konva.Line({ + name: CanvasBrushLine.LINE_NAME, + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'source-over', + stroke: rgbaColorToString(color), + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, + }), + }; + this.konva.group.add(this.konva.line); this.lastBrushLine = brushLine; } update(brushLine: BrushLine, force?: boolean): boolean { if (this.lastBrushLine !== brushLine || force) { const { points, color, clip, strokeWidth } = brushLine; - this.konvaLine.setAttrs({ + this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, stroke: rgbaColorToString(color), @@ -56,6 +60,6 @@ export class CanvasBrushLine { } destroy() { - this.konvaLineGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts index 82d20d897f..58772e8ff1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasControlAdapter.ts @@ -14,51 +14,60 @@ export class CanvasControlAdapter { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + }; + image: CanvasImage | null; - transformer: Konva.Transformer; constructor(controlAdapterState: ControlAdapterEntity, manager: CanvasManager) { const { id } = controlAdapterState; this.id = id; this.manager = manager; - this.layer = new Konva.Layer({ - name: CanvasControlAdapter.LAYER_NAME, - imageSmoothingEnabled: false, - listening: false, - }); - this.group = new Konva.Group({ - name: CanvasControlAdapter.GROUP_NAME, - listening: false, - }); - this.objectsGroup = new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - - this.transformer = new Konva.Transformer({ - name: CanvasControlAdapter.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva = { + layer: new Konva.Layer({ + name: CanvasControlAdapter.LAYER_NAME, + imageSmoothingEnabled: false, + listening: false, + }), + group: new Konva.Group({ + name: CanvasControlAdapter.GROUP_NAME, + listening: false, + }), + objectGroup: new Konva.Group({ name: CanvasControlAdapter.GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasControlAdapter.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + }; + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'control_adapter' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'control_adapter' ); }); - this.layer.add(this.transformer); + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + this.konva.layer.add(this.konva.transformer); this.image = null; this.controlAdapterState = controlAdapterState; @@ -68,7 +77,7 @@ export class CanvasControlAdapter { this.controlAdapterState = controlAdapterState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: controlAdapterState.position.x, y: controlAdapterState.position.y, scaleX: 1, @@ -81,13 +90,13 @@ export class CanvasControlAdapter { if (!imageObject) { if (this.image) { - this.image.konvaImageGroup.visible(false); + this.image.konva.group.visible(false); didDraw = true; } } else if (!this.image) { this.image = new CanvasImage(imageObject); this.updateGroup(true); - this.objectsGroup.add(this.image.konvaImageGroup); + this.konva.objectGroup.add(this.image.konva.group); await this.image.updateImageSource(imageObject.image.name); } else if (!this.image.isLoading && !this.image.isError) { if (await this.image.update(imageObject)) { @@ -99,18 +108,18 @@ export class CanvasControlAdapter { } updateGroup(didDraw: boolean) { - this.layer.visible(this.controlAdapterState.isEnabled); + this.konva.layer.visible(this.controlAdapterState.isEnabled); - this.group.opacity(this.controlAdapterState.opacity); + this.konva.group.opacity(this.controlAdapterState.opacity); const isSelected = this.manager.stateApi.getIsSelected(this.id); const selectedTool = this.manager.stateApi.getToolState().selected; - if (!this.image?.konvaImage) { + if (!this.image?.image) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -118,32 +127,32 @@ export class CanvasControlAdapter { 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + 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.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + 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.group.isCached()) { - this.group.clearCache(); + 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -151,12 +160,12 @@ export class CanvasControlAdapter { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; @@ -164,6 +173,6 @@ export class CanvasControlAdapter { } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts index b572feea04..b10b9d64fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEraserLine.ts @@ -9,40 +9,44 @@ export class CanvasEraserLine { static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`; id: string; - konvaLineGroup: Konva.Group; - konvaLine: Konva.Line; + konva: { + group: Konva.Group; + line: Konva.Line; + }; lastEraserLine: EraserLine; constructor(eraserLine: EraserLine) { const { id, strokeWidth, clip, points } = eraserLine; this.id = id; - this.konvaLineGroup = new Konva.Group({ - name: CanvasEraserLine.GROUP_NAME, - clip, - listening: false, - }); - this.konvaLine = new Konva.Line({ - name: CanvasEraserLine.LINE_NAME, - id, - listening: false, - shadowForStrokeEnabled: false, - strokeWidth, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - globalCompositeOperation: 'destination-out', - stroke: rgbaColorToString(RGBA_RED), - // A line with only one point will not be rendered, so we duplicate the points to make it visible - points: points.length === 2 ? [...points, ...points] : points, - }); - this.konvaLineGroup.add(this.konvaLine); + this.konva = { + group: new Konva.Group({ + name: CanvasEraserLine.GROUP_NAME, + clip, + listening: false, + }), + line: new Konva.Line({ + name: CanvasEraserLine.LINE_NAME, + id, + listening: false, + shadowForStrokeEnabled: false, + strokeWidth, + tension: 0, + lineCap: 'round', + lineJoin: 'round', + globalCompositeOperation: 'destination-out', + stroke: rgbaColorToString(RGBA_RED), + // A line with only one point will not be rendered, so we duplicate the points to make it visible + points: points.length === 2 ? [...points, ...points] : points, + }), + }; + this.konva.group.add(this.konva.line); this.lastEraserLine = eraserLine; } update(eraserLine: EraserLine, force?: boolean): boolean { if (this.lastEraserLine !== eraserLine || force) { const { points, clip, strokeWidth } = eraserLine; - this.konvaLine.setAttrs({ + this.konva.line.setAttrs({ // A line with only one point will not be rendered, so we duplicate the points to make it visible points: points.length === 2 ? [...points, ...points] : points, clip, @@ -56,6 +60,6 @@ export class CanvasEraserLine { } destroy() { - this.konvaLineGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts index 89243e99af..b941ea96b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasImage.ts @@ -15,48 +15,51 @@ export class CanvasImage { static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`; id: string; - konvaImageGroup: Konva.Group; - konvaPlaceholderGroup: Konva.Group; - konvaPlaceholderRect: Konva.Rect; - konvaPlaceholderText: Konva.Text; + konva: { + group: Konva.Group; + placeholder: { group: Konva.Group; rect: Konva.Rect; text: Konva.Text }; + }; imageName: string | null; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately isLoading: boolean; isError: boolean; lastImageObject: ImageObject; constructor(imageObject: ImageObject) { const { id, width, height, x, y } = imageObject; - this.konvaImageGroup = new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }); - this.konvaPlaceholderGroup = new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }); - this.konvaPlaceholderRect = new Konva.Rect({ - name: CanvasImage.PLACEHOLDER_RECT_NAME, - fill: 'hsl(220 12% 45% / 1)', // 'base.500' - width, - height, - listening: false, - }); - this.konvaPlaceholderText = new Konva.Text({ - name: CanvasImage.PLACEHOLDER_TEXT_NAME, - fill: 'hsl(220 12% 10% / 1)', // 'base.900' - width, - height, - align: 'center', - verticalAlign: 'middle', - fontFamily: '"Inter Variable", sans-serif', - fontSize: width / 16, - fontStyle: '600', - text: t('common.loadingImage', 'Loading Image'), - listening: false, - }); - - this.konvaPlaceholderGroup.add(this.konvaPlaceholderRect); - this.konvaPlaceholderGroup.add(this.konvaPlaceholderText); - this.konvaImageGroup.add(this.konvaPlaceholderGroup); + this.konva = { + group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }), + placeholder: { + group: new Konva.Group({ name: CanvasImage.PLACEHOLDER_GROUP_NAME, listening: false }), + rect: new Konva.Rect({ + name: CanvasImage.PLACEHOLDER_RECT_NAME, + fill: 'hsl(220 12% 45% / 1)', // 'base.500' + width, + height, + listening: false, + }), + text: new Konva.Text({ + name: CanvasImage.PLACEHOLDER_TEXT_NAME, + fill: 'hsl(220 12% 10% / 1)', // 'base.900' + width, + height, + align: 'center', + verticalAlign: 'middle', + fontFamily: '"Inter Variable", sans-serif', + fontSize: width / 16, + fontStyle: '600', + text: t('common.loadingImage', 'Loading Image'), + listening: false, + }), + }, + }; + this.konva.placeholder.group.add(this.konva.placeholder.rect); + this.konva.placeholder.group.add(this.konva.placeholder.text); + this.konva.group.add(this.konva.placeholder.group); this.id = id; this.imageName = null; - this.konvaImage = null; + this.image = null; this.isLoading = false; this.isError = false; this.lastImageObject = imageObject; @@ -65,51 +68,51 @@ export class CanvasImage { async updateImageSource(imageName: string) { try { this.isLoading = true; - this.konvaImageGroup.visible(true); + this.konva.group.visible(true); - if (!this.konvaImage) { - this.konvaPlaceholderGroup.visible(true); - this.konvaPlaceholderText.text(t('common.loadingImage', 'Loading Image')); + if (!this.image) { + this.konva.placeholder.group.visible(true); + this.konva.placeholder.text.text(t('common.loadingImage', 'Loading Image')); } const imageDTO = await getImageDTO(imageName); assert(imageDTO !== null, 'imageDTO is null'); const imageEl = await loadImage(imageDTO.image_url); - if (this.konvaImage) { - this.konvaImage.setAttrs({ + if (this.image) { + this.image.setAttrs({ image: imageEl, }); } else { - this.konvaImage = new Konva.Image({ + this.image = new Konva.Image({ name: CanvasImage.IMAGE_NAME, listening: false, image: imageEl, width: this.lastImageObject.width, height: this.lastImageObject.height, }); - this.konvaImageGroup.add(this.konvaImage); + this.konva.group.add(this.image); } if (this.lastImageObject.filters.length > 0) { - this.konvaImage.cache(); - this.konvaImage.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); + this.image.cache(); + this.image.filters(this.lastImageObject.filters.map((f) => FILTER_MAP[f])); } else { - this.konvaImage.clearCache(); - this.konvaImage.filters([]); + this.image.clearCache(); + this.image.filters([]); } this.imageName = imageName; this.isLoading = false; this.isError = false; - this.konvaPlaceholderGroup.visible(false); + this.konva.placeholder.group.visible(false); } catch { - this.konvaImage?.visible(false); + this.image?.visible(false); this.imageName = null; this.isLoading = false; this.isError = true; - this.konvaPlaceholderText.text(t('common.imageFailedToLoad', 'Image Failed to Load')); - this.konvaPlaceholderGroup.visible(true); + this.konva.placeholder.text.text(t('common.imageFailedToLoad', 'Image Failed to Load')); + this.konva.placeholder.group.visible(true); } } @@ -119,16 +122,16 @@ export class CanvasImage { if (this.lastImageObject.image.name !== image.name || force) { await this.updateImageSource(image.name); } - this.konvaImage?.setAttrs({ x, y, width, height }); + this.image?.setAttrs({ x, y, width, height }); if (filters.length > 0) { - this.konvaImage?.cache(); - this.konvaImage?.filters(filters.map((f) => FILTER_MAP[f])); + this.image?.cache(); + this.image?.filters(filters.map((f) => FILTER_MAP[f])); } else { - this.konvaImage?.clearCache(); - this.konvaImage?.filters([]); + this.image?.clearCache(); + this.image?.filters([]); } - this.konvaPlaceholderRect.setAttrs({ width, height }); - this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); + this.konva.placeholder.rect.setAttrs({ width, height }); + this.konva.placeholder.text.setAttrs({ width, height, fontSize: width / 16 }); this.lastImageObject = imageObject; return true; } else { @@ -137,6 +140,6 @@ export class CanvasImage { } destroy() { - this.konvaImageGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts index 7bf8bf77b8..589fbaec85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -1,33 +1,37 @@ import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { getObjectGroupId } from 'features/controlLayers/konva/naming'; import type { InitialImageEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; -import { v4 as uuidv4 } from 'uuid'; export class CanvasInitialImage { + static NAME_PREFIX = 'initial-image'; + static LAYER_NAME = `${CanvasInitialImage.NAME_PREFIX}_layer`; + static GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_group`; + static OBJECT_GROUP_NAME = `${CanvasInitialImage.NAME_PREFIX}_object-group`; + id = 'initial_image'; - manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - image: CanvasImage | null; + private initialImageState: InitialImageEntity; + manager: CanvasManager; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + }; + + image: CanvasImage | null; + constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ - id: this.id, - imageSmoothingEnabled: true, - listening: false, - }); - this.group = new Konva.Group({ - id: getObjectGroupId(this.layer.id(), uuidv4()), - listening: false, - }); - this.objectsGroup = new Konva.Group({ listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); + this.konva = { + layer: new Konva.Layer({ name: CanvasInitialImage.LAYER_NAME, imageSmoothingEnabled: true, listening: false }), + group: new Konva.Group({ name: CanvasInitialImage.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasInitialImage.OBJECT_GROUP_NAME, listening: false }), + }; + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); this.image = null; this.initialImageState = initialImageState; @@ -37,26 +41,26 @@ export class CanvasInitialImage { this.initialImageState = initialImageState; if (!this.initialImageState.imageObject) { - this.layer.visible(false); + this.konva.layer.visible(false); return; } if (!this.image) { this.image = new CanvasImage(this.initialImageState.imageObject); - this.objectsGroup.add(this.image.konvaImageGroup); + this.konva.objectGroup.add(this.image.konva.group); await this.image.update(this.initialImageState.imageObject, true); } else if (!this.image.isLoading && !this.image.isError) { await this.image.update(this.initialImageState.imageObject); } if (this.initialImageState && this.initialImageState.isEnabled && !this.image?.isLoading && !this.image?.isError) { - this.layer.visible(true); + this.konva.layer.visible(true); } else { - this.layer.visible(false); + this.konva.layer.visible(false); } } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 58264eb5a0..a529db3b94 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -23,57 +23,64 @@ export class CanvasInpaintMask { id = 'inpaint_mask'; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - compositingRect: Konva.Rect; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + compositingRect: Konva.Rect; + }; objects: Map; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }); - this.group = new Konva.Group({ - name: CanvasInpaintMask.GROUP_NAME, - listening: false, - }); - this.objectsGroup = new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); + this.konva = { + layer: new Konva.Layer({ name: CanvasInpaintMask.LAYER_NAME }), + group: new Konva.Group({ name: CanvasInpaintMask.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasInpaintMask.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + 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 }), + }; - this.transformer = new Konva.Transformer({ - name: CanvasInpaintMask.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'inpaint_mask' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'inpaint_mask' ); }); - this.layer.add(this.transformer); + this.konva.layer.add(this.konva.transformer); - this.compositingRect = new Konva.Rect({ name: CanvasInpaintMask.COMPOSITING_RECT_NAME, listening: false }); - this.group.add(this.compositingRect); + this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; this.inpaintMaskState = entity; } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -112,7 +119,7 @@ export class CanvasInpaintMask { this.inpaintMaskState = inpaintMaskState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: inpaintMaskState.position.x, y: inpaintMaskState.position.y, scaleX: 1, @@ -154,7 +161,7 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -168,7 +175,7 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -182,7 +189,7 @@ export class CanvasInpaintMask { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -195,19 +202,19 @@ export class CanvasInpaintMask { } updateGroup(didDraw: boolean) { - this.layer.visible(this.inpaintMaskState.isEnabled); + this.konva.layer.visible(this.inpaintMaskState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); + 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.inpaintMaskState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.compositingRect.setAttrs({ + 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.objectsGroup), + ...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) @@ -223,10 +230,10 @@ export class CanvasInpaintMask { if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -234,32 +241,32 @@ export class CanvasInpaintMask { 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + 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.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + 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.group.isCached()) { - this.group.clearCache(); + 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -267,12 +274,12 @@ export class CanvasInpaintMask { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts index 0539641b50..7951c9d041 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayer.ts @@ -21,40 +21,53 @@ export class CanvasLayer { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + transformer: Konva.Transformer; + }; objects: Map; constructor(entity: LayerEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }); - this.group = new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }); - this.objectsGroup = new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); + this.konva = { + layer: new Konva.Layer({ name: CanvasLayer.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasLayer.TRANSFORMER_NAME, + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }), + }; - this.transformer = new Konva.Transformer({ - name: CanvasLayer.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'layer' ); }); - this.transformer.on('dragend', () => { - this.manager.stateApi.onPosChanged({ id: this.id, position: { x: this.group.x(), y: this.group.y() } }, 'layer'); + this.konva.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged( + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, + 'layer' + ); }); - this.layer.add(this.transformer); + this.konva.layer.add(this.konva.transformer); this.objects = new Map(); this.drawingBuffer = null; @@ -62,7 +75,7 @@ export class CanvasLayer { } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -97,7 +110,7 @@ export class CanvasLayer { this.layerState = layerState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: layerState.position.x, y: layerState.position.y, scaleX: 1, @@ -139,7 +152,7 @@ export class CanvasLayer { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -153,7 +166,7 @@ export class CanvasLayer { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -167,7 +180,7 @@ export class CanvasLayer { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -181,7 +194,7 @@ export class CanvasLayer { if (!image) { image = new CanvasImage(obj); this.objects.set(image.id, image); - this.objectsGroup.add(image.konvaImageGroup); + this.konva.objectGroup.add(image.konva.group); await image.updateImageSource(obj.image.name); return true; } else { @@ -196,58 +209,58 @@ export class CanvasLayer { updateGroup(didDraw: boolean) { if (!this.layerState.isEnabled) { - this.layer.visible(false); + this.konva.layer.visible(false); return; } - this.layer.visible(true); - this.group.opacity(this.layerState.opacity); + this.konva.layer.visible(true); + this.konva.group.opacity(this.layerState.opacity); 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.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } } else 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + this.konva.layer.listening(true); + this.konva.transformer.nodes([this.konva.group]); + this.konva.transformer.forceUpdate(); } else 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.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + 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.group.isCached()) { - this.group.clearCache(); + 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } } else if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 81c0507601..0ff1ba6a16 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -97,17 +97,17 @@ export class CanvasManager { this.stage.add(this.preview.layer); this.background = new CanvasBackground(this); - this.stage.add(this.background.layer); + this.stage.add(this.background.konva.layer); this.inpaintMask = new CanvasInpaintMask(this.stateApi.getInpaintMaskState(), this); - this.stage.add(this.inpaintMask.layer); + this.stage.add(this.inpaintMask.konva.layer); this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); - this.stage.add(this.initialImage.layer); + this.stage.add(this.initialImage.konva.layer); } async renderInitialImage() { @@ -129,7 +129,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasLayer(entity, this); this.layers.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -151,7 +151,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasRegion(entity, this); this.regions.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -181,7 +181,7 @@ export class CanvasManager { if (!adapter) { adapter = new CanvasControlAdapter(entity, this); this.controlAdapters.set(adapter.id, adapter); - this.stage.add(adapter.layer); + this.stage.add(adapter.konva.layer); } await adapter.render(entity); } @@ -193,18 +193,18 @@ export class CanvasManager { const controlAdapters = getControlAdaptersState().entities; const regions = getRegionsState().entities; let zIndex = 0; - this.background.layer.zIndex(++zIndex); - this.initialImage.layer.zIndex(++zIndex); + this.background.konva.layer.zIndex(++zIndex); + this.initialImage.konva.layer.zIndex(++zIndex); for (const layer of layers) { - this.layers.get(layer.id)?.layer.zIndex(++zIndex); + this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex); } for (const ca of controlAdapters) { - this.controlAdapters.get(ca.id)?.layer.zIndex(++zIndex); + this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex); } for (const rg of regions) { - this.regions.get(rg.id)?.layer.zIndex(++zIndex); + this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex); } - this.inpaintMask.layer.zIndex(++zIndex); + this.inpaintMask.konva.layer.zIndex(++zIndex); this.preview.layer.zIndex(++zIndex); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts index 3e64b8a28d..882a01e7af 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasPreview.ts @@ -21,15 +21,15 @@ export class CanvasPreview { this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false }); this.stagingArea = stagingArea; - this.layer.add(this.stagingArea.group); + this.layer.add(this.stagingArea.konva.group); this.bbox = bbox; - this.layer.add(this.bbox.group); + this.layer.add(this.bbox.konva.group); this.tool = tool; - this.layer.add(this.tool.group); + this.layer.add(this.tool.konva.group); this.progressPreview = progressPreview; - this.layer.add(this.progressPreview.group); + this.layer.add(this.progressPreview.konva.group); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts index ac98c45702..edda6c26f5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressImage.ts @@ -8,17 +8,21 @@ export class CanvasProgressImage { id: string; progressImageId: string | null; - konvaImageGroup: Konva.Group; - konvaImage: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + konva: { + group: Konva.Group; + image: Konva.Image | null; // The image is loaded asynchronously, so it may not be available immediately + }; isLoading: boolean; isError: boolean; constructor(arg: { id: string }) { const { id } = arg; - this.konvaImageGroup = new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }); + this.konva = { + group: new Konva.Group({ name: CanvasProgressImage.GROUP_NAME, listening: false }), + image: null, + }; this.id = id; this.progressImageId = null; - this.konvaImage = null; this.isLoading = false; this.isError = false; } @@ -37,8 +41,8 @@ export class CanvasProgressImage { this.isLoading = true; try { const imageEl = await loadImage(dataURL); - if (this.konvaImage) { - this.konvaImage.setAttrs({ + if (this.konva.image) { + this.konva.image.setAttrs({ image: imageEl, x, y, @@ -46,7 +50,7 @@ export class CanvasProgressImage { height, }); } else { - this.konvaImage = new Konva.Image({ + this.konva.image = new Konva.Image({ name: CanvasProgressImage.IMAGE_NAME, listening: false, image: imageEl, @@ -55,7 +59,7 @@ export class CanvasProgressImage { width, height, }); - this.konvaImageGroup.add(this.konvaImage); + this.konva.group.add(this.konva.image); } this.isLoading = false; this.id = progressImageId; @@ -65,6 +69,6 @@ export class CanvasProgressImage { } destroy() { - this.konvaImageGroup.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts index bcf42ea438..95c7910b52 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasProgressPreview.ts @@ -7,15 +7,19 @@ export class CanvasProgressPreview { static NAME_PREFIX = 'progress-preview'; static GROUP_NAME = `${CanvasProgressPreview.NAME_PREFIX}_group`; - group: Konva.Group; - progressImage: CanvasProgressImage; + konva: { + group: Konva.Group; + progressImage: CanvasProgressImage; + }; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }); - this.progressImage = new CanvasProgressImage({ id: 'progress-image' }); - this.group.add(this.progressImage.konvaImageGroup); + this.konva = { + group: new Konva.Group({ name: CanvasProgressPreview.GROUP_NAME, listening: false }), + progressImage: new CanvasProgressImage({ id: 'progress-image' }), + }; + this.konva.group.add(this.konva.progressImage.konva.group); } async render(lastProgressEvent: InvocationDenoiseProgressEvent | null) { @@ -28,15 +32,15 @@ export class CanvasProgressPreview { const { x, y, width, height } = bboxRect; const progressImageId = `${invocation.id}_${step}`; if ( - !this.progressImage.isLoading && - !this.progressImage.isError && - this.progressImage.progressImageId !== progressImageId + !this.konva.progressImage.isLoading && + !this.konva.progressImage.isError && + this.konva.progressImage.progressImageId !== progressImageId ) { - await this.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); - this.progressImage.konvaImageGroup.visible(true); + await this.konva.progressImage.updateImageSource(progressImageId, dataURL, x, y, width, height); + this.konva.progressImage.konva.group.visible(true); } } else { - this.progressImage.konvaImageGroup.visible(false); + this.konva.progressImage.konva.group.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts index 32c755d0f5..6273309159 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRect.ts @@ -4,33 +4,40 @@ import Konva from 'konva'; export class CanvasRect { static NAME_PREFIX = 'canvas-rect'; + static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`; static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`; id: string; - konvaRect: Konva.Rect; + konva: { + group: Konva.Group; + rect: Konva.Rect; + }; lastRectShape: RectShape; constructor(rectShape: RectShape) { const { id, x, y, width, height } = rectShape; this.id = id; - const konvaRect = new Konva.Rect({ - name: CanvasRect.RECT_NAME, - id, - x, - y, - width, - height, - listening: false, - fill: rgbaColorToString(rectShape.color), - }); - this.konvaRect = konvaRect; + this.konva = { + group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }), + rect: new Konva.Rect({ + name: CanvasRect.RECT_NAME, + id, + x, + y, + width, + height, + listening: false, + fill: rgbaColorToString(rectShape.color), + }), + }; + this.konva.group.add(this.konva.rect); this.lastRectShape = rectShape; } update(rectShape: RectShape, force?: boolean): boolean { if (this.lastRectShape !== rectShape || force) { const { x, y, width, height, color } = rectShape; - this.konvaRect.setAttrs({ + this.konva.rect.setAttrs({ x, y, width, @@ -45,6 +52,6 @@ export class CanvasRect { } destroy() { - this.konvaRect.destroy(); + this.konva.group.destroy(); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts index 980675ee14..86603973ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRegion.ts @@ -23,54 +23,63 @@ export class CanvasRegion { id: string; manager: CanvasManager; - layer: Konva.Layer; - group: Konva.Group; - objectsGroup: Konva.Group; - compositingRect: Konva.Rect; - transformer: Konva.Transformer; + + konva: { + layer: Konva.Layer; + group: Konva.Group; + objectGroup: Konva.Group; + compositingRect: Konva.Rect; + transformer: Konva.Transformer; + }; + objects: Map; constructor(entity: RegionEntity, manager: CanvasManager) { this.id = entity.id; this.manager = manager; - this.layer = new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }); - this.group = new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }); - this.objectsGroup = new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }); - this.group.add(this.objectsGroup); - this.layer.add(this.group); - this.transformer = new Konva.Transformer({ - name: CanvasRegion.TRANSFORMER_NAME, - shouldOverdrawWholeArea: true, - draggable: true, - dragDistance: 0, - enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], - rotateEnabled: false, - flipEnabled: false, - }); - this.transformer.on('transformend', () => { + this.konva = { + layer: new Konva.Layer({ name: CanvasRegion.LAYER_NAME, listening: false }), + group: new Konva.Group({ name: CanvasRegion.GROUP_NAME, listening: false }), + objectGroup: new Konva.Group({ name: CanvasRegion.OBJECT_GROUP_NAME, listening: false }), + transformer: new Konva.Transformer({ + name: CanvasRegion.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: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }), + }; + this.konva.group.add(this.konva.objectGroup); + this.konva.layer.add(this.konva.group); + this.konva.transformer.on('transformend', () => { this.manager.stateApi.onScaleChanged( - { id: this.id, scale: this.group.scaleX(), position: { x: this.group.x(), y: this.group.y() } }, + { + id: this.id, + scale: this.konva.group.scaleX(), + position: { x: this.konva.group.x(), y: this.konva.group.y() }, + }, 'regional_guidance' ); }); - this.transformer.on('dragend', () => { + this.konva.transformer.on('dragend', () => { this.manager.stateApi.onPosChanged( - { id: this.id, position: { x: this.group.x(), y: this.group.y() } }, + { id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } }, 'regional_guidance' ); }); - this.layer.add(this.transformer); - - this.compositingRect = new Konva.Rect({ name: CanvasRegion.COMPOSITING_RECT_NAME, listening: false }); - this.group.add(this.compositingRect); + this.konva.layer.add(this.konva.transformer); + this.konva.group.add(this.konva.compositingRect); this.objects = new Map(); this.drawingBuffer = null; this.regionState = entity; } destroy(): void { - this.layer.destroy(); + this.konva.layer.destroy(); } getDrawingBuffer() { @@ -109,7 +118,7 @@ export class CanvasRegion { this.regionState = regionState; // Update the layer's position and listening state - this.group.setAttrs({ + this.konva.group.setAttrs({ x: regionState.position.x, y: regionState.position.y, scaleX: 1, @@ -151,7 +160,7 @@ export class CanvasRegion { if (!brushLine) { brushLine = new CanvasBrushLine(obj); this.objects.set(brushLine.id, brushLine); - this.objectsGroup.add(brushLine.konvaLineGroup); + this.konva.objectGroup.add(brushLine.konva.group); return true; } else { if (brushLine.update(obj, force)) { @@ -165,7 +174,7 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new CanvasEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); - this.objectsGroup.add(eraserLine.konvaLineGroup); + this.konva.objectGroup.add(eraserLine.konva.group); return true; } else { if (eraserLine.update(obj, force)) { @@ -179,7 +188,7 @@ export class CanvasRegion { if (!rect) { rect = new CanvasRect(obj); this.objects.set(rect.id, rect); - this.objectsGroup.add(rect.konvaRect); + this.konva.objectGroup.add(rect.konva.group); return true; } else { if (rect.update(obj, force)) { @@ -192,18 +201,18 @@ export class CanvasRegion { } updateGroup(didDraw: boolean) { - this.layer.visible(this.regionState.isEnabled); + this.konva.layer.visible(this.regionState.isEnabled); // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.group.opacity(1); + 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.regionState.fill); const maskOpacity = this.manager.stateApi.getMaskOpacity(); - this.compositingRect.setAttrs({ + 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.objectsGroup), + ...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) @@ -218,10 +227,10 @@ export class CanvasRegion { if (this.objects.size === 0) { // If the layer is totally empty, reset the cache and bail out. - this.layer.listening(false); - this.transformer.nodes([]); - if (this.group.isCached()) { - this.group.clearCache(); + this.konva.layer.listening(false); + this.konva.transformer.nodes([]); + if (this.konva.group.isCached()) { + this.konva.group.clearCache(); } return; } @@ -229,32 +238,32 @@ export class CanvasRegion { 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } // Activate the transformer - this.layer.listening(true); - this.transformer.nodes([this.group]); - this.transformer.forceUpdate(); + 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.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + 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.group.isCached()) { - this.group.clearCache(); + 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.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } } return; @@ -262,12 +271,12 @@ export class CanvasRegion { if (!isSelected) { // Unselected layers should not be listening - this.layer.listening(false); + this.konva.layer.listening(false); // The transformer also does not need to be active. - this.transformer.nodes([]); + this.konva.transformer.nodes([]); // Update the layer's cache if it's not already cached or we drew to it. - if (!this.group.isCached() || didDraw) { - this.group.cache(); + if (!this.konva.group.isCached() || didDraw) { + this.konva.group.cache(); } return; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts index eada1fadcb..cd4ae82e32 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingArea.ts @@ -4,14 +4,18 @@ import type { StagingAreaImage } from 'features/controlLayers/store/types'; import Konva from 'konva'; export class CanvasStagingArea { - group: Konva.Group; + static NAME_PREFIX = 'staging-area'; + static GROUP_NAME = `${CanvasStagingArea.NAME_PREFIX}_group`; + + konva: { group: Konva.Group }; + image: CanvasImage | null; selectedImage: StagingAreaImage | null; manager: CanvasManager; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ listening: false }); + this.konva = { group: new Konva.Group({ name: CanvasStagingArea.GROUP_NAME, listening: false }) }; this.image = null; this.selectedImage = null; } @@ -42,20 +46,20 @@ export class CanvasStagingArea { height, }, }); - this.group.add(this.image.konvaImageGroup); + this.konva.group.add(this.image.konva.group); } if (!this.image.isLoading && !this.image.isError && this.image.imageName !== imageDTO.image_name) { - this.image.konvaImage?.width(imageDTO.width); - this.image.konvaImage?.height(imageDTO.height); - this.image.konvaImageGroup.x(bboxRect.x + offsetX); - this.image.konvaImageGroup.y(bboxRect.y + offsetY); + this.image.image?.width(imageDTO.width); + this.image.image?.height(imageDTO.height); + this.image.konva.group.x(bboxRect.x + offsetX); + this.image.konva.group.y(bboxRect.y + offsetY); await this.image.updateImageSource(imageDTO.image_name); this.manager.stateApi.resetLastProgressEvent(); } - this.image.konvaImageGroup.visible(shouldShowStagedImage); + this.image.konva.group.visible(shouldShowStagedImage); } else { - this.image?.konvaImageGroup.visible(false); + this.image?.konva.group.visible(false); } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts index 93b4633379..41291ee80b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool.ts @@ -25,80 +25,82 @@ export class CanvasTool { static ERASER_OUTER_BORDER_CIRCLE_NAME = `${CanvasTool.ERASER_NAME_PREFIX}_outer-border-circle`; manager: CanvasManager; - group: Konva.Group; - brush: { + konva: { group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; - }; - eraser: { - group: Konva.Group; - fillCircle: Konva.Circle; - innerBorderCircle: Konva.Circle; - outerBorderCircle: Konva.Circle; + brush: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; + eraser: { + group: Konva.Group; + fillCircle: Konva.Circle; + innerBorderCircle: Konva.Circle; + outerBorderCircle: Konva.Circle; + }; }; constructor(manager: CanvasManager) { this.manager = manager; - this.group = new Konva.Group({ name: CanvasTool.GROUP_NAME }); - - // Create the brush preview group & circles - this.brush = { - group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), - fillCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, - listening: false, - strokeEnabled: false, - }), - innerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), + this.konva = { + group: new Konva.Group({ name: CanvasTool.GROUP_NAME }), + brush: { + group: new Konva.Group({ name: CanvasTool.BRUSH_GROUP_NAME }), + fillCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_FILL_CIRCLE_NAME, + listening: false, + strokeEnabled: false, + }), + innerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_INNER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + name: CanvasTool.BRUSH_OUTER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }, + eraser: { + group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), + fillCircle: new Konva.Circle({ + name: CanvasTool.ERASER_FILL_CIRCLE_NAME, + listening: false, + strokeEnabled: false, + fill: 'white', + globalCompositeOperation: 'destination-out', + }), + innerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_INNER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + outerBorderCircle: new Konva.Circle({ + name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, + listening: false, + stroke: BRUSH_BORDER_OUTER_COLOR, + strokeWidth: BRUSH_ERASER_BORDER_WIDTH, + strokeEnabled: true, + }), + }, }; - this.brush.group.add(this.brush.fillCircle); - this.brush.group.add(this.brush.innerBorderCircle); - this.brush.group.add(this.brush.outerBorderCircle); - this.group.add(this.brush.group); + this.konva.brush.group.add(this.konva.brush.fillCircle); + this.konva.brush.group.add(this.konva.brush.innerBorderCircle); + this.konva.brush.group.add(this.konva.brush.outerBorderCircle); + this.konva.group.add(this.konva.brush.group); - this.eraser = { - group: new Konva.Group({ name: CanvasTool.ERASER_GROUP_NAME }), - fillCircle: new Konva.Circle({ - name: CanvasTool.ERASER_FILL_CIRCLE_NAME, - listening: false, - strokeEnabled: false, - fill: 'white', - globalCompositeOperation: 'destination-out', - }), - innerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_INNER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_INNER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - outerBorderCircle: new Konva.Circle({ - name: CanvasTool.ERASER_OUTER_BORDER_CIRCLE_NAME, - listening: false, - stroke: BRUSH_BORDER_OUTER_COLOR, - strokeWidth: BRUSH_ERASER_BORDER_WIDTH, - strokeEnabled: true, - }), - }; - this.eraser.group.add(this.eraser.fillCircle); - this.eraser.group.add(this.eraser.innerBorderCircle); - this.eraser.group.add(this.eraser.outerBorderCircle); - this.group.add(this.eraser.group); + this.konva.eraser.group.add(this.konva.eraser.fillCircle); + this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle); + this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle); + this.konva.group.add(this.konva.eraser.group); // // Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position // this.rect = { @@ -110,7 +112,7 @@ export class CanvasTool { // }), // }; // this.rect.group.add(this.rect.fillRect); - // this.group.add(this.rect.group); + // this.konva.group.add(this.rect.group); } scaleTool = () => { @@ -118,15 +120,15 @@ export class CanvasTool { const scale = this.manager.stage.scaleX(); const brushRadius = toolState.brush.width / 2; - this.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.brush.outerBorderCircle.setAttrs({ + this.konva.brush.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.konva.brush.outerBorderCircle.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: brushRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); const eraserRadius = toolState.eraser.width / 2; - this.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); - this.eraser.outerBorderCircle.setAttrs({ + this.konva.eraser.innerBorderCircle.strokeWidth(BRUSH_ERASER_BORDER_WIDTH / scale); + this.konva.eraser.outerBorderCircle.setAttrs({ strokeWidth: BRUSH_ERASER_BORDER_WIDTH / scale, radius: eraserRadius + BRUSH_ERASER_BORDER_WIDTH / scale, }); @@ -175,16 +177,16 @@ export class CanvasTool { if (!cursorPos || renderedEntityCount === 0 || !isDrawableEntity) { // We can bail early if the mouse isn't over the stage or there are no layers - this.group.visible(false); + this.konva.group.visible(false); } else { - this.group.visible(true); + this.konva.group.visible(true); // No need to render the brush preview if the cursor position or color is missing if (cursorPos && tool === 'brush') { const scale = stage.scaleX(); // Update the fill circle const radius = toolState.brush.width / 2; - this.brush.fillCircle.setAttrs({ + this.konva.brush.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -192,10 +194,10 @@ export class CanvasTool { }); // Update the inner border of the brush preview - this.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the brush preview - this.brush.outerBorderCircle.setAttrs({ + this.konva.brush.outerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, @@ -203,14 +205,14 @@ export class CanvasTool { this.scaleTool(); - this.brush.group.visible(true); - this.eraser.group.visible(false); + this.konva.brush.group.visible(true); + this.konva.eraser.group.visible(false); // this.rect.group.visible(false); } else if (cursorPos && tool === 'eraser') { const scale = stage.scaleX(); // Update the fill circle const radius = toolState.eraser.width / 2; - this.eraser.fillCircle.setAttrs({ + this.konva.eraser.fillCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius, @@ -218,10 +220,10 @@ export class CanvasTool { }); // Update the inner border of the eraser preview - this.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); + this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius }); // Update the outer border of the eraser preview - this.eraser.outerBorderCircle.setAttrs({ + this.konva.eraser.outerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale, @@ -229,8 +231,8 @@ export class CanvasTool { this.scaleTool(); - this.brush.group.visible(false); - this.eraser.group.visible(true); + this.konva.brush.group.visible(false); + this.konva.eraser.group.visible(true); // this.rect.group.visible(false); // } else if (cursorPos && lastMouseDownPos && tool === 'rect') { // this.rect.fillRect.setAttrs({ @@ -241,12 +243,12 @@ export class CanvasTool { // fill: rgbaColorToString(currentFill), // visible: true, // }); - // this.brush.group.visible(false); - // this.eraser.group.visible(false); + // this.konva.brush.group.visible(false); + // this.konva.eraser.group.visible(false); // this.rect.group.visible(true); } else { - this.brush.group.visible(false); - this.eraser.group.visible(false); + this.konva.brush.group.visible(false); + this.konva.eraser.group.visible(false); // this.rect.group.visible(false); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index f8ae0b3d93..26037add09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -269,8 +269,8 @@ export const previewBlob = async (blob: Blob, label?: string) => { export function getInpaintMaskLayerClone(arg: { manager: CanvasManager }): Konva.Layer { const { manager } = arg; - const layerClone = manager.inpaintMask.layer.clone(); - const objectGroupClone = manager.inpaintMask.group.clone(); + const layerClone = manager.inpaintMask.konva.layer.clone(); + const objectGroupClone = manager.inpaintMask.konva.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -287,8 +287,8 @@ export function getRegionMaskLayerClone(arg: { manager: CanvasManager; id: strin const canvasRegion = manager.regions.get(id); assert(canvasRegion, `Canvas region with id ${id} not found`); - const layerClone = canvasRegion.layer.clone(); - const objectGroupClone = canvasRegion.group.clone(); + const layerClone = canvasRegion.konva.layer.clone(); + const objectGroupClone = canvasRegion.konva.group.clone(); layerClone.destroyChildren(); layerClone.add(objectGroupClone); @@ -305,8 +305,8 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s const controlAdapter = manager.controlAdapters.get(id); assert(controlAdapter, `Canvas region with id ${id} not found`); - const controlAdapterClone = controlAdapter.layer.clone(); - const objectGroupClone = controlAdapter.group.clone(); + const controlAdapterClone = controlAdapter.konva.layer.clone(); + const objectGroupClone = controlAdapter.konva.group.clone(); controlAdapterClone.destroyChildren(); controlAdapterClone.add(objectGroupClone); @@ -322,8 +322,8 @@ export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konv const initialImage = manager.initialImage; - const initialImageClone = initialImage.layer.clone(); - const objectGroupClone = initialImage.group.clone(); + const initialImageClone = initialImage.konva.layer.clone(); + const objectGroupClone = initialImage.konva.group.clone(); initialImageClone.destroyChildren(); initialImageClone.add(objectGroupClone);