refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 17:38:00 +10:00
parent c51253f5f6
commit 58c656224f
59 changed files with 605 additions and 519 deletions

View File

@ -19,9 +19,9 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
let wereControlAdaptersReset = false; let wereControlAdaptersReset = false;
let wereControlLayersReset = false; let wereControlLayersReset = false;
const { canvas, nodes, controlAdapters, controlLayers } = getState(); const { canvas, nodes, controlAdapters, canvasV2 } = getState();
deleted_images.forEach((image_name) => { deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, canvasV2, image_name);
if (imageUsage.isCanvasImage && !wasCanvasReset) { if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas()); dispatch(resetCanvas());

View File

@ -65,14 +65,14 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
// Delay before starting actual work // Delay before starting actual work
await delay(DEBOUNCE_MS); await delay(DEBOUNCE_MS);
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); const layer = state.canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
if (!layer) { if (!layer) {
return; return;
} }
// We should only process if the processor settings or image have changed // We should only process if the processor settings or image have changed
const originalLayer = originalState.controlLayers.present.layers const originalLayer = originalState.canvasV2.layers
.filter(isControlAdapterLayer) .filter(isControlAdapterLayer)
.find((l) => l.id === layerId); .find((l) => l.id === layerId);
const originalImage = originalLayer?.controlAdapter.image; const originalImage = originalLayer?.controlAdapter.image;
@ -161,7 +161,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
if (signal.aborted) { if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
const pendingBatchId = getState() const pendingBatchId = getState()
.controlLayers.present.layers.filter(isControlAdapterLayer) .canvasV2.layers.filter(isControlAdapterLayer)
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
if (pendingBatchId) { if (pendingBatchId) {
cancelProcessorBatch(dispatch, layerId, pendingBatchId); cancelProcessorBatch(dispatch, layerId, pendingBatchId);

View File

@ -70,7 +70,7 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima
}; };
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.controlLayers.present.layers.forEach((l) => { state.canvasV2.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) { if (isRegionalGuidanceLayer(l)) {
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
dispatch(layerDeleted(l.id)); dispatch(layerDeleted(l.id));

View File

@ -79,15 +79,15 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
const optimalDimension = getOptimalDimension(defaultModelInList); const optimalDimension = getOptimalDimension(defaultModelInList);
if ( if (
getIsSizeOptimal( getIsSizeOptimal(
state.controlLayers.present.size.width, state.canvasV2.size.width,
state.controlLayers.present.size.height, state.canvasV2.size.height,
optimalDimension optimalDimension
) )
) { ) {
return; return;
} }
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
state.controlLayers.present.size.aspectRatio.value, state.canvasV2.size.aspectRatio.value,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );

View File

@ -4,7 +4,7 @@ import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types'; import type { JSONObject } from 'common/types';
import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; import { canvasPersistConfig } from 'features/canvas/store/canvasSlice';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { import {
controlAdaptersV2PersistConfig, controlAdaptersV2PersistConfig,
@ -52,7 +52,6 @@ import { listenerMiddleware } from './middleware/listenerMiddleware';
const allReducers = { const allReducers = {
[api.reducerPath]: api.reducer, [api.reducerPath]: api.reducer,
[canvasSlice.name]: canvasSlice.reducer,
[gallerySlice.name]: gallerySlice.reducer, [gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer, [generationSlice.name]: generationSlice.reducer,
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),

View File

@ -59,8 +59,8 @@ const createSelector = (templates: Templates) =>
config config
) => { ) => {
const { model } = generation; const { model } = generation;
const { size } = controlLayers.present; const { size } = canvasV2;
const { positivePrompt } = controlLayers.present; const { positivePrompt } = canvasV2;
const { isConnected } = system; const { isConnected } = system;
@ -126,7 +126,7 @@ const createSelector = (templates: Templates) =>
if (activeTabName === 'generation') { if (activeTabName === 'generation') {
// Handling for generation tab // Handling for generation tab
controlLayers.present.layers canvasV2.layers
.filter((l) => l.isEnabled) .filter((l) => l.isEnabled)
.forEach((l, i) => { .forEach((l, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerLiteral = i18n.t('controlLayers.layers_one');

View File

@ -23,7 +23,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return { return {
canAddPositivePrompt: layer.positivePrompt === null, canAddPositivePrompt: layer.positivePrompt === null,

View File

@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
export const BrushColorPicker = memo(() => { export const BrushColorPicker = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const brushColor = useAppSelector((s) => s.controlLayers.present.brushColor); const brushColor = useAppSelector((s) => s.canvasV2.brushColor);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onChange = useCallback( const onChange = useCallback(
(color: RgbaColor) => { (color: RgbaColor) => {

View File

@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`;
export const BrushSize = memo(() => { export const BrushSize = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); const brushSize = useAppSelector((s) => s.canvasV2.brushSize);
const onChange = useCallback( const onChange = useCallback(
(v: number) => { (v: number) => {
dispatch(brushSizeChanged(Math.round(v))); dispatch(brushSizeChanged(Math.round(v)));

View File

@ -19,7 +19,7 @@ type Props = {
export const CALayer = memo(({ layerId }: Props) => { export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector( const isSelected = useAppSelector(
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).isSelected (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).isSelected
); );
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));

View File

@ -28,7 +28,7 @@ type Props = {
export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const controlAdapter = useAppSelector( const controlAdapter = useAppSelector(
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isControlAdapterLayer).controlAdapter (s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).controlAdapter
); );
const onChangeBeginEndStepPct = useCallback( const onChangeBeginEndStepPct = useCallback(

View File

@ -19,7 +19,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); const [renderableLayers, ipAdapterLayers] = partition(canvasV2.layers, isRenderableLayer);
return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse();
}); });

View File

@ -8,7 +8,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
export const DeleteAllLayersButton = memo(() => { export const DeleteAllLayersButton = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0); const isDisabled = useAppSelector((s) => s.canvasV2.layers.length === 0);
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(allLayersDeleted()); dispatch(allLayersDeleted());
}, [dispatch]); }, [dispatch]);

View File

@ -14,7 +14,7 @@ export const GlobalMaskLayerOpacity = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const globalMaskLayerOpacity = useAppSelector((s) => const globalMaskLayerOpacity = useAppSelector((s) =>
Math.round(s.controlLayers.present.globalMaskLayerOpacity * 100) Math.round(s.canvasV2.globalMaskLayerOpacity * 100)
); );
const onChange = useCallback( const onChange = useCallback(
(v: number) => { (v: number) => {

View File

@ -7,8 +7,8 @@ import { memo } from 'react';
export const HeadsUpDisplay = memo(() => { export const HeadsUpDisplay = memo(() => {
const stageAttrs = useStore($stageAttrs); const stageAttrs = useStore($stageAttrs);
const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length); const layerCount = useAppSelector((s) => s.canvasV2.layers.length);
const bbox = useAppSelector((s) => s.controlLayers.present.bbox); const bbox = useAppSelector((s) => s.canvasV2.bbox);
return ( return (
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}> <Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>

View File

@ -25,7 +25,7 @@ type Props = {
export const IILayer = memo(({ layerId }: Props) => { export const IILayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const layer = useAppSelector((s) => selectLayerOrThrow(s.controlLayers.present, layerId, isInitialImageLayer)); const layer = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, layerId, isInitialImageLayer));
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));
}, [dispatch, layerId]); }, [dispatch, layerId]);

View File

@ -16,7 +16,7 @@ type Props = {
export const IPALayer = memo(({ layerId }: Props) => { export const IPALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector( const isSelected = useAppSelector(
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).isSelected (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).isSelected
); );
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => { const onClick = useCallback(() => {

View File

@ -22,7 +22,7 @@ type Props = {
export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const ipAdapter = useAppSelector( const ipAdapter = useAppSelector(
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isIPAdapterLayer).ipAdapter (s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).ipAdapter
); );
const onChangeBeginEndStepPct = useCallback( const onChangeBeginEndStepPct = useCallback(

View File

@ -22,10 +22,10 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => {
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`);
const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId); const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId);
const layerCount = controlLayers.present.layers.length; const layerCount = canvasV2.layers.length;
return { return {
canMoveForward: layerIndex < layerCount - 1, canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0, canMoveBackward: layerIndex > 0,

View File

@ -22,7 +22,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return { return {
canAddPositivePrompt: layer.positivePrompt === null, canAddPositivePrompt: layer.positivePrompt === null,

View File

@ -37,7 +37,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => {
const selectOpacity = useMemo( const selectOpacity = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity);
return Math.round(layer.opacity * 100); return Math.round(layer.opacity * 100);
}), }),
[layerId] [layerId]

View File

@ -30,14 +30,14 @@ export const RGLayer = memo(({ layerId }: Props) => {
const selector = useMemo( const selector = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return { return {
color: rgbColorToString(layer.previewColor), color: rgbColorToString(layer.previewColor),
hasPositivePrompt: layer.positivePrompt !== null, hasPositivePrompt: layer.positivePrompt !== null,
hasNegativePrompt: layer.negativePrompt !== null, hasNegativePrompt: layer.negativePrompt !== null,
hasIPAdapters: layer.ipAdapters.length > 0, hasIPAdapters: layer.ipAdapters.length > 0,
isSelected: layerId === controlLayers.present.selectedLayerId, isSelected: layerId === canvasV2.selectedLayerId,
autoNegative: layer.autoNegative, autoNegative: layer.autoNegative,
}; };
}), }),

View File

@ -16,7 +16,7 @@ const useAutoNegative = (layerId: string) => {
const selectAutoNegative = useMemo( const selectAutoNegative = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return layer.autoNegative; return layer.autoNegative;
}), }),

View File

@ -20,7 +20,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => {
const selectColor = useMemo( const selectColor = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
return layer.previewColor; return layer.previewColor;
}), }),

View File

@ -15,7 +15,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
const selectIPAdapterIds = useMemo( const selectIPAdapterIds = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); const layer = canvasV2.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return layer.ipAdapters; return layer.ipAdapters;
}), }),

View File

@ -28,7 +28,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
const onDeleteIPAdapter = useCallback(() => { const onDeleteIPAdapter = useCallback(() => {
dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId }));
}, [dispatch, ipAdapterId, layerId]); }, [dispatch, ipAdapterId, layerId]);
const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.canvasV2, layerId, ipAdapterId));
const onChangeBeginEndStepPct = useCallback( const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => { (beginEndStepPct: [number, number]) => {

View File

@ -19,7 +19,7 @@ type Props = {
export const RasterLayer = memo(({ layerId }: Props) => { export const RasterLayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector( const isSelected = useAppSelector(
(s) => selectLayerOrThrow(s.controlLayers.present, layerId, isRasterLayer).isSelected (s) => selectLayerOrThrow(s.canvasV2, layerId, isRasterLayer).isSelected
); );
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));

View File

@ -5,51 +5,59 @@ import { logger } from 'app/logging/logger';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import { import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
BRUSH_SPACING_PCT,
MAX_BRUSH_SPACING_PX,
MIN_BRUSH_SPACING_PX,
TRANSPARENCY_CHECKER_PATTERN,
} from 'features/controlLayers/konva/constants';
import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
import { caBboxChanged, caTranslated } from 'features/controlLayers/store/controlAdaptersSlice';
import { import {
$bbox, $bbox,
$brushSpacingPx, $currentFill,
$brushWidth,
$fill,
$invertScroll,
$isDrawing, $isDrawing,
$isMouseDown, $isMouseDown,
$lastAddedPoint, $lastAddedPoint,
$lastCursorPos, $lastCursorPos,
$lastMouseDownPos, $lastMouseDownPos,
$selectedLayer, $selectedEntity,
$spaceKey, $spaceKey,
$stageAttrs, $stageAttrs,
$tool, $toolState,
$toolBuffer,
bboxChanged, bboxChanged,
brushLineAdded, brushWidthChanged,
brushSizeChanged, eraserWidthChanged,
eraserLineAdded,
layerBboxChanged,
layerTranslated,
linePointsAdded,
rectAdded,
selectCanvasV2Slice, selectCanvasV2Slice,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; import {
import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; layerBboxChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerLinePointAdded,
layerRectAdded,
layerTranslated,
selectLayersSlice,
} from 'features/controlLayers/store/layersSlice';
import {
rgBboxChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgLinePointAdded,
rgRectAdded,
rgTranslated,
selectRegionalGuidanceSlice,
} from 'features/controlLayers/store/regionalGuidanceSlice';
import type { import type {
AddBrushLineArg, BboxChangedArg,
AddEraserLineArg, BrushLineAddedArg,
AddPointToLineArg, CanvasEntity,
AddRectShapeArg, EraserLineAddedArg,
PointAddedToLineArg,
PosChangedArg,
RectShapeAddedArg,
Tool,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { clamp } from 'lodash-es';
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getImageDTO } from 'services/api/endpoints/images'; import { getImageDTO } from 'services/api/endpoints/images';
@ -61,12 +69,12 @@ Konva.showWarnings = false;
const log = logger('controlLayers'); const log = logger('controlLayers');
const selectBrushColor = createSelector( const selectBrushFill = createSelector(
selectCanvasV2Slice, selectCanvasV2Slice,
selectLayersSlice, selectLayersSlice,
selectRegionalGuidanceSlice, selectRegionalGuidanceSlice,
(canvas, layers, regionalGuidance) => { (canvas, layers, regionalGuidance) => {
const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id); const rg = regionalGuidance.regions.find((i) => i.id === canvas.selectedEntityIdentifier?.id);
if (rg) { if (rg) {
return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity });
@ -76,89 +84,121 @@ const selectBrushColor = createSelector(
} }
); );
const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => {
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
});
const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length);
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const state = useAppSelector((s) => s.controlLayers.present); const canvasV2State = useAppSelector(selectCanvasV2Slice);
const tool = useStore($tool); const layersState = useAppSelector((s) => s.layers);
const controlAdaptersState = useAppSelector((s) => s.controlAdaptersV2);
const ipAdaptersState = useAppSelector((s) => s.ipAdapters);
const regionalGuidanceState = useAppSelector((s) => s.regionalGuidance);
const lastCursorPos = useStore($lastCursorPos); const lastCursorPos = useStore($lastCursorPos);
const lastMouseDownPos = useStore($lastMouseDownPos); const lastMouseDownPos = useStore($lastMouseDownPos);
const isMouseDown = useStore($isMouseDown); const isMouseDown = useStore($isMouseDown);
const isDrawing = useStore($isDrawing); const isDrawing = useStore($isDrawing);
const brushColor = useAppSelector(selectBrushColor); const brushColor = useAppSelector(selectBrushFill);
const selectedLayer = useAppSelector(selectSelectedLayer); const selectedEntity = useMemo(() => {
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]); const identifier = canvasV2State.selectedEntityIdentifier;
const dpr = useDevicePixelRatio({ round: false }); if (!identifier) {
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); return null;
const brushSpacingPx = useMemo( } else if (identifier.type === 'layer') {
() => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), return layersState.layers.find((i) => i.id === identifier.id) ?? null;
[state.brushSize] } else if (identifier.type === 'control_adapter') {
); return controlAdaptersState.controlAdapters.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'ip_adapter') {
useLayoutEffect(() => { return ipAdaptersState.ipAdapters.find((i) => i.id === identifier.id) ?? null;
$fill.set(brushColor); } else if (identifier.type === 'regional_guidance') {
$brushWidth.set(state.brushSize); return regionalGuidanceState.regions.find((i) => i.id === identifier.id) ?? null;
$brushSpacingPx.set(brushSpacingPx); } else {
$selectedLayer.set(selectedLayer); return null;
$invertScroll.set(shouldInvertBrushSizeScrollDirection); }
$bbox.set(state.bbox);
}, [ }, [
brushSpacingPx, canvasV2State.selectedEntityIdentifier,
brushColor, controlAdaptersState.controlAdapters,
selectedLayer, ipAdaptersState.ipAdapters,
shouldInvertBrushSizeScrollDirection, layersState.layers,
state.brushSize, regionalGuidanceState.regions,
state.selectedLayerId,
state.brushColor,
state.bbox,
]); ]);
const onLayerPosChanged = useCallback( const currentFill = useMemo(() => {
(layerId: string, x: number, y: number) => { if (selectedEntity && selectedEntity.type === 'regional_guidance') {
dispatch(layerTranslated({ layerId, x, y })); return { ...selectedEntity.fill, a: regionalGuidanceState.opacity };
}
return canvasV2State.tool.fill;
}, [canvasV2State.tool.fill, regionalGuidanceState.opacity, selectedEntity]);
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
const dpr = useDevicePixelRatio({ round: false });
useLayoutEffect(() => {
$toolState.set(canvasV2State.tool);
$selectedEntity.set(selectedEntity);
$bbox.set(canvasV2State.bbox);
$currentFill.set(currentFill);
}, [selectedEntity, canvasV2State.tool, canvasV2State.bbox, currentFill]);
const onPosChanged = useCallback(
(arg: PosChangedArg, entityType: CanvasEntity['type']) => {
if (entityType === 'layer') {
dispatch(layerTranslated(arg));
} else if (entityType === 'control_adapter') {
dispatch(caTranslated(arg));
} else if (entityType === 'regional_guidance') {
dispatch(rgTranslated(arg));
}
}, },
[dispatch] [dispatch]
); );
const onBboxChanged = useCallback( const onBboxChanged = useCallback(
(layerId: string, bbox: IRect | null) => { (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
dispatch(layerBboxChanged({ layerId, bbox })); if (entityType === 'layer') {
dispatch(layerBboxChanged(arg));
} else if (entityType === 'control_adapter') {
dispatch(caBboxChanged(arg));
} else if (entityType === 'regional_guidance') {
dispatch(rgBboxChanged(arg));
}
}, },
[dispatch] [dispatch]
); );
const onBrushLineAdded = useCallback( const onBrushLineAdded = useCallback(
(arg: AddBrushLineArg) => { (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => {
dispatch(brushLineAdded(arg)); if (entityType === 'layer') {
dispatch(layerBrushLineAdded(arg));
} else if (entityType === 'regional_guidance') {
dispatch(rgBrushLineAdded(arg));
}
}, },
[dispatch] [dispatch]
); );
const onEraserLineAdded = useCallback( const onEraserLineAdded = useCallback(
(arg: AddEraserLineArg) => { (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => {
dispatch(eraserLineAdded(arg)); if (entityType === 'layer') {
dispatch(layerEraserLineAdded(arg));
} else if (entityType === 'regional_guidance') {
dispatch(rgEraserLineAdded(arg));
}
}, },
[dispatch] [dispatch]
); );
const onPointAddedToLine = useCallback( const onPointAddedToLine = useCallback(
(arg: AddPointToLineArg) => { (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => {
dispatch(linePointsAdded(arg)); if (entityType === 'layer') {
dispatch(layerLinePointAdded(arg));
} else if (entityType === 'regional_guidance') {
dispatch(rgLinePointAdded(arg));
}
}, },
[dispatch] [dispatch]
); );
const onRectShapeAdded = useCallback( const onRectShapeAdded = useCallback(
(arg: AddRectShapeArg) => { (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => {
dispatch(rectAdded(arg)); if (entityType === 'layer') {
}, dispatch(layerRectAdded(arg));
[dispatch] } else if (entityType === 'regional_guidance') {
); dispatch(rgRectAdded(arg));
const onBrushSizeChanged = useCallback( }
(size: number) => {
dispatch(brushSizeChanged(size));
}, },
[dispatch] [dispatch]
); );
@ -168,6 +208,30 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
}, },
[dispatch] [dispatch]
); );
const onBrushWidthChanged = useCallback(
(width: number) => {
dispatch(brushWidthChanged(width));
},
[dispatch]
);
const onEraserWidthChanged = useCallback(
(width: number) => {
dispatch(eraserWidthChanged(width));
},
[dispatch]
);
const setTool = useCallback(
(tool: Tool) => {
dispatch(toolChanged(tool));
},
[dispatch]
);
const setToolBuffer = useCallback(
(toolBuffer: Tool | null) => {
dispatch(toolBufferChanged(toolBuffer));
},
[dispatch]
);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Initializing stage'); log.trace('Initializing stage');
@ -189,32 +253,29 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
const cleanup = setStageEventHandlers({ const cleanup = setStageEventHandlers({
stage, stage,
getTool: $tool.get, getToolState: $toolState.get,
setTool: $tool.set, setTool,
getToolBuffer: $toolBuffer.get, setToolBuffer,
setToolBuffer: $toolBuffer.set,
getIsDrawing: $isDrawing.get, getIsDrawing: $isDrawing.get,
setIsDrawing: $isDrawing.set, setIsDrawing: $isDrawing.set,
getIsMouseDown: $isMouseDown.get, getIsMouseDown: $isMouseDown.get,
setIsMouseDown: $isMouseDown.set, setIsMouseDown: $isMouseDown.set,
getBrushColor: $fill.get, getSelectedEntity: $selectedEntity.get,
getBrushSize: $brushWidth.get,
getBrushSpacingPx: $brushSpacingPx.get,
getSelectedLayer: $selectedLayer.get,
getLastAddedPoint: $lastAddedPoint.get, getLastAddedPoint: $lastAddedPoint.get,
setLastAddedPoint: $lastAddedPoint.set, setLastAddedPoint: $lastAddedPoint.set,
getLastCursorPos: $lastCursorPos.get, getLastCursorPos: $lastCursorPos.get,
setLastCursorPos: $lastCursorPos.set, setLastCursorPos: $lastCursorPos.set,
getLastMouseDownPos: $lastMouseDownPos.get, getLastMouseDownPos: $lastMouseDownPos.get,
setLastMouseDownPos: $lastMouseDownPos.set, setLastMouseDownPos: $lastMouseDownPos.set,
getShouldInvert: $invertScroll.get,
getSpaceKey: $spaceKey.get, getSpaceKey: $spaceKey.get,
setStageAttrs: $stageAttrs.set, setStageAttrs: $stageAttrs.set,
onBrushSizeChanged,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
onBrushWidthChanged,
onEraserWidthChanged,
getCurrentFill: $currentFill.get,
}); });
return () => { return () => {
@ -224,12 +285,15 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
}, [ }, [
asPreview, asPreview,
onBrushLineAdded, onBrushLineAdded,
onBrushSizeChanged, onBrushWidthChanged,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
stage, stage,
container, container,
onEraserWidthChanged,
setTool,
setToolBuffer,
]); ]);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -267,29 +331,26 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
log.trace('Rendering tool preview'); log.trace('Rendering tool preview');
renderers.renderToolPreview( renderers.renderToolPreview(
stage, stage,
tool, canvasV2State.tool,
brushColor, currentFill,
selectedLayer?.type ?? null, selectedEntity,
state.globalMaskLayerOpacity,
lastCursorPos, lastCursorPos,
lastMouseDownPos, lastMouseDownPos,
state.brushSize,
isDrawing, isDrawing,
isMouseDown isMouseDown
); );
}, [ }, [
asPreview, asPreview,
brushColor, brushColor,
canvasV2State.tool,
currentFill,
isDrawing, isDrawing,
isMouseDown, isMouseDown,
lastCursorPos, lastCursorPos,
lastMouseDownPos, lastMouseDownPos,
renderers, renderers,
selectedLayer?.type, selectedEntity,
stage, stage,
state.brushSize,
state.globalMaskLayerOpacity,
tool,
]); ]);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -300,8 +361,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
log.trace('Rendering bbox preview'); log.trace('Rendering bbox preview');
renderers.renderBboxPreview( renderers.renderBboxPreview(
stage, stage,
state.bbox, canvasV2State.bbox,
tool, canvasV2State.tool.selected,
$bbox.get, $bbox.get,
onBboxTransformed, onBboxTransformed,
$shift.get, $shift.get,
@ -309,21 +370,41 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
$meta.get, $meta.get,
$alt.get $alt.get
); );
}, [asPreview, onBboxTransformed, renderers, stage, state.bbox, tool]); }, [asPreview, canvasV2State.bbox, canvasV2State.tool.selected, onBboxTransformed, renderers, stage]);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Rendering layers'); log.trace('Rendering layers');
renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged); renderers.renderLayers(
}, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]); stage,
layersState.layers,
controlAdaptersState.controlAdapters,
regionalGuidanceState.regions,
regionalGuidanceState.opacity,
canvasV2State.tool.selected,
selectedEntity,
getImageDTO,
onPosChanged
);
}, [
stage,
renderers,
layersState.layers,
controlAdaptersState.controlAdapters,
regionalGuidanceState.regions,
regionalGuidanceState.opacity,
onPosChanged,
canvasV2State.tool.selected,
selectedEntity,
]);
useLayoutEffect(() => { // useLayoutEffect(() => {
if (asPreview) { // if (asPreview) {
// Preview should not check for transparency // // Preview should not check for transparency
return; // return;
} // }
log.trace('Updating bboxes'); // log.trace('Updating bboxes');
debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); // debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged);
}, [stage, asPreview, state.layers, onBboxChanged]); // }, [stage, asPreview, state.layers, onBboxChanged]);
useLayoutEffect(() => { useLayoutEffect(() => {
Konva.pixelRatio = dpr; Konva.pixelRatio = dpr;
@ -395,7 +476,7 @@ StageComponent.displayName = 'StageComponent';
const NoLayersFallback = () => { const NoLayersFallback = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const layerCount = useAppSelector(selectLayerCount); const layerCount = useAppSelector((s) => s.layers.layers.length);
if (layerCount) { if (layerCount) {
return null; return null;
} }

View File

@ -21,7 +21,7 @@ import {
} from 'react-icons/pi'; } from 'react-icons/pi';
const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => {
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); const selectedLayer = canvasV2.layers.find((l) => l.id === canvasV2.selectedLayerId);
return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer';
}); });
@ -29,7 +29,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isDisabled = useAppSelector(selectIsDisabled); const isDisabled = useAppSelector(selectIsDisabled);
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId);
const tool = useStore($tool); const tool = useStore($tool);
const setToolToBrush = useCallback(() => { const setToolToBrush = useCallback(() => {

View File

@ -102,7 +102,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
export const useAddIILayer = () => { export const useAddIILayer = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); const isDisabled = useAppSelector((s) => Boolean(s.canvasV2.layers.find(isInitialImageLayer)));
const addIILayer = useCallback(() => { const addIILayer = useCallback(() => {
dispatch(iiLayerAdded(null)); dispatch(iiLayerAdded(null));
}, [dispatch]); }, [dispatch]);

View File

@ -10,7 +10,7 @@ export const useLayerPositivePrompt = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`);
return layer.positivePrompt; return layer.positivePrompt;
@ -25,7 +25,7 @@ export const useLayerNegativePrompt = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`);
return layer.negativePrompt; return layer.negativePrompt;
@ -40,7 +40,7 @@ export const useLayerIsEnabled = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return layer.isEnabled; return layer.isEnabled;
}), }),
@ -54,7 +54,7 @@ export const useLayerType = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectCanvasV2Slice, (controlLayers) => { createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId); const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return layer.type; return layer.type;
}), }),
@ -68,7 +68,7 @@ export const useCALayerOpacity = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); const layer = canvasV2.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };
}), }),

