diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index e216fc5a33..bea047c4c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -13,6 +13,7 @@ import type { Rect, RectShapeAddedArg, RgbaColor, + ScaleChangedArg, StageAttrs, Tool, } from 'features/controlLayers/store/types'; @@ -63,6 +64,8 @@ export type StateApi = { onBrushWidthChanged: (size: number) => void; onEraserWidthChanged: (size: number) => void; getMaskOpacity: () => number; + getIsSelected: (id: string) => boolean; + onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void; onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void; onBboxTransformed: (bbox: Rect) => void; getShiftKey: () => boolean; @@ -155,9 +158,8 @@ export class KonvaNodeManager { this.controlAdapters = new Map(); } - renderLayers() { + async renderLayers() { const { entities } = this.stateApi.getLayersState(); - const toolState = this.stateApi.getToolState(); for (const canvasLayer of this.layers.values()) { if (!entities.find((l) => l.id === canvasLayer.id)) { @@ -169,11 +171,11 @@ export class KonvaNodeManager { for (const entity of entities) { let adapter = this.layers.get(entity.id); if (!adapter) { - adapter = new CanvasLayer(entity, this.stateApi.onPosChanged); + adapter = new CanvasLayer(entity, this); this.layers.set(adapter.id, adapter); this.stage.add(adapter.layer); } - adapter.render(entity, toolState.selected); + await adapter.render(entity); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts index ea696cf2dc..421ec4ad9a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/controlAdapters.ts @@ -43,8 +43,7 @@ export class CanvasControlAdapter { const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : []; if (!this.image) { - this.image = await new KonvaImage({ - imageObject, + this.image = await new KonvaImage(imageObject, { onLoad: (konvaImage) => { konvaImage.filters(filters); konvaImage.cache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index eb7be06742..85b6efff90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -79,7 +79,7 @@ export class CanvasInpaintMask { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; @@ -94,7 +94,7 @@ export class CanvasInpaintMask { assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; @@ -109,7 +109,7 @@ export class CanvasInpaintMask { assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); groupNeedsCache = true; @@ -129,7 +129,6 @@ export class CanvasInpaintMask { return; } - // We must clear the cache first so Konva will re-draw the group with the new compositing rect if (this.group.isCached()) { this.group.clearCache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index c34155648e..1935be1371 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -1,38 +1,53 @@ import { getObjectGroupId } from 'features/controlLayers/konva/naming'; -import type { StateApi } from 'features/controlLayers/konva/nodeManager'; +import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager'; import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects'; import { mapId } from 'features/controlLayers/konva/util'; -import type { LayerEntity, Tool } from 'features/controlLayers/store/types'; +import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types'; import Konva from 'konva'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasLayer { id: string; + manager: KonvaNodeManager; layer: Konva.Layer; group: Konva.Group; + transformer: Konva.Transformer; objects: Map; - constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) { + constructor(entity: LayerEntity, manager: KonvaNodeManager) { this.id = entity.id; - + this.manager = manager; this.layer = new Konva.Layer({ id: entity.id, - draggable: true, - dragDistance: 0, + listening: false, }); - // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing - // the position - we do not need to call this on the `dragmove` event. - this.layer.on('dragend', function (e) { - onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer'); - }); - const group = new Konva.Group({ + this.group = new Konva.Group({ id: getObjectGroupId(this.layer.id(), uuidv4()), listening: false, }); - this.group = group; this.layer.add(this.group); + + this.transformer = new Konva.Transformer({ + shouldOverdrawWholeArea: true, + draggable: true, + dragDistance: 0, + enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + rotateEnabled: false, + flipEnabled: false, + }); + this.transformer.on('transformend', () => { + this.manager.stateApi.onScaleChanged( + { id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() }, + 'layer' + ); + }); + this.transformer.on('dragend', () => { + this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer'); + }); + this.layer.add(this.transformer); + this.objects = new Map(); } @@ -40,20 +55,24 @@ export class CanvasLayer { this.layer.destroy(); } - async render(layerState: LayerEntity, selectedTool: Tool) { + async render(layerState: LayerEntity) { // Update the layer's position and listening state - this.layer.setAttrs({ - listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events - x: Math.floor(layerState.x), - y: Math.floor(layerState.y), + this.group.setAttrs({ + x: layerState.x, + y: layerState.y, + scaleX: 1, + scaleY: 1, }); + let didDraw = false; + const objectIds = layerState.objects.map(mapId); // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { this.objects.delete(object.id); object.destroy(); + didDraw = true; } } @@ -63,45 +82,60 @@ export class CanvasLayer { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); - } - if (obj.points.length !== brushLine.konvaLine.points().length) { - brushLine.konvaLine.points(obj.points); + didDraw = true; + } else { + if (brushLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'eraser_line') { let eraserLine = this.objects.get(obj.id); assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); - } - if (obj.points.length !== eraserLine.konvaLine.points().length) { - eraserLine.konvaLine.points(obj.points); + didDraw = true; + } else { + if (eraserLine.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'rect_shape') { let rect = this.objects.get(obj.id); assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); + didDraw = true; + } else { + if (rect.update(obj)) { + didDraw = true; + } } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); assert(image instanceof KonvaImage || image === undefined); if (!image) { - image = await new KonvaImage({ imageObject: obj }); + image = await new KonvaImage(obj, { + onLoad: () => { + this.updateGroup(true); + }, + }); this.objects.set(image.id, image); this.group.add(image.konvaImageGroup); - } - if (image.imageName !== obj.image.name) { - image.updateImageSource(obj.image.name); + await image.updateImageSource(obj.image.name); + } else { + if (await image.update(obj)) { + didDraw = true; + } } } } @@ -111,22 +145,71 @@ export class CanvasLayer { this.layer.visible(layerState.isEnabled); } - // const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); - // if (layerState.bbox) { - // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; - // bboxRect.setAttrs({ - // visible: active, - // listening: active, - // x: layerState.bbox.x, - // y: layerState.bbox.y, - // width: layerState.bbox.width, - // height: layerState.bbox.height, - // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', - // strokeWidth: 1 / stage.scaleX(), - // }); - // } else { - // bboxRect.visible(false); - // } this.group.opacity(layerState.opacity); + + // The layer only listens when using the move tool - otherwise the stage is handling mouse events + this.updateGroup(didDraw); + } + + updateGroup(didDraw: boolean) { + 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(); + } + return; + } + + if (isSelected && selectedTool === 'move') { + // When the layer is selected and being moved, we should always cache it. + // We should update the cache if we drew to the layer. + if (!this.group.isCached() || didDraw) { + this.group.cache(); + } + // Activate the transformer + this.layer.listening(true); + this.transformer.nodes([this.group]); + this.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); + // The transformer also does not need to be active. + this.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(); + } + } 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(); + } + } + return; + } + + if (!isSelected) { + // Unselected layers should not be listening + this.layer.listening(false); + // The transformer also does not need to be active. + this.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(); + } + + return; + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index 1c1230e0c5..3a6ecbd060 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -27,10 +27,10 @@ export class KonvaBrushLine { id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; + lastBrushLine: BrushLine; - constructor(arg: { brushLine: BrushLine }) { - const { brushLine } = arg; - const { id, strokeWidth, clip, color } = brushLine; + constructor(brushLine: BrushLine) { + const { id, strokeWidth, clip, color, points } = brushLine; this.id = id; this.konvaLineGroup = new Konva.Group({ clip, @@ -46,8 +46,26 @@ export class KonvaBrushLine { lineJoin: 'round', globalCompositeOperation: 'source-over', stroke: rgbaColorToString(color), + points, }); this.konvaLineGroup.add(this.konvaLine); + this.lastBrushLine = brushLine; + } + + update(brushLine: BrushLine, force?: boolean): boolean { + if (this.lastBrushLine !== brushLine || force) { + const { points, color, clip, strokeWidth } = brushLine; + this.konvaLine.setAttrs({ + points, + stroke: rgbaColorToString(color), + clip, + strokeWidth, + }); + this.lastBrushLine = brushLine; + return true; + } else { + return false; + } } destroy() { @@ -59,10 +77,10 @@ export class KonvaEraserLine { id: string; konvaLineGroup: Konva.Group; konvaLine: Konva.Line; + lastEraserLine: EraserLine; - constructor(arg: { eraserLine: EraserLine }) { - const { eraserLine } = arg; - const { id, strokeWidth, clip } = eraserLine; + constructor(eraserLine: EraserLine) { + const { id, strokeWidth, clip, points } = eraserLine; this.id = id; this.konvaLineGroup = new Konva.Group({ clip, @@ -78,8 +96,25 @@ export class KonvaEraserLine { lineJoin: 'round', globalCompositeOperation: 'destination-out', stroke: rgbaColorToString(RGBA_RED), + points, }); this.konvaLineGroup.add(this.konvaLine); + this.lastEraserLine = eraserLine; + } + + update(eraserLine: EraserLine, force?: boolean): boolean { + if (this.lastEraserLine !== eraserLine || force) { + const { points, clip, strokeWidth } = eraserLine; + this.konvaLine.setAttrs({ + points, + clip, + strokeWidth, + }); + this.lastEraserLine = eraserLine; + return true; + } else { + return false; + } } destroy() { @@ -90,9 +125,9 @@ export class KonvaEraserLine { export class KonvaRect { id: string; konvaRect: Konva.Rect; + lastRectShape: RectShape; - constructor(arg: { rectShape: RectShape }) { - const { rectShape } = arg; + constructor(rectShape: RectShape) { const { id, x, y, width, height } = rectShape; this.id = id; const konvaRect = new Konva.Rect({ @@ -105,6 +140,24 @@ export class KonvaRect { fill: rgbaColorToString(rectShape.color), }); this.konvaRect = konvaRect; + this.lastRectShape = rectShape; + } + + update(rectShape: RectShape, force?: boolean): boolean { + if (this.lastRectShape !== rectShape || force) { + const { x, y, width, height, color } = rectShape; + this.konvaRect.setAttrs({ + x, + y, + width, + height, + fill: rgbaColorToString(color), + }); + this.lastRectShape = rectShape; + return true; + } else { + return false; + } } destroy() { @@ -126,15 +179,18 @@ export class KonvaImage { onLoading: () => void; onLoad: (imageName: string, imageEl: HTMLImageElement) => void; onError: () => void; + lastImageObject: ImageObject; - constructor(arg: { - imageObject: ImageObject; - getImageDTO?: (imageName: string) => Promise; - onLoading?: () => void; - onLoad?: (konvaImage: Konva.Image) => void; - onError?: () => void; - }) { - const { imageObject, getImageDTO, onLoading, onLoad, onError } = arg; + constructor( + imageObject: ImageObject, + options: { + getImageDTO?: (imageName: string) => Promise; + onLoading?: () => void; + onLoad?: (konvaImage: Konva.Image) => void; + onError?: () => void; + } + ) { + const { getImageDTO, onLoading, onLoad, onError } = options; const { id, width, height, x, y } = imageObject; this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y }); this.konvaPlaceholderGroup = new Konva.Group({ listening: false }); @@ -188,6 +244,8 @@ export class KonvaImage { id: this.id, listening: false, image: imageEl, + width, + height, }); this.konvaImageGroup.add(this.konvaImage); } @@ -213,6 +271,7 @@ export class KonvaImage { onError(); } }; + this.lastImageObject = imageObject; } async updateImageSource(imageName: string) { @@ -238,6 +297,22 @@ export class KonvaImage { } } + async update(imageObject: ImageObject, force?: boolean): Promise { + if (this.lastImageObject !== imageObject || force) { + const { width, height, x, y, image } = imageObject; + if (this.lastImageObject.image.name !== image.name || force) { + await this.updateImageSource(image.name); + } + this.konvaImage?.setAttrs({ x, y, width, height }); + this.konvaPlaceholderRect.setAttrs({ width, height }); + this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 }); + this.lastImageObject = imageObject; + return true; + } else { + return false; + } + } + destroy() { this.konvaImageGroup.destroy(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index be6be27699..0a2c5353d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -79,7 +79,7 @@ export class CanvasRegion { assert(brushLine instanceof KonvaBrushLine || brushLine === undefined); if (!brushLine) { - brushLine = new KonvaBrushLine({ brushLine: obj }); + brushLine = new KonvaBrushLine(obj); this.objects.set(brushLine.id, brushLine); this.group.add(brushLine.konvaLineGroup); groupNeedsCache = true; @@ -94,7 +94,7 @@ export class CanvasRegion { assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined); if (!eraserLine) { - eraserLine = new KonvaEraserLine({ eraserLine: obj }); + eraserLine = new KonvaEraserLine(obj); this.objects.set(eraserLine.id, eraserLine); this.group.add(eraserLine.konvaLineGroup); groupNeedsCache = true; @@ -109,7 +109,7 @@ export class CanvasRegion { assert(rect instanceof KonvaRect || rect === undefined); if (!rect) { - rect = new KonvaRect({ rectShape: obj }); + rect = new KonvaRect(obj); this.objects.set(rect.id, rect); this.group.add(rect.konvaRect); groupNeedsCache = true; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 24763e238e..e5c67cbde7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -28,6 +28,7 @@ import { layerImageCacheChanged, layerLinePointAdded, layerRectAdded, + layerScaled, layerTranslated, rgBboxChanged, rgBrushLineAdded, @@ -48,6 +49,7 @@ import type { PointAddedToLineArg, PosChangedArg, RectShapeAddedArg, + ScaleChangedArg, Tool, } from 'features/controlLayers/store/types'; import type Konva from 'konva'; @@ -90,7 +92,7 @@ export const initializeRenderer = ( // Set up callbacks for various events const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => { - logIfDebugging('Position changed'); + logIfDebugging('onPosChanged'); if (entityType === 'layer') { dispatch(layerTranslated(arg)); } else if (entityType === 'control_adapter') { @@ -101,6 +103,12 @@ export const initializeRenderer = ( dispatch(imTranslated(arg)); } }; + const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => { + logIfDebugging('onScaleChanged'); + if (entityType === 'layer') { + dispatch(layerScaled(arg)); + } + }; const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => { logIfDebugging('Entity bbox changed'); if (entityType === 'layer') { @@ -249,7 +257,12 @@ export const initializeRenderer = ( const getInpaintMaskState = () => canvasV2.inpaintMask; const getMaskOpacity = () => canvasV2.settings.maskOpacity; const getStagingAreaState = () => canvasV2.stagingArea; + const getIsSelected = (id: string) => getSelectedEntity()?.id === id; + // Read-only state, derived from nanostores + const resetLastProgressEvent = () => { + $lastProgressEvent.set(null); + }; // Read-write state, ephemeral interaction state let isDrawing = false; const getIsDrawing = () => isDrawing; @@ -307,9 +320,8 @@ export const initializeRenderer = ( getStagingAreaState, getShouldShowStagedImage: $shouldShowStagedImage.get, getLastProgressEvent: $lastProgressEvent.get, - resetLastProgressEvent: () => { - $lastProgressEvent.set(null); - }, + resetLastProgressEvent, + getIsSelected, // Read-write state setTool, @@ -340,6 +352,7 @@ export const initializeRenderer = ( onRegionMaskImageCached, onInpaintMaskImageCached, onLayerImageCached, + onScaleChanged, }; const manager = new KonvaNodeManager(stage, container, stateApi); @@ -367,7 +380,8 @@ export const initializeRenderer = ( if ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering layers'); manager.renderLayers(); @@ -377,7 +391,8 @@ export const initializeRenderer = ( isFirstRender || canvasV2.regions.entities !== prevCanvasV2.regions.entities || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering regions'); manager.renderRegions(); @@ -387,13 +402,18 @@ export const initializeRenderer = ( isFirstRender || canvasV2.inpaintMask !== prevCanvasV2.inpaintMask || canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity || - canvasV2.tool.selected !== prevCanvasV2.tool.selected + canvasV2.tool.selected !== prevCanvasV2.tool.selected || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Rendering inpaint mask'); manager.renderInpaintMask(); } - if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) { + if ( + isFirstRender || + canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || + prevSelectedEntity?.id !== selectedEntity?.id + ) { logIfDebugging('Rendering control adapters'); manager.renderControlAdapters(); } @@ -427,7 +447,8 @@ export const initializeRenderer = ( isFirstRender || canvasV2.layers.entities !== prevCanvasV2.layers.entities || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities || - canvasV2.regions.entities !== prevCanvasV2.regions.entities + canvasV2.regions.entities !== prevCanvasV2.regions.entities || + prevSelectedEntity?.id !== selectedEntity?.id ) { logIfDebugging('Arranging entities'); manager.arrangeEntities(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 41df1dc7fb..186d47946c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -215,6 +215,7 @@ export const { layerImageAdded, layerAllDeleted, layerImageCacheChanged, + layerScaled, // IP Adapters ipaAdded, ipaRecalled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 9edabb1acf..c81083273a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -172,6 +172,36 @@ export const layersReducers = { payload: { ...payload, lineId: uuidv4() }, }), }, + layerScaled: (state, action: PayloadAction<{ id: string; scale: number; x: number; y: number }>) => { + const { id, scale, x, y } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + for (const obj of layer.objects) { + if (obj.type === 'brush_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'eraser_line') { + obj.points = obj.points.map((point) => point * scale); + obj.strokeWidth *= scale; + } else if (obj.type === 'rect_shape') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } else if (obj.type === 'image') { + obj.x *= scale; + obj.y *= scale; + obj.height *= scale; + obj.width *= scale; + } + } + layer.x = x; + layer.y = y; + layer.bboxNeedsUpdate = true; + state.layers.imageCache = null; + }, layerEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width, clip } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a5beb70ead..4693654147 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -462,6 +462,9 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); export type Tool = z.infer; +export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' { + return tool === 'brush' || tool === 'eraser' || tool === 'rect'; +} const zDrawingTool = zTool.extract(['brush', 'eraser']); @@ -891,6 +894,7 @@ export type CanvasV2State = { export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type PosChangedArg = { id: string; x: number; y: number }; +export type ScaleChangedArg = { id: string; scale: number; x: number; y: number }; export type BboxChangedArg = { id: string; bbox: Rect | null }; export type EraserLineAddedArg = { id: string;