mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): better scale changer component, reset view functionality
This commit is contained in:
parent
1216c6f9c9
commit
d74843be31
@ -1659,6 +1659,7 @@
|
|||||||
"brushSize": "Brush Size",
|
"brushSize": "Brush Size",
|
||||||
"width": "Width",
|
"width": "Width",
|
||||||
"zoom": "Zoom",
|
"zoom": "Zoom",
|
||||||
|
"resetView": "Reset View",
|
||||||
"controlLayers": "Control Layers",
|
"controlLayers": "Control Layers",
|
||||||
"globalMaskOpacity": "Global Mask Opacity",
|
"globalMaskOpacity": "Global Mask Opacity",
|
||||||
"autoNegative": "Auto Negative",
|
"autoNegative": "Auto Negative",
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import { $shift, IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const CanvasResetViewButton = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const canvasManager = useStore($canvasManager);
|
||||||
|
|
||||||
|
const resetZoom = useCallback(() => {
|
||||||
|
if (!canvasManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvasManager.setStageScale(1);
|
||||||
|
}, [canvasManager]);
|
||||||
|
|
||||||
|
const resetView = useCallback(() => {
|
||||||
|
if (!canvasManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvasManager.resetView();
|
||||||
|
}, [canvasManager]);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
if ($shift.get()) {
|
||||||
|
resetZoom();
|
||||||
|
} else {
|
||||||
|
resetView();
|
||||||
|
}
|
||||||
|
}, [resetView, resetZoom]);
|
||||||
|
|
||||||
|
useHotkeys('r', resetView);
|
||||||
|
useHotkeys('shift+r', resetZoom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
tooltip={t('controlLayers.resetView')}
|
||||||
|
aria-label={t('controlLayers.resetView')}
|
||||||
|
onClick={onReset}
|
||||||
|
icon={<PiArrowCounterClockwiseBold />}
|
||||||
|
variant="link"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasResetViewButton.displayName = 'CanvasResetViewButton';
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
$shift,
|
||||||
CompositeSlider,
|
CompositeSlider,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@ -6,6 +7,7 @@ import {
|
|||||||
NumberInput,
|
NumberInput,
|
||||||
NumberInputField,
|
NumberInputField,
|
||||||
Popover,
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -14,14 +16,60 @@ import {
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants';
|
import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants';
|
||||||
|
import { snapToNearest } from 'features/controlLayers/konva/util';
|
||||||
import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice';
|
import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import { clamp, round } from 'lodash-es';
|
import { clamp, round } from 'lodash-es';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
import { PiCaretDownBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const formatPct = (v: number | string) => (isNaN(Number(v)) ? '' : `${round(Number(v), 2).toLocaleString()}%`);
|
function formatPct(v: number | string) {
|
||||||
|
if (isNaN(Number(v))) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${round(Number(v), 2).toLocaleString()}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSliderValueToScale(value: number) {
|
||||||
|
if (value <= 40) {
|
||||||
|
// 0 to 40 -> 10% to 100%
|
||||||
|
return 10 + (90 * value) / 40;
|
||||||
|
} else if (value <= 70) {
|
||||||
|
// 40 to 70 -> 100% to 500%
|
||||||
|
return 100 + (400 * (value - 40)) / 30;
|
||||||
|
} else {
|
||||||
|
// 70 to 100 -> 500% to 2000%
|
||||||
|
return 500 + (1500 * (value - 70)) / 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapScaleToSliderValue(scale: number) {
|
||||||
|
if (scale <= 100) {
|
||||||
|
return ((scale - 10) * 40) / 90;
|
||||||
|
} else if (scale <= 500) {
|
||||||
|
return 40 + ((scale - 100) * 30) / 400;
|
||||||
|
} else {
|
||||||
|
return 70 + ((scale - 500) * 30) / 1500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSliderValue(value: number) {
|
||||||
|
return String(mapSliderValueToScale(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
const marks = [
|
||||||
|
mapScaleToSliderValue(10),
|
||||||
|
mapScaleToSliderValue(50),
|
||||||
|
mapScaleToSliderValue(100),
|
||||||
|
mapScaleToSliderValue(500),
|
||||||
|
mapScaleToSliderValue(2000),
|
||||||
|
];
|
||||||
|
|
||||||
|
const sliderDefaultValue = mapScaleToSliderValue(100);
|
||||||
|
|
||||||
|
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||||
|
|
||||||
export const CanvasScale = memo(() => {
|
export const CanvasScale = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -29,29 +77,29 @@ export const CanvasScale = memo(() => {
|
|||||||
const stageAttrs = useStore($stageAttrs);
|
const stageAttrs = useStore($stageAttrs);
|
||||||
const [localScale, setLocalScale] = useState(stageAttrs.scale * 100);
|
const [localScale, setLocalScale] = useState(stageAttrs.scale * 100);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChangeSlider = useCallback(
|
||||||
(scale: number) => {
|
(scale: number) => {
|
||||||
if (!canvasManager) {
|
if (!canvasManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
canvasManager.setStageScale(scale / 100);
|
let snappedScale = scale;
|
||||||
|
// Do not snap if shift key is held
|
||||||
|
if (!$shift.get()) {
|
||||||
|
snappedScale = snapToNearest(scale, snapCandidates, 2);
|
||||||
|
}
|
||||||
|
const mappedScale = mapSliderValueToScale(snappedScale);
|
||||||
|
canvasManager.setStageScale(mappedScale / 100);
|
||||||
},
|
},
|
||||||
[canvasManager]
|
[canvasManager]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
|
||||||
if (!canvasManager) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasManager.setStageScale(1);
|
|
||||||
}, [canvasManager]);
|
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
const onBlur = useCallback(() => {
|
||||||
if (!canvasManager) {
|
if (!canvasManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isNaN(Number(localScale))) {
|
if (isNaN(Number(localScale))) {
|
||||||
|
canvasManager.setStageScale(1);
|
||||||
|
setLocalScale(100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
canvasManager.setStageScale(clamp(localScale / 100, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE));
|
canvasManager.setStageScale(clamp(localScale / 100, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE));
|
||||||
@ -75,39 +123,54 @@ export const CanvasScale = memo(() => {
|
|||||||
}, [stageAttrs.scale]);
|
}, [stageAttrs.scale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl w="min-content" gap={2}>
|
<Popover>
|
||||||
<FormLabel m={0}>{t('controlLayers.zoom')}</FormLabel>
|
<FormControl w="min-content" gap={2}>
|
||||||
<Popover isLazy trigger="hover" openDelay={300}>
|
<FormLabel m={0}>{t('controlLayers.zoom')}</FormLabel>
|
||||||
<PopoverTrigger>
|
<PopoverAnchor>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
min={MIN_CANVAS_SCALE * 100}
|
min={MIN_CANVAS_SCALE * 100}
|
||||||
max={MAX_CANVAS_SCALE * 100}
|
max={MAX_CANVAS_SCALE * 100}
|
||||||
value={localScale}
|
value={localScale}
|
||||||
onChange={onChangeNumberInput}
|
onChange={onChangeNumberInput}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
w="64px"
|
w="76px"
|
||||||
format={formatPct}
|
format={formatPct}
|
||||||
defaultValue={100}
|
defaultValue={100}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
clampValueOnBlur={false}
|
||||||
>
|
>
|
||||||
<NumberInputField textAlign="center" paddingInlineEnd={3} />
|
<NumberInputField paddingInlineEnd={7} />
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
aria-label="open-slider"
|
||||||
|
icon={<PiCaretDownBold />}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
position="absolute"
|
||||||
|
insetInlineEnd={0}
|
||||||
|
h="full"
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
</NumberInput>
|
</NumberInput>
|
||||||
</PopoverTrigger>
|
</PopoverAnchor>
|
||||||
<PopoverContent w={200} py={2} px={4}>
|
</FormControl>
|
||||||
<PopoverArrow />
|
<PopoverContent w={200} pt={0} pb={2} px={4}>
|
||||||
<PopoverBody>
|
<PopoverArrow />
|
||||||
<CompositeSlider
|
<PopoverBody>
|
||||||
min={MIN_CANVAS_SCALE * 100}
|
<CompositeSlider
|
||||||
max={MAX_CANVAS_SCALE * 100}
|
min={0}
|
||||||
value={stageAttrs.scale * 100}
|
max={100}
|
||||||
onChange={onChange}
|
value={mapScaleToSliderValue(localScale)}
|
||||||
defaultValue={100}
|
onChange={onChangeSlider}
|
||||||
/>
|
defaultValue={sliderDefaultValue}
|
||||||
</PopoverBody>
|
marks={marks}
|
||||||
</PopoverContent>
|
formatValue={formatSliderValue}
|
||||||
</Popover>
|
/>
|
||||||
<IconButton aria-label="reset" onClick={onReset} icon={<PiArrowCounterClockwiseBold />} variant="link" />
|
</PopoverBody>
|
||||||
</FormControl>
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Flex, Switch } from '@invoke-ai/ui-library';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
|
import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
|
||||||
|
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
||||||
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
||||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
import { EraserWidth } from 'features/controlLayers/components/EraserWidth';
|
import { EraserWidth } from 'features/controlLayers/components/EraserWidth';
|
||||||
@ -54,6 +55,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
{tool === 'eraser' && <EraserWidth />}
|
{tool === 'eraser' && <EraserWidth />}
|
||||||
</Flex>
|
</Flex>
|
||||||
<CanvasScale />
|
<CanvasScale />
|
||||||
|
<CanvasResetViewButton />
|
||||||
<Button onClick={bbox}>bbox</Button>
|
<Button onClick={bbox}>bbox</Button>
|
||||||
<Switch onChange={onChangeDebugging}>debug</Switch>
|
<Switch onChange={onChangeDebugging}>debug</Switch>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
|
@ -14,9 +14,19 @@ export class CanvasBackground {
|
|||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of subscriptions that should be cleaned up when the transformer is destroyed.
|
||||||
|
*/
|
||||||
|
subscriptions: Set<() => void> = new Set();
|
||||||
|
|
||||||
constructor(manager: CanvasManager) {
|
constructor(manager: CanvasManager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) };
|
this.konva = { layer: new Konva.Layer({ name: CanvasBackground.LAYER_NAME, listening: false }) };
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -94,6 +104,13 @@ export class CanvasBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy = () => {
|
||||||
|
for (const cleanup of this.subscriptions) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
this.konva.layer.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
* Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
||||||
* @param scale The stage scale
|
* @param scale The stage scale
|
||||||
|
@ -156,6 +156,19 @@ export class CanvasManager {
|
|||||||
this.container = container;
|
this.container = container;
|
||||||
this._store = store;
|
this._store = store;
|
||||||
this.stateApi = new CanvasStateApi(this._store, this);
|
this.stateApi = new CanvasStateApi(this._store, this);
|
||||||
|
|
||||||
|
this.transformingEntity = new PubSub<CanvasEntityIdentifier | null>(null);
|
||||||
|
this.toolState = new PubSub(this.stateApi.getToolState());
|
||||||
|
this.currentFill = new PubSub(this.getCurrentFill());
|
||||||
|
this.selectedEntityIdentifier = new PubSub(
|
||||||
|
this.stateApi.getState().selectedEntityIdentifier,
|
||||||
|
(a, b) => a?.id === b?.id
|
||||||
|
);
|
||||||
|
this.selectedEntity = new PubSub(
|
||||||
|
this.getSelectedEntity(),
|
||||||
|
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
|
||||||
|
);
|
||||||
|
|
||||||
this._prevState = this.stateApi.getState();
|
this._prevState = this.stateApi.getState();
|
||||||
|
|
||||||
this.log = logger('canvas').child((message) => {
|
this.log = logger('canvas').child((message) => {
|
||||||
@ -213,18 +226,6 @@ export class CanvasManager {
|
|||||||
this.log.error('Worker message error');
|
this.log.error('Worker message error');
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transformingEntity = new PubSub<CanvasEntityIdentifier | null>(null);
|
|
||||||
this.toolState = new PubSub(this.stateApi.getToolState());
|
|
||||||
this.currentFill = new PubSub(this.getCurrentFill());
|
|
||||||
this.selectedEntityIdentifier = new PubSub(
|
|
||||||
this.stateApi.getState().selectedEntityIdentifier,
|
|
||||||
(a, b) => a?.id === b?.id
|
|
||||||
);
|
|
||||||
this.selectedEntity = new PubSub(
|
|
||||||
this.getSelectedEntity(),
|
|
||||||
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
|
|
||||||
);
|
|
||||||
|
|
||||||
this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this);
|
this.inpaintMask = new CanvasMaskAdapter(this.stateApi.getInpaintMaskState(), this);
|
||||||
this.stage.add(this.inpaintMask.konva.layer);
|
this.stage.add(this.inpaintMask.konva.layer);
|
||||||
}
|
}
|
||||||
@ -303,7 +304,34 @@ export class CanvasManager {
|
|||||||
height: this.stage.height(),
|
height: this.stage.height(),
|
||||||
scale: this.stage.scaleX(),
|
scale: this.stage.scaleX(),
|
||||||
});
|
});
|
||||||
this.background.render();
|
}
|
||||||
|
|
||||||
|
resetView() {
|
||||||
|
const { width, height } = this.getStageSize();
|
||||||
|
const { rect } = this.stateApi.getBbox();
|
||||||
|
|
||||||
|
const padding = 20; // Padding in absolute pixels
|
||||||
|
|
||||||
|
const availableWidth = width - padding * 2;
|
||||||
|
const availableHeight = height - padding * 2;
|
||||||
|
|
||||||
|
const scale = Math.min(availableWidth / rect.width, availableHeight / rect.height);
|
||||||
|
const x = -rect.x * scale + padding + (availableWidth - rect.width * scale) / 2;
|
||||||
|
const y = -rect.y * scale + padding + (availableHeight - rect.height * scale) / 2;
|
||||||
|
|
||||||
|
this.stage.setAttrs({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
scaleX: scale,
|
||||||
|
scaleY: scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stateApi.$stageAttrs.set({
|
||||||
|
...this.stateApi.$stageAttrs.get(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null {
|
getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null {
|
||||||
@ -618,6 +646,8 @@ export class CanvasManager {
|
|||||||
for (const controlAdapter of this.controlAdapters.values()) {
|
for (const controlAdapter of this.controlAdapters.values()) {
|
||||||
controlAdapter.destroy();
|
controlAdapter.destroy();
|
||||||
}
|
}
|
||||||
|
this.background.destroy();
|
||||||
|
this.preview.destroy();
|
||||||
unsubscribeRenderer();
|
unsubscribeRenderer();
|
||||||
unsubscribeListeners();
|
unsubscribeListeners();
|
||||||
unsubscribeShouldShowStagedImage();
|
unsubscribeShouldShowStagedImage();
|
||||||
@ -678,8 +708,6 @@ export class CanvasManager {
|
|||||||
height: this.stage.height(),
|
height: this.stage.height(),
|
||||||
scale: this.stage.scaleX(),
|
scale: this.stage.scaleX(),
|
||||||
});
|
});
|
||||||
this.background.render();
|
|
||||||
this.preview.tool.render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,4 +32,9 @@ export class CanvasPreview {
|
|||||||
this.progressPreview = progressPreview;
|
this.progressPreview = progressPreview;
|
||||||
this.layer.add(this.progressPreview.konva.group);
|
this.layer.add(this.progressPreview.konva.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.tool.destroy();
|
||||||
|
this.layer.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,11 @@ export class CanvasTool {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of subscriptions that should be cleaned up when the transformer is destroyed.
|
||||||
|
*/
|
||||||
|
subscriptions: Set<() => void> = new Set();
|
||||||
|
|
||||||
constructor(manager: CanvasManager) {
|
constructor(manager: CanvasManager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.konva = {
|
this.konva = {
|
||||||
@ -102,8 +107,27 @@ export class CanvasTool {
|
|||||||
this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle);
|
this.konva.eraser.group.add(this.konva.eraser.innerBorderCircle);
|
||||||
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
|
this.konva.eraser.group.add(this.konva.eraser.outerBorderCircle);
|
||||||
this.konva.group.add(this.konva.eraser.group);
|
this.konva.group.add(this.konva.eraser.group);
|
||||||
|
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscriptions.add(
|
||||||
|
this.manager.toolState.subscribe(() => {
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy = () => {
|
||||||
|
for (const cleanup of this.subscriptions) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
this.konva.group.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
scaleTool = () => {
|
scaleTool = () => {
|
||||||
const toolState = this.manager.stateApi.getToolState();
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
const scale = this.manager.stage.scaleX();
|
const scale = this.manager.stage.scaleX();
|
||||||
|
@ -492,8 +492,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
});
|
});
|
||||||
manager.background.render();
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region dragend
|
//#region dragend
|
||||||
@ -531,13 +529,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
$spaceKey.set(true);
|
$spaceKey.set(true);
|
||||||
$lastCursorPos.set(null);
|
$lastCursorPos.set(null);
|
||||||
$lastMouseDownPos.set(null);
|
$lastMouseDownPos.set(null);
|
||||||
} else if (e.key === 'r') {
|
|
||||||
$lastCursorPos.set(null);
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
manager.background.render();
|
|
||||||
// TODO(psyche): restore some kind of fit
|
|
||||||
}
|
}
|
||||||
manager.preview.tool.render();
|
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKeyDown);
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
@ -374,3 +374,17 @@ export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean)
|
|||||||
export const getEmptyRect = (): Rect => {
|
export const getEmptyRect = (): Rect => {
|
||||||
return { x: 0, y: 0, width: 0, height: 0 };
|
return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
};
|
};
|
||||||
|
export function snapToNearest(value: number, candidateValues: number[], threshold: number): number {
|
||||||
|
let closest = value;
|
||||||
|
let minDiff = Number.MAX_VALUE;
|
||||||
|
|
||||||
|
for (const candidate of candidateValues) {
|
||||||
|
const diff = Math.abs(value - candidate);
|
||||||
|
if (diff < minDiff && diff <= threshold) {
|
||||||
|
minDiff = diff;
|
||||||
|
closest = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user