tidy(ui): remove unused code, initial image

This commit is contained in:
psychedelicious 2024-08-06 18:00:19 +10:00
parent a2f91b1055
commit da3888ba9e
19 changed files with 10 additions and 960 deletions

View File

@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addCanvasSessionRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSessionRequested';
import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
@ -90,7 +89,6 @@ addBatchEnqueuedListener(startAppListening);
// addStagingAreaImageSavedListener(startAppListening);
// addCommitStagingAreaImageListener(startAppListening);
addStagingListeners(startAppListening);
addCanvasSessionRequestedListener(startAppListening);
// Socket.IO
addGeneratorProgressEventListener(startAppListening);

View File

@ -1,29 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
layerAdded,
layerImageAdded,
sessionRequested,
sessionStarted,
} from 'features/controlLayers/store/canvasV2Slice';
import { getImageDTO } from 'services/api/endpoints/images';
import { assert } from 'tsafe';
export const addCanvasSessionRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: sessionRequested,
effect: async (action, { getState, dispatch }) => {
const initialImageObject = getState().canvasV2.initialImage.imageObject;
if (initialImageObject) {
// We have an initial image that needs to be converted to a layer
dispatch(layerAdded());
const newLayer = getState().canvasV2.layers.entities[0];
assert(newLayer, 'Expected new layer to be created');
const imageDTO = await getImageDTO(initialImageObject.image.name);
assert(imageDTO, 'Unable to fetch initial image DTO');
dispatch(layerImageAdded({ id: newLayer.id, imageDTO }));
}
dispatch(sessionStarted());
},
});
};

View File

@ -1,9 +1,7 @@
/* eslint-disable i18next/no-literal-string */
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { CAEntityList } from 'features/controlLayers/components/ControlAdapter/CAEntityList';
import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage';
import { IM } from 'features/controlLayers/components/InpaintMask/IM';
import { IPAEntityList } from 'features/controlLayers/components/IPAdapter/IPAEntityList';
import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList';
@ -11,17 +9,14 @@ import { RGEntityList } from 'features/controlLayers/components/RegionalGuidance
import { memo } from 'react';
export const CanvasEntityList = memo(() => {
const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive);
return (
<ScrollableContent>
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">
{isCanvasSessionActive && <IM />}
<IM />
<RGEntityList />
<CAEntityList />
<IPAEntityList />
<LayerEntityList />
{!isCanvasSessionActive && <InitialImage />}
</Flex>
</ScrollableContent>
);

View File

@ -7,7 +7,6 @@ import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { EraserWidth } from 'features/controlLayers/components/EraserWidth';
import { FillColorPicker } from 'features/controlLayers/components/FillColorPicker';
import { NewSessionButton } from 'features/controlLayers/components/NewSessionButton';
import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
@ -25,7 +24,7 @@ export const ControlLayersToolbar = memo(() => {
return;
}
for (const l of canvasManager.layers.values()) {
l.calculateBbox();
l.transformer.requestRectCalculation();
}
}, [canvasManager]);
const onChangeDebugging = useCallback(
@ -61,7 +60,6 @@ export const ControlLayersToolbar = memo(() => {
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />
<ResetCanvasButton />
<NewSessionButton />
<ViewerToggleMenu />
</Flex>
</Flex>

View File

@ -1,25 +0,0 @@
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { InitialImageHeader } from 'features/controlLayers/components/InitialImage/InitialImageHeader';
import { InitialImageSettings } from 'features/controlLayers/components/InitialImage/InitialImageSettings';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
export const InitialImage = memo(() => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'initial_image');
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id: 'initial_image', type: 'initial_image' }));
}, [dispatch]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}>
<InitialImageHeader onToggleVisibility={onToggle} />
{isOpen && <InitialImageSettings />}
</CanvasEntityContainer>
);
});
InitialImage.displayName = 'InitialImage';

View File

