mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-21 13:48:24 -05:00
Compare commits
10 Commits
ryan/flux-
...
v5.0.0.a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f82640b5df | ||
|
|
e3e50abc5a | ||
|
|
061bff2814 | ||
|
|
e5a53be42b | ||
|
|
54c94bd713 | ||
|
|
8d56becf04 | ||
|
|
dc51ccd9a6 | ||
|
|
f5eefedc49 | ||
|
|
136891ec3d | ||
|
|
c5543e42c7 |
@@ -58,7 +58,7 @@
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/inter": "^5.0.20",
|
||||
"@invoke-ai/ui-library": "^0.0.33",
|
||||
"@invoke-ai/ui-library": "^0.0.34",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@reduxjs/toolkit": "2.2.3",
|
||||
"@roarr/browser-log-writer": "^1.3.0",
|
||||
|
||||
8
invokeai/frontend/web/pnpm-lock.yaml
generated
8
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ dependencies:
|
||||
specifier: ^5.0.20
|
||||
version: 5.0.20
|
||||
'@invoke-ai/ui-library':
|
||||
specifier: ^0.0.33
|
||||
version: 0.0.33(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
|
||||
specifier: ^0.0.34
|
||||
version: 0.0.34(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@nanostores/react':
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(nanostores@0.11.2)(react@18.3.1)
|
||||
@@ -3574,8 +3574,8 @@ packages:
|
||||
prettier: 3.3.3
|
||||
dev: true
|
||||
|
||||
/@invoke-ai/ui-library@0.0.33(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-YLydTCOTUEgju4Ex6yXt/bvNBcO97y6zc1cGYjt7vtJMS8e6deA89cC5JejjbmVgntdnn49cDyeUxB8Z24gZew==}
|
||||
/@invoke-ai/ui-library@0.0.34(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-iDSjFQV2U4LfQ8+UdZ9Uy6J1iKKTSsXM0uhkWrwcIghbgN5QwY3ABVLhqJrSWVTwp7puEDhe/lRQ9QhTZBkVzw==}
|
||||
peerDependencies:
|
||||
'@fontsource-variable/inter': ^5.0.16
|
||||
react: ^18.2.0
|
||||
|
||||
@@ -1662,8 +1662,12 @@
|
||||
"bookmark": "Bookmark for Quick Switch",
|
||||
"fitBboxToLayers": "Fit Bbox To Layers",
|
||||
"removeBookmark": "Remove Bookmark",
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
"saveCanvasToGallery": "Save Canvas to Gallery",
|
||||
"saveBboxToGallery": "Save Bbox to Gallery",
|
||||
"sendBboxToRegionalIPAdapter": "Send Bbox to Regional IP Adapter",
|
||||
"sendBboxToGlobalIPAdapter": "Send Bbox to Global IP Adapter",
|
||||
"sendBboxToControlLayer": "Send Bbox to Control Layer",
|
||||
"sendBboxToRasterLayer": "Send Bbox to Raster Layer",
|
||||
"savedToGalleryOk": "Saved to Gallery",
|
||||
"savedToGalleryError": "Error saving to gallery",
|
||||
"mergeVisible": "Merge Visible",
|
||||
@@ -1756,6 +1760,8 @@
|
||||
"noLayersAdded": "No Layers Added",
|
||||
"layer_one": "Layer",
|
||||
"layer_other": "Layers",
|
||||
"layer_withCount_one": "Layer ({{count}})",
|
||||
"layer_withCount_other": "Layers ({{count}})",
|
||||
"objects_zero": "empty",
|
||||
"objects_one": "{{count}} object",
|
||||
"objects_other": "{{count}} objects",
|
||||
@@ -1925,7 +1931,9 @@
|
||||
"queue": "Queue",
|
||||
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||
"upscaling": "Upscaling",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"gallery": "Gallery",
|
||||
"galleryTab": "$t(ui.tabs.gallery) $t(common.tab)"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
useIsSavingCanvas,
|
||||
useSaveBboxAsControlLayer,
|
||||
useSaveBboxAsGlobalIPAdapter,
|
||||
useSaveBboxAsRasterLayer,
|
||||
useSaveBboxAsRegionalGuidanceIPAdapter,
|
||||
useSaveBboxToGallery,
|
||||
useSaveCanvasToGallery,
|
||||
} from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold, PiShareFatBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasContextMenuItems = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isSaving = useIsSavingCanvas();
|
||||
const saveCanvasToGallery = useSaveCanvasToGallery();
|
||||
const saveBboxToGallery = useSaveBboxToGallery();
|
||||
const saveBboxAsRegionalGuidanceIPAdapter = useSaveBboxAsRegionalGuidanceIPAdapter();
|
||||
const saveBboxAsIPAdapter = useSaveBboxAsGlobalIPAdapter();
|
||||
const saveBboxAsRasterLayer = useSaveBboxAsRasterLayer();
|
||||
const saveBboxAsControlLayer = useSaveBboxAsControlLayer();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isLoading={isSaving.isTrue} onClick={saveCanvasToGallery}>
|
||||
{t('controlLayers.saveCanvasToGallery')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isLoading={isSaving.isTrue} onClick={saveBboxToGallery}>
|
||||
{t('controlLayers.saveBboxToGallery')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} isLoading={isSaving.isTrue} onClick={saveBboxAsIPAdapter}>
|
||||
{t('controlLayers.sendBboxToGlobalIPAdapter')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} isLoading={isSaving.isTrue} onClick={saveBboxAsRegionalGuidanceIPAdapter}>
|
||||
{t('controlLayers.sendBboxToRegionalIPAdapter')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} isLoading={isSaving.isTrue} onClick={saveBboxAsControlLayer}>
|
||||
{t('controlLayers.sendBboxToControlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} isLoading={isSaving.isTrue} onClick={saveBboxAsRasterLayer}>
|
||||
{t('controlLayers.sendBboxToRasterLayer')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasContextMenuItems.displayName = 'CanvasContextMenuItems';
|
||||
@@ -5,8 +5,9 @@ import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanelContent = memo(() => {
|
||||
@@ -37,6 +38,7 @@ CanvasRightPanelContent.displayName = 'CanvasRightPanelContent';
|
||||
|
||||
const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const entityCount = useAppSelector(selectEntityCount);
|
||||
const sendToCanvas = useAppSelector(selectSendToCanvas);
|
||||
const tabTimeout = useRef<number | null>(null);
|
||||
const dndCtx = useDndContext();
|
||||
@@ -44,7 +46,7 @@ const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
|
||||
const onOnMouseOverLayersTab = useCallback(() => {
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
setTab(1);
|
||||
setTab(0);
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, setTab]);
|
||||
@@ -52,7 +54,7 @@ const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
|
||||
const onOnMouseOverGalleryTab = useCallback(() => {
|
||||
tabTimeout.current = window.setTimeout(() => {
|
||||
if (dndCtx.active) {
|
||||
setTab(0);
|
||||
setTab(1);
|
||||
}
|
||||
}, 300);
|
||||
}, [dndCtx.active, setTab]);
|
||||
@@ -62,10 +64,20 @@ const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
|
||||
clearTimeout(tabTimeout.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const layersTabLabel = useMemo(() => {
|
||||
if (entityCount === 0) {
|
||||
return t('controlLayers.layer_other');
|
||||
}
|
||||
return `${t('controlLayers.layer_other')} (${entityCount})`;
|
||||
}, [entityCount, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut}>
|
||||
{t('controlLayers.layer_other')}
|
||||
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut} w={32}>
|
||||
<Box as="span" w="full">
|
||||
{layersTabLabel}
|
||||
</Box>
|
||||
{sendToCanvas && (
|
||||
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasContextMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItems';
|
||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { CanvasSelectedEntityStatusAlert } from 'features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert';
|
||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo, useRef } from 'react';
|
||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
|
||||
export const CanvasTabContent = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const renderMenu = useCallback(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<MenuList>
|
||||
<CanvasContextMenuItems />
|
||||
</MenuList>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
}, []);
|
||||
|
||||
useScopeOnFocus('canvas', ref);
|
||||
|
||||
return (
|
||||
@@ -29,7 +47,42 @@ export const CanvasTabContent = memo(() => {
|
||||
justifyContent="center"
|
||||
>
|
||||
<CanvasToolbar />
|
||||
<StageComponent />
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
position="relative"
|
||||
w="full"
|
||||
h="full"
|
||||
bg={dynamicGrid ? 'base.850' : 'base.900'}
|
||||
borderRadius="base"
|
||||
>
|
||||
{!dynamicGrid && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
bgImage={TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL}
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
opacity={0.1}
|
||||
/>
|
||||
)}
|
||||
<InvokeCanvasComponent />
|
||||
<CanvasManagerProviderGate>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<CanvasHUD />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1} pointerEvents="none">
|
||||
<CanvasSelectedEntityStatusAlert />
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
|
||||
<CanvasManagerProviderGate>
|
||||
<StagingAreaIsStagingGate>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { useInvokeCanvas } from 'features/controlLayers/hooks/useInvokeCanvas';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const InvokeCanvasComponent = memo(() => {
|
||||
const ref = useInvokeCanvas();
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
ref={ref}
|
||||
borderRadius="base"
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
InvokeCanvasComponent.displayName = 'InvokeCanvasComponent';
|
||||
@@ -1,116 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $socket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { CanvasSelectedEntityStatusAlert } from 'features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||
Konva.showWarnings = false;
|
||||
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null) => {
|
||||
const store = useAppStore();
|
||||
const socket = useStore($socket);
|
||||
const dpr = useDevicePixelRatio({ round: false });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.debug('Initializing renderer');
|
||||
if (!container) {
|
||||
// Nothing to clean up
|
||||
log.debug('No stage container, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!socket) {
|
||||
log.debug('Socket not connected, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const manager = new CanvasManager(stage, container, store, socket);
|
||||
manager.initialize();
|
||||
return manager.destroy;
|
||||
}, [container, socket, stage, store]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Konva.pixelRatio = dpr;
|
||||
}, [dpr]);
|
||||
};
|
||||
|
||||
export const StageComponent = memo(() => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const [stage] = useState(
|
||||
() =>
|
||||
new Konva.Stage({
|
||||
id: getPrefixedId('konva_stage'),
|
||||
container: document.createElement('div'),
|
||||
})
|
||||
);
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const containerRef = useCallback((el: HTMLDivElement | null) => {
|
||||
setContainer(el);
|
||||
}, []);
|
||||
|
||||
useStageRenderer(stage, container);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
stage.destroy();
|
||||
},
|
||||
[stage]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" bg={dynamicGrid ? 'base.850' : 'base.900'} borderRadius="base">
|
||||
{!dynamicGrid && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
bgImage={TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL}
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
opacity={0.1}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
ref={containerRef}
|
||||
borderRadius="base"
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
<CanvasManagerProviderGate>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<CanvasHUD />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1} pointerEvents="none">
|
||||
<CanvasSelectedEntityStatusAlert />
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
StageComponent.displayName = 'StageComponent';
|
||||
@@ -1,47 +1,24 @@
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { isOk, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import {
|
||||
useIsSavingCanvas,
|
||||
useSaveBboxToGallery,
|
||||
useSaveCanvasToGallery,
|
||||
} from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const [useIsSaving] = buildUseBoolean(false);
|
||||
|
||||
export const CanvasToolbarSaveToGalleryButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const shift = useShiftModifier();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isSaving = useIsSaving();
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
isSaving.setTrue();
|
||||
|
||||
const rect = shift ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
|
||||
|
||||
const result = await withResultAsync(() =>
|
||||
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, true)
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
toast({ title: t('controlLayers.savedToGalleryOk') });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
|
||||
toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
|
||||
}
|
||||
|
||||
isSaving.setFalse();
|
||||
}, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, isSaving, shift, t]);
|
||||
const isSaving = useIsSavingCanvas();
|
||||
const saveCanvasToGallery = useSaveCanvasToGallery();
|
||||
const saveBboxToGallery = useSaveBboxToGallery();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
onClick={shift ? saveBboxToGallery : saveCanvasToGallery}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
isLoading={isSaving.isTrue}
|
||||
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
|
||||
|
||||
@@ -49,20 +49,24 @@ export const selectDefaultControlAdapter = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const selectDefaultIPAdapter = createSelector(selectModelConfigsQuery, selectBase, (query, base): IPAdapterConfig => {
|
||||
const { data } = query;
|
||||
let model: IPAdapterModelConfig | null = null;
|
||||
if (data) {
|
||||
const modelConfigs = modelConfigsAdapterSelectors.selectAll(data).filter(isIPAdapterModelConfig);
|
||||
const compatibleModels = modelConfigs.filter((m) => (base ? m.base === base : true));
|
||||
model = compatibleModels[0] ?? modelConfigs[0] ?? null;
|
||||
export const selectDefaultIPAdapter = createSelector(
|
||||
selectModelConfigsQuery,
|
||||
selectBase,
|
||||
(query, base): IPAdapterConfig => {
|
||||
const { data } = query;
|
||||
let model: IPAdapterModelConfig | null = null;
|
||||
if (data) {
|
||||
const modelConfigs = modelConfigsAdapterSelectors.selectAll(data).filter(isIPAdapterModelConfig);
|
||||
const compatibleModels = modelConfigs.filter((m) => (base ? m.base === base : true));
|
||||
model = compatibleModels[0] ?? modelConfigs[0] ?? null;
|
||||
}
|
||||
const ipAdapter = deepClone(initialIPAdapter);
|
||||
if (model) {
|
||||
ipAdapter.model = zModelIdentifierField.parse(model);
|
||||
}
|
||||
return ipAdapter;
|
||||
}
|
||||
const ipAdapter = deepClone(initialIPAdapter);
|
||||
if (model) {
|
||||
ipAdapter.model = zModelIdentifierField.parse(model);
|
||||
}
|
||||
return ipAdapter;
|
||||
});
|
||||
);
|
||||
|
||||
export const useAddControlLayer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { isOk, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { controlLayerAdded, ipaAdded, rasterLayerAdded, rgAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasIPAdapterState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
Rect,
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
export const [useIsSavingCanvas] = buildUseBoolean(false);
|
||||
|
||||
type UseSaveCanvasArg = {
|
||||
region: 'canvas' | 'bbox';
|
||||
saveToGallery: boolean;
|
||||
onSave?: (imageDTO: ImageDTO, rect: Rect) => void;
|
||||
};
|
||||
|
||||
const useSaveCanvas = ({ region, saveToGallery, onSave }: UseSaveCanvasArg) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvasManager = useCanvasManager();
|
||||
const isSaving = useIsSavingCanvas();
|
||||
|
||||
const saveCanvas = useCallback(async () => {
|
||||
isSaving.setTrue();
|
||||
|
||||
const rect =
|
||||
region === 'bbox' ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
|
||||
|
||||
const result = await withResultAsync(() =>
|
||||
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, saveToGallery)
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
if (onSave) {
|
||||
onSave(result.value, rect);
|
||||
}
|
||||
toast({ title: t('controlLayers.savedToGalleryOk') });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
|
||||
toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
|
||||
}
|
||||
|
||||
isSaving.setFalse();
|
||||
}, [
|
||||
canvasManager.compositor,
|
||||
canvasManager.stage,
|
||||
canvasManager.stateApi,
|
||||
isSaving,
|
||||
onSave,
|
||||
region,
|
||||
saveToGallery,
|
||||
t,
|
||||
]);
|
||||
|
||||
return saveCanvas;
|
||||
};
|
||||
|
||||
export const useSaveCanvasToGallery = () => {
|
||||
const saveCanvasToGalleryArg = useMemo<UseSaveCanvasArg>(() => ({ region: 'canvas', saveToGallery: true }), []);
|
||||
const saveCanvasToGallery = useSaveCanvas(saveCanvasToGalleryArg);
|
||||
return saveCanvasToGallery;
|
||||
};
|
||||
|
||||
export const useSaveBboxToGallery = () => {
|
||||
const saveBboxToGalleryArg = useMemo<UseSaveCanvasArg>(() => ({ region: 'bbox', saveToGallery: true }), []);
|
||||
const saveBboxToGallery = useSaveCanvas(saveBboxToGalleryArg);
|
||||
return saveBboxToGallery;
|
||||
};
|
||||
|
||||
export const useSaveBboxAsRegionalGuidanceIPAdapter = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
|
||||
const saveBboxAsRegionalGuidanceIPAdapterArg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO) => {
|
||||
const ipAdapter: RegionalGuidanceIPAdapterConfig = {
|
||||
...defaultIPAdapter,
|
||||
id: getPrefixedId('regional_guidance_ip_adapter'),
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
};
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
ipAdapters: [ipAdapter],
|
||||
};
|
||||
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: true, onSave };
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
const saveBboxAsRegionalGuidanceIPAdapter = useSaveCanvas(saveBboxAsRegionalGuidanceIPAdapterArg);
|
||||
return saveBboxAsRegionalGuidanceIPAdapter;
|
||||
};
|
||||
|
||||
export const useSaveBboxAsGlobalIPAdapter = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
|
||||
const saveBboxAsIPAdapterArg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO) => {
|
||||
const overrides: Partial<CanvasIPAdapterState> = {
|
||||
ipAdapter: {
|
||||
...defaultIPAdapter,
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
},
|
||||
};
|
||||
dispatch(ipaAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: true, onSave };
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
const saveBboxAsIPAdapter = useSaveCanvas(saveBboxAsIPAdapterArg);
|
||||
return saveBboxAsIPAdapter;
|
||||
};
|
||||
|
||||
export const useSaveBboxAsRasterLayer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const saveBboxAsRasterLayerArg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, rect: Rect) => {
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
position: { x: rect.x, y: rect.y },
|
||||
};
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: true, onSave };
|
||||
}, [dispatch]);
|
||||
const saveBboxAsRasterLayer = useSaveCanvas(saveBboxAsRasterLayerArg);
|
||||
return saveBboxAsRasterLayer;
|
||||
};
|
||||
|
||||
export const useSaveBboxAsControlLayer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
|
||||
|
||||
const saveBboxAsControlLayerArg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, rect: Rect) => {
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
controlAdapter: defaultControlAdapter,
|
||||
position: { x: rect.x, y: rect.y },
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: true, onSave };
|
||||
}, [defaultControlAdapter, dispatch]);
|
||||
const saveBboxAsControlLayer = useSaveCanvas(saveBboxAsControlLayerArg);
|
||||
return saveBboxAsControlLayer;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $socket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
|
||||
import Konva from 'konva';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
// This will log warnings when layers > 5
|
||||
Konva.showWarnings = import.meta.env.MODE === 'development';
|
||||
|
||||
const useKonvaPixelRatioWatcher = () => {
|
||||
useAssertSingleton('useKonvaPixelRatioWatcher');
|
||||
|
||||
const dpr = useDevicePixelRatio({ round: false });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Konva.pixelRatio = dpr;
|
||||
}, [dpr]);
|
||||
};
|
||||
|
||||
export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => {
|
||||
useAssertSingleton('useInvokeCanvas');
|
||||
useKonvaPixelRatioWatcher();
|
||||
const store = useAppStore();
|
||||
const socket = useStore($socket);
|
||||
const [container, containerRef] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.debug('Initializing renderer');
|
||||
if (!container) {
|
||||
// Nothing to clean up
|
||||
log.debug('No stage container, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!socket) {
|
||||
log.debug('Socket not connected, skipping initialization');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const currentManager = $canvasManager.get();
|
||||
if (currentManager) {
|
||||
currentManager.stage.setContainer(container);
|
||||
return;
|
||||
}
|
||||
|
||||
const manager = new CanvasManager(container, store, socket);
|
||||
manager.initialize();
|
||||
}, [container, socket, store]);
|
||||
|
||||
return containerRef;
|
||||
};
|
||||
@@ -61,16 +61,19 @@ export const useNextPrevEntityHotkeys = () => {
|
||||
|
||||
useHotkeys(
|
||||
// “ === alt+[
|
||||
['alt+[', '“'],
|
||||
['“'],
|
||||
selectPrevEntity,
|
||||
{ preventDefault: true, ignoreModifiers: true },
|
||||
[selectPrevEntity]
|
||||
);
|
||||
|
||||
useHotkeys(['alt+['], selectPrevEntity, { preventDefault: true }, [selectPrevEntity]);
|
||||
useHotkeys(
|
||||
// ‘ === alt+]
|
||||
['alt+]', '‘'],
|
||||
['‘'],
|
||||
selectNextEntity,
|
||||
{ preventDefault: true, ignoreModifiers: true },
|
||||
[selectNextEntity]
|
||||
);
|
||||
useHotkeys(['alt+]'], selectNextEntity, { preventDefault: true }, [selectNextEntity]);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
*/
|
||||
$isBusy: Atom<boolean>;
|
||||
|
||||
constructor(stage: Konva.Stage, container: HTMLDivElement, store: AppStore, socket: AppSocket) {
|
||||
constructor(container: HTMLDivElement, store: AppStore, socket: AppSocket) {
|
||||
super();
|
||||
this.id = getPrefixedId(this.type);
|
||||
this.path = [this.id];
|
||||
@@ -98,7 +98,7 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
this.socket = socket;
|
||||
|
||||
this.stateApi = new CanvasStateApiModule(this.store, this);
|
||||
this.stage = new CanvasStageModule(stage, container, this);
|
||||
this.stage = new CanvasStageModule(container, this);
|
||||
this.worker = new CanvasWorkerModule(this);
|
||||
this.cache = new CanvasCacheModule(this);
|
||||
this.entityRenderer = new CanvasEntityRendererModule(this);
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
Rect,
|
||||
StageAttrs,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
@@ -59,7 +59,7 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
|
||||
constructor(stage: Konva.Stage, container: HTMLDivElement, manager: CanvasManager) {
|
||||
constructor(container: HTMLDivElement, manager: CanvasManager) {
|
||||
super();
|
||||
this.id = getPrefixedId('stage');
|
||||
this.parent = manager;
|
||||
@@ -70,9 +70,19 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating module');
|
||||
|
||||
this.container = container;
|
||||
this.konva = { stage };
|
||||
this.konva = {
|
||||
stage: new Konva.Stage({
|
||||
id: getPrefixedId('konva_stage'),
|
||||
container,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
setContainer = (container: HTMLDivElement) => {
|
||||
this.container = container;
|
||||
this.konva.stage.container(container);
|
||||
};
|
||||
|
||||
setEventListeners = () => {
|
||||
this.konva.stage.on('wheel', this.onStageMouseWheel);
|
||||
this.konva.stage.on('dragmove', this.onStageDragMove);
|
||||
|
||||
@@ -1302,4 +1302,7 @@ function actionsThrottlingFilter(action: UnknownAction) {
|
||||
}
|
||||
|
||||
export const $lastCanvasProgressEvent = atom<S['InvocationDenoiseProgressEvent'] | null>(null);
|
||||
/**
|
||||
* The global canvas manager instance.
|
||||
*/
|
||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const selectCanvasSlice = (state: RootState) => state.canvas.present;
|
||||
*
|
||||
* It does not check for validity of the entities.
|
||||
*/
|
||||
const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
|
||||
export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
|
||||
return (
|
||||
canvas.regions.entities.length +
|
||||
canvas.ipAdapters.entities.length +
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.0.0.a1"
|
||||
__version__ = "5.0.0.a2"
|
||||
|
||||
Reference in New Issue
Block a user