mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-21 00:48:17 -05:00
Compare commits
16 Commits
v4.2.9.dev
...
v4.2.9.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
056c56d322 | ||
|
|
afc6f83d72 | ||
|
|
c776ac3af2 | ||
|
|
b7b3683bef | ||
|
|
fb26b6824a | ||
|
|
63d8ad912f | ||
|
|
bbd7d7fc17 | ||
|
|
6507a78182 | ||
|
|
22f46517f4 | ||
|
|
45596e1f94 | ||
|
|
6de0dbe854 | ||
|
|
011827fa29 | ||
|
|
fc6d244071 | ||
|
|
cd3da886d6 | ||
|
|
c013c55d92 | ||
|
|
cd3dd7db0d |
@@ -1032,7 +1032,11 @@ class CanvasV2MaskAndCropOutput(ImageOutput):
|
||||
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
"""Handles Canvas V2 image output masking and cropping"""
|
||||
|
||||
image: ImageField = InputField(description="The image to apply the mask to")
|
||||
source_image: ImageField | None = InputField(
|
||||
default=None,
|
||||
description="The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.",
|
||||
)
|
||||
generated_image: ImageField = InputField(description="The image to apply the mask to")
|
||||
mask: ImageField = InputField(description="The mask to apply")
|
||||
mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
|
||||
|
||||
@@ -1046,33 +1050,25 @@ class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
return ImageOps.invert(mask.convert("L"))
|
||||
|
||||
def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
|
||||
image = context.images.get_pil(self.image.image_name)
|
||||
mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
|
||||
image.putalpha(mask)
|
||||
|
||||
if self.source_image:
|
||||
generated_image = context.images.get_pil(self.generated_image.image_name)
|
||||
source_image = context.images.get_pil(self.source_image.image_name)
|
||||
source_image.paste(generated_image, (0, 0), mask)
|
||||
image_dto = context.images.save(image=source_image)
|
||||
else:
|
||||
generated_image = context.images.get_pil(self.generated_image.image_name)
|
||||
generated_image.putalpha(mask)
|
||||
image_dto = context.images.save(image=generated_image)
|
||||
|
||||
# bbox = image.getbbox()
|
||||
# image = image.crop(bbox)
|
||||
image_dto = context.images.save(image=image)
|
||||
|
||||
return CanvasV2MaskAndCropOutput(
|
||||
image=ImageField(image_name=image_dto.image_name),
|
||||
offset_x=0,
|
||||
offset_y=0,
|
||||
width=image.width,
|
||||
height=image.height,
|
||||
width=image_dto.width,
|
||||
height=image_dto.height,
|
||||
)
|
||||
|
||||
# def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
|
||||
# image = context.images.get_pil(self.image.image_name)
|
||||
# mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
|
||||
# image.putalpha(mask)
|
||||
# bbox = image.getbbox()
|
||||
# image = image.crop(bbox)
|
||||
# image_dto = context.images.save(image=image)
|
||||
|
||||
# return CanvasV2MaskAndCropOutput(
|
||||
# image=ImageField(image_name=image_dto.image_name),
|
||||
# offset_x=bbox[0],
|
||||
# offset_y=bbox[1],
|
||||
# width=image.width,
|
||||
# height=image.height,
|
||||
# )
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "pnpm run lint && vite build",
|
||||
"typegen": "node scripts/typegen.js",
|
||||
"preview": "vite preview",
|
||||
"lint:knip": "knip",
|
||||
"lint:knip": "knip --tags=-knipignore",
|
||||
"lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx",
|
||||
"lint:eslint": "eslint --max-warnings=0 .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
|
||||
@@ -1657,6 +1657,7 @@
|
||||
"recalculateRects": "Recalculate Rects",
|
||||
"clipToBbox": "Clip Strokes to Bbox",
|
||||
"addLayer": "Add Layer",
|
||||
"duplicate": "Duplicate",
|
||||
"moveToFront": "Move to Front",
|
||||
"moveToBack": "Move to Back",
|
||||
"moveForward": "Move Forward",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
@@ -15,23 +14,21 @@ import { PiPlusBold } from 'react-icons/pi';
|
||||
export const CanvasAddEntityButtons = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultControlAdapter = useDefaultControlAdapter();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const addInpaintMask = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded());
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRegionalGuidance = useCallback(() => {
|
||||
dispatch(rgAdded());
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRasterLayer = useCallback(() => {
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addControlLayer = useCallback(() => {
|
||||
dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } }));
|
||||
}, [defaultControlAdapter, dispatch]);
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
dispatch(ipaAdded({ ipAdapter: defaultIPAdapter }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
dispatch(ipaAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useDefaultControlAdapter, useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import {
|
||||
allEntitiesDeleted,
|
||||
controlLayerAdded,
|
||||
@@ -21,23 +20,21 @@ export const CanvasEntityListMenu = memo(() => {
|
||||
const count = selectEntityCount(s);
|
||||
return count > 0;
|
||||
});
|
||||
const defaultControlAdapter = useDefaultControlAdapter();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const addInpaintMask = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded());
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRegionalGuidance = useCallback(() => {
|
||||
dispatch(rgAdded());
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRasterLayer = useCallback(() => {
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addControlLayer = useCallback(() => {
|
||||
dispatch(controlLayerAdded({ isSelected: true, overrides: { controlAdapter: defaultControlAdapter } }));
|
||||
}, [defaultControlAdapter, dispatch]);
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
dispatch(ipaAdded({ ipAdapter: defaultIPAdapter }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
dispatch(ipaAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const deleteAll = useCallback(() => {
|
||||
dispatch(allEntitiesDeleted());
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { ControlLayerMenuItemsControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster';
|
||||
@@ -17,6 +18,7 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const IPAdapterMenuItems = memo(() => {
|
||||
@@ -8,6 +9,7 @@ export const IPAdapterMenuItems = memo(() => {
|
||||
<>
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -11,6 +12,7 @@ export const InpaintMaskMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl';
|
||||
@@ -15,6 +16,7 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import { nanoid } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
rgIPAdapterAdded,
|
||||
rgNegativePromptChanged,
|
||||
@@ -20,7 +18,6 @@ type AddPromptButtonProps = {
|
||||
export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const selectValidActions = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
|
||||
@@ -40,8 +37,8 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButt
|
||||
dispatch(rgNegativePromptChanged({ id, prompt: '' }));
|
||||
}, [dispatch, id]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } }));
|
||||
}, [defaultIPAdapter, dispatch, id]);
|
||||
dispatch(rgIPAdapterAdded({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<Flex w="full" p={2} justifyContent="space-between">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
|
||||
import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative';
|
||||
@@ -16,6 +17,7 @@ export const RegionalGuidanceMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,6 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import { nanoid } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
rgIPAdapterAdded,
|
||||
rgNegativePromptChanged,
|
||||
@@ -17,7 +15,6 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
||||
const { id } = useEntityIdentifierContext();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const selectValidActions = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
|
||||
@@ -37,8 +34,8 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
||||
dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
|
||||
}, [dispatch, id]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...defaultIPAdapter, id: nanoid() } }));
|
||||
}, [defaultIPAdapter, dispatch, id]);
|
||||
dispatch(rgIPAdapterAdded({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { entityDuplicated } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsDuplicate = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityDuplicated({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCopyFill />}>
|
||||
{t('controlLayers.duplicate')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsDuplicate.displayName = 'CanvasEntityMenuItemsDuplicate';
|
||||
@@ -1,13 +1,36 @@
|
||||
import { Box, chakra, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const ChakraCanvas = chakra.canvas;
|
||||
|
||||
const PADDING = 4;
|
||||
|
||||
export const CanvasEntityPreviewImage = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapter();
|
||||
const selectMaskColor = useMemo(
|
||||
() =>
|
||||
createSelector(selectCanvasV2Slice, (state) => {
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
|
||||
return rgbColorToString(entity.fill.color);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
[entityIdentifier]
|
||||
);
|
||||
const maskColor = useSelector(selectMaskColor);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const cache = useStore(adapter.renderer.$canvasCache);
|
||||
@@ -26,8 +49,25 @@ export const CanvasEntityPreviewImage = memo(() => {
|
||||
canvasRef.current.width = rect.width;
|
||||
canvasRef.current.height = rect.height;
|
||||
|
||||
ctx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
|
||||
}, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache]);
|
||||
const scale = containerRef.current.offsetWidth / rect.width;
|
||||
|
||||
const sx = rect.x;
|
||||
const sy = rect.y;
|
||||
const sWidth = rect.width;
|
||||
const sHeight = rect.height;
|
||||
const dx = PADDING / scale;
|
||||
const dy = PADDING / scale;
|
||||
const dWidth = rect.width - (PADDING * 2) / scale;
|
||||
const dHeight = rect.height - (PADDING * 2) / scale;
|
||||
|
||||
ctx.drawImage(canvas, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
|
||||
|
||||
if (maskColor) {
|
||||
ctx.fillStyle = maskColor;
|
||||
ctx.globalCompositeOperation = 'source-in';
|
||||
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||
}
|
||||
}, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache, maskColor]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -27,6 +27,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden
|
||||
return controlAdapter;
|
||||
};
|
||||
|
||||
/** @knipignore */
|
||||
export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => {
|
||||
const [modelConfigs] = useControlNetAndT2IAdapterModels();
|
||||
|
||||
@@ -47,6 +48,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig
|
||||
return defaultControlAdapter;
|
||||
};
|
||||
|
||||
/** @knipignore */
|
||||
export const useDefaultIPAdapter = (): IPAdapterConfig => {
|
||||
const [modelConfigs] = useIPAdapterModels();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { GroupConfig } from 'konva/lib/Group';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
|
||||
@@ -209,9 +210,7 @@ export class CanvasObjectRenderer {
|
||||
this.log.trace('Caching object group');
|
||||
this.konva.objectGroup.clearCache();
|
||||
this.konva.objectGroup.cache({ pixelRatio: 1 });
|
||||
if (!this.parent.transformer.isPendingRectCalculation) {
|
||||
this.parent.renderer.updatePreviewCanvas();
|
||||
}
|
||||
this.parent.renderer.updatePreviewCanvas();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -542,7 +541,10 @@ export class CanvasObjectRenderer {
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
updatePreviewCanvas = () => {
|
||||
updatePreviewCanvas = debounce(() => {
|
||||
if (this.parent.transformer.isPendingRectCalculation) {
|
||||
return;
|
||||
}
|
||||
if (this.parent.transformer.pixelRect.width === 0 || this.parent.transformer.pixelRect.height === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -558,7 +560,7 @@ export class CanvasObjectRenderer {
|
||||
};
|
||||
this.$canvasCache.set({ rect, canvas });
|
||||
}
|
||||
};
|
||||
}, 300);
|
||||
|
||||
cloneObjectGroup = (attrs?: GroupConfig): Konva.Group => {
|
||||
const clone = this.konva.objectGroup.clone();
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
entitySelected,
|
||||
eraserWidthChanged,
|
||||
fillChanged,
|
||||
selectAllRenderableEntities,
|
||||
toolBufferChanged,
|
||||
toolChanged,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
@@ -43,6 +44,7 @@ import type {
|
||||
EntityRectAddedPayload,
|
||||
Rect,
|
||||
RgbaColor,
|
||||
RgbColor,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
@@ -198,6 +200,17 @@ export class CanvasStateApiModule {
|
||||
return null;
|
||||
}
|
||||
|
||||
getRenderedEntityCount = () => {
|
||||
const renderableEntities = selectAllRenderableEntities(this.getState());
|
||||
let count = 0;
|
||||
for (const entity of renderableEntities) {
|
||||
if (entity.isEnabled) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
getSelectedEntity = () => {
|
||||
const state = this.getState();
|
||||
if (state.selectedEntityIdentifier) {
|
||||
@@ -237,7 +250,7 @@ export class CanvasStateApiModule {
|
||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||
$selectedEntity: WritableAtom<EntityStateAndAdapter | null> = atom();
|
||||
$selectedEntityIdentifier: WritableAtom<CanvasEntityIdentifier | null> = atom();
|
||||
$colorUnderCursor: WritableAtom<RgbaColor | null> = atom();
|
||||
$colorUnderCursor: WritableAtom<RgbColor> = atom(RGBA_BLACK);
|
||||
|
||||
// Read-write state, ephemeral interaction state
|
||||
$isDrawing = $isDrawing;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule';
|
||||
import {
|
||||
BRUSH_BORDER_INNER_COLOR,
|
||||
BRUSH_BORDER_OUTER_COLOR,
|
||||
BRUSH_ERASER_BORDER_WIDTH,
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants';
|
||||
import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Tool } from 'features/controlLayers/store/types';
|
||||
import { isDrawableEntity } from 'features/controlLayers/store/types';
|
||||
@@ -15,6 +11,12 @@ import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasToolModule {
|
||||
readonly type = 'tool_preview';
|
||||
static readonly COLOR_PICKER_RADIUS = 25;
|
||||
static readonly COLOR_PICKER_THICKNESS = 15;
|
||||
static readonly COLOR_PICKER_CROSSHAIR_SPACE = 5;
|
||||
static readonly COLOR_PICKER_CROSSHAIR_INNER_THICKNESS = 1.5;
|
||||
static readonly COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS = 3;
|
||||
static readonly COLOR_PICKER_CROSSHAIR_SIZE = 10;
|
||||
|
||||
id: string;
|
||||
path: string[];
|
||||
@@ -27,19 +29,29 @@ export class CanvasToolModule {
|
||||
brush: {
|
||||
group: Konva.Group;
|
||||
fillCircle: Konva.Circle;
|
||||
innerBorderCircle: Konva.Circle;
|
||||
outerBorderCircle: Konva.Circle;
|
||||
innerBorder: Konva.Ring;
|
||||
outerBorder: Konva.Ring;
|
||||
};
|
||||
eraser: {
|
||||
group: Konva.Group;
|
||||
fillCircle: Konva.Circle;
|
||||
innerBorderCircle: Konva.Circle;
|
||||
outerBorderCircle: Konva.Circle;
|
||||
innerBorder: Konva.Ring;
|
||||
outerBorder: Konva.Ring;
|
||||
};
|
||||
colorPicker: {
|
||||
group: Konva.Group;
|
||||
fillCircle: Konva.Circle;
|
||||
transparentCenterCircle: Konva.Circle;
|
||||
newColor: Konva.Ring;
|
||||
oldColor: Konva.Arc;
|
||||
innerBorder: Konva.Ring;
|
||||
outerBorder: Konva.Ring;
|
||||
crosshairNorthInner: Konva.Line;
|
||||
crosshairNorthOuter: Konva.Line;
|
||||
crosshairEastInner: Konva.Line;
|
||||
crosshairEastOuter: Konva.Line;
|
||||
crosshairSouthInner: Konva.Line;
|
||||
crosshairSouthOuter: Konva.Line;
|
||||
crosshairWestInner: Konva.Line;
|
||||
crosshairWestOuter: Konva.Line;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,19 +75,21 @@ export class CanvasToolModule {
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
innerBorderCircle: new Konva.Circle({
|
||||
name: `${this.type}:brush_inner_border_circle`,
|
||||
innerBorder: new Konva.Ring({
|
||||
name: `${this.type}:brush_inner_border_ring`,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
|
||||
strokeEnabled: true,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
outerBorderCircle: new Konva.Circle({
|
||||
name: `${this.type}:brush_outer_border_circle`,
|
||||
outerBorder: new Konva.Ring({
|
||||
name: `${this.type}:brush_outer_border_ring`,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
|
||||
strokeEnabled: true,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
},
|
||||
eraser: {
|
||||
@@ -87,54 +101,110 @@ export class CanvasToolModule {
|
||||
fill: 'white',
|
||||
globalCompositeOperation: 'destination-out',
|
||||
}),
|
||||
innerBorderCircle: new Konva.Circle({
|
||||
name: `${this.type}:eraser_inner_border_circle`,
|
||||
innerBorder: new Konva.Ring({
|
||||
name: `${this.type}:eraser_inner_border_ring`,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
|
||||
strokeEnabled: true,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
outerBorderCircle: new Konva.Circle({
|
||||
name: `${this.type}:eraser_outer_border_circle`,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeWidth: BRUSH_ERASER_BORDER_WIDTH,
|
||||
strokeEnabled: true,
|
||||
outerBorder: new Konva.Ring({
|
||||
name: `${this.type}:eraser_outer_border_ring`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
},
|
||||
colorPicker: {
|
||||
group: new Konva.Group({ name: `${this.type}:color_picker_group`, listening: false }),
|
||||
fillCircle: new Konva.Circle({
|
||||
name: `${this.type}:color_picker_fill_circle`,
|
||||
listening: false,
|
||||
fill: '',
|
||||
radius: 20,
|
||||
strokeWidth: 1,
|
||||
stroke: 'black',
|
||||
strokeScaleEnabled: false,
|
||||
}),
|
||||
transparentCenterCircle: new Konva.Circle({
|
||||
name: `${this.type}:color_picker_fill_circle`,
|
||||
listening: false,
|
||||
newColor: new Konva.Ring({
|
||||
name: `${this.type}:color_picker_new_color_ring`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
strokeEnabled: false,
|
||||
fill: 'white',
|
||||
radius: 5,
|
||||
globalCompositeOperation: 'destination-out',
|
||||
}),
|
||||
oldColor: new Konva.Arc({
|
||||
name: `${this.type}:color_picker_old_color_arc`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
angle: 180,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
innerBorder: new Konva.Ring({
|
||||
name: `${this.type}:color_picker_inner_border_ring`,
|
||||
listening: false,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
outerBorder: new Konva.Ring({
|
||||
name: `${this.type}:color_picker_outer_border_ring`,
|
||||
innerRadius: 0,
|
||||
outerRadius: 0,
|
||||
fill: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeEnabled: false,
|
||||
}),
|
||||
crosshairNorthInner: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_north1_line`,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
}),
|
||||
crosshairNorthOuter: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_north2_line`,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
}),
|
||||
crosshairEastInner: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_east1_line`,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
}),
|
||||
crosshairEastOuter: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_east2_line`,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
}),
|
||||
crosshairSouthInner: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_south1_line`,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
}),
|
||||
crosshairSouthOuter: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_south2_line`,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
}),
|
||||
crosshairWestInner: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_west1_line`,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
}),
|
||||
crosshairWestOuter: new Konva.Line({
|
||||
name: `${this.type}:color_picker_crosshair_west2_line`,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
}),
|
||||
},
|
||||
};
|
||||
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.brush.group.add(this.konva.brush.fillCircle, this.konva.brush.innerBorder, this.konva.brush.outerBorder);
|
||||
this.konva.group.add(this.konva.brush.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.eraser.group.add(
|
||||
this.konva.eraser.fillCircle,
|
||||
this.konva.eraser.innerBorder,
|
||||
this.konva.eraser.outerBorder
|
||||
);
|
||||
this.konva.group.add(this.konva.eraser.group);
|
||||
|
||||
this.konva.colorPicker.group.add(this.konva.colorPicker.fillCircle);
|
||||
this.konva.colorPicker.group.add(this.konva.colorPicker.transparentCenterCircle);
|
||||
this.konva.colorPicker.group.add(
|
||||
this.konva.colorPicker.newColor,
|
||||
this.konva.colorPicker.oldColor,
|
||||
this.konva.colorPicker.innerBorder,
|
||||
this.konva.colorPicker.outerBorder,
|
||||
this.konva.colorPicker.crosshairNorthOuter,
|
||||
this.konva.colorPicker.crosshairNorthInner,
|
||||
this.konva.colorPicker.crosshairEastOuter,
|
||||
this.konva.colorPicker.crosshairEastInner,
|
||||
this.konva.colorPicker.crosshairSouthOuter,
|
||||
this.konva.colorPicker.crosshairSouthInner,
|
||||
this.konva.colorPicker.crosshairWestOuter,
|
||||
this.konva.colorPicker.crosshairWestInner
|
||||
);
|
||||
this.konva.group.add(this.konva.colorPicker.group);
|
||||
|
||||
this.subscriptions.add(
|
||||
@@ -157,25 +227,6 @@ export class CanvasToolModule {
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
scaleTool = () => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const scale = this.manager.stage.getScale();
|
||||
|
||||
const brushRadius = toolState.brush.width / 2;
|
||||
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.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,
|
||||
});
|
||||
};
|
||||
|
||||
setToolVisibility = (tool: Tool) => {
|
||||
this.konva.brush.group.visible(tool === 'brush');
|
||||
this.konva.eraser.group.visible(tool === 'eraser');
|
||||
@@ -184,13 +235,11 @@ export class CanvasToolModule {
|
||||
|
||||
render() {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const isDrawing = this.manager.stateApi.$isDrawing.get();
|
||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
|
||||
const tool = toolState.selected;
|
||||
|
||||
@@ -238,32 +287,38 @@ export class CanvasToolModule {
|
||||
if (cursorPos && tool === 'brush') {
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
|
||||
const scale = stage.getScale();
|
||||
// Update the fill circle
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.brush.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.brush.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: isDrawing ? '' : rgbaColorToString(brushPreviewFill),
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
});
|
||||
|
||||
// Update the inner border of the brush preview
|
||||
this.konva.brush.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
|
||||
|
||||
// Update the outer border of the brush preview
|
||||
this.konva.brush.outerBorderCircle.setAttrs({
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.brush.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.brush.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
|
||||
const scale = stage.getScale();
|
||||
// Update the fill circle
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.eraser.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.eraser.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
@@ -271,28 +326,97 @@ export class CanvasToolModule {
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
// Update the inner border of the eraser preview
|
||||
this.konva.eraser.innerBorderCircle.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.eraser.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.eraser.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
|
||||
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS
|
||||
);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
|
||||
// Update the outer border of the eraser preview
|
||||
this.konva.eraser.outerBorderCircle.setAttrs({
|
||||
this.konva.colorPicker.newColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: radius + BRUSH_ERASER_BORDER_WIDTH / scale,
|
||||
fill: rgbColorToString(colorUnderCursor),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
} else if (cursorPos && colorUnderCursor) {
|
||||
this.konva.colorPicker.fillCircle.setAttrs({
|
||||
this.konva.colorPicker.oldColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbaColorToString(colorUnderCursor),
|
||||
fill: rgbColorToString(toolState.fill),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.colorPicker.transparentCenterCircle.setAttrs({
|
||||
this.konva.colorPicker.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius,
|
||||
outerRadius: colorPickerOuterRadius + onePixel,
|
||||
});
|
||||
this.konva.colorPicker.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius + onePixel,
|
||||
outerRadius: colorPickerOuterRadius + twoPixels,
|
||||
});
|
||||
|
||||
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
|
||||
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
|
||||
const innerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS
|
||||
);
|
||||
const outerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS
|
||||
);
|
||||
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairNorthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
}
|
||||
|
||||
this.scaleTool();
|
||||
this.setToolVisibility(tool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +602,6 @@ export class CanvasTransformer {
|
||||
|
||||
if (this.isPendingRectCalculation) {
|
||||
this.syncInteractionState();
|
||||
this.parent.renderer.updatePreviewCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -613,20 +612,19 @@ export class CanvasTransformer {
|
||||
// The layer is fully transparent but has objects - reset it
|
||||
this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() });
|
||||
this.syncInteractionState();
|
||||
this.parent.renderer.updatePreviewCanvas();
|
||||
return;
|
||||
} else {
|
||||
this.syncInteractionState();
|
||||
this.update(this.parent.state.position, this.pixelRect);
|
||||
const groupAttrs: Partial<GroupConfig> = {
|
||||
x: this.parent.state.position.x + this.pixelRect.x,
|
||||
y: this.parent.state.position.y + this.pixelRect.y,
|
||||
offsetX: this.pixelRect.x,
|
||||
offsetY: this.pixelRect.y,
|
||||
};
|
||||
this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs);
|
||||
this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs);
|
||||
}
|
||||
|
||||
this.syncInteractionState();
|
||||
this.update(this.parent.state.position, this.pixelRect);
|
||||
const groupAttrs: Partial<GroupConfig> = {
|
||||
x: this.parent.state.position.x + this.pixelRect.x,
|
||||
y: this.parent.state.position.y + this.pixelRect.y,
|
||||
offsetX: this.pixelRect.x,
|
||||
offsetY: this.pixelRect.y,
|
||||
};
|
||||
this.parent.renderer.konva.objectGroup.setAttrs(groupAttrs);
|
||||
this.parent.renderer.konva.bufferGroup.setAttrs(groupAttrs);
|
||||
this.parent.renderer.updatePreviewCanvas();
|
||||
};
|
||||
|
||||
@@ -649,8 +647,8 @@ export class CanvasTransformer {
|
||||
if (!this.parent.renderer.needsPixelBbox()) {
|
||||
this.nodeRect = { ...rect };
|
||||
this.pixelRect = { ...rect };
|
||||
this.isPendingRectCalculation = false;
|
||||
this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect }, 'Got bbox from client rect');
|
||||
this.isPendingRectCalculation = false;
|
||||
this.updateBbox();
|
||||
return;
|
||||
}
|
||||
@@ -674,8 +672,8 @@ export class CanvasTransformer {
|
||||
this.nodeRect = getEmptyRect();
|
||||
this.pixelRect = getEmptyRect();
|
||||
}
|
||||
this.isPendingRectCalculation = false;
|
||||
this.log.trace({ nodeRect: this.nodeRect, pixelRect: this.pixelRect, extents }, `Got bbox from worker`);
|
||||
this.isPendingRectCalculation = false;
|
||||
this.updateBbox();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -15,11 +15,6 @@ export const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
||||
*/
|
||||
export const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
|
||||
|
||||
/**
|
||||
* The border width for the brush preview.
|
||||
*/
|
||||
export const BRUSH_ERASER_BORDER_WIDTH = 1;
|
||||
|
||||
/**
|
||||
* The target spacing of individual points of brush strokes, as a percentage of the brush size.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
RgbaColor,
|
||||
RgbColor,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
@@ -115,7 +115,7 @@ const getLastPointOfLastLineOfEntity = (
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => {
|
||||
const getColorUnderCursor = (stage: Konva.Stage): RgbColor | null => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (!pos) {
|
||||
return null;
|
||||
@@ -126,12 +126,12 @@ const getColorUnderCursor = (stage: Konva.Stage): RgbaColor | null => {
|
||||
if (!ctx) {
|
||||
return null;
|
||||
}
|
||||
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
if (r === undefined || g === undefined || b === undefined || a === undefined) {
|
||||
const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
if (r === undefined || g === undefined || b === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { r, g, b, a };
|
||||
return { r, g, b };
|
||||
};
|
||||
|
||||
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
@@ -195,9 +195,11 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
if (toolState.selected === 'colorPicker') {
|
||||
const color = getColorUnderCursor(stage);
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
if (color) {
|
||||
manager.stateApi.setFill(color);
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
}
|
||||
if (color) {
|
||||
manager.stateApi.setFill({ ...color, a: 1 });
|
||||
}
|
||||
manager.preview.tool.render();
|
||||
} else {
|
||||
@@ -345,7 +347,9 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
if (toolState.selected === 'colorPicker') {
|
||||
const color = getColorUnderCursor(stage);
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
if (color) {
|
||||
manager.stateApi.$colorUnderCursor.set(color);
|
||||
}
|
||||
} else {
|
||||
const isDrawable = selectedEntity?.state.isEnabled;
|
||||
if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import type { CanvasEntityIdentifier, Coordinate, Rect } from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
@@ -335,7 +335,7 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
*/
|
||||
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
export function getPrefixedId(prefix: string): string {
|
||||
export function getPrefixedId(prefix: CanvasEntityIdentifier['type'] | (string & Record<never, never>)): string {
|
||||
return `${prefix}:${nanoid()}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
|
||||
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
|
||||
import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers';
|
||||
@@ -24,8 +25,12 @@ import { atom } from 'nanostores';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityState,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
EntityBrushLineAddedPayload,
|
||||
@@ -181,6 +186,17 @@ function selectAllEntities(state: CanvasV2State): CanvasEntityState[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function selectAllRenderableEntities(
|
||||
state: CanvasV2State
|
||||
): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] {
|
||||
return [
|
||||
...state.rasterLayers.entities,
|
||||
...state.controlLayers.entities,
|
||||
...state.inpaintMasks.entities,
|
||||
...state.regions.entities,
|
||||
];
|
||||
}
|
||||
|
||||
export const canvasV2Slice = createSlice({
|
||||
name: 'canvasV2',
|
||||
initialState,
|
||||
@@ -222,6 +238,42 @@ export const canvasV2Slice = createSlice({
|
||||
assert(false, 'Not implemented');
|
||||
}
|
||||
},
|
||||
entityDuplicated: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newEntity = deepClone(entity);
|
||||
if (newEntity.name) {
|
||||
newEntity.name = `${newEntity.name} (Copy)`;
|
||||
}
|
||||
switch (newEntity.type) {
|
||||
case 'raster_layer':
|
||||
newEntity.id = getPrefixedId('raster_layer');
|
||||
state.rasterLayers.entities.push(newEntity);
|
||||
break;
|
||||
case 'control_layer':
|
||||
newEntity.id = getPrefixedId('control_layer');
|
||||
state.controlLayers.entities.push(newEntity);
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
newEntity.id = getPrefixedId('regional_guidance');
|
||||
state.regions.entities.push(newEntity);
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
newEntity.id = getPrefixedId('ip_adapter');
|
||||
state.ipAdapters.entities.push(newEntity);
|
||||
break;
|
||||
case 'inpaint_mask':
|
||||
newEntity.id = getPrefixedId('inpaint_mask');
|
||||
state.inpaintMasks.entities.push(newEntity);
|
||||
break;
|
||||
}
|
||||
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(newEntity);
|
||||
},
|
||||
entityIsEnabledToggled: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
@@ -266,6 +318,8 @@ export const canvasV2Slice = createSlice({
|
||||
assert(false, `Cannot add a brush line to a non-drawable entity of type ${entity.type}`);
|
||||
}
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) });
|
||||
},
|
||||
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
||||
@@ -279,6 +333,8 @@ export const canvasV2Slice = createSlice({
|
||||
assert(false, `Cannot add a eraser line to a non-drawable entity of type ${entity.type}`);
|
||||
}
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) });
|
||||
},
|
||||
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
||||
@@ -292,7 +348,9 @@ export const canvasV2Slice = createSlice({
|
||||
assert(false, `Cannot add a rect to a non-drawable entity of type ${entity.type}`);
|
||||
}
|
||||
|
||||
entity.objects.push(rect);
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...rect });
|
||||
},
|
||||
entityDeleted: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
@@ -439,6 +497,7 @@ export const {
|
||||
entityReset,
|
||||
entityIsEnabledToggled,
|
||||
entityMoved,
|
||||
entityDuplicated,
|
||||
entityRasterized,
|
||||
entityBrushLineAdded,
|
||||
entityEraserLineAdded,
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
ControlNetConfig,
|
||||
T2IAdapterConfig,
|
||||
} from './types';
|
||||
import { initialControlNet } from './types';
|
||||
import { getEntityIdentifier, initialControlNet } from './types';
|
||||
|
||||
const selectControlLayerEntity = (state: CanvasV2State, id: string) =>
|
||||
state.controlLayers.entities.find((entity) => entity.id === id);
|
||||
@@ -31,7 +31,7 @@ export const controlLayersReducers = {
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const layer: CanvasControlLayerState = {
|
||||
const entity: CanvasControlLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'control_layer',
|
||||
@@ -42,10 +42,10 @@ export const controlLayersReducers = {
|
||||
position: { x: 0, y: 0 },
|
||||
controlAdapter: deepClone(initialControlNet),
|
||||
};
|
||||
merge(layer, overrides);
|
||||
state.controlLayers.entities.push(layer);
|
||||
merge(entity, overrides);
|
||||
state.controlLayers.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = { type: 'control_layer', id };
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasInpaintMaskState,
|
||||
CanvasV2State,
|
||||
EntityIdentifierPayload,
|
||||
FillStyle,
|
||||
RgbColor,
|
||||
import {
|
||||
type CanvasInpaintMaskState,
|
||||
type CanvasV2State,
|
||||
type EntityIdentifierPayload,
|
||||
type FillStyle,
|
||||
getEntityIdentifier,
|
||||
type RgbColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { merge } from 'lodash-es';
|
||||
import { assert } from 'tsafe';
|
||||
@@ -41,7 +42,7 @@ export const inpaintMaskReducers = {
|
||||
merge(entity, overrides);
|
||||
state.inpaintMasks.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = { type: 'inpaint_mask', id };
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { merge } from 'lodash-es';
|
||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPMethodV2 } from './types';
|
||||
import { imageDTOToImageWithDims } from './types';
|
||||
import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from './types';
|
||||
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types';
|
||||
|
||||
const selectIPAdapterEntity = (state: CanvasV2State, id: string) =>
|
||||
state.ipAdapters.entities.find((ipa) => ipa.id === id);
|
||||
@@ -17,19 +20,27 @@ export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) =
|
||||
|
||||
export const ipAdaptersReducers = {
|
||||
ipaAdded: {
|
||||
reducer: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterConfig }>) => {
|
||||
const { id, ipAdapter } = action.payload;
|
||||
const layer: CanvasIPAdapterState = {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasIPAdapterState = {
|
||||
id,
|
||||
type: 'ip_adapter',
|
||||
name: null,
|
||||
isEnabled: true,
|
||||
ipAdapter,
|
||||
ipAdapter: deepClone(initialIPAdapter),
|
||||
};
|
||||
state.ipAdapters.entities.push(layer);
|
||||
state.selectedEntityIdentifier = { type: 'ip_adapter', id };
|
||||
merge(entity, overrides);
|
||||
state.ipAdapters.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload: { ipAdapter: IPAdapterConfig }) => ({ payload: { id: uuidv4(), ...payload } }),
|
||||
prepare: (payload?: { overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('ip_adapter') },
|
||||
}),
|
||||
},
|
||||
ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => {
|
||||
const { data } = action.payload;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types';
|
||||
import { initialControlNet } from './types';
|
||||
import { getEntityIdentifier, initialControlNet } from './types';
|
||||
|
||||
const selectRasterLayerEntity = (state: CanvasV2State, id: string) =>
|
||||
state.rasterLayers.entities.find((layer) => layer.id === id);
|
||||
@@ -16,7 +16,7 @@ export const rasterLayersReducers = {
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const layer: CanvasRasterLayerState = {
|
||||
const entity: CanvasRasterLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'raster_layer',
|
||||
@@ -25,10 +25,10 @@ export const rasterLayersReducers = {
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
merge(layer, overrides);
|
||||
state.rasterLayers.entities.push(layer);
|
||||
merge(entity, overrides);
|
||||
state.rasterLayers.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = { type: 'raster_layer', id };
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasV2State,
|
||||
@@ -8,9 +9,9 @@ import type {
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
RgbColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { isEqual, merge } from 'lodash-es';
|
||||
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -56,9 +57,12 @@ const getRGMaskFill = (state: CanvasV2State): RgbColor => {
|
||||
|
||||
export const regionsReducers = {
|
||||
rgAdded: {
|
||||
reducer: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg: CanvasRegionalGuidanceState = {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasRegionalGuidanceState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'regional_guidance',
|
||||
@@ -75,10 +79,15 @@ export const regionsReducers = {
|
||||
negativePrompt: null,
|
||||
ipAdapters: [],
|
||||
};
|
||||
state.regions.entities.push(rg);
|
||||
state.selectedEntityIdentifier = { type: 'regional_guidance', id };
|
||||
merge(entity, overrides);
|
||||
state.regions.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }),
|
||||
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('regional_guidance') },
|
||||
}),
|
||||
},
|
||||
rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => {
|
||||
const { data } = action.payload;
|
||||
@@ -126,13 +135,23 @@ export const regionsReducers = {
|
||||
}
|
||||
rg.autoNegative = !rg.autoNegative;
|
||||
},
|
||||
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: RegionalGuidanceIPAdapterConfig }>) => {
|
||||
const { id, ipAdapter } = action.payload;
|
||||
const entity = selectRegionalGuidanceEntity(state, id);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
entity.ipAdapters.push(ipAdapter);
|
||||
rgIPAdapterAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> }>
|
||||
) => {
|
||||
const { id, overrides, ipAdapterId } = action.payload;
|
||||
const entity = selectRegionalGuidanceEntity(state, id);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
const ipAdapter = { ...deepClone(initialIPAdapter), id: ipAdapterId };
|
||||
merge(ipAdapter, overrides);
|
||||
entity.ipAdapters.push(ipAdapter);
|
||||
},
|
||||
prepare: (payload: { id: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> }) => ({
|
||||
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
|
||||
}),
|
||||
},
|
||||
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
|
||||
const { id, ipAdapterId } = action.payload;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types';
|
||||
@@ -7,6 +8,7 @@ import { isEqual } from 'lodash-es';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
|
||||
export const addInpaint = async (
|
||||
state: RootState,
|
||||
g: Graph,
|
||||
manager: CanvasManager,
|
||||
l2i: Invocation<'l2i'>,
|
||||
@@ -22,6 +24,7 @@ export const addInpaint = async (
|
||||
): Promise<Invocation<'canvas_v2_mask_and_crop'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
|
||||
const mode = state.canvasV2.session.mode;
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
|
||||
@@ -87,9 +90,13 @@ export const addInpaint = async (
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image');
|
||||
|
||||
// Finally, paste the generated masked image back onto the original image
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'image');
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
} else {
|
||||
// No scale before processing, much simpler
|
||||
@@ -114,6 +121,7 @@ export const addInpaint = async (
|
||||
type: 'canvas_v2_mask_and_crop',
|
||||
mask_blur: compositing.maskBlur,
|
||||
});
|
||||
|
||||
g.addEdge(alphaToMask, 'image', createGradientMask, 'mask');
|
||||
g.addEdge(i2l, 'latents', denoise, 'latents');
|
||||
g.addEdge(vaeSource, 'vae', i2l, 'vae');
|
||||
@@ -122,7 +130,11 @@ export const addInpaint = async (
|
||||
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'image');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasV2State, Dimensions } from 'features/controlLayers/store/types';
|
||||
@@ -8,6 +9,7 @@ import { isEqual } from 'lodash-es';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
|
||||
export const addOutpaint = async (
|
||||
state: RootState,
|
||||
g: Graph,
|
||||
manager: CanvasManager,
|
||||
l2i: Invocation<'l2i'>,
|
||||
@@ -23,6 +25,7 @@ export const addOutpaint = async (
|
||||
): Promise<Invocation<'canvas_v2_mask_and_crop'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
|
||||
const mode = state.canvasV2.session.mode;
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
const infill = getInfill(g, compositing);
|
||||
@@ -111,9 +114,13 @@ export const addOutpaint = async (
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image');
|
||||
|
||||
// Finally, paste the generated masked image back onto the original image
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'image');
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
} else {
|
||||
infill.image = { image_name: initialImage.image_name };
|
||||
@@ -158,7 +165,11 @@ export const addOutpaint = async (
|
||||
g.addEdge(modelLoader, 'unet', createGradientMask, 'unet');
|
||||
g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask');
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'image');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
return canvasPasteBack;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ export const buildSD1Graph = async (
|
||||
} else if (generationMode === 'inpaint') {
|
||||
const { compositing } = state.canvasV2;
|
||||
canvasOutput = await addInpaint(
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
@@ -189,6 +190,7 @@ export const buildSD1Graph = async (
|
||||
} else if (generationMode === 'outpaint') {
|
||||
const { compositing } = state.canvasV2;
|
||||
canvasOutput = await addOutpaint(
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
|
||||
@@ -176,6 +176,7 @@ export const buildSDXLGraph = async (
|
||||
} else if (generationMode === 'inpaint') {
|
||||
const { compositing } = state.canvasV2;
|
||||
canvasOutput = await addInpaint(
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
@@ -192,6 +193,7 @@ export const buildSDXLGraph = async (
|
||||
} else if (generationMode === 'outpaint') {
|
||||
const { compositing } = state.canvasV2;
|
||||
canvasOutput = await addOutpaint(
|
||||
state,
|
||||
g,
|
||||
manager,
|
||||
l2i,
|
||||
|
||||
@@ -3061,11 +3061,16 @@ export type components = {
|
||||
* @default true
|
||||
*/
|
||||
use_cache?: boolean;
|
||||
/**
|
||||
* @description The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.
|
||||
* @default null
|
||||
*/
|
||||
source_image?: components["schemas"]["ImageField"] | null;
|
||||
/**
|
||||
* @description The image to apply the mask to
|
||||
* @default null
|
||||
*/
|
||||
image?: components["schemas"]["ImageField"];
|
||||
generated_image?: components["schemas"]["ImageField"];
|
||||
/**
|
||||
* @description The mask to apply
|
||||
* @default null
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.2.9.dev20240823"
|
||||
__version__ = "4.2.9.dev20240824"
|
||||
|
||||
Reference in New Issue
Block a user