@ -1,34 +0,0 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { iiIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
onToggleVisibility: () => void;
};
export const InitialImageHeader = memo(({ onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => s.canvasV2.initialImage.isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(iiIsEnabledToggled());
}, [dispatch]);
const title = useMemo(() => {
return `${t('controlLayers.initialImage')}`;
}, [t]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={title} />
<Spacer />
</CanvasEntityHeader>
);
});
InitialImageHeader.displayName = 'InitialImageHeader';

View File

@ -1,100 +0,0 @@
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { bboxHeightChanged, bboxWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
export const InitialImagePreview = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const initialImage = useAppSelector((s) => s.canvasV2.initialImage);
const isConnected = useAppSelector((s) => s.system.isConnected);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(
initialImage.imageObject?.image.name ?? skipToken
);
const onReset = useCallback(() => {
dispatch(iiReset());
}, [dispatch]);
const onUseSize = useCallback(() => {
if (!imageDTO) {
return;
}
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = imageDTO;
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension);
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
}
}, [imageDTO, dispatch, optimalDimension, shift]);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: 'initial_image',
payloadType: 'IMAGE_DTO',
payload: { imageDTO },
};
}
}, [imageDTO]);
const droppableData = useMemo<InitialImageDropData>(
() => ({ id: 'initial_image', actionType: 'SET_INITIAL_IMAGE' }),
[]
);
useEffect(() => {
if (isConnected && isErrorControlImage) {
onReset();
}
}, [onReset, isConnected, isErrorControlImage]);
return (
<Flex w="full" alignItems="center" justifyContent="center">
<Flex position="relative" w="full" h="full" alignItems="center" justifyContent="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={imageDTO}
// postUploadAction={postUploadAction}
/>
{imageDTO && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={onReset}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={onUseSize}
icon={<PiRulerBold size={16} />}
tooltip={
shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
}
/>
</Flex>
)}
</Flex>
</Flex>
);
});
InitialImagePreview.displayName = 'InitialImagePreview';

View File

@ -1,13 +0,0 @@
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { InitialImagePreview } from 'features/controlLayers/components/InitialImage/InitialImagePreview';
import { memo } from 'react';
export const InitialImageSettings = memo(() => {
return (
<CanvasEntitySettings>
<InitialImagePreview />
</CanvasEntitySettings>
);
});
InitialImageSettings.displayName = 'InitialImageSettings';

View File

@ -1,15 +0,0 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { sessionRequested } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
export const NewSessionButton = memo(() => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(sessionRequested());
}, [dispatch]);
return <Button onClick={onClick}>New</Button>;
});
NewSessionButton.displayName = 'NewSessionButton';

View File

