mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip generation bbox
This commit is contained in:
parent
ae96c479f2
commit
db90e1fe8b
@ -12,11 +12,12 @@ import {
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
|
||||
import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
||||
import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/previewLayer';
|
||||
import {
|
||||
$brushColor,
|
||||
$brushSize,
|
||||
$brushSpacingPx,
|
||||
$genBbox,
|
||||
$isDrawing,
|
||||
$isMouseDown,
|
||||
$isSpaceDown,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
$stageScale,
|
||||
$tool,
|
||||
$toolBuffer,
|
||||
bboxChanged,
|
||||
brushLineAdded,
|
||||
brushSizeChanged,
|
||||
eraserLineAdded,
|
||||
@ -80,12 +82,7 @@ const selectLayerCount = createSelector(
|
||||
(controlLayers) => controlLayers.present.layers.length
|
||||
);
|
||||
|
||||
const useStageRenderer = (
|
||||
stage: Konva.Stage,
|
||||
container: HTMLDivElement | null,
|
||||
wrapper: HTMLDivElement | null,
|
||||
asPreview: boolean
|
||||
) => {
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const state = useAppSelector((s) => s.controlLayers.present);
|
||||
const tool = useStore($tool);
|
||||
@ -103,6 +100,10 @@ const useStageRenderer = (
|
||||
() => clamp(state.brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX),
|
||||
[state.brushSize]
|
||||
);
|
||||
const bbox = useMemo(
|
||||
() => ({ x: state.x, y: state.y, width: state.size.width, height: state.size.height }),
|
||||
[state.x, state.y, state.size.width, state.size.height]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
$brushColor.set(brushColor);
|
||||
@ -110,6 +111,7 @@ const useStageRenderer = (
|
||||
$brushSpacingPx.set(brushSpacingPx);
|
||||
$selectedLayer.set(selectedLayer);
|
||||
$shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection);
|
||||
$genBbox.set(bbox);
|
||||
}, [
|
||||
brushSpacingPx,
|
||||
brushColor,
|
||||
@ -118,6 +120,7 @@ const useStageRenderer = (
|
||||
state.brushSize,
|
||||
state.selectedLayerId,
|
||||
state.brushColor,
|
||||
bbox,
|
||||
]);
|
||||
|
||||
const onLayerPosChanged = useCallback(
|
||||
@ -164,6 +167,12 @@ const useStageRenderer = (
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const onBboxTransformed = useCallback(
|
||||
(bbox: IRect) => {
|
||||
dispatch(bboxChanged(bbox));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Initializing stage');
|
||||
@ -224,28 +233,23 @@ const useStageRenderer = (
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Updating stage dimensions');
|
||||
if (!wrapper) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fitStageToContainer = () => {
|
||||
const newXScale = wrapper.offsetWidth / state.size.width;
|
||||
const newYScale = wrapper.offsetHeight / state.size.height;
|
||||
const newScale = Math.min(newXScale, newYScale, 1);
|
||||
stage.width(state.size.width * newScale);
|
||||
stage.height(state.size.height * newScale);
|
||||
stage.scaleX(newScale);
|
||||
stage.scaleY(newScale);
|
||||
stage.width(container.offsetWidth);
|
||||
stage.height(container.offsetHeight);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(fitStageToContainer);
|
||||
resizeObserver.observe(wrapper);
|
||||
resizeObserver.observe(container);
|
||||
fitStageToContainer();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [stage, state.size.width, state.size.height, wrapper]);
|
||||
}, [stage, container]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (asPreview) {
|
||||
@ -253,7 +257,7 @@ const useStageRenderer = (
|
||||
return;
|
||||
}
|
||||
log.trace('Rendering tool preview');
|
||||
renderers.renderToolPreview(
|
||||
renderers.renderPreviewLayer(
|
||||
stage,
|
||||
tool,
|
||||
brushColor,
|
||||
@ -263,8 +267,11 @@ const useStageRenderer = (
|
||||
lastMouseDownPos,
|
||||
state.brushSize,
|
||||
isDrawing,
|
||||
isMouseDown
|
||||
isMouseDown,
|
||||
$genBbox,
|
||||
onBboxTransformed
|
||||
);
|
||||
renderImageDimsPreview(stage, bbox, tool);
|
||||
}, [
|
||||
asPreview,
|
||||
stage,
|
||||
@ -278,46 +285,23 @@ const useStageRenderer = (
|
||||
renderers,
|
||||
isDrawing,
|
||||
isMouseDown,
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (asPreview) {
|
||||
// Preview should not display tool
|
||||
return;
|
||||
}
|
||||
log.trace('Rendering tool preview');
|
||||
renderImageDimsPreview(stage, state.size.width, state.size.height, stageScale);
|
||||
}, [
|
||||
asPreview,
|
||||
stage,
|
||||
tool,
|
||||
brushColor,
|
||||
selectedLayer,
|
||||
state.globalMaskLayerOpacity,
|
||||
lastCursorPos,
|
||||
lastMouseDownPos,
|
||||
state.brushSize,
|
||||
renderers,
|
||||
isDrawing,
|
||||
isMouseDown,
|
||||
state.size.width,
|
||||
state.size.height,
|
||||
bbox,
|
||||
stageScale,
|
||||
onBboxTransformed,
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Rendering layers');
|
||||
renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, getImageDTO, onLayerPosChanged);
|
||||
}, [
|
||||
stage,
|
||||
state.layers,
|
||||
state.globalMaskLayerOpacity,
|
||||
tool,
|
||||
onLayerPosChanged,
|
||||
renderers,
|
||||
state.size.width,
|
||||
state.size.height,
|
||||
]);
|
||||
renderers.renderLayers(
|
||||
stage,
|
||||
bbox,
|
||||
state.layers,
|
||||
state.globalMaskLayerOpacity,
|
||||
tool,
|
||||
getImageDTO,
|
||||
onLayerPosChanged
|
||||
);
|
||||
}, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers, bbox]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (asPreview) {
|
||||
@ -349,45 +333,42 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
})
|
||||
);
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const containerRef = useCallback((el: HTMLDivElement | null) => {
|
||||
setContainer(el);
|
||||
}, []);
|
||||
|
||||
const wrapperRef = useCallback((el: HTMLDivElement | null) => {
|
||||
setWrapper(el);
|
||||
}, []);
|
||||
|
||||
useStageRenderer(stage, container, wrapper, asPreview);
|
||||
useStageRenderer(stage, container, asPreview);
|
||||
|
||||
return (
|
||||
<Flex overflow="hidden" w="full" h="full">
|
||||
<Flex position="relative" ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Box
|
||||
position="absolute"
|
||||
w="full"
|
||||
h="full"
|
||||
borderRadius="base"
|
||||
backgroundImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||
backgroundRepeat="repeat"
|
||||
opacity={0.2}
|
||||
/>
|
||||
{layerCount === 0 && !asPreview && (
|
||||
<Flex position="absolute" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Heading color="base.200">{t('controlLayers.noLayersAdded')}</Heading>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex position="relative" w="full" h="full">
|
||||
<Box
|
||||
position="absolute"
|
||||
w="full"
|
||||
h="full"
|
||||
borderRadius="base"
|
||||
backgroundImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||
backgroundRepeat="repeat"
|
||||
opacity={0.2}
|
||||
/>
|
||||
{layerCount === 0 && !asPreview && (
|
||||
<Flex position="absolute" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Heading color="base.200">{t('controlLayers.noLayersAdded')}</Heading>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
borderRadius="base"
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
border="1px solid red"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
ref={containerRef}
|
||||
tabIndex={-1}
|
||||
borderRadius="base"
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ import { clamp } from 'lodash-es';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
|
||||
import { TOOL_PREVIEW_TOOL_GROUP_ID } from './naming';
|
||||
import { PREVIEW_TOOL_GROUP_ID } from './naming';
|
||||
|
||||
type SetStageEventHandlersArg = {
|
||||
stage: Konva.Stage;
|
||||
@ -114,7 +114,7 @@ export const setStageEventHandlers = ({
|
||||
return;
|
||||
}
|
||||
const tool = $tool.get();
|
||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||
});
|
||||
|
||||
//#region mousedown
|
||||
@ -219,7 +219,7 @@ export const setStageEventHandlers = ({
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos);
|
||||
const selectedLayer = $selectedLayer.get();
|
||||
|
||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||
|
||||
if (!pos || !selectedLayer) {
|
||||
return;
|
||||
@ -277,7 +277,7 @@ export const setStageEventHandlers = ({
|
||||
const selectedLayer = $selectedLayer.get();
|
||||
const tool = $tool.get();
|
||||
|
||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
|
||||
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
|
||||
|
||||
if (!pos || !selectedLayer) {
|
||||
return;
|
||||
@ -338,6 +338,12 @@ export const setStageEventHandlers = ({
|
||||
}
|
||||
});
|
||||
|
||||
stage.on('dragend', () => {
|
||||
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
||||
stage.x(Math.floor(stage.x()));
|
||||
stage.y(Math.floor(stage.y()));
|
||||
});
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.repeat) {
|
||||
return;
|
||||
@ -367,7 +373,7 @@ export const setStageEventHandlers = ({
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
return () => {
|
||||
stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel');
|
||||
stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel dragend');
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
};
|
||||
|
@ -3,15 +3,16 @@
|
||||
*/
|
||||
|
||||
// IDs for singleton Konva layers and objects
|
||||
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
||||
export const TOOL_PREVIEW_TOOL_GROUP_ID = 'tool_preview_layer.tool_group';
|
||||
export const TOOL_PREVIEW_BRUSH_GROUP_ID = 'tool_preview_layer.brush_group';
|
||||
export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill';
|
||||
export const TOOL_PREVIEW_BRUSH_BORDER_INNER_ID = 'tool_preview_layer.brush_border_inner';
|
||||
export const TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer';
|
||||
export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect';
|
||||
export const TOOL_PREVIEW_IMAGE_DIMS_RECT = 'tool_preview_layer.image_dims_rect';
|
||||
|
||||
export const PREVIEW_LAYER_ID = 'preview_layer';
|
||||
export const PREVIEW_TOOL_GROUP_ID = 'preview_layer.tool_group';
|
||||
export const PREVIEW_BRUSH_GROUP_ID = 'preview_layer.brush_group';
|
||||
export const PREVIEW_BRUSH_FILL_ID = 'preview_layer.brush_fill';
|
||||
export const PREVIEW_BRUSH_BORDER_INNER_ID = 'preview_layer.brush_border_inner';
|
||||
export const PREVIEW_BRUSH_BORDER_OUTER_ID = 'preview_layer.brush_border_outer';
|
||||
export const PREVIEW_RECT_ID = 'preview_layer.rect';
|
||||
export const PREVIEW_GENERATION_BBOX_GROUP = 'preview_layer.gen_bbox_group';
|
||||
export const PREVIEW_GENERATION_BBOX_TRANSFORMER = 'preview_layer.gen_bbox_transformer';
|
||||
export const PREVIEW_GENERATION_BBOX_DUMMY_RECT = 'preview_layer.gen_bbox_dummy_rect';
|
||||
|
||||
// Names for Konva layers and objects (comparable to CSS classes)
|
||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||
|
@ -2,6 +2,7 @@ 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 Konva from 'konva';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
@ -18,7 +19,7 @@ const createCALayer = (stage: Konva.Stage, layerState: ControlAdapterLayer): Kon
|
||||
const konvaLayer = new Konva.Layer({
|
||||
id: layerState.id,
|
||||
name: CA_LAYER_NAME,
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingEnabled: false,
|
||||
listening: false,
|
||||
});
|
||||
stage.add(konvaLayer);
|
||||
@ -51,6 +52,7 @@ const updateCALayerImageSource = async (
|
||||
stage: Konva.Stage,
|
||||
konvaLayer: Konva.Layer,
|
||||
layerState: ControlAdapterLayer,
|
||||
bbox: IRect,
|
||||
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||
): Promise<void> => {
|
||||
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
|
||||
@ -72,7 +74,7 @@ const updateCALayerImageSource = async (
|
||||
id: imageId,
|
||||
image: imageEl,
|
||||
});
|
||||
updateCALayerImageAttrs(stage, konvaImage, layerState);
|
||||
updateCALayerImageAttrs(stage, konvaImage, layerState, bbox);
|
||||
// Must cache after this to apply the filters
|
||||
konvaImage.cache();
|
||||
imageEl.id = imageId;
|
||||
@ -93,18 +95,19 @@ const updateCALayerImageSource = async (
|
||||
const updateCALayerImageAttrs = (
|
||||
stage: Konva.Stage,
|
||||
konvaImage: Konva.Image,
|
||||
layerState: ControlAdapterLayer
|
||||
layerState: ControlAdapterLayer,
|
||||
bbox: IRect
|
||||
): 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 newWidth = stage.width() / stage.scaleX();
|
||||
const newHeight = stage.height() / stage.scaleY();
|
||||
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
||||
if (
|
||||
konvaImage.width() !== newWidth ||
|
||||
konvaImage.height() !== newHeight ||
|
||||
konvaImage.x() !== bbox.x ||
|
||||
konvaImage.y() !== bbox.y ||
|
||||
konvaImage.width() !== bbox.width ||
|
||||
konvaImage.height() !== bbox.height ||
|
||||
konvaImage.visible() !== layerState.isEnabled ||
|
||||
hasFilter !== layerState.isFilterEnabled
|
||||
) {
|
||||
@ -112,8 +115,7 @@ const updateCALayerImageAttrs = (
|
||||
opacity: layerState.opacity,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: stage.width() / stage.scaleX(),
|
||||
height: stage.height() / stage.scaleY(),
|
||||
...bbox,
|
||||
visible: layerState.isEnabled,
|
||||
filters: layerState.isFilterEnabled ? [LightnessToAlphaFilter] : [],
|
||||
});
|
||||
@ -137,12 +139,19 @@ const updateCALayerImageAttrs = (
|
||||
export const renderCALayer = (
|
||||
stage: Konva.Stage,
|
||||
layerState: ControlAdapterLayer,
|
||||
bbox: IRect,
|
||||
zIndex: number,
|
||||
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||
): void => {
|
||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createCALayer(stage, layerState);
|
||||
|
||||
konvaLayer.zIndex(zIndex);
|
||||
|
||||
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`);
|
||||
const canvasImageSource = konvaImage?.image();
|
||||
|
||||
let imageSourceNeedsUpdate = false;
|
||||
|
||||
if (canvasImageSource instanceof HTMLImageElement) {
|
||||
const image = layerState.controlAdapter.processedImage ?? layerState.controlAdapter.image;
|
||||
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
|
||||
@ -155,8 +164,8 @@ export const renderCALayer = (
|
||||
}
|
||||
|
||||
if (imageSourceNeedsUpdate) {
|
||||
updateCALayerImageSource(stage, konvaLayer, layerState, getImageDTO);
|
||||
updateCALayerImageSource(stage, konvaLayer, layerState, bbox, getImageDTO);
|
||||
} else if (konvaImage) {
|
||||
updateCALayerImageAttrs(stage, konvaImage, layerState);
|
||||
updateCALayerImageAttrs(stage, konvaImage, layerState, bbox);
|
||||
}
|
||||
};
|
||||
|
@ -124,12 +124,18 @@ const updateIILayerImageSource = async (
|
||||
export const renderIILayer = (
|
||||
stage: Konva.Stage,
|
||||
layerState: InitialImageLayer,
|
||||
zIndex: number,
|
||||
getImageDTO: (imageName: string) => Promise<ImageDTO | null>
|
||||
): void => {
|
||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`) ?? createIILayer(stage, layerState);
|
||||
|
||||
konvaLayer.zIndex(zIndex);
|
||||
|
||||
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`);
|
||||
const canvasImageSource = konvaImage?.image();
|
||||
|
||||
let imageSourceNeedsUpdate = false;
|
||||
|
||||
if (canvasImageSource instanceof HTMLImageElement) {
|
||||
const image = layerState.image;
|
||||
if (image && canvasImageSource.id !== getCALayerImageId(layerState.id, image.name)) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants';
|
||||
import { TOOL_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 { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
|
||||
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer';
|
||||
import { renderPreviewLayer } from 'features/controlLayers/konva/renderers/previewLayer';
|
||||
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
|
||||
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
|
||||
import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
||||
import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util';
|
||||
import type { Layer, Tool } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
@ -16,6 +16,7 @@ import {
|
||||
isRenderableLayer,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@ -34,6 +35,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
*/
|
||||
const renderLayers = (
|
||||
stage: Konva.Stage,
|
||||
bbox: IRect,
|
||||
layerStates: Layer[],
|
||||
globalMaskLayerOpacity: number,
|
||||
tool: Tool,
|
||||
@ -48,33 +50,33 @@ const renderLayers = (
|
||||
}
|
||||
}
|
||||
// We'll need to ensure the tool preview layer is on top of the rest of the layers
|
||||
let toolLayerZIndex = 0;
|
||||
let zIndex = 0;
|
||||
for (const layer of layerStates) {
|
||||
if (isRegionalGuidanceLayer(layer)) {
|
||||
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged);
|
||||
}
|
||||
if (isControlAdapterLayer(layer)) {
|
||||
renderCALayer(stage, layer, getImageDTO);
|
||||
renderCALayer(stage, layer, bbox, zIndex, getImageDTO);
|
||||
}
|
||||
if (isInitialImageLayer(layer)) {
|
||||
renderIILayer(stage, layer, getImageDTO);
|
||||
renderIILayer(stage, layer, zIndex, getImageDTO);
|
||||
}
|
||||
if (isRasterLayer(layer)) {
|
||||
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
|
||||
renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged);
|
||||
}
|
||||
// IP Adapter layers are not rendered
|
||||
// Increment the z-index for the tool layer
|
||||
toolLayerZIndex++;
|
||||
zIndex++;
|
||||
}
|
||||
// Arrange the tool preview layer
|
||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(toolLayerZIndex);
|
||||
stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`)?.zIndex(zIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* All the renderers for the Konva stage.
|
||||
*/
|
||||
export const renderers = {
|
||||
renderToolPreview,
|
||||
renderPreviewLayer,
|
||||
renderLayers,
|
||||
updateBboxes,
|
||||
};
|
||||
@ -85,7 +87,7 @@ export const renderers = {
|
||||
* @returns The renderers with debouncing applied
|
||||
*/
|
||||
const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({
|
||||
renderToolPreview: debounce(renderToolPreview, ms),
|
||||
renderPreviewLayer: debounce(renderPreviewLayer, ms),
|
||||
renderLayers: debounce(renderLayers, ms),
|
||||
updateBboxes: debounce(updateBboxes, ms),
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
getLayerBboxId,
|
||||
getObjectGroupId,
|
||||
LAYER_BBOX_NAME,
|
||||
TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||
} from 'features/controlLayers/konva/naming';
|
||||
import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types';
|
||||
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||
@ -206,7 +206,7 @@ export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva.
|
||||
|
||||
export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => {
|
||||
const imageDimsPreview = new Konva.Rect({
|
||||
id: TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||
id: PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
|
@ -0,0 +1,301 @@
|
||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
import {
|
||||
BBOX_SELECTED_STROKE,
|
||||
BRUSH_BORDER_INNER_COLOR,
|
||||
BRUSH_BORDER_OUTER_COLOR,
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import {
|
||||
PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||
PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||
PREVIEW_BRUSH_FILL_ID,
|
||||
PREVIEW_BRUSH_GROUP_ID,
|
||||
PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||
PREVIEW_GENERATION_BBOX_GROUP,
|
||||
PREVIEW_GENERATION_BBOX_TRANSFORMER,
|
||||
PREVIEW_LAYER_ID,
|
||||
PREVIEW_RECT_ID,
|
||||
PREVIEW_TOOL_GROUP_ID,
|
||||
} from 'features/controlLayers/konva/naming';
|
||||
import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util';
|
||||
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* Creates the singleton preview layer and all its objects.
|
||||
* @param stage The konva stage
|
||||
*/
|
||||
const getPreviewLayer = (
|
||||
stage: Konva.Stage,
|
||||
$genBbox: WritableAtom<IRect>,
|
||||
onBboxTransformed: (bbox: IRect) => void
|
||||
): Konva.Layer => {
|
||||
let previewLayer = stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`);
|
||||
if (previewLayer) {
|
||||
return previewLayer;
|
||||
}
|
||||
// Initialize the preview layer & add to the stage
|
||||
previewLayer = new Konva.Layer({ id: PREVIEW_LAYER_ID, listening: true });
|
||||
stage.add(previewLayer);
|
||||
|
||||
// Create the brush preview group & circles
|
||||
const brushPreviewGroup = new Konva.Group({ id: PREVIEW_BRUSH_GROUP_ID });
|
||||
const brushPreviewFill = new Konva.Circle({
|
||||
id: PREVIEW_BRUSH_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewFill);
|
||||
const brushPreviewBorderInner = new Konva.Circle({
|
||||
id: PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewBorderInner);
|
||||
const brushPreviewBorderOuter = new Konva.Circle({
|
||||
id: PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||
|
||||
// Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
|
||||
const rectPreview = new Konva.Rect({
|
||||
id: PREVIEW_RECT_ID,
|
||||
listening: false,
|
||||
stroke: BBOX_SELECTED_STROKE,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
|
||||
const toolGroup = new Konva.Group({ id: PREVIEW_TOOL_GROUP_ID });
|
||||
|
||||
toolGroup.add(rectPreview);
|
||||
toolGroup.add(brushPreviewGroup);
|
||||
|
||||
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
||||
// transparent rect for this purpose.
|
||||
const generationBboxGroup = new Konva.Group({ id: PREVIEW_GENERATION_BBOX_GROUP });
|
||||
const generationBboxDummyRect = new Konva.Rect({
|
||||
id: PREVIEW_GENERATION_BBOX_DUMMY_RECT,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
generationBboxDummyRect.on('dragmove', (e) => {
|
||||
const bbox: IRect = {
|
||||
x: roundToMultiple(Math.round(generationBboxDummyRect.x()), 64),
|
||||
y: roundToMultiple(Math.round(generationBboxDummyRect.y()), 64),
|
||||
width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()),
|
||||
height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()),
|
||||
};
|
||||
generationBboxDummyRect.setAttrs(bbox);
|
||||
const genBbox = $genBbox.get();
|
||||
if (
|
||||
genBbox.x !== bbox.x ||
|
||||
genBbox.y !== bbox.y ||
|
||||
genBbox.width !== bbox.width ||
|
||||
genBbox.height !== bbox.height
|
||||
) {
|
||||
onBboxTransformed(bbox);
|
||||
}
|
||||
});
|
||||
const generationBboxTransformer = new Konva.Transformer({
|
||||
id: PREVIEW_GENERATION_BBOX_TRANSFORMER,
|
||||
borderDash: [5, 5],
|
||||
borderStroke: 'rgba(212,216,234,1)',
|
||||
borderEnabled: true,
|
||||
rotateEnabled: false,
|
||||
keepRatio: false,
|
||||
ignoreStroke: true,
|
||||
listening: false,
|
||||
flipEnabled: false,
|
||||
anchorFill: 'rgba(212,216,234,1)',
|
||||
anchorStroke: 'rgb(42,42,42)',
|
||||
anchorSize: 12,
|
||||
anchorCornerRadius: 3,
|
||||
anchorStyleFunc: (anchor) => {
|
||||
// Make the x/y resize anchors little bars
|
||||
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
|
||||
anchor.height(8);
|
||||
anchor.offsetY(4);
|
||||
anchor.width(30);
|
||||
anchor.offsetX(15);
|
||||
}
|
||||
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
|
||||
anchor.height(30);
|
||||
anchor.offsetY(15);
|
||||
anchor.width(8);
|
||||
anchor.offsetX(4);
|
||||
}
|
||||
},
|
||||
});
|
||||
generationBboxTransformer.on('transform', (e) => {
|
||||
const bbox: IRect = {
|
||||
x: Math.round(generationBboxDummyRect.x()),
|
||||
y: Math.round(generationBboxDummyRect.y()),
|
||||
width: Math.round(generationBboxDummyRect.width() * generationBboxDummyRect.scaleX()),
|
||||
height: Math.round(generationBboxDummyRect.height() * generationBboxDummyRect.scaleY()),
|
||||
};
|
||||
generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1 });
|
||||
onBboxTransformed(bbox);
|
||||
});
|
||||
// The transformer will always be transforming the dummy rect
|
||||
generationBboxTransformer.nodes([generationBboxDummyRect]);
|
||||
generationBboxGroup.add(generationBboxDummyRect);
|
||||
generationBboxGroup.add(generationBboxTransformer);
|
||||
previewLayer.add(toolGroup);
|
||||
previewLayer.add(generationBboxGroup);
|
||||
|
||||
return previewLayer;
|
||||
};
|
||||
|
||||
const ALL_ANCHORS: string[] = [
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'middle-right',
|
||||
'middle-left',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
const NO_ANCHORS: string[] = [];
|
||||
|
||||
export const renderImageDimsPreview = (stage: Konva.Stage, bbox: IRect, tool: Tool): void => {
|
||||
const previewLayer = stage.findOne<Konva.Layer>(`#${PREVIEW_LAYER_ID}`);
|
||||
const generationBboxGroup = stage.findOne<Konva.Rect>(`#${PREVIEW_GENERATION_BBOX_GROUP}`);
|
||||
const generationBboxDummyRect = stage.findOne<Konva.Rect>(`#${PREVIEW_GENERATION_BBOX_DUMMY_RECT}`);
|
||||
const generationBboxTransformer = stage.findOne<Konva.Transformer>(`#${PREVIEW_GENERATION_BBOX_TRANSFORMER}`);
|
||||
assert(
|
||||
previewLayer && generationBboxGroup && generationBboxDummyRect && generationBboxTransformer,
|
||||
'Generation bbox konva objects not found'
|
||||
);
|
||||
generationBboxDummyRect.setAttrs({ ...bbox, scaleX: 1, scaleY: 1, listening: tool === 'move' });
|
||||
generationBboxTransformer.setAttrs({
|
||||
listening: tool === 'move',
|
||||
enabledAnchors: tool === 'move' ? ALL_ANCHORS : NO_ANCHORS,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* @param brushSize The brush size
|
||||
*/
|
||||
export const renderPreviewLayer = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
brushColor: RgbaColor,
|
||||
selectedLayerType: Layer['type'] | null,
|
||||
globalMaskLayerOpacity: number,
|
||||
cursorPos: Vector2d | null,
|
||||
lastMouseDownPos: Vector2d | null,
|
||||
brushSize: number,
|
||||
isDrawing: boolean,
|
||||
isMouseDown: boolean,
|
||||
$genBbox: WritableAtom<IRect>,
|
||||
onBboxTransformed: (bbox: IRect) => void
|
||||
): void => {
|
||||
const layerCount = stage.find(selectRenderableLayers).length;
|
||||
// Update the stage's pointer style
|
||||
if (tool === 'view') {
|
||||
// View gets a hand
|
||||
stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab';
|
||||
} 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') {
|
||||
// Non-mask-guidance layers don't have tools
|
||||
stage.container().style.cursor = 'not-allowed';
|
||||
} else if (tool === 'move') {
|
||||
// Move tool gets a pointer
|
||||
stage.container().style.cursor = 'default';
|
||||
} else if (tool === 'rect') {
|
||||
// Rect gets a crosshair
|
||||
stage.container().style.cursor = 'crosshair';
|
||||
} else {
|
||||
// Else we hide the native cursor and use the konva-rendered brush preview
|
||||
stage.container().style.cursor = 'none';
|
||||
}
|
||||
|
||||
stage.draggable(tool === 'view');
|
||||
|
||||
const previewLayer = getPreviewLayer(stage, $genBbox, onBboxTransformed);
|
||||
const toolGroup = previewLayer.findOne<Konva.Group>(`#${PREVIEW_TOOL_GROUP_ID}`);
|
||||
|
||||
assert(toolGroup, 'Tool group not found');
|
||||
|
||||
if (
|
||||
!cursorPos ||
|
||||
layerCount === 0 ||
|
||||
(selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer')
|
||||
) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
toolGroup.visible(false);
|
||||
} else {
|
||||
toolGroup.visible(true);
|
||||
|
||||
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${PREVIEW_BRUSH_GROUP_ID}`);
|
||||
assert(brushPreviewGroup, 'Brush preview group not found');
|
||||
|
||||
const rectPreview = stage.findOne<Konva.Rect>(`#${PREVIEW_RECT_ID}`);
|
||||
assert(rectPreview, 'Rect preview not found');
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
|
||||
// Update the fill circle
|
||||
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_FILL_ID}`);
|
||||
brushPreviewFill?.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2,
|
||||
fill: isDrawing ? '' : rgbaColorToString(brushColor),
|
||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
});
|
||||
|
||||
// Update the inner border of the brush preview
|
||||
const brushPreviewInner = previewLayer.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_INNER_ID}`);
|
||||
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||
|
||||
// Update the outer border of the brush preview
|
||||
const brushPreviewOuter = previewLayer.findOne<Konva.Circle>(`#${PREVIEW_BRUSH_BORDER_OUTER_ID}`);
|
||||
brushPreviewOuter?.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2 + 1,
|
||||
});
|
||||
|
||||
brushPreviewGroup.visible(true);
|
||||
} else {
|
||||
brushPreviewGroup.visible(false);
|
||||
}
|
||||
|
||||
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
||||
const snappedPos = snapPosToStage(cursorPos, stage);
|
||||
const rectPreview = previewLayer.findOne<Konva.Rect>(`#${PREVIEW_RECT_ID}`);
|
||||
rectPreview?.setAttrs({
|
||||
x: Math.min(snappedPos.x, lastMouseDownPos.x),
|
||||
y: Math.min(snappedPos.y, lastMouseDownPos.y),
|
||||
width: Math.abs(snappedPos.x - lastMouseDownPos.x),
|
||||
height: Math.abs(snappedPos.y - lastMouseDownPos.y),
|
||||
fill: rgbaColorToString(brushColor),
|
||||
});
|
||||
rectPreview?.visible(true);
|
||||
} else {
|
||||
rectPreview?.visible(false);
|
||||
}
|
||||
}
|
||||
};
|
@ -67,6 +67,7 @@ export const renderRasterLayer = async (
|
||||
stage: Konva.Stage,
|
||||
layerState: RasterLayer,
|
||||
tool: Tool,
|
||||
zIndex: number,
|
||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||
) => {
|
||||
const konvaLayer =
|
||||
@ -77,6 +78,7 @@ export const renderRasterLayer = async (
|
||||
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),
|
||||
zIndex,
|
||||
});
|
||||
|
||||
const konvaObjectGroup =
|
||||
|
@ -83,6 +83,7 @@ export const renderRGLayer = (
|
||||
layerState: RegionalGuidanceLayer,
|
||||
globalMaskLayerOpacity: number,
|
||||
tool: Tool,
|
||||
zIndex: number,
|
||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||
): void => {
|
||||
const konvaLayer =
|
||||
@ -93,6 +94,7 @@ export const renderRGLayer = (
|
||||
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),
|
||||
zIndex,
|
||||
});
|
||||
|
||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||
|
@ -1,212 +0,0 @@
|
||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||
import {
|
||||
BBOX_SELECTED_STROKE,
|
||||
BRUSH_BORDER_INNER_COLOR,
|
||||
BRUSH_BORDER_OUTER_COLOR,
|
||||
} from 'features/controlLayers/konva/constants';
|
||||
import {
|
||||
TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||
TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
||||
TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||
TOOL_PREVIEW_LAYER_ID,
|
||||
TOOL_PREVIEW_RECT_ID,
|
||||
TOOL_PREVIEW_TOOL_GROUP_ID,
|
||||
} from 'features/controlLayers/konva/naming';
|
||||
import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util';
|
||||
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* Logic to create and render the singleton tool preview layer.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates the singleton tool preview layer and all its objects.
|
||||
* @param stage The konva stage
|
||||
*/
|
||||
const getToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||
let toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`);
|
||||
if (toolPreviewLayer) {
|
||||
return toolPreviewLayer;
|
||||
}
|
||||
// Initialize the brush preview layer & add to the stage
|
||||
toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, listening: false });
|
||||
stage.add(toolPreviewLayer);
|
||||
|
||||
// Create the brush preview group & circles
|
||||
const brushPreviewGroup = new Konva.Group({ id: TOOL_PREVIEW_BRUSH_GROUP_ID });
|
||||
const brushPreviewFill = new Konva.Circle({
|
||||
id: TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||
listening: false,
|
||||
strokeEnabled: false,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewFill);
|
||||
const brushPreviewBorderInner = new Konva.Circle({
|
||||
id: TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_INNER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewBorderInner);
|
||||
const brushPreviewBorderOuter = new Konva.Circle({
|
||||
id: TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||
listening: false,
|
||||
stroke: BRUSH_BORDER_OUTER_COLOR,
|
||||
strokeWidth: 1,
|
||||
strokeEnabled: true,
|
||||
});
|
||||
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||
|
||||
// Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
|
||||
const rectPreview = new Konva.Rect({
|
||||
id: TOOL_PREVIEW_RECT_ID,
|
||||
listening: false,
|
||||
stroke: BBOX_SELECTED_STROKE,
|
||||
strokeWidth: 1,
|
||||
});
|
||||
|
||||
const toolGroup = new Konva.Group({ id: TOOL_PREVIEW_TOOL_GROUP_ID });
|
||||
|
||||
toolGroup.add(rectPreview);
|
||||
toolGroup.add(brushPreviewGroup);
|
||||
|
||||
const imageDimsPreview = new Konva.Rect({
|
||||
id: TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
stroke: 'rgb(255,0,255)',
|
||||
strokeWidth: 1 / toolPreviewLayer.getStage().scaleX(),
|
||||
listening: false,
|
||||
});
|
||||
|
||||
toolPreviewLayer.add(toolGroup);
|
||||
toolPreviewLayer.add(imageDimsPreview);
|
||||
|
||||
return toolPreviewLayer;
|
||||
};
|
||||
|
||||
export const renderImageDimsPreview = (stage: Konva.Stage, width: number, height: number, stageScale: number): void => {
|
||||
const imageDimsPreview = stage.findOne<Konva.Rect>(`#${TOOL_PREVIEW_IMAGE_DIMS_RECT}`);
|
||||
imageDimsPreview?.setAttrs({
|
||||
width,
|
||||
height,
|
||||
strokeWidth: 1 / stageScale,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the brush preview for the selected tool.
|
||||
* @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 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
|
||||
* @param brushSize The brush size
|
||||
*/
|
||||
export const renderToolPreview = (
|
||||
stage: Konva.Stage,
|
||||
tool: Tool,
|
||||
brushColor: RgbaColor,
|
||||
selectedLayerType: Layer['type'] | null,
|
||||
globalMaskLayerOpacity: number,
|
||||
cursorPos: Vector2d | null,
|
||||
lastMouseDownPos: Vector2d | null,
|
||||
brushSize: number,
|
||||
isDrawing: boolean,
|
||||
isMouseDown: boolean
|
||||
): void => {
|
||||
const layerCount = stage.find(selectRenderableLayers).length;
|
||||
// Update the stage's pointer style
|
||||
if (tool === 'view') {
|
||||
// View gets a hand
|
||||
stage.container().style.cursor = isMouseDown ? 'grabbing' : 'grab';
|
||||
} 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') {
|
||||
// Non-mask-guidance layers don't have tools
|
||||
stage.container().style.cursor = 'not-allowed';
|
||||
} else if (tool === 'move') {
|
||||
// Move tool gets a pointer
|
||||
stage.container().style.cursor = 'default';
|
||||
} else if (tool === 'rect') {
|
||||
// Rect gets a crosshair
|
||||
stage.container().style.cursor = 'crosshair';
|
||||
} else {
|
||||
// Else we hide the native cursor and use the konva-rendered brush preview
|
||||
stage.container().style.cursor = 'none';
|
||||
}
|
||||
|
||||
stage.draggable(tool === 'view');
|
||||
|
||||
const toolPreviewLayer = getToolPreviewLayer(stage);
|
||||
const toolGroup = toolPreviewLayer.findOne<Konva.Group>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`);
|
||||
|
||||
assert(toolGroup, 'Tool group not found');
|
||||
|
||||
if (!cursorPos || layerCount === 0) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
toolGroup.visible(false);
|
||||
} else {
|
||||
toolGroup.visible(true);
|
||||
|
||||
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`);
|
||||
assert(brushPreviewGroup, 'Brush preview group not found');
|
||||
|
||||
const rectPreview = stage.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||
assert(rectPreview, 'Rect preview not found');
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
|
||||
// Update the fill circle
|
||||
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
||||
brushPreviewFill?.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2,
|
||||
fill: isDrawing ? '' : rgbaColorToString(brushColor),
|
||||
globalCompositeOperation: tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
});
|
||||
|
||||
// Update the inner border of the brush preview
|
||||
const brushPreviewInner = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_INNER_ID}`);
|
||||
brushPreviewInner?.setAttrs({ x: cursorPos.x, y: cursorPos.y, radius: brushSize / 2 });
|
||||
|
||||
// Update the outer border of the brush preview
|
||||
const brushPreviewOuter = toolPreviewLayer.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID}`);
|
||||
brushPreviewOuter?.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
radius: brushSize / 2 + 1,
|
||||
});
|
||||
|
||||
brushPreviewGroup.visible(true);
|
||||
} else {
|
||||
brushPreviewGroup.visible(false);
|
||||
}
|
||||
|
||||
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
||||
const snappedPos = snapPosToStage(cursorPos, stage);
|
||||
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||
rectPreview?.setAttrs({
|
||||
x: Math.min(snappedPos.x, lastMouseDownPos.x),
|
||||
y: Math.min(snappedPos.y, lastMouseDownPos.y),
|
||||
width: Math.abs(snappedPos.x - lastMouseDownPos.x),
|
||||
height: Math.abs(snappedPos.y - lastMouseDownPos.y),
|
||||
fill: rgbaColorToString(brushColor),
|
||||
});
|
||||
rectPreview?.visible(true);
|
||||
} else {
|
||||
rectPreview?.visible(false);
|
||||
}
|
||||
}
|
||||
};
|
@ -92,6 +92,8 @@ export const initialControlLayersState: ControlLayersState = {
|
||||
height: 512,
|
||||
aspectRatio: deepClone(initialAspectRatioState),
|
||||
},
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -797,6 +799,12 @@ export const controlLayersSlice = createSlice({
|
||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||
state.size.aspectRatio = action.payload;
|
||||
},
|
||||
bboxChanged: (state, action: PayloadAction<IRect>) => {
|
||||
state.x = action.payload.x;
|
||||
state.y = action.payload.y;
|
||||
state.size.width = action.payload.width;
|
||||
state.size.height = action.payload.height;
|
||||
},
|
||||
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
||||
state.brushSize = Math.round(action.payload);
|
||||
},
|
||||
@ -950,6 +958,7 @@ export const {
|
||||
widthChanged,
|
||||
heightChanged,
|
||||
aspectRatioChanged,
|
||||
bboxChanged,
|
||||
brushSizeChanged,
|
||||
brushColorChanged,
|
||||
globalMaskLayerOpacityChanged,
|
||||
@ -989,6 +998,7 @@ export const $lastAddedPoint = atom<Vector2d | null>(null);
|
||||
export const $isSpaceDown = atom(false);
|
||||
export const $stageScale = atom<number>(1);
|
||||
export const $stagePos = atom<Vector2d>({ x: 0, y: 0 });
|
||||
export const $genBbox = atom<IRect>({ x: 0, y: 0, width: 0, height: 0 });
|
||||
|
||||
// Some nanostores that are manually synced to redux state to provide imperative access
|
||||
// TODO(psyche): This is a hack, figure out another way to handle this...
|
||||
|
@ -269,6 +269,8 @@ export type ControlLayersState = {
|
||||
height: ParameterHeight;
|
||||
aspectRatio: AspectRatioState;
|
||||
};
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] };
|
||||
|
Loading…
x
Reference in New Issue
Block a user