View File

@ -2,27 +2,27 @@ import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util'; import { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type { import type {
AddBrushLineArg, BrushLineAddedArg,
AddEraserLineArg, CanvasEntity,
AddPointToLineArg, CanvasV2State,
AddRectShapeArg, EraserLineAddedArg,
LayerData, PointAddedToLineArg,
RectShapeAddedArg,
RgbaColor,
StageAttrs, StageAttrs,
Tool, Tool,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
import { PREVIEW_TOOL_GROUP_ID } from './naming'; import { PREVIEW_TOOL_GROUP_ID } from './naming';
type Arg = { type Arg = {
stage: Konva.Stage; stage: Konva.Stage;
getTool: () => Tool; getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
setTool: (tool: Tool) => void; setTool: (tool: Tool) => void;
getToolBuffer: () => Tool | null;
setToolBuffer: (tool: Tool | null) => void; setToolBuffer: (tool: Tool | null) => void;
getIsDrawing: () => boolean; getIsDrawing: () => boolean;
setIsDrawing: (isDrawing: boolean) => void; setIsDrawing: (isDrawing: boolean) => void;
@ -35,17 +35,14 @@ type Arg = {
getLastAddedPoint: () => Vector2d | null; getLastAddedPoint: () => Vector2d | null;
setLastAddedPoint: (pos: Vector2d | null) => void; setLastAddedPoint: (pos: Vector2d | null) => void;
setStageAttrs: (attrs: StageAttrs) => void; setStageAttrs: (attrs: StageAttrs) => void;
getBrushColor: () => RgbaColor; getSelectedEntity: () => CanvasEntity | null;
getBrushSize: () => number;
getBrushSpacingPx: () => number;
getSelectedLayer: () => LayerData | null;
getShouldInvert: () => boolean;
getSpaceKey: () => boolean; getSpaceKey: () => boolean;
onBrushLineAdded: (arg: AddBrushLineArg) => void; onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
onEraserLineAdded: (arg: AddEraserLineArg) => void; onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
onPointAddedToLine: (arg: AddPointToLineArg) => void; onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
onRectShapeAdded: (arg: AddRectShapeArg) => void; onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => void;
onBrushSizeChanged: (size: number) => void; onBrushWidthChanged: (size: number) => void;
onEraserWidthChanged: (size: number) => void;
}; };
/** /**
@ -72,30 +69,41 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC
* @param onPointAddedToLine The callback to add a point to a line * @param onPointAddedToLine The callback to add a point to a line
*/ */
const maybeAddNextPoint = ( const maybeAddNextPoint = (
selectedLayer: LayerData, selectedEntity: CanvasEntity,
currentPos: Vector2d, currentPos: Vector2d,
getToolState: Arg['getToolState'],
getLastAddedPoint: Arg['getLastAddedPoint'], getLastAddedPoint: Arg['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'],
getBrushSpacingPx: Arg['getBrushSpacingPx'],
onPointAddedToLine: Arg['onPointAddedToLine'] onPointAddedToLine: Arg['onPointAddedToLine']
) => { ) => {
if (selectedEntity.type !== 'layer' && selectedEntity.type !== 'regional_guidance') {
return;
}
// Continue the last line // Continue the last line
const lastAddedPoint = getLastAddedPoint(); const lastAddedPoint = getLastAddedPoint();
const toolState = getToolState();
const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * 0.05 : toolState.eraser.width * 0.05;
if (lastAddedPoint) { if (lastAddedPoint) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < getBrushSpacingPx()) { if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) {
return; return;
} }
} }
setLastAddedPoint(currentPos); setLastAddedPoint(currentPos);
onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] }); onPointAddedToLine(
{
id: selectedEntity.id,
point: [currentPos.x - selectedEntity.x, currentPos.y - selectedEntity.y],
},
selectedEntity.type
);
}; };
export const setStageEventHandlers = ({ export const setStageEventHandlers = ({
stage, stage,
getTool, getToolState,
getCurrentFill,
setTool, setTool,
getToolBuffer,
setToolBuffer, setToolBuffer,
getIsDrawing, getIsDrawing,
setIsDrawing, setIsDrawing,
@ -108,17 +116,14 @@ export const setStageEventHandlers = ({
getLastAddedPoint, getLastAddedPoint,
setLastAddedPoint, setLastAddedPoint,
setStageAttrs, setStageAttrs,
getBrushColor, getSelectedEntity,
getBrushSize,
getBrushSpacingPx,
getSelectedLayer,
getShouldInvert,
getSpaceKey, getSpaceKey,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
onBrushSizeChanged, onBrushWidthChanged: onBrushSizeChanged,
onEraserWidthChanged: onEraserSizeChanged,
}: Arg): (() => void) => { }: Arg): (() => void) => {
//#region mouseenter //#region mouseenter
stage.on('mouseenter', (e) => { stage.on('mouseenter', (e) => {
@ -126,7 +131,7 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
const tool = getTool(); const tool = getToolState().selected;
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
}); });
@ -137,13 +142,13 @@ export const setStageEventHandlers = ({
return; return;
} }
setIsMouseDown(true); setIsMouseDown(true);
const tool = getTool(); const toolState = getToolState();
const pos = updateLastCursorPos(stage, setLastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedLayer = getSelectedLayer(); const selectedEntity = getSelectedEntity();
if (!pos || !selectedLayer) { if (!pos || !selectedEntity) {
return; return;
} }
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return; return;
} }
@ -155,23 +160,37 @@ export const setStageEventHandlers = ({
setIsDrawing(true); setIsDrawing(true);
setLastMouseDownPos(pos); setLastMouseDownPos(pos);
if (tool === 'brush') { if (toolState.selected === 'brush') {
onBrushLineAdded({ onBrushLineAdded(
layerId: selectedLayer.id, {
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], id: selectedEntity.id,
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, points: [
}); pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
],
color: getCurrentFill(),
width: toolState.brush.width,
},
selectedEntity.type
);
} }
if (tool === 'eraser') { if (toolState.selected === 'eraser') {
onEraserLineAdded({ onEraserLineAdded(
layerId: selectedLayer.id, {
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], id: selectedEntity.id,
}); points: [
} pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
if (tool === 'rect') { pos.x - selectedEntity.x,
// Setting the last mouse down pos starts a rect pos.y - selectedEntity.y,
],
width: toolState.eraser.width,
},
selectedEntity.type
);
} }
}); });
@ -183,12 +202,12 @@ export const setStageEventHandlers = ({
} }
setIsMouseDown(false); setIsMouseDown(false);
const pos = getLastCursorPos(); const pos = getLastCursorPos();
const selectedLayer = getSelectedLayer(); const selectedEntity = getSelectedEntity();
if (!pos || !selectedLayer) { if (!pos || !selectedEntity) {
return; return;
} }
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return; return;
} }
@ -197,21 +216,24 @@ export const setStageEventHandlers = ({
return; return;
} }
const tool = getTool(); const toolState = getToolState();
if (tool === 'rect') { if (toolState.selected === 'rect') {
const lastMouseDownPos = getLastMouseDownPos(); const lastMouseDownPos = getLastMouseDownPos();
if (lastMouseDownPos) { if (lastMouseDownPos) {
onRectShapeAdded({ onRectShapeAdded(
layerId: selectedLayer.id, {
rect: { id: selectedEntity.id,
x: Math.min(pos.x, lastMouseDownPos.x), rect: {
y: Math.min(pos.y, lastMouseDownPos.y), x: Math.min(pos.x, lastMouseDownPos.x),
width: Math.abs(pos.x - lastMouseDownPos.x), y: Math.min(pos.y, lastMouseDownPos.y),
height: Math.abs(pos.y - lastMouseDownPos.y), width: Math.abs(pos.x - lastMouseDownPos.x),
height: Math.abs(pos.y - lastMouseDownPos.y),
},
color: getCurrentFill(),
}, },
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, selectedEntity.type
}); );
} }
} }
@ -225,16 +247,18 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
const tool = getTool(); const toolState = getToolState();
const pos = updateLastCursorPos(stage, setLastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedLayer = getSelectedLayer(); const selectedEntity = getSelectedEntity();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); stage
.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)
?.visible(toolState.selected === 'brush' || toolState.selected === 'eraser');
if (!pos || !selectedLayer) { if (!pos || !selectedEntity) {
return; return;
} }
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return; return;
} }
@ -247,45 +271,49 @@ export const setStageEventHandlers = ({
return; return;
} }
if (tool === 'brush') { if (toolState.selected === 'brush') {
if (getIsDrawing()) { if (getIsDrawing()) {
// Continue the last line // Continue the last line
maybeAddNextPoint( maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
} else { } else {
// Start a new line // Start a new line
onBrushLineAdded({ onBrushLineAdded(
layerId: selectedLayer.id, {
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], id: selectedEntity.id,
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, points: [
}); pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
],
width: toolState.brush.width,
color: getCurrentFill(),
},
selectedEntity.type
);
setIsDrawing(true); setIsDrawing(true);
} }
} }
if (tool === 'eraser') { if (toolState.selected === 'eraser') {
if (getIsDrawing()) { if (getIsDrawing()) {
// Continue the last line // Continue the last line
maybeAddNextPoint( maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
} else { } else {
// Start a new line // Start a new line
onEraserLineAdded({ onEraserLineAdded(
layerId: selectedLayer.id, {
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], id: selectedEntity.id,
}); points: [
pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
pos.x - selectedEntity.x,
pos.y - selectedEntity.y,
],
width: toolState.eraser.width,
},
selectedEntity.type
);
setIsDrawing(true); setIsDrawing(true);
} }
} }
@ -301,15 +329,15 @@ export const setStageEventHandlers = ({
setIsDrawing(false); setIsDrawing(false);
setLastCursorPos(null); setLastCursorPos(null);
setLastMouseDownPos(null); setLastMouseDownPos(null);
const selectedLayer = getSelectedLayer(); const selectedEntity = getSelectedEntity();
const tool = getTool(); const toolState = getToolState();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
if (!pos || !selectedLayer) { if (!pos || !selectedEntity) {
return; return;
} }
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return; return;
} }
if (getSpaceKey()) { if (getSpaceKey()) {
@ -317,12 +345,11 @@ export const setStageEventHandlers = ({
return; return;
} }
if (getIsMouseDown()) { if (getIsMouseDown()) {
if (tool === 'brush') { if (toolState.selected === 'brush') {
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type);
} }
if (toolState.selected === 'eraser') {
if (tool === 'eraser') { onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type);
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
} }
} }
}); });
@ -331,12 +358,17 @@ export const setStageEventHandlers = ({
e.evt.preventDefault(); e.evt.preventDefault();
if (e.evt.ctrlKey || e.evt.metaKey) { if (e.evt.ctrlKey || e.evt.metaKey) {
const toolState = getToolState();
let delta = e.evt.deltaY; let delta = e.evt.deltaY;
if (getShouldInvert()) { if (toolState.invertScroll) {
delta = -delta; delta = -delta;
} }
// Holding ctrl or meta while scrolling changes the brush size // Holding ctrl or meta while scrolling changes the brush size
onBrushSizeChanged(calculateNewBrushSize(getBrushSize(), delta)); if (toolState.selected === 'brush') {
onBrushSizeChanged(calculateNewBrushSize(toolState.brush.width, delta));
} else if (toolState.selected === 'eraser') {
onEraserSizeChanged(calculateNewBrushSize(toolState.eraser.width, delta));
}
} else { } else {
// We need the absolute cursor position - not the scaled position // We need the absolute cursor position - not the scaled position
const cursorPos = stage.getPointerPosition(); const cursorPos = stage.getPointerPosition();
@ -396,7 +428,7 @@ export const setStageEventHandlers = ({
setIsDrawing(false); setIsDrawing(false);
setLastMouseDownPos(null); setLastMouseDownPos(null);
} else if (e.key === ' ') { } else if (e.key === ' ') {
setToolBuffer(getTool()); setToolBuffer(getToolState().selected);
setTool('view'); setTool('view');
} }
}; };
@ -408,7 +440,7 @@ export const setStageEventHandlers = ({
return; return;
} }
if (e.key === ' ') { if (e.key === ' ') {
const toolBuffer = getToolBuffer(); const toolBuffer = getToolState().selectedBuffer;
setTool(toolBuffer ?? 'move'); setTool(toolBuffer ?? 'move');
setToolBuffer(null); setToolBuffer(null);
} }

View File

@ -1,6 +1,6 @@
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming'; import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId } from 'features/controlLayers/konva/naming';
import type { ControlAdapterLayer } from 'features/controlLayers/store/types'; import type { ControlAdapterData } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
@ -12,11 +12,11 @@ import type { ImageDTO } from 'services/api/types';
/** /**
* Creates a control adapter layer. * Creates a control adapter layer.
* @param stage The konva stage * @param stage The konva stage
* @param layerState The control adapter layer state * @param ca The control adapter layer state
*/ */
const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Konva.Layer => { const createCALayer = (stage: Konva.Stage, ca: ControlAdapterData): Konva.Layer => {
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: layerState.id, id: ca.id,
name: CA_LAYER_NAME, name: CA_LAYER_NAME,
imageSmoothingEnabled: false, imageSmoothingEnabled: false,
listening: false, listening: false,
@ -44,16 +44,16 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement):
* the konva image. * the konva image.
* @param stage The konva stage * @param stage The konva stage
* @param konvaLayer The konva layer * @param konvaLayer The konva layer
* @param layerState The control adapter layer state * @param ca The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/ */
const updateCALayerImageSource = async ( const updateCALayerImageSource = async (
stage: Konva.Stage, stage: Konva.Stage,
konvaLayer: Konva.Layer, konvaLayer: Konva.Layer,
layerState: ControlAdapterLayer, ca: ControlAdapterData,
getImageDTO: (imageName: string) => Promise<ImageDTO | null> getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): Promise<void> => { ): Promise<void> => {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; const image = ca.processedImage ?? ca.image;
if (image) { if (image) {
const imageName = image.name; const imageName = image.name;
const imageDTO = await getImageDTO(imageName); const imageDTO = await getImageDTO(imageName);
@ -61,7 +61,7 @@ const updateCALayerImageSource = async (
return; return;
} }
const imageEl = new Image(); const imageEl = new Image();
const imageId = getCALayerImageId(layerState.id, imageName); const imageId = getCALayerImageId(ca.id, imageName);
imageEl.onload = () => { imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed // Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage = const konvaImage =
@ -72,7 +72,7 @@ const updateCALayerImageSource = async (
id: imageId, id: imageId,
image: imageEl, image: imageEl,
}); });
updateCALayerImageAttrs(stage, konvaImage, layerState); updateCALayerImageAttrs(stage, konvaImage, ca);
// Must cache after this to apply the filters // Must cache after this to apply the filters
konvaImage.cache(); konvaImage.cache();
imageEl.id = imageId; imageEl.id = imageId;
@ -87,36 +87,33 @@ const updateCALayerImageSource = async (
* Updates the image attributes for a control adapter layer's image (width, height, visibility, opacity, filters). * Updates the image attributes for a control adapter layer's image (width, height, visibility, opacity, filters).
* @param stage The konva stage * @param stage The konva stage
* @param konvaImage The konva image * @param konvaImage The konva image
* @param layerState The control adapter layer state * @param ca The control adapter layer state
*/ */
const updateCALayerImageAttrs = ( const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => {
stage: Konva.Stage,
konvaImage: Konva.Image,
layerState: ControlAdapterLayer
): void => {
let needsCache = false; let needsCache = false;
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
// but it doesn't seem to break anything. // but it doesn't seem to break anything.
// TODO(psyche): Investigate and report upstream. // TODO(psyche): Investigate and report upstream.
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; const filter = konvaImage.filters()[0] ?? null;
const filterNeedsUpdate = (filter === null && ca.filter !== 'none') || (filter && filter.name !== ca.filter);
if ( if (
konvaImage.x() !== layerState.x || konvaImage.x() !== ca.x ||
konvaImage.y() !== layerState.y || konvaImage.y() !== ca.y ||
konvaImage.visible() !== layerState.isEnabled || konvaImage.visible() !== ca.isEnabled ||
hasFilter !== layerState.isFilterEnabled filterNeedsUpdate
) { ) {
konvaImage.setAttrs({ konvaImage.setAttrs({
opacity: layerState.opacity, opacity: ca.opacity,
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
visible: layerState.isEnabled, visible: ca.isEnabled,
filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [], filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [],
}); });
needsCache = true; needsCache = true;
} }
if (konvaImage.opacity() !== layerState.opacity) { if (konvaImage.opacity() !== ca.opacity) {
konvaImage.opacity(layerState.opacity); konvaImage.opacity(ca.opacity);
} }
if (needsCache) { if (needsCache) {
konvaImage.cache(); konvaImage.cache();
@ -127,16 +124,16 @@ const updateCALayerImageAttrs = (
* Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated * Renders a control adapter layer. If the layer doesn't already exist, it is created. Otherwise, the layer is updated
* with the current image source and attributes. * with the current image source and attributes.
* @param stage The konva stage * @param stage The konva stage
* @param layerState The control adapter layer state * @param ca The control adapter layer state
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
*/ */
export const renderCALayer = ( export const renderCALayer = (
stage: Konva.Stage, stage: Konva.Stage,
layerState: ControlAdapterLayer, ca: ControlAdapterData,
zIndex: number, zIndex: number,
getImageDTO: (imageName: string) => Promise<ImageDTO | null> getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): void => { ): void => {
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createCALayer(stage, layerState); const konvaLayer = stage.findOne<Konva.Layer>(`#${ca.id}`) ?? createCALayer(stage, ca);
konvaLayer.zIndex(zIndex); konvaLayer.zIndex(zIndex);
@ -146,8 +143,8 @@ export const renderCALayer = (
let imageSourceNeedsUpdate = false; let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) { if (canvasImageSource instanceof HTMLImageElement) {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image; const image = ca.processedImage ?? ca.image;
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) { if (image && canvasImageSource.id !== getCALayerImageId(ca.id, image.name)) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
} else if (!image) { } else if (!image) {
imageSourceNeedsUpdate = true; imageSourceNeedsUpdate = true;
@ -157,8 +154,8 @@ export const renderCALayer = (
} }
if (imageSourceNeedsUpdate) { if (imageSourceNeedsUpdate) {
updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO); updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO);
} else if (konvaImage) { } else if (konvaImage) {
updateCALayerImageAttrs(stage, konvaImage, layerState); updateCALayerImageAttrs(stage, konvaImage, ca);
} }
}; };

View File

@ -2,19 +2,17 @@ import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants';
import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming'; import { PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox'; import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer'; import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer';
import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer'; import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/konva/renderers/previewLayer';
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
import type { LayerData, Tool } from 'features/controlLayers/store/types'; import type {
import { CanvasEntity,
isControlAdapterLayer, ControlAdapterData,
isInitialImageLayer, LayerData,
isInpaintMaskLayer, PosChangedArg,
isRasterLayer, RegionalGuidanceData,
isRegionalGuidanceLayer, Tool,
isRenderableLayer,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
@ -27,43 +25,42 @@ import type { ImageDTO } from 'services/api/types';
/** /**
* Renders the layers on the stage. * Renders the layers on the stage.
* @param stage The konva stage * @param stage The konva stage
* @param layerStates Array of all layer states * @param layers Array of all layer states
* @param globalMaskLayerOpacity The global mask layer opacity * @param rgGlobalOpacity The global mask layer opacity
* @param tool The current tool * @param tool The current tool
* @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source * @param getImageDTO A function to retrieve an image DTO from the server, used to update the image source
* @param onLayerPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
*/ */
const renderLayers = ( const renderLayers = (
stage: Konva.Stage, stage: Konva.Stage,
layerStates: LayerData[], layers: LayerData[],
globalMaskLayerOpacity: number, controlAdapters: ControlAdapterData[],
regions: RegionalGuidanceData[],
rgGlobalOpacity: number,
tool: Tool, tool: Tool,
selectedEntity: CanvasEntity | null,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>, getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => { ): void => {
const layerIds = layerStates.filter(isRenderableLayer).map(mapId); const renderableIds = [...layers.map(mapId), ...controlAdapters.map(mapId), ...regions.map(mapId)];
// Remove un-rendered layers // Remove un-rendered layers
for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) { for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) {
if (!layerIds.includes(konvaLayer.id())) { if (!renderableIds.includes(konvaLayer.id())) {
konvaLayer.destroy(); konvaLayer.destroy();
} }
} }
// We'll need to ensure the tool preview layer is on top of the rest of the layers // We'll need to ensure the tool preview layer is on top of the rest of the layers
let zIndex = 0; let zIndex = 0;
for (const layer of layerStates) { for (const layer of layers) {
if (isRegionalGuidanceLayer(layer)) { renderRasterLayer(stage, layer, tool, zIndex, onPosChanged);
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); zIndex++;
} else if (isControlAdapterLayer(layer)) { }
renderCALayer(stage, layer, zIndex, getImageDTO); for (const ca of controlAdapters) {
} else if (isInitialImageLayer(layer)) { renderCALayer(stage, ca, zIndex, getImageDTO);
renderIILayer(stage, layer, zIndex, getImageDTO); zIndex++;
} else if (isRasterLayer(layer)) { }
renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); for (const rg of regions) {
} else if (isInpaintMaskLayer(layer)) { renderRGLayer(stage, rg, rgGlobalOpacity, tool, zIndex, selectedEntity, onPosChanged);
//
}
// IP Adapter layers are not rendered
// Increment the z-index for the tool layer
zIndex++; zIndex++;
} }
// Arrange the tool preview layer // Arrange the tool preview layer

View File

@ -5,7 +5,13 @@ import {
LAYER_BBOX_NAME, LAYER_BBOX_NAME,
PREVIEW_GENERATION_BBOX_DUMMY_RECT, PREVIEW_GENERATION_BBOX_DUMMY_RECT,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types'; import type {
BrushLine,
CanvasEntity,
EraserLine,
ImageObject,
RectShape,
} from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import { t } from 'i18next'; import { t } from 'i18next';
import Konva from 'konva'; import Konva from 'konva';
@ -174,12 +180,12 @@ export const createImageObjectGroup = async (
/** /**
* Creates a bounding box rect for a layer. * Creates a bounding box rect for a layer.
* @param layerState The layer state for the layer to create the bounding box for * @param entity The layer state for the layer to create the bounding box for
* @param konvaLayer The konva layer to attach the bounding box to * @param konvaLayer The konva layer to attach the bounding box to
*/ */
export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => { export const createBboxRect = (entity: CanvasEntity, konvaLayer: Konva.Layer): Konva.Rect => {
const rect = new Konva.Rect({ const rect = new Konva.Rect({
id: getLayerBboxId(layerState.id), id: getLayerBboxId(entity.id),
name: LAYER_BBOX_NAME, name: LAYER_BBOX_NAME,
strokeWidth: 1, strokeWidth: 1,
visible: false, visible: false,

View File

@ -18,7 +18,7 @@ import {
PREVIEW_TOOL_GROUP_ID, PREVIEW_TOOL_GROUP_ID,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { selectRenderableLayers } from 'features/controlLayers/konva/util'; import { selectRenderableLayers } from 'features/controlLayers/konva/util';
import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, CanvasV2State, RgbaColor, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
@ -327,8 +327,8 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
* Renders the preview layer. * Renders the preview layer.
* @param stage The konva stage * @param stage The konva stage
* @param tool The selected tool * @param tool The selected tool
* @param color The selected layer's color * @param currentFill The selected layer's color
* @param selectedLayerType The selected layer's type * @param selectedEntity The selected layer's type
* @param globalMaskLayerOpacity The global mask layer opacity * @param globalMaskLayerOpacity The global mask layer opacity
* @param cursorPos The cursor position * @param cursorPos The cursor position
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool * @param lastMouseDownPos The position of the last mouse down event - used for the rect tool
@ -336,17 +336,16 @@ export const getToolPreviewGroup = (stage: Konva.Stage): Konva.Group => {
*/ */
export const renderToolPreview = ( export const renderToolPreview = (
stage: Konva.Stage, stage: Konva.Stage,
tool: Tool, toolState: CanvasV2State['tool'],
brushColor: RgbaColor, currentFill: RgbaColor,
selectedLayerType: LayerData['type'] | null, selectedEntity: CanvasEntity | null,
globalMaskLayerOpacity: number,
cursorPos: Vector2d | null, cursorPos: Vector2d | null,
lastMouseDownPos: Vector2d | null, lastMouseDownPos: Vector2d | null,
brushSize: number,
isDrawing: boolean, isDrawing: boolean,
isMouseDown: boolean isMouseDown: boolean
): void => { ): void => {
const layerCount = stage.find(selectRenderableLayers).length; const layerCount = stage.find(selectRenderableLayers).length;
const tool = toolState.selected;
// Update the stage's pointer style // Update the stage's pointer style
if (tool === 'view') { if (tool === 'view') {
// View gets a hand // View gets a hand
@ -354,7 +353,7 @@ export const renderToolPreview = (
} else if (layerCount === 0) { } else if (layerCount === 0) {
// We have no layers, so we should not render any tool // We have no layers, so we should not render any tool
stage.container().style.cursor = 'default'; stage.container().style.cursor = 'default';
} else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') { } else if (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer') {
// Non-mask-guidance layers don't have tools // Non-mask-guidance layers don't have tools
stage.container().style.cursor = 'not-allowed'; stage.container().style.cursor = 'not-allowed';
} else if (tool === 'move') { } else if (tool === 'move') {
@ -377,7 +376,7 @@ export const renderToolPreview = (
if ( if (
!cursorPos || !cursorPos ||
layerCount === 0 || layerCount === 0 ||
(selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') (selectedEntity?.type !== 'regional_guidance' && selectedEntity?.type !== 'layer')
) { ) {
// We can bail early if the mouse isn't over the stage or there are no layers // We can bail early if the mouse isn't over the stage or there are no layers
toolPreviewGroup.visible(false); toolPreviewGroup.visible(false);
@ -394,24 +393,25 @@ export const renderToolPreview = (
if (cursorPos && (tool === 'brush' || tool === 'eraser')) { if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
// Update the fill circle // Update the fill circle
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_FILL_ID}`); const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_FILL_ID}`);
const radius = (tool === 'brush' ? toolState.brush.width : toolState.eraser.width) / 2;
brushPreviewFill?.setAttrs({ brushPreviewFill?.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius: brushSize / 2, radius,
fill: isDrawing ? '' : rgbaColorToString(brushColor), fill: isDrawing ? '' : rgbaColorToString(currentFill),
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out', globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
}); });
// Update the inner border of the brush preview // Update the inner border of the brush preview
const brushPreviewInner = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`); const brushPreviewInner = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`);
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 }); brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius });
// Update the outer border of the brush preview // Update the outer border of the brush preview
const brushPreviewOuter = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`); const brushPreviewOuter = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`);
brushPreviewOuter?.setAttrs({ brushPreviewOuter?.setAttrs({
x: cursorPos.x, x: cursorPos.x,
y: cursorPos.y, y: cursorPos.y,
radius: brushSize / 2 + 1, radius: radius + 1,
}); });
brushPreviewGroup.visible(true); brushPreviewGroup.visible(true);
@ -426,7 +426,7 @@ export const renderToolPreview = (
y: Math.min(cursorPos.y, lastMouseDownPos.y), y: Math.min(cursorPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x), width: Math.abs(cursorPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y), height: Math.abs(cursorPos.y - lastMouseDownPos.y),
fill: rgbaColorToString(brushColor), fill: rgbaColorToString(currentFill),
}); });
rectPreview?.visible(true); rectPreview?.visible(true);
} else { } else {

View File

@ -1,6 +1,4 @@
import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
import { import {
LAYER_BBOX_NAME,
RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME, RASTER_LAYER_IMAGE_NAME,
@ -9,7 +7,6 @@ import {
RASTER_LAYER_RECT_SHAPE_NAME, RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming'; } from 'features/controlLayers/konva/naming';
import { import {
createBboxRect,
createBrushLine, createBrushLine,
createEraserLine, createEraserLine,
createImageObjectGroup, createImageObjectGroup,
@ -17,7 +14,7 @@ import {
createRectShape, createRectShape,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util'; import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util';
import type { RasterLayer, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, LayerData, PosChangedArg, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
/** /**
@ -28,12 +25,12 @@ import Konva from 'konva';
* Creates a raster layer. * Creates a raster layer.
* @param stage The konva stage * @param stage The konva stage
* @param layerState The raster layer state * @param layerState The raster layer state
* @param onLayerPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
*/ */
const createRasterLayer = ( const createRasterLayer = (
stage: Konva.Stage, stage: Konva.Stage,
layerState: RasterLayer, layerState: LayerData,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): Konva.Layer => { ): Konva.Layer => {
// This layer hasn't been added to the konva state yet // This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
@ -45,9 +42,9 @@ const createRasterLayer = (
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
// the position - we do not need to call this on the `dragmove` event. // the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) { if (onPosChanged) {
konvaLayer.on('dragend', function (e) { konvaLayer.on('dragend', function (e) {
onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); onPosChanged({ id: layerState.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer');
}); });
} }
@ -61,17 +58,17 @@ const createRasterLayer = (
* @param stage The konva stage * @param stage The konva stage
* @param layerState The regional guidance layer state * @param layerState The regional guidance layer state
* @param tool The current tool * @param tool The current tool
* @param onLayerPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
*/ */
export const renderRasterLayer = async ( export const renderRasterLayer = async (
stage: Konva.Stage, stage: Konva.Stage,
layerState: RasterLayer, layerState: LayerData,
tool: Tool, tool: Tool,
zIndex: number, zIndex: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
) => { ) => {
const konvaLayer = const konvaLayer =
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onLayerPosChanged); stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRasterLayer(stage, layerState, onPosChanged);
// Update the layer's position and listening state // Update the layer's position and listening state
konvaLayer.setAttrs({ konvaLayer.setAttrs({
@ -128,23 +125,23 @@ export const renderRasterLayer = async (
konvaLayer.visible(layerState.isEnabled); konvaLayer.visible(layerState.isEnabled);
} }
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); // const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
if (layerState.bbox) { // if (layerState.bbox) {
const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; // const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
bboxRect.setAttrs({ // bboxRect.setAttrs({
visible: active, // visible: active,
listening: active, // listening: active,
x: layerState.bbox.x, // x: layerState.bbox.x,
y: layerState.bbox.y, // y: layerState.bbox.y,
width: layerState.bbox.width, // width: layerState.bbox.width,
height: layerState.bbox.height, // height: layerState.bbox.height,
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', // stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
strokeWidth: 1 / stage.scaleX(), // strokeWidth: 1 / stage.scaleX(),
}); // });
} else { // } else {
bboxRect.visible(false); // bboxRect.visible(false);
} // }
konvaObjectGroup.opacity(layerState.opacity); konvaObjectGroup.opacity(layerState.opacity);
}; };

View File

@ -18,7 +18,7 @@ import {
createRectShape, createRectShape,
} from 'features/controlLayers/konva/renderers/objects'; } from 'features/controlLayers/konva/renderers/objects';
import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util'; import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types'; import type { CanvasEntity, PosChangedArg, RegionalGuidanceData, Tool } from 'features/controlLayers/store/types';
import Konva from 'konva'; import Konva from 'konva';
/** /**
@ -41,17 +41,17 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
/** /**
* Creates a regional guidance layer. * Creates a regional guidance layer.
* @param stage The konva stage * @param stage The konva stage
* @param layerState The regional guidance layer state * @param rg The regional guidance layer state
* @param onLayerPosChanged Callback for when the layer's position changes * @param onLayerPosChanged Callback for when the layer's position changes
*/ */
const createRGLayer = ( const createRGLayer = (
stage: Konva.Stage, stage: Konva.Stage,
layerState: RegionalGuidanceLayer, rg: RegionalGuidanceData,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): Konva.Layer => { ): Konva.Layer => {
// This layer hasn't been added to the konva state yet // This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: layerState.id, id: rg.id,
name: RG_LAYER_NAME, name: RG_LAYER_NAME,
draggable: true, draggable: true,
dragDistance: 0, dragDistance: 0,
@ -59,9 +59,9 @@ const createRGLayer = (
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing // When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
// the position - we do not need to call this on the `dragmove` event. // the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) { if (onPosChanged) {
konvaLayer.on('dragend', function (e) { konvaLayer.on('dragend', function (e) {
onLayerPosChanged(layerState.id, Math.floor(e.target.x()), Math.floor(e.target.y())); onPosChanged({ id: rg.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'regional_guidance');
}); });
} }
@ -73,32 +73,32 @@ const createRGLayer = (
/** /**
* Renders a raster layer. * Renders a raster layer.
* @param stage The konva stage * @param stage The konva stage
* @param layerState The regional guidance layer state * @param rg The regional guidance layer state
* @param globalMaskLayerOpacity The global mask layer opacity * @param globalMaskLayerOpacity The global mask layer opacity
* @param tool The current tool * @param tool The current tool
* @param onLayerPosChanged Callback for when the layer's position changes * @param onPosChanged Callback for when the layer's position changes
*/ */
export const renderRGLayer = ( export const renderRGLayer = (
stage: Konva.Stage, stage: Konva.Stage,
layerState: RegionalGuidanceLayer, rg: RegionalGuidanceData,
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
tool: Tool, tool: Tool,
zIndex: number, zIndex: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void selectedEntity: CanvasEntity | null,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => { ): void => {
const konvaLayer = const konvaLayer = stage.findOne<Konva.Layer>(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged);
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged);
// Update the layer's position and listening state // Update the layer's position and listening state
konvaLayer.setAttrs({ konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(layerState.x), x: Math.floor(rg.x),
y: Math.floor(layerState.y), y: Math.floor(rg.y),
zIndex, zIndex,
}); });
// Convert the color to a string, stripping the alpha - the object group will handle opacity. // Convert the color to a string, stripping the alpha - the object group will handle opacity.
const rgbColor = rgbColorToString(layerState.previewColor); const rgbColor = rgbColorToString(rg.fill);
const konvaObjectGroup = const konvaObjectGroup =
konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ?? konvaLayer.findOne<Konva.Group>(`.${RG_LAYER_OBJECT_GROUP_NAME}`) ??
@ -107,7 +107,7 @@ export const renderRGLayer = (
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
let groupNeedsCache = false; let groupNeedsCache = false;
const objectIds = layerState.objects.map(mapId); const objectIds = rg.objects.map(mapId);
// Destroy any objects that are no longer in the redux state // Destroy any objects that are no longer in the redux state
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) { for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
if (!objectIds.includes(objectNode.id())) { if (!objectIds.includes(objectNode.id())) {
@ -116,7 +116,7 @@ export const renderRGLayer = (
} }
} }
for (const obj of layerState.objects) { for (const obj of rg.objects) {
if (obj.type === 'brush_line') { if (obj.type === 'brush_line') {
const konvaBrushLine = const konvaBrushLine =
stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME); stage.findOne<Konva.Line>(`#${obj.id}`) ?? createBrushLine(obj, konvaObjectGroup, RG_LAYER_BRUSH_LINE_NAME);
@ -160,8 +160,8 @@ export const renderRGLayer = (
} }
// Only update layer visibility if it has changed. // Only update layer visibility if it has changed.
if (konvaLayer.visible() !== layerState.isEnabled) { if (konvaLayer.visible() !== rg.isEnabled) {
konvaLayer.visible(layerState.isEnabled); konvaLayer.visible(rg.isEnabled);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -173,6 +173,7 @@ export const renderRGLayer = (
const compositingRect = const compositingRect =
konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer);
const isSelected = selectedEntity?.id === rg.id;
/** /**
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
@ -185,7 +186,7 @@ export const renderRGLayer = (
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
* a single raster image, and _then_ applied the 50% opacity. * a single raster image, and _then_ applied the 50% opacity.
*/ */
if (layerState.isSelected && tool !== 'move') { if (isSelected && tool !== 'move') {
// We must clear the cache first so Konva will re-draw the group with the new compositing rect // We must clear the cache first so Konva will re-draw the group with the new compositing rect
if (konvaObjectGroup.isCached()) { if (konvaObjectGroup.isCached()) {
konvaObjectGroup.clearCache(); konvaObjectGroup.clearCache();
@ -195,7 +196,7 @@ export const renderRGLayer = (
compositingRect.setAttrs({ compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)), ...(!rg.bboxNeedsUpdate && rg.bbox ? rg.bbox : getLayerBboxFast(konvaLayer)),
fill: rgbColor, fill: rgbColor,
opacity: globalMaskLayerOpacity, opacity: globalMaskLayerOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
@ -215,18 +216,18 @@ export const renderRGLayer = (
konvaObjectGroup.opacity(globalMaskLayerOpacity); konvaObjectGroup.opacity(globalMaskLayerOpacity);
} }
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer); const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, konvaLayer);
if (layerState.bbox) { if (rg.bbox) {
const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move'; const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move';
bboxRect.setAttrs({ bboxRect.setAttrs({
visible: active, visible: active,
listening: active, listening: active,
x: layerState.bbox.x, x: rg.bbox.x,
y: layerState.bbox.y, y: rg.bbox.y,
width: layerState.bbox.width, width: rg.bbox.width,
height: layerState.bbox.height, height: rg.bbox.height,
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '', stroke: isSelected ? BBOX_SELECTED_STROKE : '',
}); });
} else { } else {
bboxRect.visible(false); bboxRect.visible(false);

View File

@ -11,21 +11,12 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types';
CanvasV2State,
ControlAdapterData,
IPAdapterData,
LayerData,
RegionalGuidanceData,
RgbaColor,
StageAttrs,
Tool,
} from './types';
import { DEFAULT_RGBA_COLOR } from './types'; import { DEFAULT_RGBA_COLOR } from './types';
const initialState: CanvasV2State = { const initialState: CanvasV2State = {
_version: 3, _version: 3,
lastSelectedItem: null, selectedEntityIdentifier: null,
prompts: { prompts: {
positivePrompt: '', positivePrompt: '',
negativePrompt: '', negativePrompt: '',
@ -113,6 +104,12 @@ export const canvasV2Slice = createSlice({
invertScrollChanged: (state, action: PayloadAction<boolean>) => { invertScrollChanged: (state, action: PayloadAction<boolean>) => {
state.tool.invertScroll = action.payload; state.tool.invertScroll = action.payload;
}, },
toolChanged: (state, action: PayloadAction<Tool>) => {
state.tool.selected = action.payload;
},
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
state.tool.selectedBuffer = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
builder.addCase(modelChanged, (state, action) => { builder.addCase(modelChanged, (state, action) => {
@ -146,6 +143,8 @@ export const {
eraserWidthChanged, eraserWidthChanged,
fillChanged, fillChanged,
invertScrollChanged, invertScrollChanged,
toolChanged,
toolBufferChanged,
} = canvasV2Slice.actions; } = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
@ -173,18 +172,9 @@ export const $stageAttrs = atom<StageAttrs>({
// Some nanostores that are manually synced to redux state to provide imperative access // Some nanostores that are manually synced to redux state to provide imperative access
// TODO(psyche): // TODO(psyche):
export const $tool = atom<Tool>('brush'); export const $toolState = atom<CanvasV2State['tool']>(deepClone(initialState.tool));
export const $toolBuffer = atom<Tool | null>(null); export const $currentFill = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $brushWidth = atom<number>(0); export const $selectedEntity = atom<CanvasEntity | null>(null);
export const $brushSpacingPx = atom<number>(0);
export const $eraserWidth = atom<number>(0);
export const $eraserSpacingPx = atom<number>(0);
export const $fill = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $selectedLayer = atom<LayerData | null>(null);
export const $selectedRG = atom<RegionalGuidanceData | null>(null);
export const $selectedCA = atom<ControlAdapterData | null>(null);
export const $selectedIPA = atom<IPAdapterData | null>(null);
export const $invertScroll = atom(false);
export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 }); export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = { export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {

View File

@ -7,12 +7,12 @@ import type { IRect } from 'konva/lib/types';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type {
AddBrushLineArg, BrushLineAddedArg,
AddEraserLineArg, EraserLineAddedArg,
AddImageObjectArg, ImageObjectAddedArg,
AddPointToLineArg,
AddRectShapeArg,
LayerData, LayerData,
PointAddedToLineArg,
RectShapeAddedArg,
} from './types'; } from './types';
import { isLine } from './types'; import { isLine } from './types';
@ -133,7 +133,7 @@ export const layersSlice = createSlice({
moveToStart(state.layers, layer); moveToStart(state.layers, layer);
}, },
layerBrushLineAdded: { layerBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload; const { id, points, lineId, color, width } = action.payload;
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
@ -149,12 +149,12 @@ export const layersSlice = createSlice({
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
prepare: (payload: AddBrushLineArg) => ({ prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
layerEraserLineAdded: { layerEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload; const { id, points, lineId, width } = action.payload;
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
@ -169,11 +169,11 @@ export const layersSlice = createSlice({
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
prepare: (payload: AddEraserLineArg) => ({ prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
layerLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => { layerLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload; const { id, point } = action.payload;
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
@ -187,7 +187,7 @@ export const layersSlice = createSlice({
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
layerRectAdded: { layerRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => { reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload; const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) { if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles // Ignore zero-area rectangles
@ -200,18 +200,15 @@ export const layersSlice = createSlice({
layer.objects.push({ layer.objects.push({
type: 'rect_shape', type: 'rect_shape',
id: getRectShapeId(id, rectId), id: getRectShapeId(id, rectId),
x: rect.x - layer.x, ...rect,
y: rect.y - layer.y,
width: rect.width,
height: rect.height,
color, color,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
}, },
layerImageAdded: { layerImageAdded: {
reducer: (state, action: PayloadAction<AddImageObjectArg & { imageId: string }>) => { reducer: (state, action: PayloadAction<ImageObjectAddedArg & { imageId: string }>) => {
const { id, imageId, imageDTO } = action.payload; const { id, imageId, imageDTO } = action.payload;
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
@ -229,7 +226,7 @@ export const layersSlice = createSlice({
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
}, },
prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }), prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }),
}, },
}, },
}); });

View File

@ -14,11 +14,11 @@ import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type {
AddBrushLineArg, BrushLineAddedArg,
AddEraserLineArg, EraserLineAddedArg,
AddPointToLineArg,
AddRectShapeArg,
IPAdapterData, IPAdapterData,
PointAddedToLineArg,
RectShapeAddedArg,
RegionalGuidanceData, RegionalGuidanceData,
RgbColor, RgbColor,
} from './types'; } from './types';
@ -306,7 +306,7 @@ export const regionalGuidanceSlice = createSlice({
ipa.clipVisionModel = clipVisionModel; ipa.clipVisionModel = clipVisionModel;
}, },
rgBrushLineAdded: { rgBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload; const { id, points, lineId, color, width } = action.payload;
const rg = selectRg(state, id); const rg = selectRg(state, id);
if (!rg) { if (!rg) {
@ -315,21 +315,19 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({ rg.objects.push({
id: getBrushLineId(id, lineId), id: getBrushLineId(id, lineId),
type: 'brush_line', type: 'brush_line',
// Points must be offset by the layer's x and y coordinates points,
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
strokeWidth: width, strokeWidth: width,
color, color,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;
}, },
prepare: (payload: AddBrushLineArg) => ({ prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
rgEraserLineAdded: { rgEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload; const { id, points, lineId, width } = action.payload;
const rg = selectRg(state, id); const rg = selectRg(state, id);
if (!rg) { if (!rg) {
@ -338,19 +336,17 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({ rg.objects.push({
id: getEraserLineId(id, lineId), id: getEraserLineId(id, lineId),
type: 'eraser_line', type: 'eraser_line',
// Points must be offset by the layer's x and y coordinates points,
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
strokeWidth: width, strokeWidth: width,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;
}, },
prepare: (payload: AddEraserLineArg) => ({ prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() }, payload: { ...payload, lineId: uuidv4() },
}), }),
}, },
rgLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => { rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload; const { id, point } = action.payload;
const rg = selectRg(state, id); const rg = selectRg(state, id);
if (!rg) { if (!rg) {
@ -360,14 +356,12 @@ export const regionalGuidanceSlice = createSlice({
if (!lastObject || !isLine(lastObject)) { if (!lastObject || !isLine(lastObject)) {
return; return;
} }
// Points must be offset by the layer's x and y coordinates lastObject.points.push(...point);
// TODO: Handle this in the event listener
lastObject.points.push(point[0] - rg.x, point[1] - rg.y);
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;
}, },
rgRectAdded: { rgRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => { reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload; const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) { if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles // Ignore zero-area rectangles
@ -380,16 +374,13 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({ rg.objects.push({
type: 'rect_shape', type: 'rect_shape',
id: getRectShapeId(id, rectId), id: getRectShapeId(id, rectId),
x: rect.x - rg.x, ...rect,
y: rect.y - rg.y,
width: rect.width,
height: rect.height,
color, color,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;
}, },
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
}, },
}, },
}); });

View File

@ -1,3 +1,4 @@
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
import { import {
zBeginEndStepPct, zBeginEndStepPct,
zCLIPVisionModelV2, zCLIPVisionModelV2,
@ -243,7 +244,7 @@ const zInpaintMaskData = z.object({
}); });
export type InpaintMaskData = z.infer<typeof zInpaintMaskData>; export type InpaintMaskData = z.infer<typeof zInpaintMaskData>;
const zFilter = z.enum(['none', 'lightness_to_alpha']); const zFilter = z.enum(['none', LightnessToAlphaFilter.name]);
export type Filter = z.infer<typeof zFilter>; export type Filter = z.infer<typeof zFilter>;
const zControlAdapterData = z.object({ const zControlAdapterData = z.object({
@ -271,21 +272,12 @@ export type ControlAdapterConfig = Pick<
'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode'
>; >;
const zCanvasItemIdentifier = z.object({ export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData;
type: z.enum([ export type CanvasEntityIdentifier = Pick<CanvasEntity, 'id' | 'type'>;
zLayerData.shape.type.value,
zIPAdapterData.shape.type.value,
zControlAdapterData.shape.type.value,
zRegionalGuidanceData.shape.type.value,
zInpaintMaskData.shape.type.value,
]),
id: zId,
});
type CanvasItemIdentifier = z.infer<typeof zCanvasItemIdentifier>;
export type CanvasV2State = { export type CanvasV2State = {
_version: 3; _version: 3;
lastSelectedItem: CanvasItemIdentifier | null; selectedEntityIdentifier: CanvasEntityIdentifier | null;
prompts: { prompts: {
positivePrompt: ParameterPositivePrompt; positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt; negativePrompt: ParameterNegativePrompt;
@ -314,11 +306,17 @@ export type CanvasV2State = {
}; };
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };
export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number }; export type PosChangedArg = { id: string; x: number; y: number };
export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; export type BboxChangedArg = { id: string; bbox: IRect };
export type AddPointToLineArg = { id: string; point: [number, number] }; export type EraserLineAddedArg = {
export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor }; id: string;
export type AddImageObjectArg = { id: string; imageDTO: ImageDTO }; points: [number, number, number, number];
width: number;
};
export type BrushLineAddedArg = EraserLineAddedArg & { color: RgbaColor };
export type PointAddedToLineArg = { id: string; point: [number, number] };
export type RectShapeAddedArg = { id: string; rect: IRect; color: RgbaColor };
export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO };
//#region Type guards //#region Type guards
export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => {

View File

@ -34,7 +34,7 @@ const selectImageUsages = createMemoizedSelector(
const { imagesToDelete } = deleteImageModal; const { imagesToDelete } = deleteImageModal;
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name) getImageUsage(canvas, nodes, controlAdapters, canvasV2, image_name)
); );
const imageUsageSummary: ImageUsage = { const imageUsageSummary: ImageUsage = {

View File

@ -84,7 +84,7 @@ export const selectImageUsage = createMemoizedSelector(
} }
const imagesUsage = imagesToDelete.map((i) => const imagesUsage = imagesToDelete.map((i) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name) getImageUsage(canvas, nodes, controlAdapters, canvasV2, i.image_name)
); );
return imagesUsage; return imagesUsage;

View File

@ -45,7 +45,7 @@ const DeleteBoardModal = (props: Props) => {
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice],
(canvas, nodes, controlAdapters, controlLayers) => { (canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) => const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) getImageUsage(canvas, nodes, controlAdapters, canvasV2, imageName)
); );
const imageUsageSummary: ImageUsage = { const imageUsageSummary: ImageUsage = {

View File

@ -10,7 +10,7 @@ import { CANVAS_COHERENCE_NOISE, METADATA, NOISE, POSITIVE_CONDITIONING } from '
export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => { export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => {
const { iterations, model, shouldRandomizeSeed, seed } = state.generation; const { iterations, model, shouldRandomizeSeed, seed } = state.generation;
const { shouldConcatPrompts } = state.controlLayers.present; const { shouldConcatPrompts } = state.canvasV2;
const { prompts, seedBehaviour } = state.dynamicPrompts; const { prompts, seedBehaviour } = state.dynamicPrompts;
const data: Batch['data'] = []; const data: Batch['data'] = [];

View File

@ -73,7 +73,7 @@ export const addControlLayers = async (
): Promise<LayerData[]> => { ): Promise<LayerData[]> => {
const isSDXL = base === 'sdxl'; const isSDXL = base === 'sdxl';
const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); const validLayers = state.canvasV2.layers.filter((l) => isValidLayer(l, base));
const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter); const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter);
for (const ca of validControlAdapters) { for (const ca of validControlAdapters) {
@ -259,7 +259,7 @@ export const addControlLayers = async (
} }
} }
g.upsertMetadata({ control_layers: { layers: validLayers, version: state.controlLayers.present._version } }); g.upsertMetadata({ control_layers: { layers: validLayers, version: state.canvasV2._version } });
return validLayers; return validLayers;
}; };
//#endregion //#endregion
@ -421,7 +421,7 @@ const addInitialImageLayerToGraph = (
) => { ) => {
const { vaePrecision } = state.generation; const { vaePrecision } = state.generation;
const { refinerModel, refinerStart } = state.sdxl; const { refinerModel, refinerStart } = state.sdxl;
const { width, height } = state.controlLayers.present.size; const { width, height } = state.canvasV2.size;
assert(layer.isEnabled, 'Initial image layer is not enabled'); assert(layer.isEnabled, 'Initial image layer is not enabled');
assert(layer.image, 'Initial image layer has no image'); assert(layer.image, 'Initial image layer has no image');
@ -567,8 +567,8 @@ const buildControlImage = (
*/ */
const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise<Record<string, Blob>> => { const getRGLayerBlobs = async (layerIds?: string[], preview: boolean = false): Promise<Record<string, Blob>> => {
const state = getStore().getState(); const state = getStore().getState();
const { layers } = state.controlLayers.present; const { layers } = state.canvasV2;
const { width, height } = state.controlLayers.present.size; const { width, height } = state.canvasV2.size;
const reduxLayers = layers.filter(isRegionalGuidanceLayer); const reduxLayers = layers.filter(isRegionalGuidanceLayer);
const container = document.createElement('div'); const container = document.createElement('div');
const stage = new Konva.Stage({ container, width, height }); const stage = new Konva.Stage({ container, width, height });

View File

@ -74,7 +74,7 @@ export const addHRF = (
vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'> vaeSource: Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'seamless'>
): Invocation<'l2i'> => { ): Invocation<'l2i'> => {
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
const { width, height } = state.controlLayers.present.size; const { width, height } = state.canvasV2.size;
const optimalDimension = selectOptimalDimension(state); const optimalDimension = selectOptimalDimension(state);
const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height); const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height);

View File

@ -22,7 +22,7 @@ export const getPresetModifiedPrompts = (
state: RootState state: RootState
): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => { ): { positivePrompt: string; negativePrompt: string; positiveStylePrompt?: string; negativeStylePrompt?: string } => {
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } = const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
state.controlLayers.present; state.canvasV2.prompts;
const { activeStylePresetId } = state.stylePreset; const { activeStylePresetId } = state.stylePreset;
if (activeStylePresetId) { if (activeStylePresetId) {

View File

@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamNegativePrompt = memo(() => { export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt); const prompt = useAppSelector((s) => s.canvasV2.prompts.negativePrompt);
const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);

View File

@ -17,7 +17,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamPositivePrompt = memo(() => { export const ParamPositivePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt); const prompt = useAppSelector((s) => s.canvasV2.positivePrompt);
const baseModel = useAppSelector((s) => s.generation.model)?.base; const baseModel = useAppSelector((s) => s.generation.model)?.base;
const viewMode = useAppSelector((s) => s.stylePreset.viewMode); const viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);

View File

@ -15,7 +15,7 @@ const selectPromptsCount = createSelector(
selectCanvasV2Slice, selectCanvasV2Slice,
selectDynamicPromptsSlice, selectDynamicPromptsSlice,
(controlLayers, dynamicPrompts) => (controlLayers, dynamicPrompts) =>
getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 getShouldProcessPrompt(canvasV2.positivePrompt) ? dynamicPrompts.prompts.length : 1
); );
type Props = { type Props = {

View File

@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
export const ParamSDXLNegativeStylePrompt = memo(() => { export const ParamSDXLNegativeStylePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt2); const prompt = useAppSelector((s) => s.canvasV2.negativePrompt2);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback( const handleChange = useCallback(

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
export const ParamSDXLPositiveStylePrompt = memo(() => { export const ParamSDXLPositiveStylePrompt = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt2); const prompt = useAppSelector((s) => s.canvasV2.positivePrompt2);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback( const handleChange = useCallback(

View File

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi';
export const SDXLConcatButton = memo(() => { export const SDXLConcatButton = memo(() => {
const shouldConcatPrompts = useAppSelector((s) => s.controlLayers.present.shouldConcatPrompts); const shouldConcatPrompts = useAppSelector((s) => s.canvasV2.shouldConcatPrompts);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -42,7 +42,7 @@ const selector = createMemoizedSelector(
badges.push('locked'); badges.push('locked');
} }
} else { } else {
const { aspectRatio, width, height } = controlLayers.present.size; const { aspectRatio, width, height } = canvasV2.size;
badges.push(`${width}×${height}`); badges.push(`${width}×${height}`);
badges.push(aspectRatio.id); badges.push(aspectRatio.id);
if (aspectRatio.isLocked) { if (aspectRatio.isLocked) {

View File

@ -9,9 +9,9 @@ import { memo, useCallback } from 'react';
export const ImageSizeLinear = memo(() => { export const ImageSizeLinear = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const width = useAppSelector((s) => s.controlLayers.present.size.width); const width = useAppSelector((s) => s.canvasV2.size.width);
const height = useAppSelector((s) => s.controlLayers.present.size.height); const height = useAppSelector((s) => s.canvasV2.size.height);
const aspectRatioState = useAppSelector((s) => s.controlLayers.present.size.aspectRatio); const aspectRatioState = useAppSelector((s) => s.canvasV2.size.aspectRatio);
const onChangeWidth = useCallback( const onChangeWidth = useCallback(
(width: number) => { (width: number) => {

View File

@ -10,7 +10,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
}; };
export const usePresetModifiedPrompts = () => { export const usePresetModifiedPrompts = () => {
const { positivePrompt, negativePrompt } = useAppSelector((s) => s.controlLayers.present); const { positivePrompt, negativePrompt } = useAppSelector((s) => s.canvasV2.prompts);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);

View File

@ -44,7 +44,7 @@ const ParametersPanelTextToImage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length); const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length);
const controlLayersTitle = useMemo(() => { const controlLayersTitle = useMemo(() => {
if (controlLayersCount === 0) { if (controlLayersCount === 0) {
return t('controlLayers.controlLayers'); return t('controlLayers.controlLayers');