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 wereControlLayersReset = false;
const { canvas, nodes, controlAdapters, controlLayers } = getState();
const { canvas, nodes, controlAdapters, canvasV2 } = getState();
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) {
dispatch(resetCanvas());

View File

@ -65,14 +65,14 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
// Delay before starting actual work
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) {
return;
}
// 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)
.find((l) => l.id === layerId);
const originalImage = originalLayer?.controlAdapter.image;
@ -161,7 +161,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
if (signal.aborted) {
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
const pendingBatchId = getState()
.controlLayers.present.layers.filter(isControlAdapterLayer)
.canvasV2.layers.filter(isControlAdapterLayer)
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
if (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) => {
state.controlLayers.present.layers.forEach((l) => {
state.canvasV2.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) {
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
dispatch(layerDeleted(l.id));

View File

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

View File

@ -4,7 +4,7 @@ import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
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 {
controlAdaptersV2PersistConfig,
@ -52,7 +52,6 @@ import { listenerMiddleware } from './middleware/listenerMiddleware';
const allReducers = {
[api.reducerPath]: api.reducer,
[canvasSlice.name]: canvasSlice.reducer,
[gallerySlice.name]: gallerySlice.reducer,
[generationSlice.name]: generationSlice.reducer,
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),

View File

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

View File

@ -23,7 +23,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
const selectValidActions = useMemo(
() =>
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`);
return {
canAddPositivePrompt: layer.positivePrompt === null,

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
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();
});

View File

@ -8,7 +8,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
export const DeleteAllLayersButton = memo(() => {
const { t } = useTranslation();
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(() => {
dispatch(allLayersDeleted());
}, [dispatch]);

View File

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

View File

@ -7,8 +7,8 @@ import { memo } from 'react';
export const HeadsUpDisplay = memo(() => {
const stageAttrs = useStore($stageAttrs);
const layerCount = useAppSelector((s) => s.controlLayers.present.layers.length);
const bbox = useAppSelector((s) => s.controlLayers.present.bbox);
const layerCount = useAppSelector((s) => s.canvasV2.layers.length);
const bbox = useAppSelector((s) => s.canvasV2.bbox);
return (
<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) => {
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(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);

View File

@ -16,7 +16,7 @@ type Props = {
export const IPALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
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 onClick = useCallback(() => {

View File

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

View File

@ -22,10 +22,10 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => {
const selectValidActions = useMemo(
() =>
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`);
const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId);
const layerCount = controlLayers.present.layers.length;
const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId);
const layerCount = canvasV2.layers.length;
return {
canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0,

View File

@ -22,7 +22,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const selectValidActions = useMemo(
() =>
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`);
return {
canAddPositivePrompt: layer.positivePrompt === null,

View File

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

View File

@ -30,14 +30,14 @@ export const RGLayer = memo(({ layerId }: Props) => {
const selector = useMemo(
() =>
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`);
return {
color: rgbColorToString(layer.previewColor),
hasPositivePrompt: layer.positivePrompt !== null,
hasNegativePrompt: layer.negativePrompt !== null,
hasIPAdapters: layer.ipAdapters.length > 0,
isSelected: layerId === controlLayers.present.selectedLayerId,
isSelected: layerId === canvasV2.selectedLayerId,
autoNegative: layer.autoNegative,
};
}),

View File

@ -16,7 +16,7 @@ const useAutoNegative = (layerId: string) => {
const selectAutoNegative = useMemo(
() =>
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`);
return layer.autoNegative;
}),

View File

@ -20,7 +20,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => {
const selectColor = useMemo(
() =>
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`);
return layer.previewColor;
}),

View File

@ -15,7 +15,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
const selectIPAdapterIds = useMemo(
() =>
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`);
return layer.ipAdapters;
}),

View File

@ -28,7 +28,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
const onDeleteIPAdapter = useCallback(() => {
dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId }));
}, [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(
(beginEndStepPct: [number, number]) => {

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import {
} from 'react-icons/pi';
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';
});
@ -29,7 +29,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isDisabled = useAppSelector(selectIsDisabled);
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId);
const selectedLayerId = useAppSelector((s) => s.canvasV2.selectedLayerId);
const tool = useStore($tool);
const setToolToBrush = useCallback(() => {

View File

@ -102,7 +102,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
export const useAddIILayer = () => {
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(() => {
dispatch(iiLayerAdded(null));
}, [dispatch]);

View File

@ -10,7 +10,7 @@ export const useLayerPositivePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
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(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`);
return layer.positivePrompt;
@ -25,7 +25,7 @@ export const useLayerNegativePrompt = (layerId: string) => {
const selectLayer = useMemo(
() =>
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(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`);
return layer.negativePrompt;
@ -40,7 +40,7 @@ export const useLayerIsEnabled = (layerId: string) => {
const selectLayer = useMemo(
() =>
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`);
return layer.isEnabled;
}),
@ -54,7 +54,7 @@ export const useLayerType = (layerId: string) => {
const selectLayer = useMemo(
() =>
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`);
return layer.type;
}),
@ -68,7 +68,7 @@ export const useCALayerOpacity = (layerId: string) => {
const selectLayer = useMemo(
() =>
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`);
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 { getScaledFlooredCursorPosition } from 'features/controlLayers/konva/util';
import type {
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
LayerData,
BrushLineAddedArg,
CanvasEntity,
CanvasV2State,
EraserLineAddedArg,
PointAddedToLineArg,
RectShapeAddedArg,
RgbaColor,
StageAttrs,
Tool,
} from 'features/controlLayers/store/types';
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
import { PREVIEW_TOOL_GROUP_ID } from './naming';
type Arg = {
stage: Konva.Stage;
getTool: () => Tool;
getToolState: () => CanvasV2State['tool'];
getCurrentFill: () => RgbaColor;
setTool: (tool: Tool) => void;
getToolBuffer: () => Tool | null;
setToolBuffer: (tool: Tool | null) => void;
getIsDrawing: () => boolean;
setIsDrawing: (isDrawing: boolean) => void;
@ -35,17 +35,14 @@ type Arg = {
getLastAddedPoint: () => Vector2d | null;
setLastAddedPoint: (pos: Vector2d | null) => void;
setStageAttrs: (attrs: StageAttrs) => void;
getBrushColor: () => RgbaColor;
getBrushSize: () => number;
getBrushSpacingPx: () => number;
getSelectedLayer: () => LayerData | null;
getShouldInvert: () => boolean;
getSelectedEntity: () => CanvasEntity | null;
getSpaceKey: () => boolean;
onBrushLineAdded: (arg: AddBrushLineArg) => void;
onEraserLineAdded: (arg: AddEraserLineArg) => void;
onPointAddedToLine: (arg: AddPointToLineArg) => void;
onRectShapeAdded: (arg: AddRectShapeArg) => void;
onBrushSizeChanged: (size: number) => void;
onBrushLineAdded: (arg: BrushLineAddedArg, entityType: CanvasEntity['type']) => void;
onEraserLineAdded: (arg: EraserLineAddedArg, entityType: CanvasEntity['type']) => void;
onPointAddedToLine: (arg: PointAddedToLineArg, entityType: CanvasEntity['type']) => void;
onRectShapeAdded: (arg: RectShapeAddedArg, entityType: CanvasEntity['type']) => 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
*/
const maybeAddNextPoint = (
selectedLayer: LayerData,
selectedEntity: CanvasEntity,
currentPos: Vector2d,
getToolState: Arg['getToolState'],
getLastAddedPoint: Arg['getLastAddedPoint'],
setLastAddedPoint: Arg['setLastAddedPoint'],
getBrushSpacingPx: Arg['getBrushSpacingPx'],
onPointAddedToLine: Arg['onPointAddedToLine']
) => {
if (selectedEntity.type !== 'layer' && selectedEntity.type !== 'regional_guidance') {
return;
}
// Continue the last line
const lastAddedPoint = getLastAddedPoint();
const toolState = getToolState();
const minSpacingPx = toolState.selected === 'brush' ? toolState.brush.width * 0.05 : toolState.eraser.width * 0.05;
if (lastAddedPoint) {
// 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;
}
}
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 = ({
stage,
getTool,
getToolState,
getCurrentFill,
setTool,
getToolBuffer,
setToolBuffer,
getIsDrawing,
setIsDrawing,
@ -108,17 +116,14 @@ export const setStageEventHandlers = ({
getLastAddedPoint,
setLastAddedPoint,
setStageAttrs,
getBrushColor,
getBrushSize,
getBrushSpacingPx,
getSelectedLayer,
getShouldInvert,
getSelectedEntity,
getSpaceKey,
onBrushLineAdded,
onEraserLineAdded,
onPointAddedToLine,
onRectShapeAdded,
onBrushSizeChanged,
onBrushWidthChanged: onBrushSizeChanged,
onEraserWidthChanged: onEraserSizeChanged,
}: Arg): (() => void) => {
//#region mouseenter
stage.on('mouseenter', (e) => {
@ -126,7 +131,7 @@ export const setStageEventHandlers = ({
if (!stage) {
return;
}
const tool = getTool();
const tool = getToolState().selected;
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
});
@ -137,13 +142,13 @@ export const setStageEventHandlers = ({
return;
}
setIsMouseDown(true);
const tool = getTool();
const toolState = getToolState();
const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedLayer = getSelectedLayer();
if (!pos || !selectedLayer) {
const selectedEntity = getSelectedEntity();
if (!pos || !selectedEntity) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return;
}
@ -155,23 +160,37 @@ export const setStageEventHandlers = ({
setIsDrawing(true);
setLastMouseDownPos(pos);
if (tool === 'brush') {
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
});
if (toolState.selected === 'brush') {
onBrushLineAdded(
{
id: selectedEntity.id,
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') {
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
});
}
if (tool === 'rect') {
// Setting the last mouse down pos starts a rect
if (toolState.selected === 'eraser') {
onEraserLineAdded(
{
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
);
}
});
@ -183,12 +202,12 @@ export const setStageEventHandlers = ({
}
setIsMouseDown(false);
const pos = getLastCursorPos();
const selectedLayer = getSelectedLayer();
const selectedEntity = getSelectedEntity();
if (!pos || !selectedLayer) {
if (!pos || !selectedEntity) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return;
}
@ -197,21 +216,24 @@ export const setStageEventHandlers = ({
return;
}
const tool = getTool();
const toolState = getToolState();
if (tool === 'rect') {
if (toolState.selected === 'rect') {
const lastMouseDownPos = getLastMouseDownPos();
if (lastMouseDownPos) {
onRectShapeAdded({
layerId: selectedLayer.id,
onRectShapeAdded(
{
id: selectedEntity.id,
rect: {
x: Math.min(pos.x, lastMouseDownPos.x),
y: Math.min(pos.y, lastMouseDownPos.y),
width: Math.abs(pos.x - lastMouseDownPos.x),
height: Math.abs(pos.y - lastMouseDownPos.y),
},
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
});
color: getCurrentFill(),
},
selectedEntity.type
);
}
}
@ -225,16 +247,18 @@ export const setStageEventHandlers = ({
if (!stage) {
return;
}
const tool = getTool();
const toolState = getToolState();
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;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return;
}
@ -247,45 +271,49 @@ export const setStageEventHandlers = ({
return;
}
if (tool === 'brush') {
if (toolState.selected === 'brush') {
if (getIsDrawing()) {
// Continue the last line
maybeAddNextPoint(
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
} else {
// Start a new line
onBrushLineAdded({
layerId: selectedLayer.id,
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
});
onBrushLineAdded(
{
id: selectedEntity.id,
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);
}
}
if (tool === 'eraser') {
if (toolState.selected === 'eraser') {
if (getIsDrawing()) {
// Continue the last line
maybeAddNextPoint(
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
maybeAddNextPoint(selectedEntity, pos, getToolState, getLastAddedPoint, setLastAddedPoint, onPointAddedToLine);
} else {
// Start a new line
onEraserLineAdded({
layerId: selectedLayer.id,
points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y],
});
onEraserLineAdded(
{
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);
}
}
@ -301,15 +329,15 @@ export const setStageEventHandlers = ({
setIsDrawing(false);
setLastCursorPos(null);
setLastMouseDownPos(null);
const selectedLayer = getSelectedLayer();
const tool = getTool();
const selectedEntity = getSelectedEntity();
const toolState = getToolState();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
if (!pos || !selectedLayer) {
if (!pos || !selectedEntity) {
return;
}
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
if (selectedEntity.type !== 'regional_guidance' && selectedEntity.type !== 'layer') {
return;
}
if (getSpaceKey()) {
@ -317,12 +345,11 @@ export const setStageEventHandlers = ({
return;
}
if (getIsMouseDown()) {
if (tool === 'brush') {
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
if (toolState.selected === 'brush') {
onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type);
}
if (tool === 'eraser') {
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
if (toolState.selected === 'eraser') {
onPointAddedToLine({ id: selectedEntity.id, point: [pos.x, pos.y] }, selectedEntity.type);
}
}
});
@ -331,12 +358,17 @@ export const setStageEventHandlers = ({
e.evt.preventDefault();
if (e.evt.ctrlKey || e.evt.metaKey) {
const toolState = getToolState();
let delta = e.evt.deltaY;
if (getShouldInvert()) {
if (toolState.invertScroll) {
delta = -delta;
}
// 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 {
// We need the absolute cursor position - not the scaled position
const cursorPos = stage.getPointerPosition();
@ -396,7 +428,7 @@ export const setStageEventHandlers = ({
setIsDrawing(false);
setLastMouseDownPos(null);
} else if (e.key === ' ') {
setToolBuffer(getTool());
setToolBuffer(getToolState().selected);
setTool('view');
}
};
@ -408,7 +440,7 @@ export const setStageEventHandlers = ({
return;
}
if (e.key === ' ') {
const toolBuffer = getToolBuffer();
const toolBuffer = getToolState().selectedBuffer;
setTool(toolBuffer ?? 'move');
setToolBuffer(null);
}

View File

@ -1,6 +1,6 @@
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
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 type { ImageDTO } from 'services/api/types';
@ -12,11 +12,11 @@ import type { ImageDTO } from 'services/api/types';
/**
* Creates a control adapter layer.
* @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({
id: layerState.id,
id: ca.id,
name: CA_LAYER_NAME,
imageSmoothingEnabled: false,
listening: false,
@ -44,16 +44,16 @@ const createCALayerImage = (konvaLayer: Konva.Layer, imageEl: HTMLImageElement):
* the konva image.
* @param stage The konva stage
* @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
*/
const updateCALayerImageSource = async (
stage: Konva.Stage,
konvaLayer: Konva.Layer,
layerState: ControlAdapterLayer,
ca: ControlAdapterData,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): Promise<void> => {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
const image = ca.processedImage ?? ca.image;
if (image) {
const imageName = image.name;
const imageDTO = await getImageDTO(imageName);
@ -61,7 +61,7 @@ const updateCALayerImageSource = async (
return;
}
const imageEl = new Image();
const imageId = getCALayerImageId(layerState.id, imageName);
const imageId = getCALayerImageId(ca.id, imageName);
imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage =
@ -72,7 +72,7 @@ const updateCALayerImageSource = async (
id: imageId,
image: imageEl,
});
updateCALayerImageAttrs(stage, konvaImage, layerState);
updateCALayerImageAttrs(stage, konvaImage, ca);
// Must cache after this to apply the filters
konvaImage.cache();
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).
* @param stage The konva stage
* @param konvaImage The konva image
* @param layerState The control adapter layer state
* @param ca The control adapter layer state
*/
const updateCALayerImageAttrs = (
stage: Konva.Stage,
konvaImage: Konva.Image,
layerState: ControlAdapterLayer
): void => {
const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca: ControlAdapterData): void => {
let needsCache = false;
// 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.
// 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 (
konvaImage.x() !== layerState.x ||
konvaImage.y() !== layerState.y ||
konvaImage.visible() !== layerState.isEnabled ||
hasFilter !== layerState.isFilterEnabled
konvaImage.x() !== ca.x ||
konvaImage.y() !== ca.y ||
konvaImage.visible() !== ca.isEnabled ||
filterNeedsUpdate
) {
konvaImage.setAttrs({
opacity: layerState.opacity,
opacity: ca.opacity,
scaleX: 1,
scaleY: 1,
visible: layerState.isEnabled,
filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [],
visible: ca.isEnabled,
filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [],
});
needsCache = true;
}
if (konvaImage.opacity() !== layerState.opacity) {
konvaImage.opacity(layerState.opacity);
if (konvaImage.opacity() !== ca.opacity) {
konvaImage.opacity(ca.opacity);
}
if (needsCache) {
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
* with the current image source and attributes.
* @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
*/
export const renderCALayer = (
stage: Konva.Stage,
layerState: ControlAdapterLayer,
ca: ControlAdapterData,
zIndex: number,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
): 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);
@ -146,8 +143,8 @@ export const renderCALayer = (
let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) {
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
const image = ca.processedImage ?? ca.image;
if (image && canvasImageSource.id !== getCALayerImageId(ca.id, image.name)) {
imageSourceNeedsUpdate = true;
} else if (!image) {
imageSourceNeedsUpdate = true;
@ -157,8 +154,8 @@ export const renderCALayer = (
}
if (imageSourceNeedsUpdate) {
updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO);
updateCALayerImageSource(stage, konvaLayer, ca, getImageDTO);
} 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 { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
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 { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
import type { LayerData, Tool } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isInpaintMaskLayer,
isRasterLayer,
isRegionalGuidanceLayer,
isRenderableLayer,
import type {
CanvasEntity,
ControlAdapterData,
LayerData,
PosChangedArg,
RegionalGuidanceData,
Tool,
} from 'features/controlLayers/store/types';
import type Konva from 'konva';
import { debounce } from 'lodash-es';
@ -27,43 +25,42 @@ import type { ImageDTO } from 'services/api/types';
/**
* Renders the layers on the stage.
* @param stage The konva stage
* @param layerStates Array of all layer states
* @param globalMaskLayerOpacity The global mask layer opacity
* @param layers Array of all layer states
* @param rgGlobalOpacity The global mask layer opacity
* @param tool The current tool
* @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 = (
stage: Konva.Stage,
layerStates: LayerData[],
globalMaskLayerOpacity: number,
layers: LayerData[],
controlAdapters: ControlAdapterData[],
regions: RegionalGuidanceData[],
rgGlobalOpacity: number,
tool: Tool,
selectedEntity: CanvasEntity | null,
getImageDTO: (imageName: string) => Promise<ImageDTO | null>,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => {
const layerIds = layerStates.filter(isRenderableLayer).map(mapId);
const renderableIds = [...layers.map(mapId), ...controlAdapters.map(mapId), ...regions.map(mapId)];
// Remove un-rendered layers
for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) {
if (!layerIds.includes(konvaLayer.id())) {
if (!renderableIds.includes(konvaLayer.id())) {
konvaLayer.destroy();
}
}
// We'll need to ensure the tool preview layer is on top of the rest of the layers
let zIndex = 0;
for (const layer of layerStates) {
if (isRegionalGuidanceLayer(layer)) {
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged);
} else if (isControlAdapterLayer(layer)) {
renderCALayer(stage, layer, zIndex, getImageDTO);
} else if (isInitialImageLayer(layer)) {
renderIILayer(stage, layer, zIndex, getImageDTO);
} else if (isRasterLayer(layer)) {
renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged);
} else if (isInpaintMaskLayer(layer)) {
//
for (const layer of layers) {
renderRasterLayer(stage, layer, tool, zIndex, onPosChanged);
zIndex++;
}
// IP Adapter layers are not rendered
// Increment the z-index for the tool layer
for (const ca of controlAdapters) {
renderCALayer(stage, ca, zIndex, getImageDTO);
zIndex++;
}
for (const rg of regions) {
renderRGLayer(stage, rg, rgGlobalOpacity, tool, zIndex, selectedEntity, onPosChanged);
zIndex++;
}
// Arrange the tool preview layer

View File

@ -5,7 +5,13 @@ import {
LAYER_BBOX_NAME,
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
} 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 { t } from 'i18next';
import Konva from 'konva';
@ -174,12 +180,12 @@ export const createImageObjectGroup = async (
/**
* 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
*/
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({
id: getLayerBboxId(layerState.id),
id: getLayerBboxId(entity.id),
name: LAYER_BBOX_NAME,
strokeWidth: 1,
visible: false,

View File

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

View File

@ -1,6 +1,4 @@
import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
import {
LAYER_BBOX_NAME,
RASTER_LAYER_BRUSH_LINE_NAME,
RASTER_LAYER_ERASER_LINE_NAME,
RASTER_LAYER_IMAGE_NAME,
@ -9,7 +7,6 @@ import {
RASTER_LAYER_RECT_SHAPE_NAME,
} from 'features/controlLayers/konva/naming';
import {
createBboxRect,
createBrushLine,
createEraserLine,
createImageObjectGroup,
@ -17,7 +14,7 @@ import {
createRectShape,
} from 'features/controlLayers/konva/renderers/objects';
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';
/**
@ -28,12 +25,12 @@ import Konva from 'konva';
* Creates a raster layer.
* @param stage The konva stage
* @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 = (
stage: Konva.Stage,
layerState: RasterLayer,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
layerState: LayerData,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): Konva.Layer => {
// This layer hasn't been added to the konva state yet
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
// the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) {
if (onPosChanged) {
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 layerState The regional guidance layer state
* @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 (
stage: Konva.Stage,
layerState: RasterLayer,
layerState: LayerData,
tool: Tool,
zIndex: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
) => {
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
konvaLayer.setAttrs({
@ -128,23 +125,23 @@ export const renderRasterLayer = async (
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) {
const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
bboxRect.setAttrs({
visible: active,
listening: active,
x: layerState.bbox.x,
y: layerState.bbox.y,
width: layerState.bbox.width,
height: layerState.bbox.height,
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
strokeWidth: 1 / stage.scaleX(),
});
} else {
bboxRect.visible(false);
}
// if (layerState.bbox) {
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
// bboxRect.setAttrs({
// visible: active,
// listening: active,
// x: layerState.bbox.x,
// y: layerState.bbox.y,
// width: layerState.bbox.width,
// height: layerState.bbox.height,
// stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
// strokeWidth: 1 / stage.scaleX(),
// });
// } else {
// bboxRect.visible(false);
// }
konvaObjectGroup.opacity(layerState.opacity);
};

View File

@ -18,7 +18,7 @@ import {
createRectShape,
} from 'features/controlLayers/konva/renderers/objects';
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';
/**
@ -41,17 +41,17 @@ const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
/**
* Creates a regional guidance layer.
* @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
*/
const createRGLayer = (
stage: Konva.Stage,
layerState: RegionalGuidanceLayer,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
rg: RegionalGuidanceData,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): Konva.Layer => {
// This layer hasn't been added to the konva state yet
const konvaLayer = new Konva.Layer({
id: layerState.id,
id: rg.id,
name: RG_LAYER_NAME,
draggable: true,
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
// the position - we do not need to call this on the `dragmove` event.
if (onLayerPosChanged) {
if (onPosChanged) {
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.
* @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 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 = (
stage: Konva.Stage,
layerState: RegionalGuidanceLayer,
rg: RegionalGuidanceData,
globalMaskLayerOpacity: number,
tool: Tool,
zIndex: number,
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
selectedEntity: CanvasEntity | null,
onPosChanged?: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void
): void => {
const konvaLayer =
stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createRGLayer(stage, layerState, onLayerPosChanged);
const konvaLayer = stage.findOne<Konva.Layer>(`#${rg.id}`) ?? createRGLayer(stage, rg, onPosChanged);
// Update the layer's position and listening state
konvaLayer.setAttrs({
listening: tool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
x: Math.floor(layerState.x),
y: Math.floor(layerState.y),
x: Math.floor(rg.x),
y: Math.floor(rg.y),
zIndex,
});
// 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 =
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.
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
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
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') {
const konvaBrushLine =
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.
if (konvaLayer.visible() !== layerState.isEnabled) {
konvaLayer.visible(layerState.isEnabled);
if (konvaLayer.visible() !== rg.isEnabled) {
konvaLayer.visible(rg.isEnabled);
groupNeedsCache = true;
}
@ -173,6 +173,7 @@ export const renderRGLayer = (
const compositingRect =
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
@ -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
* 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
if (konvaObjectGroup.isCached()) {
konvaObjectGroup.clearCache();
@ -195,7 +196,7 @@ export const renderRGLayer = (
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
...(!layerState.bboxNeedsUpdate && layerState.bbox ? layerState.bbox : getLayerBboxFast(konvaLayer)),
...(!rg.bboxNeedsUpdate && rg.bbox ? rg.bbox : getLayerBboxFast(konvaLayer)),
fill: rgbColor,
opacity: globalMaskLayerOpacity,
// 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);
}
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) {
const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
if (rg.bbox) {
const active = !rg.bboxNeedsUpdate && isSelected && tool === 'move';
bboxRect.setAttrs({
visible: active,
listening: active,
x: layerState.bbox.x,
y: layerState.bbox.y,
width: layerState.bbox.width,
height: layerState.bbox.height,
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
x: rg.bbox.x,
y: rg.bbox.y,
width: rg.bbox.width,
height: rg.bbox.height,
stroke: isSelected ? BBOX_SELECTED_STROKE : '',
});
} else {
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 { atom } from 'nanostores';
import type {
CanvasV2State,
ControlAdapterData,
IPAdapterData,
LayerData,
RegionalGuidanceData,
RgbaColor,
StageAttrs,
Tool,
} from './types';
import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types';
import { DEFAULT_RGBA_COLOR } from './types';
const initialState: CanvasV2State = {
_version: 3,
lastSelectedItem: null,
selectedEntityIdentifier: null,
prompts: {
positivePrompt: '',
negativePrompt: '',
@ -113,6 +104,12 @@ export const canvasV2Slice = createSlice({
invertScrollChanged: (state, action: PayloadAction<boolean>) => {
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) {
builder.addCase(modelChanged, (state, action) => {
@ -146,6 +143,8 @@ export const {
eraserWidthChanged,
fillChanged,
invertScrollChanged,
toolChanged,
toolBufferChanged,
} = canvasV2Slice.actions;
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
// TODO(psyche):
export const $tool = atom<Tool>('brush');
export const $toolBuffer = atom<Tool | null>(null);
export const $brushWidth = atom<number>(0);
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 $toolState = atom<CanvasV2State['tool']>(deepClone(initialState.tool));
export const $currentFill = atom<RgbaColor>(DEFAULT_RGBA_COLOR);
export const $selectedEntity = atom<CanvasEntity | null>(null);
export const $bbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
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 type {
AddBrushLineArg,
AddEraserLineArg,
AddImageObjectArg,
AddPointToLineArg,
AddRectShapeArg,
BrushLineAddedArg,
EraserLineAddedArg,
ImageObjectAddedArg,
LayerData,
PointAddedToLineArg,
RectShapeAddedArg,
} from './types';
import { isLine } from './types';
@ -133,7 +133,7 @@ export const layersSlice = createSlice({
moveToStart(state.layers, layer);
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
@ -149,12 +149,12 @@ export const layersSlice = createSlice({
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddBrushLineArg) => ({
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
@ -169,11 +169,11 @@ export const layersSlice = createSlice({
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddEraserLineArg) => ({
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
layerLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
@ -187,7 +187,7 @@ export const layersSlice = createSlice({
layer.bboxNeedsUpdate = true;
},
layerRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
@ -200,18 +200,15 @@ export const layersSlice = createSlice({
layer.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
x: rect.x - layer.x,
y: rect.y - layer.y,
width: rect.width,
height: rect.height,
...rect,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
layerImageAdded: {
reducer: (state, action: PayloadAction<AddImageObjectArg & { imageId: string }>) => {
reducer: (state, action: PayloadAction<ImageObjectAddedArg & { imageId: string }>) => {
const { id, imageId, imageDTO } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
@ -229,7 +226,7 @@ export const layersSlice = createSlice({
});
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 type {
AddBrushLineArg,
AddEraserLineArg,
AddPointToLineArg,
AddRectShapeArg,
BrushLineAddedArg,
EraserLineAddedArg,
IPAdapterData,
PointAddedToLineArg,
RectShapeAddedArg,
RegionalGuidanceData,
RgbColor,
} from './types';
@ -306,7 +306,7 @@ export const regionalGuidanceSlice = createSlice({
ipa.clipVisionModel = clipVisionModel;
},
rgBrushLineAdded: {
reducer: (state, action: PayloadAction<AddBrushLineArg & { lineId: string }>) => {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
@ -315,21 +315,19 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
points,
strokeWidth: width,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: AddBrushLineArg) => ({
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgEraserLineAdded: {
reducer: (state, action: PayloadAction<AddEraserLineArg & { lineId: string }>) => {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
@ -338,19 +336,17 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener?
points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y],
points,
strokeWidth: width,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: AddEraserLineArg) => ({
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgLinePointAdded: (state, action: PayloadAction<AddPointToLineArg>) => {
rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const rg = selectRg(state, id);
if (!rg) {
@ -360,14 +356,12 @@ export const regionalGuidanceSlice = createSlice({
if (!lastObject || !isLine(lastObject)) {
return;
}
// Points must be offset by the layer's x and y coordinates
// TODO: Handle this in the event listener
lastObject.points.push(point[0] - rg.x, point[1] - rg.y);
lastObject.points.push(...point);
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
rgRectAdded: {
reducer: (state, action: PayloadAction<AddRectShapeArg & { rectId: string }>) => {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
@ -380,16 +374,13 @@ export const regionalGuidanceSlice = createSlice({
rg.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
x: rect.x - rg.x,
y: rect.y - rg.y,
width: rect.width,
height: rect.height,
...rect,
color,
});
rg.bboxNeedsUpdate = true;
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 {
zBeginEndStepPct,
zCLIPVisionModelV2,
@ -243,7 +244,7 @@ const zInpaintMaskData = z.object({
});
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>;
const zControlAdapterData = z.object({
@ -271,21 +272,12 @@ export type ControlAdapterConfig = Pick<
'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode'
>;
const zCanvasItemIdentifier = z.object({
type: z.enum([
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 CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData;
export type CanvasEntityIdentifier = Pick<CanvasEntity, 'id' | 'type'>;
export type CanvasV2State = {
_version: 3;
lastSelectedItem: CanvasItemIdentifier | null;
selectedEntityIdentifier: CanvasEntityIdentifier | null;
prompts: {
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
@ -314,11 +306,17 @@ export type CanvasV2State = {
};
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 AddBrushLineArg = AddEraserLineArg & { color: RgbaColor };
export type AddPointToLineArg = { id: string; point: [number, number] };
export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor };
export type AddImageObjectArg = { id: string; imageDTO: ImageDTO };
export type PosChangedArg = { id: string; x: number; y: number };
export type BboxChangedArg = { id: string; bbox: IRect };
export type EraserLineAddedArg = {
id: string;
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
export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => {

View File

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

View File

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

View File

@ -45,7 +45,7 @@ const DeleteBoardModal = (props: Props) => {
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice],
(canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName)
getImageUsage(canvas, nodes, controlAdapters, canvasV2, imageName)
);
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 => {
const { iterations, model, shouldRandomizeSeed, seed } = state.generation;
const { shouldConcatPrompts } = state.controlLayers.present;
const { shouldConcatPrompts } = state.canvasV2;
const { prompts, seedBehaviour } = state.dynamicPrompts;
const data: Batch['data'] = [];

View File

@ -73,7 +73,7 @@ export const addControlLayers = async (
): Promise<LayerData[]> => {
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);
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;
};
//#endregion
@ -421,7 +421,7 @@ const addInitialImageLayerToGraph = (
) => {
const { vaePrecision } = state.generation;
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.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 state = getStore().getState();
const { layers } = state.controlLayers.present;
const { width, height } = state.controlLayers.present.size;
const { layers } = state.canvasV2;
const { width, height } = state.canvasV2.size;
const reduxLayers = layers.filter(isRegionalGuidanceLayer);
const container = document.createElement('div');
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'>
): Invocation<'l2i'> => {
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
const { width, height } = state.controlLayers.present.size;
const { width, height } = state.canvasV2.size;
const optimalDimension = selectOptimalDimension(state);
const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height);

View File

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

View File

@ -13,7 +13,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamNegativePrompt = memo(() => {
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 activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);

View File

@ -17,7 +17,7 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets';
export const ParamPositivePrompt = memo(() => {
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 viewMode = useAppSelector((s) => s.stylePreset.viewMode);
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s
};
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);

View File

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