@ -1,65 +0,0 @@
import { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import type { InitialImageEntity } from 'features/controlLayers/store/types';
import Konva from 'konva';
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`;
private state: InitialImageEntity;
id = 'initial_image';
manager: CanvasManager;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
};
image: CanvasImageRenderer | null;
constructor(state: InitialImageEntity, manager: CanvasManager) {
this.manager = manager;
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.state = state;
}
async render(state: InitialImageEntity) {
this.state = state;
if (!this.state.imageObject) {
this.konva.layer.visible(false);
return;
}
if (!this.image) {
this.image = new CanvasImageRenderer(this.state.imageObject);
this.konva.objectGroup.add(this.image.konva.group);
await this.image.update(this.state.imageObject, true);
} else if (!this.image.isLoading && !this.image.isError) {
await this.image.update(this.state.imageObject);
}
if (this.state && this.state.isEnabled && !this.image?.isLoading && !this.image?.isError) {
this.konva.layer.visible(true);
} else {
this.konva.layer.visible(false);
}
}
destroy(): void {
this.konva.layer.destroy();
}
}

View File

@ -6,7 +6,6 @@ import { PubSub } from 'common/util/PubSub/PubSub';
import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage';
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
import { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasProgressPreview';
import type { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
@ -15,7 +14,6 @@ import {
getCompositeLayerImage,
getControlAdapterImage,
getGenerationMode,
getInitialImage,
getInpaintMaskImage,
getPrefixedId,
getRegionMaskImage,
@ -48,7 +46,6 @@ import { CanvasControlAdapter } from './CanvasControlAdapter';
import { CanvasLayerAdapter } from './CanvasLayerAdapter';
import { CanvasMaskAdapter } from './CanvasMaskAdapter';
import { CanvasPreview } from './CanvasPreview';
import type { CanvasRegion } from './CanvasRegion';
import { CanvasStagingArea } from './CanvasStagingArea';
import { CanvasStateApi } from './CanvasStateApi';
import { CanvasTool } from './CanvasTool';
@ -120,7 +117,6 @@ export class CanvasManager {
layers: Map<string, CanvasLayerAdapter>;
regions: Map<string, CanvasMaskAdapter>;
inpaintMask: CanvasMaskAdapter;
initialImage: CanvasInitialImage;
util: Util;
stateApi: CanvasStateApi;
preview: CanvasPreview;
@ -188,9 +184,6 @@ export class CanvasManager {
this.regions = new Map();
this.controlAdapters = new Map();
this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
this.stage.add(this.initialImage.konva.layer);
this._worker.onmessage = (event: MessageEvent<ExtentsResult | WorkerLogMessage>) => {
const { type, data } = event.data;
if (type === 'log') {
@ -250,10 +243,6 @@ export class CanvasManager {
this._worker.postMessage(task, [data.buffer]);
}
async renderInitialImage() {
await this.initialImage.render(this.stateApi.getInitialImageState());
}
async renderProgressPreview() {
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
}
@ -286,7 +275,6 @@ export class CanvasManager {
const regions = getRegionsState().entities;
let zIndex = 0;
this.background.konva.layer.zIndex(++zIndex);
this.initialImage.konva.layer.zIndex(++zIndex);
for (const layer of layers) {
this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex);
}
@ -322,7 +310,7 @@ export class CanvasManager {
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
| null = null;
let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasRegion | CanvasMaskAdapter | null = null;
let entityAdapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasMaskAdapter | null = null;
if (identifier.type === 'layer') {
entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null;
@ -470,17 +458,6 @@ export class CanvasManager {
}
}
if (
this._isFirstRender ||
state.initialImage !== this._prevState.initialImage ||
state.bbox.rect !== this._prevState.bbox.rect ||
state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) {
this.log.debug('Rendering initial image');
await this.renderInitialImage();
}
if (
this._isFirstRender ||
state.regions.entities !== this._prevState.regions.entities ||
@ -546,8 +523,7 @@ export class CanvasManager {
if (
this._isFirstRender ||
state.bbox !== this._prevState.bbox ||
state.tool.selected !== this._prevState.tool.selected ||
state.session.isActive !== this._prevState.session.isActive
state.tool.selected !== this._prevState.tool.selected
) {
this.log.debug('Rendering generation bbox');
await this.preview.bbox.render();
@ -656,20 +632,9 @@ export class CanvasManager {
}
getGenerationMode(): GenerationMode {
const session = this.stateApi.getSession();
if (session.isActive) {
return getGenerationMode({ manager: this });
}
const initialImageState = this.stateApi.getInitialImageState();
if (initialImageState.imageObject && initialImageState.isEnabled) {
return 'img2img';
}
return 'txt2img';
}
getControlAdapterImage(arg: Omit<Parameters<typeof getControlAdapterImage>[0], 'manager'>) {
return getControlAdapterImage({ ...arg, manager: this });
}
@ -683,11 +648,7 @@ export class CanvasManager {
}
getInitialImage(arg: Omit<Parameters<typeof getCompositeLayerImage>[0], 'manager'>) {
if (this.stateApi.getSession().isActive) {
return getCompositeLayerImage({ ...arg, manager: this });
} else {
return getInitialImage({ ...arg, manager: this });
}
}
getLoggingContext() {

View File

@ -1,292 +0,0 @@
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
import { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
import { mapId } from 'features/controlLayers/konva/util';
import type {
CanvasBrushLineState,
CanvasEraserLineState,
CanvasRectState,
CanvasRegionalGuidanceState,
} from 'features/controlLayers/store/types';
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { assert } from 'tsafe';
export class CanvasRegion {
static NAME_PREFIX = 'region';
static LAYER_NAME = `${CanvasRegion.NAME_PREFIX}_layer`;
static TRANSFORMER_NAME = `${CanvasRegion.NAME_PREFIX}_transformer`;
static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`;
static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`;
static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`;
static TYPE = 'regional_guidance' as const;
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
private state: CanvasRegionalGuidanceState;
id: string;
type = CanvasRegion.TYPE;
manager: CanvasManager;
konva: {
layer: Konva.Layer;
group: Konva.Group;
objectGroup: Konva.Group;
compositingRect: Konva.Rect;
transformer: Konva.Transformer;
};
objects: Map<string, CanvasBrushLineRenderer | CanvasEraserLineRenderer | CanvasRectRenderer>;
constructor(state: CanvasRegionalGuidanceState, manager: CanvasManager) {
this.id = state.id;
this.manager = manager;
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.konva.group.scaleX(),
position: { x: this.konva.group.x(), y: this.konva.group.y() },
},
'regional_guidance'
);
});
this.konva.transformer.on('dragend', () => {
this.manager.stateApi.setEntityPosition(
{ id: this.id, position: { x: this.konva.group.x(), y: this.konva.group.y() } },
'regional_guidance'
);
});
this.konva.layer.add(this.konva.transformer);
this.konva.group.add(this.konva.compositingRect);
this.objects = new Map();
this.drawingBuffer = null;
this.state = state;
}
destroy(): void {
this.konva.layer.destroy();
}
getDrawingBuffer() {
return this.drawingBuffer;
}
async setDrawingBuffer(obj: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null) {
this.drawingBuffer = obj;
if (this.drawingBuffer) {
if (this.drawingBuffer.type === 'brush_line') {
this.drawingBuffer.color = RGBA_RED;
} else if (this.drawingBuffer.type === 'rect') {
this.drawingBuffer.color = RGBA_RED;
}
await this.renderObject(this.drawingBuffer, true);
this.updateGroup(true);
}
}
finalizeDrawingBuffer() {
if (!this.drawingBuffer) {
return;
}
if (this.drawingBuffer.type === 'brush_line') {
this.manager.stateApi.addBrushLine({ id: this.id, brushLine: this.drawingBuffer }, 'regional_guidance');
} else if (this.drawingBuffer.type === 'eraser_line') {
this.manager.stateApi.addEraserLine({ id: this.id, eraserLine: this.drawingBuffer }, 'regional_guidance');
} else if (this.drawingBuffer.type === 'rect') {
this.manager.stateApi.addRect({ id: this.id, rect: this.drawingBuffer }, 'regional_guidance');
}
this.setDrawingBuffer(null);
}
async render(state: CanvasRegionalGuidanceState) {
this.state = state;
// Update the layer's position and listening state
this.konva.group.setAttrs({
x: state.position.x,
y: state.position.y,
scaleX: 1,
scaleY: 1,
});
let didDraw = false;
const objectIds = state.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;
}
}
for (const obj of state.objects) {
if (await this.renderObject(obj)) {
didDraw = true;
}
}
if (this.drawingBuffer) {
if (await this.renderObject(this.drawingBuffer)) {
didDraw = true;
}
}
this.updateGroup(didDraw);
}
private async renderObject(obj: CanvasRegionalGuidanceState['objects'][number], force = false): Promise<boolean> {
if (obj.type === 'brush_line') {
let brushLine = this.objects.get(obj.id);
assert(brushLine instanceof CanvasBrushLineRenderer || brushLine === undefined);
if (!brushLine) {
brushLine = new CanvasBrushLineRenderer(obj);
this.objects.set(brushLine.id, brushLine);
this.konva.objectGroup.add(brushLine.konva.group);
return true;
} else {
if (brushLine.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'eraser_line') {
let eraserLine = this.objects.get(obj.id);
assert(eraserLine instanceof CanvasEraserLineRenderer || eraserLine === undefined);
if (!eraserLine) {
eraserLine = new CanvasEraserLineRenderer(obj);
this.objects.set(eraserLine.id, eraserLine);
this.konva.objectGroup.add(eraserLine.konva.group);
return true;
} else {
if (eraserLine.update(obj, force)) {
return true;
}
}
} else if (obj.type === 'rect') {
let rect = this.objects.get(obj.id);
assert(rect instanceof CanvasRectRenderer || rect === undefined);
if (!rect) {
rect = new CanvasRectRenderer(obj);
this.objects.set(rect.id, rect);
this.konva.objectGroup.add(rect.konva.group);
return true;
} else {
if (rect.update(obj, force)) {
return true;
}
}
}
return false;
}
updateGroup(didDraw: boolean) {
this.konva.layer.visible(this.state.isEnabled);
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
this.konva.group.opacity(1);
if (didDraw) {
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(this.state.fill);
const maskOpacity = this.manager.stateApi.getMaskOpacity();
this.konva.compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...getNodeBboxFast(this.konva.objectGroup),
fill: rgbColor,
opacity: maskOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
globalCompositeOperation: 'source-in',
// This rect must always be on top of all other shapes
zIndex: this.objects.size + 1,
});
}
const isSelected = this.manager.stateApi.getIsSelected(this.id);
const selectedTool = this.manager.stateApi.getToolState().selected;
if (this.objects.size === 0) {
// If the layer is totally empty, reset the cache and bail out.
this.konva.layer.listening(false);
this.konva.transformer.nodes([]);
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
return;
}
if (isSelected && selectedTool === 'move') {
// When the layer is selected and being moved, we should always cache it.
// We should update the cache if we drew to the layer.
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
// Activate the transformer
this.konva.layer.listening(true);
this.konva.transformer.nodes([this.konva.group]);
this.konva.transformer.forceUpdate();
return;
}
if (isSelected && selectedTool !== 'move') {
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.konva.transformer.nodes([]);
if (isDrawingTool(selectedTool)) {
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
// should never be cached.
if (this.konva.group.isCached()) {
this.konva.group.clearCache();
}
} else {
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
// We should update the cache if we drew to the layer.
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
}
return;
}
if (!isSelected) {
// Unselected layers should not be listening
this.konva.layer.listening(false);
// The transformer also does not need to be active.
this.konva.transformer.nodes([]);
// Update the layer's cache if it's not already cached or we drew to it.
if (!this.konva.group.isCached() || didDraw) {
this.konva.group.cache();
}
return;
}
}
}

