mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): CL zoom and pan, some rendering optimizations
This commit is contained in:
parent
1f58e5756b
commit
d8a83acd3a
@ -1,23 +1,34 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex, Heading } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { BRUSH_SPACING_PCT, MAX_BRUSH_SPACING_PX, MIN_BRUSH_SPACING_PX } from 'features/controlLayers/konva/constants';
|
import {
|
||||||
|
BRUSH_SPACING_PCT,
|
||||||
|
MAX_BRUSH_SPACING_PX,
|
||||||
|
MIN_BRUSH_SPACING_PX,
|
||||||
|
TRANSPARENCY_CHECKER_PATTERN,
|
||||||
|
} from 'features/controlLayers/konva/constants';
|
||||||
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
|
||||||
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
|
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
|
||||||
|
import { renderImageDimsPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
||||||
import {
|
import {
|
||||||
$brushColor,
|
$brushColor,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
|
$isMouseDown,
|
||||||
|
$isSpaceDown,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$selectedLayer,
|
$selectedLayer,
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
|
$stagePos,
|
||||||
|
$stageScale,
|
||||||
$tool,
|
$tool,
|
||||||
|
$toolBuffer,
|
||||||
brushLineAdded,
|
brushLineAdded,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
eraserLineAdded,
|
eraserLineAdded,
|
||||||
@ -38,6 +49,7 @@ import Konva from 'konva';
|
|||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getImageDTO } from 'services/api/endpoints/images';
|
import { getImageDTO } from 'services/api/endpoints/images';
|
||||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -63,6 +75,11 @@ const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLay
|
|||||||
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
|
return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectLayerCount = createSelector(
|
||||||
|
selectControlLayersSlice,
|
||||||
|
(controlLayers) => controlLayers.present.layers.length
|
||||||
|
);
|
||||||
|
|
||||||
const useStageRenderer = (
|
const useStageRenderer = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
container: HTMLDivElement | null,
|
container: HTMLDivElement | null,
|
||||||
@ -74,11 +91,11 @@ const useStageRenderer = (
|
|||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
const lastCursorPos = useStore($lastCursorPos);
|
const lastCursorPos = useStore($lastCursorPos);
|
||||||
const lastMouseDownPos = useStore($lastMouseDownPos);
|
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||||
|
const isMouseDown = useStore($isMouseDown);
|
||||||
|
const stageScale = useStore($stageScale);
|
||||||
const isDrawing = useStore($isDrawing);
|
const isDrawing = useStore($isDrawing);
|
||||||
const brushColor = useAppSelector(selectBrushColor);
|
const brushColor = useAppSelector(selectBrushColor);
|
||||||
const selectedLayer = useAppSelector(selectSelectedLayer);
|
const selectedLayer = useAppSelector(selectSelectedLayer);
|
||||||
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
|
|
||||||
const layerCount = useMemo(() => state.layers.length, [state.layers]);
|
|
||||||
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
||||||
const dpr = useDevicePixelRatio({ round: false });
|
const dpr = useDevicePixelRatio({ round: false });
|
||||||
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
||||||
@ -166,28 +183,23 @@ const useStageRenderer = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelShape = (e: KeyboardEvent) => {
|
|
||||||
// Cancel shape drawing on escape
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
$isDrawing.set(false);
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
container.addEventListener('keydown', cancelShape);
|
|
||||||
|
|
||||||
const cleanup = setStageEventHandlers({
|
const cleanup = setStageEventHandlers({
|
||||||
stage,
|
stage,
|
||||||
$tool,
|
$tool,
|
||||||
|
$toolBuffer,
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
|
$isMouseDown,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
|
$stageScale,
|
||||||
|
$stagePos,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
$brushColor,
|
$brushColor,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$selectedLayer,
|
$selectedLayer,
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
|
$isSpaceDown,
|
||||||
onBrushSizeChanged,
|
onBrushSizeChanged,
|
||||||
onBrushLineAdded,
|
onBrushLineAdded,
|
||||||
onEraserLineAdded,
|
onEraserLineAdded,
|
||||||
@ -198,7 +210,6 @@ const useStageRenderer = (
|
|||||||
return () => {
|
return () => {
|
||||||
log.trace('Removing stage listeners');
|
log.trace('Removing stage listeners');
|
||||||
cleanup();
|
cleanup();
|
||||||
container.removeEventListener('keydown', cancelShape);
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
asPreview,
|
asPreview,
|
||||||
@ -251,7 +262,8 @@ const useStageRenderer = (
|
|||||||
lastCursorPos,
|
lastCursorPos,
|
||||||
lastMouseDownPos,
|
lastMouseDownPos,
|
||||||
state.brushSize,
|
state.brushSize,
|
||||||
isDrawing
|
isDrawing,
|
||||||
|
isMouseDown
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
asPreview,
|
asPreview,
|
||||||
@ -265,6 +277,32 @@ const useStageRenderer = (
|
|||||||
state.brushSize,
|
state.brushSize,
|
||||||
renderers,
|
renderers,
|
||||||
isDrawing,
|
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,
|
||||||
|
stageScale,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -290,29 +328,6 @@ const useStageRenderer = (
|
|||||||
debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged);
|
debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged);
|
||||||
}, [stage, asPreview, state.layers, onBboxChanged]);
|
}, [stage, asPreview, state.layers, onBboxChanged]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (asPreview) {
|
|
||||||
// The preview should not have a background
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.trace('Rendering background');
|
|
||||||
renderers.renderBackground(stage, state.size.width, state.size.height);
|
|
||||||
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
log.trace('Arranging layers');
|
|
||||||
renderers.arrangeLayers(stage, layerIds);
|
|
||||||
}, [stage, layerIds, renderers]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (asPreview) {
|
|
||||||
// The preview should not display the no layers message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.trace('Rendering no layers message');
|
|
||||||
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
|
||||||
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
Konva.pixelRatio = dpr;
|
Konva.pixelRatio = dpr;
|
||||||
}, [dpr]);
|
}, [dpr]);
|
||||||
@ -323,8 +338,15 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const layerCount = useAppSelector(selectLayerCount);
|
||||||
const [stage] = useState(
|
const [stage] = useState(
|
||||||
() => new Konva.Stage({ id: uuidv4(), container: document.createElement('div'), listening: !asPreview })
|
() =>
|
||||||
|
new Konva.Stage({
|
||||||
|
id: uuidv4(),
|
||||||
|
container: document.createElement('div'),
|
||||||
|
listening: !asPreview,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
||||||
@ -341,14 +363,29 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" w="full" h="full">
|
<Flex overflow="hidden" w="full" h="full">
|
||||||
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
<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
|
<Flex
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
bg="base.850"
|
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
data-testid="control-layers-canvas"
|
data-testid="control-layers-canvas"
|
||||||
|
border="1px solid red"
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi';
|
import { PiArrowsOutCardinalBold, PiEraserBold, PiHandBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
|
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
||||||
@ -41,6 +41,10 @@ export const ToolChooser: React.FC = () => {
|
|||||||
$tool.set('move');
|
$tool.set('move');
|
||||||
}, []);
|
}, []);
|
||||||
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
|
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
|
||||||
|
const setToolToView = useCallback(() => {
|
||||||
|
$tool.set('view');
|
||||||
|
}, []);
|
||||||
|
useHotkeys('h', setToolToView, { enabled: !isDisabled }, [isDisabled]);
|
||||||
|
|
||||||
const resetSelectedLayer = useCallback(() => {
|
const resetSelectedLayer = useCallback(() => {
|
||||||
if (selectedLayerId === null) {
|
if (selectedLayerId === null) {
|
||||||
@ -89,6 +93,14 @@ export const ToolChooser: React.FC = () => {
|
|||||||
onClick={setToolToMove}
|
onClick={setToolToMove}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.view')} (H)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.view')} (H)`}
|
||||||
|
icon={<PiHandBold />}
|
||||||
|
variant={tool === 'view' ? 'solid' : 'outline'}
|
||||||
|
onClick={setToolToView}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -39,3 +39,18 @@ export const MAX_BRUSH_SPACING_PX = 15;
|
|||||||
* The debounce time in milliseconds for debounced renderers.
|
* The debounce time in milliseconds for debounced renderers.
|
||||||
*/
|
*/
|
||||||
export const DEBOUNCE_MS = 300;
|
export const DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konva wheel zoom exponential scale factor
|
||||||
|
*/
|
||||||
|
export const CANVAS_SCALE_BY = 0.999;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum (furthest-zoomed-out) scale
|
||||||
|
*/
|
||||||
|
export const MIN_CANVAS_SCALE = 0.1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum (furthest-zoomed-in) scale
|
||||||
|
*/
|
||||||
|
export const MAX_CANVAS_SCALE = 20;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
|
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
|
||||||
|
import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants';
|
||||||
import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util';
|
import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util';
|
||||||
import {
|
import {
|
||||||
type AddBrushLineArg,
|
type AddBrushLineArg,
|
||||||
@ -11,23 +12,29 @@ import {
|
|||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
import type { WritableAtom } from 'nanostores';
|
import type { WritableAtom } from 'nanostores';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
|
|
||||||
import { TOOL_PREVIEW_LAYER_ID } from './naming';
|
import { TOOL_PREVIEW_TOOL_GROUP_ID } from './naming';
|
||||||
|
|
||||||
type SetStageEventHandlersArg = {
|
type SetStageEventHandlersArg = {
|
||||||
stage: Konva.Stage;
|
stage: Konva.Stage;
|
||||||
$tool: WritableAtom<Tool>;
|
$tool: WritableAtom<Tool>;
|
||||||
|
$toolBuffer: WritableAtom<Tool | null>;
|
||||||
$isDrawing: WritableAtom<boolean>;
|
$isDrawing: WritableAtom<boolean>;
|
||||||
|
$isMouseDown: WritableAtom<boolean>;
|
||||||
$lastMouseDownPos: WritableAtom<Vector2d | null>;
|
$lastMouseDownPos: WritableAtom<Vector2d | null>;
|
||||||
$lastCursorPos: WritableAtom<Vector2d | null>;
|
$lastCursorPos: WritableAtom<Vector2d | null>;
|
||||||
$lastAddedPoint: WritableAtom<Vector2d | null>;
|
$lastAddedPoint: WritableAtom<Vector2d | null>;
|
||||||
|
$stageScale: WritableAtom<number>;
|
||||||
|
$stagePos: WritableAtom<Vector2d>;
|
||||||
$brushColor: WritableAtom<RgbaColor>;
|
$brushColor: WritableAtom<RgbaColor>;
|
||||||
$brushSize: WritableAtom<number>;
|
$brushSize: WritableAtom<number>;
|
||||||
$brushSpacingPx: WritableAtom<number>;
|
$brushSpacingPx: WritableAtom<number>;
|
||||||
$selectedLayer: WritableAtom<Layer | null>;
|
$selectedLayer: WritableAtom<Layer | null>;
|
||||||
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>;
|
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>;
|
||||||
|
$isSpaceDown: WritableAtom<boolean>;
|
||||||
onBrushLineAdded: (arg: AddBrushLineArg) => void;
|
onBrushLineAdded: (arg: AddBrushLineArg) => void;
|
||||||
onEraserLineAdded: (arg: AddEraserLineArg) => void;
|
onEraserLineAdded: (arg: AddEraserLineArg) => void;
|
||||||
onPointAddedToLine: (arg: AddPointToLineArg) => void;
|
onPointAddedToLine: (arg: AddPointToLineArg) => void;
|
||||||
@ -80,15 +87,20 @@ const maybeAddNextPoint = (
|
|||||||
export const setStageEventHandlers = ({
|
export const setStageEventHandlers = ({
|
||||||
stage,
|
stage,
|
||||||
$tool,
|
$tool,
|
||||||
|
$toolBuffer,
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
|
$isMouseDown,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$lastCursorPos,
|
$lastCursorPos,
|
||||||
$lastAddedPoint,
|
$lastAddedPoint,
|
||||||
|
$stagePos,
|
||||||
|
$stageScale,
|
||||||
$brushColor,
|
$brushColor,
|
||||||
$brushSize,
|
$brushSize,
|
||||||
$brushSpacingPx,
|
$brushSpacingPx,
|
||||||
$selectedLayer,
|
$selectedLayer,
|
||||||
$shouldInvertBrushSizeScrollDirection,
|
$shouldInvertBrushSizeScrollDirection,
|
||||||
|
$isSpaceDown,
|
||||||
onBrushLineAdded,
|
onBrushLineAdded,
|
||||||
onEraserLineAdded,
|
onEraserLineAdded,
|
||||||
onPointAddedToLine,
|
onPointAddedToLine,
|
||||||
@ -102,7 +114,7 @@ export const setStageEventHandlers = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region mousedown
|
//#region mousedown
|
||||||
@ -111,6 +123,7 @@ export const setStageEventHandlers = ({
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$isMouseDown.set(true);
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
const pos = updateLastCursorPos(stage, $lastCursorPos);
|
const pos = updateLastCursorPos(stage, $lastCursorPos);
|
||||||
const selectedLayer = $selectedLayer.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
@ -120,6 +133,12 @@ export const setStageEventHandlers = ({
|
|||||||
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isSpaceDown.get()) {
|
||||||
|
// No drawing when space is down - we are panning the stage
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tool === 'brush') {
|
if (tool === 'brush') {
|
||||||
onBrushLineAdded({
|
onBrushLineAdded({
|
||||||
layerId: selectedLayer.id,
|
layerId: selectedLayer.id,
|
||||||
@ -151,6 +170,7 @@ export const setStageEventHandlers = ({
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$isMouseDown.set(false);
|
||||||
const pos = $lastCursorPos.get();
|
const pos = $lastCursorPos.get();
|
||||||
const selectedLayer = $selectedLayer.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
|
|
||||||
@ -160,6 +180,12 @@ export const setStageEventHandlers = ({
|
|||||||
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isSpaceDown.get()) {
|
||||||
|
// No drawing when space is down - we are panning the stage
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
|
|
||||||
if (tool === 'rect') {
|
if (tool === 'rect') {
|
||||||
@ -193,7 +219,7 @@ export const setStageEventHandlers = ({
|
|||||||
const pos = updateLastCursorPos(stage, $lastCursorPos);
|
const pos = updateLastCursorPos(stage, $lastCursorPos);
|
||||||
const selectedLayer = $selectedLayer.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
|
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
|
||||||
|
|
||||||
if (!pos || !selectedLayer) {
|
if (!pos || !selectedLayer) {
|
||||||
return;
|
return;
|
||||||
@ -202,6 +228,11 @@ export const setStageEventHandlers = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isSpaceDown.get()) {
|
||||||
|
// No drawing when space is down - we are panning the stage
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!getIsMouseDown(e)) {
|
if (!getIsMouseDown(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -246,7 +277,7 @@ export const setStageEventHandlers = ({
|
|||||||
const selectedLayer = $selectedLayer.get();
|
const selectedLayer = $selectedLayer.get();
|
||||||
const tool = $tool.get();
|
const tool = $tool.get();
|
||||||
|
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.visible(false);
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
|
||||||
|
|
||||||
if (!pos || !selectedLayer) {
|
if (!pos || !selectedLayer) {
|
||||||
return;
|
return;
|
||||||
@ -254,6 +285,10 @@ export const setStageEventHandlers = ({
|
|||||||
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ($isSpaceDown.get()) {
|
||||||
|
// No drawing when space is down - we are panning the stage
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (getIsMouseDown(e)) {
|
if (getIsMouseDown(e)) {
|
||||||
if (tool === 'brush') {
|
if (tool === 'brush') {
|
||||||
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
|
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
|
||||||
@ -267,28 +302,73 @@ export const setStageEventHandlers = ({
|
|||||||
|
|
||||||
stage.on('wheel', (e) => {
|
stage.on('wheel', (e) => {
|
||||||
e.evt.preventDefault();
|
e.evt.preventDefault();
|
||||||
const tool = $tool.get();
|
|
||||||
const selectedLayer = $selectedLayer.get();
|
|
||||||
|
|
||||||
if (tool !== 'brush' && tool !== 'eraser') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedLayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Invert the delta if the property is set to true
|
|
||||||
let delta = e.evt.deltaY;
|
|
||||||
if ($shouldInvertBrushSizeScrollDirection.get()) {
|
|
||||||
delta = -delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.evt.ctrlKey || e.evt.metaKey) {
|
if (e.evt.ctrlKey || e.evt.metaKey) {
|
||||||
|
let delta = e.evt.deltaY;
|
||||||
|
if ($shouldInvertBrushSizeScrollDirection.get()) {
|
||||||
|
delta = -delta;
|
||||||
|
}
|
||||||
|
// Holding ctrl or meta while scrolling changes the brush size
|
||||||
onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta));
|
onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta));
|
||||||
|
} else {
|
||||||
|
// We need the absolute cursor position - not the scaled position
|
||||||
|
const cursorPos = stage.getPointerPosition();
|
||||||
|
if (!cursorPos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Stage's x and y scale are always the same
|
||||||
|
const stageScale = stage.scaleX();
|
||||||
|
// When wheeling on trackpad, e.evt.ctrlKey is true - in that case, let's reverse the direction
|
||||||
|
const delta = e.evt.ctrlKey ? -e.evt.deltaY : e.evt.deltaY;
|
||||||
|
const mousePointTo = {
|
||||||
|
x: (cursorPos.x - stage.x()) / stageScale,
|
||||||
|
y: (cursorPos.y - stage.y()) / stageScale,
|
||||||
|
};
|
||||||
|
const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE);
|
||||||
|
const newPos = {
|
||||||
|
x: cursorPos.x - mousePointTo.x * newScale,
|
||||||
|
y: cursorPos.y - mousePointTo.y * newScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
stage.scaleX(newScale);
|
||||||
|
stage.scaleY(newScale);
|
||||||
|
stage.position(newPos);
|
||||||
|
$stageScale.set(newScale);
|
||||||
|
$stagePos.set(newPos);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel');
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.repeat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cancel shape drawing on escape
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
$isDrawing.set(false);
|
||||||
|
$lastMouseDownPos.set(null);
|
||||||
|
} else if (e.key === ' ') {
|
||||||
|
$toolBuffer.set($tool.get());
|
||||||
|
$tool.set('view');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
|
// Cancel shape drawing on escape
|
||||||
|
if (e.repeat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === ' ') {
|
||||||
|
const toolBuffer = $toolBuffer.get();
|
||||||
|
$tool.set(toolBuffer ?? 'move');
|
||||||
|
$toolBuffer.set(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel');
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
// IDs for singleton Konva layers and objects
|
// IDs for singleton Konva layers and objects
|
||||||
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
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_GROUP_ID = 'tool_preview_layer.brush_group';
|
||||||
export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill';
|
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_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_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer';
|
||||||
export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect';
|
export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect';
|
||||||
export const BACKGROUND_LAYER_ID = 'background_layer';
|
export const TOOL_PREVIEW_IMAGE_DIMS_RECT = 'tool_preview_layer.image_dims_rect';
|
||||||
export const BACKGROUND_RECT_ID = 'background_layer.rect';
|
|
||||||
export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message';
|
|
||||||
|
|
||||||
// Names for Konva layers and objects (comparable to CSS classes)
|
// Names for Konva layers and objects (comparable to CSS classes)
|
||||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
|
||||||
import { BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID } from 'features/controlLayers/konva/naming';
|
|
||||||
import Konva from 'konva';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The stage background is a semi-transparent checkerboard pattern. We use konva's `fillPatternImage` to apply the
|
|
||||||
* a data URL of the pattern image to the background rect. Some scaling and positioning is required to ensure the
|
|
||||||
* everything lines up correctly.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the background layer for the stage.
|
|
||||||
* @param stage The konva stage
|
|
||||||
*/
|
|
||||||
const createBackgroundLayer = (stage: Konva.Stage): Konva.Layer => {
|
|
||||||
const layer = new Konva.Layer({
|
|
||||||
id: BACKGROUND_LAYER_ID,
|
|
||||||
});
|
|
||||||
const background = new Konva.Rect({
|
|
||||||
id: BACKGROUND_RECT_ID,
|
|
||||||
x: stage.x(),
|
|
||||||
y: 0,
|
|
||||||
width: stage.width() / stage.scaleX(),
|
|
||||||
height: stage.height() / stage.scaleY(),
|
|
||||||
listening: false,
|
|
||||||
opacity: 0.2,
|
|
||||||
});
|
|
||||||
layer.add(background);
|
|
||||||
stage.add(layer);
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = () => {
|
|
||||||
background.fillPatternImage(image);
|
|
||||||
};
|
|
||||||
image.src = TRANSPARENCY_CHECKER_PATTERN;
|
|
||||||
return layer;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the background layer for the stage.
|
|
||||||
* @param stage The konva stage
|
|
||||||
* @param width The unscaled width of the canvas
|
|
||||||
* @param height The unscaled height of the canvas
|
|
||||||
*/
|
|
||||||
export const renderBackground = (stage: Konva.Stage, width: number, height: number): void => {
|
|
||||||
const layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`) ?? createBackgroundLayer(stage);
|
|
||||||
|
|
||||||
const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`);
|
|
||||||
assert(background, 'Background rect not found');
|
|
||||||
// ensure background rect is in the top-left of the canvas
|
|
||||||
background.absolutePosition({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
// set the dimensions of the background rect to match the canvas - not the stage!!!
|
|
||||||
background.size({
|
|
||||||
width: width / stage.scaleX(),
|
|
||||||
height: height / stage.scaleY(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate the amount the stage is moved - including the effect of scaling
|
|
||||||
const stagePos = {
|
|
||||||
x: -stage.x() / stage.scaleX(),
|
|
||||||
y: -stage.y() / stage.scaleY(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply that movement to the fill pattern
|
|
||||||
background.fillPatternOffset(stagePos);
|
|
||||||
};
|
|
@ -1,10 +1,8 @@
|
|||||||
import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants';
|
import { DEBOUNCE_MS } from 'features/controlLayers/konva/constants';
|
||||||
import { BACKGROUND_LAYER_ID, TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
|
import { TOOL_PREVIEW_LAYER_ID } from 'features/controlLayers/konva/naming';
|
||||||
import { renderBackground } from 'features/controlLayers/konva/renderers/background';
|
|
||||||
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
|
import { updateBboxes } from 'features/controlLayers/konva/renderers/bbox';
|
||||||
import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
|
import { renderCALayer } from 'features/controlLayers/konva/renderers/caLayer';
|
||||||
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer';
|
import { renderIILayer } from 'features/controlLayers/konva/renderers/iiLayer';
|
||||||
import { renderNoLayersMessage } from 'features/controlLayers/konva/renderers/noLayersMessage';
|
|
||||||
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
|
import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer';
|
||||||
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
|
import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer';
|
||||||
import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
import { renderToolPreview } from 'features/controlLayers/konva/renderers/toolPreview';
|
||||||
@ -25,23 +23,6 @@ import type { ImageDTO } from 'services/api/types';
|
|||||||
* Logic for rendering arranging and rendering all layers.
|
* Logic for rendering arranging and rendering all layers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Arranges all layers in the z-axis by updating their z-indices.
|
|
||||||
* @param stage The konva stage
|
|
||||||
* @param layerIds An array of redux layer ids, in their z-index order
|
|
||||||
*/
|
|
||||||
const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => {
|
|
||||||
let nextZIndex = 0;
|
|
||||||
// Background is the first layer
|
|
||||||
stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`)?.zIndex(nextZIndex++);
|
|
||||||
// Then arrange the redux layers in order
|
|
||||||
for (const layerId of layerIds) {
|
|
||||||
stage.findOne<Konva.Layer>(`#${layerId}`)?.zIndex(nextZIndex++);
|
|
||||||
}
|
|
||||||
// Finally, the tool preview layer is always on top
|
|
||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the layers on the stage.
|
* Renders the layers on the stage.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
@ -66,7 +47,8 @@ const renderLayers = (
|
|||||||
konvaLayer.destroy();
|
konvaLayer.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// We'll need to ensure the tool preview layer is on top of the rest of the layers
|
||||||
|
let toolLayerZIndex = 0;
|
||||||
for (const layer of layerStates) {
|
for (const layer of layerStates) {
|
||||||
if (isRegionalGuidanceLayer(layer)) {
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
@ -81,7 +63,11 @@ const renderLayers = (
|
|||||||
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
|
renderRasterLayer(stage, layer, tool, onLayerPosChanged);
|
||||||
}
|
}
|
||||||
// IP Adapter layers are not rendered
|
// IP Adapter layers are not rendered
|
||||||
|
// Increment the z-index for the tool layer
|
||||||
|
toolLayerZIndex++;
|
||||||
}
|
}
|
||||||
|
// Arrange the tool preview layer
|
||||||
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(toolLayerZIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,9 +76,6 @@ const renderLayers = (
|
|||||||
export const renderers = {
|
export const renderers = {
|
||||||
renderToolPreview,
|
renderToolPreview,
|
||||||
renderLayers,
|
renderLayers,
|
||||||
renderBackground,
|
|
||||||
renderNoLayersMessage,
|
|
||||||
arrangeLayers,
|
|
||||||
updateBboxes,
|
updateBboxes,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,9 +87,6 @@ export const renderers = {
|
|||||||
const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({
|
const getDebouncedRenderers = (ms = DEBOUNCE_MS): typeof renderers => ({
|
||||||
renderToolPreview: debounce(renderToolPreview, ms),
|
renderToolPreview: debounce(renderToolPreview, ms),
|
||||||
renderLayers: debounce(renderLayers, ms),
|
renderLayers: debounce(renderLayers, ms),
|
||||||
renderBackground: debounce(renderBackground, ms),
|
|
||||||
renderNoLayersMessage: debounce(renderNoLayersMessage, ms),
|
|
||||||
arrangeLayers: debounce(arrangeLayers, ms),
|
|
||||||
updateBboxes: debounce(updateBboxes, ms),
|
updateBboxes: debounce(updateBboxes, ms),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import { NO_LAYERS_MESSAGE_LAYER_ID } from 'features/controlLayers/konva/naming';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import Konva from 'konva';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logic for creating and rendering a fallback message when there are no layers to render.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the "no layers" fallback layer
|
|
||||||
* @param stage The konva stage
|
|
||||||
*/
|
|
||||||
const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
|
|
||||||
const noLayersMessageLayer = new Konva.Layer({
|
|
||||||
id: NO_LAYERS_MESSAGE_LAYER_ID,
|
|
||||||
opacity: 0.7,
|
|
||||||
listening: false,
|
|
||||||
});
|
|
||||||
const text = new Konva.Text({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
align: 'center',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
text: t('controlLayers.noLayersAdded', 'No Layers Added'),
|
|
||||||
fontFamily: '"Inter Variable", sans-serif',
|
|
||||||
fontStyle: '600',
|
|
||||||
fill: 'white',
|
|
||||||
});
|
|
||||||
noLayersMessageLayer.add(text);
|
|
||||||
stage.add(noLayersMessageLayer);
|
|
||||||
return noLayersMessageLayer;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the "no layers" message when there are no layers to render
|
|
||||||
* @param stage The konva stage
|
|
||||||
* @param layerCount The current number of layers
|
|
||||||
* @param width The target width of the text
|
|
||||||
* @param height The target height of the text
|
|
||||||
*/
|
|
||||||
export const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number): void => {
|
|
||||||
const noLayersMessageLayer =
|
|
||||||
stage.findOne<Konva.Layer>(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage);
|
|
||||||
if (layerCount === 0) {
|
|
||||||
noLayersMessageLayer.findOne<Konva.Text>('Text')?.setAttrs({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fontSize: 32 / stage.scaleX(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
noLayersMessageLayer?.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,5 +1,10 @@
|
|||||||
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { getLayerBboxId, getObjectGroupId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming';
|
import {
|
||||||
|
getLayerBboxId,
|
||||||
|
getObjectGroupId,
|
||||||
|
LAYER_BBOX_NAME,
|
||||||
|
TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types';
|
import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types';
|
||||||
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
@ -198,3 +203,18 @@ export const createObjectGroup = (konvaLayer: Konva.Layer, name: string): Konva.
|
|||||||
konvaLayer.add(konvaObjectGroup);
|
konvaLayer.add(konvaObjectGroup);
|
||||||
return konvaObjectGroup;
|
return konvaObjectGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createImageDimsPreview = (konvaLayer: Konva.Layer, width: number, height: number): Konva.Rect => {
|
||||||
|
const imageDimsPreview = new Konva.Rect({
|
||||||
|
id: TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
stroke: 'rgb(255,0,255)',
|
||||||
|
strokeWidth: 1 / konvaLayer.getStage().scaleX(),
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
konvaLayer.add(imageDimsPreview);
|
||||||
|
return imageDimsPreview;
|
||||||
|
};
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
createObjectGroup,
|
createObjectGroup,
|
||||||
createRectShape,
|
createRectShape,
|
||||||
} from 'features/controlLayers/konva/renderers/objects';
|
} from 'features/controlLayers/konva/renderers/objects';
|
||||||
import { getScaledFlooredCursorPosition, mapId, selectRasterObjects } from 'features/controlLayers/konva/util';
|
import { mapId, selectRasterObjects } from 'features/controlLayers/konva/util';
|
||||||
import type { RasterLayer, Tool } from 'features/controlLayers/store/types';
|
import type { RasterLayer, Tool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
@ -51,24 +51,6 @@ const createRasterLayer = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dragBoundFunc limits how far the layer can be dragged
|
|
||||||
konvaLayer.dragBoundFunc(function (pos) {
|
|
||||||
const cursorPos = getScaledFlooredCursorPosition(stage);
|
|
||||||
if (!cursorPos) {
|
|
||||||
return this.getAbsolutePosition();
|
|
||||||
}
|
|
||||||
// Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
|
|
||||||
if (
|
|
||||||
cursorPos.x < 0 ||
|
|
||||||
cursorPos.x > stage.width() / stage.scaleX() ||
|
|
||||||
cursorPos.y < 0 ||
|
|
||||||
cursorPos.y > stage.height() / stage.scaleY()
|
|
||||||
) {
|
|
||||||
return this.getAbsolutePosition();
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
});
|
|
||||||
|
|
||||||
stage.add(konvaLayer);
|
stage.add(konvaLayer);
|
||||||
|
|
||||||
return konvaLayer;
|
return konvaLayer;
|
||||||
@ -156,6 +138,7 @@ export const renderRasterLayer = async (
|
|||||||
width: layerState.bbox.width,
|
width: layerState.bbox.width,
|
||||||
height: layerState.bbox.height,
|
height: layerState.bbox.height,
|
||||||
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
|
stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
|
||||||
|
strokeWidth: 1 / stage.scaleX(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
bboxRect.visible(false);
|
bboxRect.visible(false);
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
createObjectGroup,
|
createObjectGroup,
|
||||||
createRectShape,
|
createRectShape,
|
||||||
} from 'features/controlLayers/konva/renderers/objects';
|
} from 'features/controlLayers/konva/renderers/objects';
|
||||||
import { getScaledFlooredCursorPosition, mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
|
import { mapId, selectVectorMaskObjects } from 'features/controlLayers/konva/util';
|
||||||
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types';
|
import type { RegionalGuidanceLayer, Tool } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
@ -65,24 +65,6 @@ const createRGLayer = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dragBoundFunc limits how far the layer can be dragged
|
|
||||||
konvaLayer.dragBoundFunc(function (pos) {
|
|
||||||
const cursorPos = getScaledFlooredCursorPosition(stage);
|
|
||||||
if (!cursorPos) {
|
|
||||||
return this.getAbsolutePosition();
|
|
||||||
}
|
|
||||||
// Prevent the user from dragging the layer out of the stage bounds by constaining the cursor position to the stage bounds
|
|
||||||
if (
|
|
||||||
cursorPos.x < 0 ||
|
|
||||||
cursorPos.x > stage.width() / stage.scaleX() ||
|
|
||||||
cursorPos.y < 0 ||
|
|
||||||
cursorPos.y > stage.height() / stage.scaleY()
|
|
||||||
) {
|
|
||||||
return this.getAbsolutePosition();
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
});
|
|
||||||
|
|
||||||
stage.add(konvaLayer);
|
stage.add(konvaLayer);
|
||||||
|
|
||||||
return konvaLayer;
|
return konvaLayer;
|
||||||
|
@ -9,8 +9,10 @@ import {
|
|||||||
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
TOOL_PREVIEW_BRUSH_FILL_ID,
|
TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
||||||
|
TOOL_PREVIEW_IMAGE_DIMS_RECT,
|
||||||
TOOL_PREVIEW_LAYER_ID,
|
TOOL_PREVIEW_LAYER_ID,
|
||||||
TOOL_PREVIEW_RECT_ID,
|
TOOL_PREVIEW_RECT_ID,
|
||||||
|
TOOL_PREVIEW_TOOL_GROUP_ID,
|
||||||
} from 'features/controlLayers/konva/naming';
|
} from 'features/controlLayers/konva/naming';
|
||||||
import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util';
|
import { selectRenderableLayers, snapPosToStage } from 'features/controlLayers/konva/util';
|
||||||
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
|
import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types';
|
||||||
@ -26,9 +28,13 @@ import { assert } from 'tsafe';
|
|||||||
* Creates the singleton tool preview layer and all its objects.
|
* Creates the singleton tool preview layer and all its objects.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
*/
|
*/
|
||||||
const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
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
|
// Initialize the brush preview layer & add to the stage
|
||||||
const toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, visible: false, listening: false });
|
toolPreviewLayer = new Konva.Layer({ id: TOOL_PREVIEW_LAYER_ID, listening: false });
|
||||||
stage.add(toolPreviewLayer);
|
stage.add(toolPreviewLayer);
|
||||||
|
|
||||||
// Create the brush preview group & circles
|
// Create the brush preview group & circles
|
||||||
@ -55,7 +61,6 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
|||||||
strokeEnabled: true,
|
strokeEnabled: true,
|
||||||
});
|
});
|
||||||
brushPreviewGroup.add(brushPreviewBorderOuter);
|
brushPreviewGroup.add(brushPreviewBorderOuter);
|
||||||
toolPreviewLayer.add(brushPreviewGroup);
|
|
||||||
|
|
||||||
// Create the rect preview - this is a rectangle drawn from the last mouse down position to the current cursor position
|
// 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({
|
const rectPreview = new Konva.Rect({
|
||||||
@ -64,11 +69,38 @@ const createToolPreviewLayer = (stage: Konva.Stage): Konva.Layer => {
|
|||||||
stroke: BBOX_SELECTED_STROKE,
|
stroke: BBOX_SELECTED_STROKE,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
});
|
});
|
||||||
toolPreviewLayer.add(rectPreview);
|
|
||||||
|
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;
|
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.
|
* Renders the brush preview for the selected tool.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
@ -89,11 +121,15 @@ export const renderToolPreview = (
|
|||||||
cursorPos: Vector2d | null,
|
cursorPos: Vector2d | null,
|
||||||
lastMouseDownPos: Vector2d | null,
|
lastMouseDownPos: Vector2d | null,
|
||||||
brushSize: number,
|
brushSize: number,
|
||||||
isDrawing: boolean
|
isDrawing: boolean,
|
||||||
|
isMouseDown: boolean
|
||||||
): void => {
|
): void => {
|
||||||
const layerCount = stage.find(selectRenderableLayers).length;
|
const layerCount = stage.find(selectRenderableLayers).length;
|
||||||
// Update the stage's pointer style
|
// Update the stage's pointer style
|
||||||
if (layerCount === 0) {
|
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
|
// We have no layers, so we should not render any tool
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
} else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') {
|
} else if (selectedLayerType !== 'regional_guidance_layer' && selectedLayerType !== 'raster_layer') {
|
||||||
@ -103,70 +139,74 @@ export const renderToolPreview = (
|
|||||||
// Move tool gets a pointer
|
// Move tool gets a pointer
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
} else if (tool === 'rect') {
|
} else if (tool === 'rect') {
|
||||||
// Move rect gets a crosshair
|
// Rect gets a crosshair
|
||||||
stage.container().style.cursor = 'crosshair';
|
stage.container().style.cursor = 'crosshair';
|
||||||
} else {
|
} else {
|
||||||
// Else we hide the native cursor and use the konva-rendered brush preview
|
// Else we hide the native cursor and use the konva-rendered brush preview
|
||||||
stage.container().style.cursor = 'none';
|
stage.container().style.cursor = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage);
|
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) {
|
if (!cursorPos || layerCount === 0) {
|
||||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||||
toolPreviewLayer.visible(false);
|
toolGroup.visible(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolPreviewLayer.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 {
|
} else {
|
||||||
brushPreviewGroup.visible(false);
|
toolGroup.visible(true);
|
||||||
}
|
|
||||||
|
|
||||||
if (cursorPos && lastMouseDownPos && tool === 'rect') {
|
const brushPreviewGroup = stage.findOne<Konva.Group>(`#${TOOL_PREVIEW_BRUSH_GROUP_ID}`);
|
||||||
const snappedPos = snapPosToStage(cursorPos, stage);
|
assert(brushPreviewGroup, 'Brush preview group not found');
|
||||||
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
|
||||||
rectPreview?.setAttrs({
|
const rectPreview = stage.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
|
||||||
x: Math.min(snappedPos.x, lastMouseDownPos.x),
|
assert(rectPreview, 'Rect preview not found');
|
||||||
y: Math.min(snappedPos.y, lastMouseDownPos.y),
|
|
||||||
width: Math.abs(snappedPos.x - lastMouseDownPos.x),
|
// No need to render the brush preview if the cursor position or color is missing
|
||||||
height: Math.abs(snappedPos.y - lastMouseDownPos.y),
|
if (cursorPos && (tool === 'brush' || tool === 'eraser')) {
|
||||||
fill: rgbaColorToString(brushColor),
|
// Update the fill circle
|
||||||
});
|
const brushPreviewFill = brushPreviewGroup.findOne<Konva.Circle>(`#${TOOL_PREVIEW_BRUSH_FILL_ID}`);
|
||||||
rectPreview?.visible(true);
|
brushPreviewFill?.setAttrs({
|
||||||
} else {
|
x: cursorPos.x,
|
||||||
rectPreview?.visible(false);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -979,11 +979,16 @@ const migrateControlLayersState = (state: any): any => {
|
|||||||
|
|
||||||
// Ephemeral interaction state
|
// Ephemeral interaction state
|
||||||
export const $isDrawing = atom(false);
|
export const $isDrawing = atom(false);
|
||||||
|
export const $isMouseDown = atom(false);
|
||||||
export const $lastMouseDownPos = atom<Vector2d | null>(null);
|
export const $lastMouseDownPos = atom<Vector2d | null>(null);
|
||||||
export const $tool = atom<Tool>('brush');
|
export const $tool = atom<Tool>('brush');
|
||||||
|
export const $toolBuffer = atom<Tool | null>(null);
|
||||||
export const $lastCursorPos = atom<Vector2d | null>(null);
|
export const $lastCursorPos = atom<Vector2d | null>(null);
|
||||||
export const $isPreviewVisible = atom(true);
|
export const $isPreviewVisible = atom(true);
|
||||||
export const $lastAddedPoint = atom<Vector2d | null>(null);
|
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 });
|
||||||
|
|
||||||
// Some nanostores that are manually synced to redux state to provide imperative access
|
// Some nanostores that are manually synced to redux state to provide imperative access
|
||||||
// TODO(psyche): This is a hack, figure out another way to handle this...
|
// TODO(psyche): This is a hack, figure out another way to handle this...
|
||||||
|
@ -23,7 +23,7 @@ import type { IRect } from 'konva/lib/types';
|
|||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const zTool = z.enum(['brush', 'eraser', 'move', 'rect']);
|
const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view']);
|
||||||
export type Tool = z.infer<typeof zTool>;
|
export type Tool = z.infer<typeof zTool>;
|
||||||
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user