View File

@ -188,9 +188,6 @@ export class CanvasStateApi {
getInpaintMaskState = () => {
return this.getState().inpaintMask;
};
getInitialImageState = () => {
return this.getState().initialImage;
};
getMaskOpacity = () => {
return this.getState().settings.maskOpacity;
};

View File

@ -1,250 +0,0 @@
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { getLayerBboxId } from 'features/controlLayers/konva/naming';
import { imageDataToDataURL } from 'features/controlLayers/konva/util';
import type {
BboxChangedArg,
CanvasControlAdapterState,
CanvasEntityState,
CanvasLayerState,
CanvasRegionalGuidanceState,
} from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
/**
* Creates a bounding box rect for a layer.
* @param entity The layer state for the layer to create the bounding box for
* @param konvaLayer The konva layer to attach the bounding box to
*/
export const createBboxRect = (entity: CanvasEntityState, konvaLayer: Konva.Layer): Konva.Rect => {
const rect = new Konva.Rect({
id: getLayerBboxId(entity.id),
name: 'bbox',
strokeWidth: 1,
visible: false,
});
konvaLayer.add(rect);
return rect;
};
/**
* Logic to create and render bounding boxes for layers.
* Some utils are included for calculating bounding boxes.
*/
type Extents = {
minX: number;
minY: number;
maxX: number;
maxY: number;
};
const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
/**
* Get the bounding box of an image.
* @param imageData The ImageData object to get the bounding box of.
* @returns The minimum and maximum x and y values of the image's bounding box, or null if the image has no pixels.
*/
export const getImageDataBbox = (imageData: ImageData): Extents | null => {
const { data, width, height } = imageData;
let minX = width;
let minY = height;
let maxX = -1;
let maxY = -1;
let alpha = 0;
let isEmpty = true;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
alpha = data[(y * width + x) * 4 + 3] ?? 0;
if (alpha > 0) {
isEmpty = false;
if (x < minX) {
minX = x;
}
if (x > maxX) {
maxX = x;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
}
}
}
return isEmpty ? null : { minX, minY, maxX: maxX + 1, maxY: maxY + 1 };
};
/**
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
* to be captured, manipulated or analyzed without interference from other layers.
* @param layer The konva layer to clone.
* @param filterChildren A callback to filter out unwanted children
* @returns The cloned stage and layer.
*/
const getIsolatedLayerClone = (
layer: Konva.Layer,
filterChildren: (node: Konva.Node) => boolean
): { stageClone: Konva.Stage; layerClone: Konva.Layer } => {
const stage = layer.getStage();
// Construct an offscreen canvas with the same dimensions as the layer's stage.
const offscreenStageContainer = document.createElement('div');
const stageClone = new Konva.Stage({
container: offscreenStageContainer,
x: stage.x(),
y: stage.y(),
width: stage.width(),
height: stage.height(),
});
// Clone the layer and filter out unwanted children.
const layerClone = layer.clone();
stageClone.add(layerClone);
for (const child of layerClone.getChildren()) {
if (filterChildren(child) && child.hasChildren()) {
// We need to cache the group to ensure it composites out eraser strokes correctly
child.opacity(1);
child.cache();
} else {
// Filter out unwanted children.
child.destroy();
}
}
return { stageClone, layerClone };
};
/**
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
* @param layer The konva layer to get the bounding box of.
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
*/
const getLayerBboxPixels = (
layer: Konva.Layer,
filterChildren: (node: Konva.Node) => boolean,
preview: boolean = false
): IRect | null => {
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
//
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
// by calculating the extents of individual shapes from their "vector" shape data.
//
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren);
// Get a worst-case rect using the relatively fast `getClientRect`.
const layerRect = layerClone.getClientRect();
if (layerRect.width === 0 || layerRect.height === 0) {
return null;
}
// Capture the image data with the above rect.
const layerImageData = stageClone
.toCanvas(layerRect)
.getContext('2d')
?.getImageData(0, 0, layerRect.width, layerRect.height);
assert(layerImageData, "Unable to get layer's image data");
if (preview) {
openBase64ImageInTab([{ base64: imageDataToDataURL(layerImageData), caption: layer.id() }]);
}
// Calculate the layer's bounding box.
const layerBbox = getImageDataBbox(layerImageData);
if (!layerBbox) {
return null;
}
// Correct the bounding box to be relative to the layer's position.
const correctedLayerBbox = {
x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()),
y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()),
width: layerBbox.maxX - layerBbox.minX,
height: layerBbox.maxY - layerBbox.minY,
};
return correctedLayerBbox;
};
/**
* Get the bounding box of a konva node. This function is faster than `getLayerBboxPixels` but less accurate. It
* should only be used when there are no eraser strokes or shapes in the node.
* @param node The konva node to get the bounding box of.
* @returns The bounding box of the node.
*/
export const getNodeBboxFast = (node: Konva.Node): IRect => {
const bbox = node.getClientRect(GET_CLIENT_RECT_CONFIG);
return bbox;
};
// TODO(psyche): fix this
const filterRGChildren = (node: Konva.Node): boolean => true;
const filterLayerChildren = (node: Konva.Node): boolean => true;
const filterCAChildren = (node: Konva.Node): boolean => true;
/**
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
* @param stage The konva stage
* @param entityStates An array of layers to calculate bboxes for
* @param onBboxChanged Callback for when the bounding box changes
*/
export const updateBboxes = (
stage: Konva.Stage,
layers: CanvasLayerState[],
controlAdapters: CanvasControlAdapterState[],
regions: CanvasRegionalGuidanceState[],
onBboxChanged: (arg: BboxChangedArg, entityType: CanvasEntityState['type']) => void
): void => {
for (const entityState of [...layers, ...controlAdapters, ...regions]) {
const konvaLayer = stage.findOne<Konva.Layer>(`#${entityState.id}`);
assert(konvaLayer, `Layer ${entityState.id} not found in stage`);
// We only need to recalculate the bbox if the layer has changed
if (entityState.bboxNeedsUpdate) {
const bboxRect = konvaLayer.findOne<Konva.Rect>('.bbox') ?? createBboxRect(entityState, konvaLayer);
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
const visible = bboxRect.visible();
bboxRect.visible(false);
if (entityState.type === 'layer') {
if (entityState.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged({ id: entityState.id, bbox: null }, 'layer');
} else {
onBboxChanged({ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterLayerChildren) }, 'layer');
}
} else if (entityState.type === 'control_adapter') {
if (!entityState.imageObject && !entityState.processedImageObject) {
// No objects - no bbox to calculate
onBboxChanged({ id: entityState.id, bbox: null }, 'control_adapter');
} else {
onBboxChanged(
{ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterCAChildren) },
'control_adapter'
);
}
} else if (entityState.type === 'regional_guidance') {
if (entityState.objects.length === 0) {
// No objects - no bbox to calculate
onBboxChanged({ id: entityState.id, bbox: null }, 'regional_guidance');
} else {
onBboxChanged(
{ id: entityState.id, bbox: getLayerBboxPixels(konvaLayer, filterRGChildren) },
'regional_guidance'
);
}
}
// Restore the visibility of the bbox
bboxRect.visible(visible);
}
}
};

View File

@ -5,7 +5,6 @@ import { deepClone } from 'common/util/deepClone';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers';
import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers';
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
import { layersReducers } from 'features/controlLayers/store/layersReducers';
@ -33,14 +32,6 @@ const initialState: CanvasV2State = {
ipAdapters: { entities: [] },
regions: { entities: [] },
loras: [],
initialImage: {
id: 'initial_image',
type: 'initial_image',
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
imageObject: null,
},
inpaintMask: {
id: 'inpaint_mask',
type: 'inpaint_mask',
@ -125,7 +116,6 @@ const initialState: CanvasV2State = {
refinerStart: 0.8,
},
session: {
isActive: false,
isStaging: false,
stagedImages: [],
selectedStagedImageIndex: 0,
@ -148,7 +138,6 @@ export const canvasV2Slice = createSlice({
...bboxReducers,
...inpaintMaskReducers,
...sessionReducers,
...initialImageReducers,
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
},
@ -175,7 +164,6 @@ export const canvasV2Slice = createSlice({
state.session = deepClone(initialState.session);
state.tool = deepClone(initialState.tool);
state.inpaintMask = deepClone(initialState.inpaintMask);
state.initialImage = deepClone(initialState.initialImage);
},
},
});
@ -342,18 +330,12 @@ export const {
imRectAdded,
inpaintMaskRasterized,
// Staging
sessionStarted,
sessionStartedStaging,
sessionImageStaged,
sessionStagedImageDiscarded,
sessionStagingAreaReset,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
// Initial image
iiRecalled,
iiIsEnabledToggled,
iiReset,
iiImageChanged,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;

View File

@ -1,38 +0,0 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es';
import type { ImageDTO } from 'services/api/types';
import type { CanvasV2State, InitialImageEntity } from './types';
import { imageDTOToImageObject } from './types';
export const initialImageReducers = {
iiRecalled: (state, action: PayloadAction<{ data: InitialImageEntity }>) => {
const { data } = action.payload;
state.initialImage = data;
state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' };
},
iiIsEnabledToggled: (state) => {
if (!state.initialImage) {
return;
}
state.initialImage.isEnabled = !state.initialImage.isEnabled;
},
iiReset: (state) => {
state.initialImage.imageObject = null;
},
iiImageChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => {
const { imageDTO } = action.payload;
if (!state.initialImage) {
return;
}
const newImageObject = imageDTOToImageObject(imageDTO);
if (isEqual(newImageObject, state.initialImage.imageObject)) {
return;
}
state.initialImage.bbox = null;
state.initialImage.bboxNeedsUpdate = true;
state.initialImage.isEnabled = true;
state.initialImage.imageObject = newImageObject;
state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' };
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -2,10 +2,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { CanvasV2State, StagingAreaImage } from 'features/controlLayers/store/types';
export const sessionReducers = {
sessionStarted: (state) => {
state.session.isActive = true;
state.selectedEntityIdentifier = { id: 'inpaint_mask', type: 'inpaint_mask' };
},
sessionStartedStaging: (state) => {
state.session.isStaging = true;
state.session.selectedStagedImageIndex = 0;

View File

@ -2,7 +2,6 @@ import type { JSONObject } from 'common/types';
import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter';
import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter';
import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
import { CanvasRegion } from 'features/controlLayers/konva/CanvasRegion';
import { getObjectId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
@ -686,16 +685,6 @@ const zCanvasInpaintMaskState = z.object({
});
export type CanvasInpaintMaskState = z.infer<typeof zCanvasInpaintMaskState>;
const zInitialImageEntity = z.object({
id: z.literal('initial_image'),
type: z.literal('initial_image'),
isEnabled: z.boolean(),
bbox: zRect.nullable(),
bboxNeedsUpdate: z.boolean(),
imageObject: zCanvasImageState.nullable(),
});
export type InitialImageEntity = z.infer<typeof zInitialImageEntity>;
const zCanvasControlAdapterStateBase = z.object({
id: zId,
type: z.literal('control_adapter'),
@ -818,8 +807,7 @@ export type CanvasEntityState =
| CanvasControlAdapterState
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
| CanvasIPAdapterState
| InitialImageEntity;
| CanvasIPAdapterState;
export type CanvasEntityIdentifier = Pick<CanvasEntityState, 'id' | 'type'>;
export type LoRA = {
@ -847,7 +835,6 @@ export type CanvasV2State = {
ipAdapters: { entities: CanvasIPAdapterState[] };
regions: { entities: CanvasRegionalGuidanceState[] };
loras: LoRA[];
initialImage: InitialImageEntity;
tool: {
selected: Tool;
selectedBuffer: Tool | null;
@ -920,7 +907,6 @@ export type CanvasV2State = {
refinerStart: number;
};
session: {
isActive: boolean;
isStaging: boolean;
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
@ -969,11 +955,9 @@ export function isDrawableEntity(
}
export function isDrawableEntityAdapter(
adapter: CanvasLayerAdapter | CanvasRegion | CanvasControlAdapter | CanvasMaskAdapter
): adapter is CanvasLayerAdapter | CanvasRegion | CanvasMaskAdapter {
return (
adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasRegion || adapter instanceof CanvasMaskAdapter
);
adapter: CanvasLayerAdapter | CanvasControlAdapter | CanvasMaskAdapter
): adapter is CanvasLayerAdapter | CanvasMaskAdapter {
return adapter instanceof CanvasLayerAdapter || adapter instanceof CanvasMaskAdapter;
}
export function isDrawableEntityType(