Files
InvokeAI/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts
2024-10-02 08:02:30 -04:00

861 lines
28 KiB
TypeScript

import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasToolBrush } from 'features/controlLayers/konva/CanvasTool/CanvasToolBrush';
import { CanvasToolColorPicker } from 'features/controlLayers/konva/CanvasTool/CanvasToolColorPicker';
import { CanvasToolEraser } from 'features/controlLayers/konva/CanvasTool/CanvasToolEraser';
import {
alignCoordForTool,
calculateNewBrushSizeFromWheelDelta,
floorCoord,
getIsPrimaryMouseDown,
getLastPointOfLastLine,
getLastPointOfLastLineWithPressure,
getLastPointOfLine,
getPrefixedId,
getScaledCursorPosition,
isDistanceMoreThanMin,
offsetCoord,
} from 'features/controlLayers/konva/util';
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
Coordinate,
RgbColor,
Tool,
} from 'features/controlLayers/store/types';
import { RGBA_BLACK } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { atom } from 'nanostores';
import type { Logger } from 'roarr';
type CanvasToolModuleConfig = {
BRUSH_SPACING_TARGET_SCALE: number;
};
const DEFAULT_CONFIG: CanvasToolModuleConfig = {
BRUSH_SPACING_TARGET_SCALE: 0.1,
};
export class CanvasToolModule extends CanvasModuleBase {
readonly type = 'tool';
readonly id: string;
readonly path: string[];
readonly parent: CanvasManager;
readonly manager: CanvasManager;
readonly log: Logger;
subscriptions: Set<() => void> = new Set();
config: CanvasToolModuleConfig = DEFAULT_CONFIG;
brushToolPreview: CanvasToolBrush;
eraserToolPreview: CanvasToolEraser;
colorPickerToolPreview: CanvasToolColorPicker;
/**
* The currently selected tool.
*/
$tool = atom<Tool>('brush');
/**
* A buffer for the currently selected tool. This is used to temporarily store the tool while the user is using any
* hold-to-activate tools, like the view or color picker tools.
*/
$toolBuffer = atom<Tool | null>(null);
/**
* Whether the mouse is currently down.
*/
$isMouseDown = atom<boolean>(false);
/**
* The last cursor position.
*/
$cursorPos = atom<Coordinate | null>(null);
/**
* The color currently under the cursor. Only has a value when the color picker tool is active.
*/
$colorUnderCursor = atom<RgbColor>(RGBA_BLACK);
/**
* The last pointer type that was used on the stage. This is used to determine if we should show a tool preview. For
* example, when using a pen, we should not show a brush preview.
*/
$lastPointerType = atom<string | null>(null);
konva: {
stage: Konva.Stage;
group: Konva.Group;
};
constructor(manager: CanvasManager) {
super();
this.id = getPrefixedId(this.type);
this.parent = manager;
this.manager = manager;
this.path = this.manager.buildPath(this);
this.log = this.manager.buildLogger(this);
this.log.debug('Creating tool module');
this.brushToolPreview = new CanvasToolBrush(this);
this.eraserToolPreview = new CanvasToolEraser(this);
this.colorPickerToolPreview = new CanvasToolColorPicker(this);
this.konva = {
stage: this.manager.stage.konva.stage,
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
};
this.konva.group.add(this.brushToolPreview.konva.group);
this.konva.group.add(this.eraserToolPreview.konva.group);
this.konva.group.add(this.colorPickerToolPreview.konva.group);
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render));
this.subscriptions.add(this.manager.$isBusy.listen(this.render));
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSettingsSlice, this.render));
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSlice, this.render));
this.subscriptions.add(
this.$tool.listen(() => {
// On tool switch, reset mouse state
this.manager.tool.$isMouseDown.set(false);
this.render();
})
);
const cleanupListeners = this.setEventListeners();
this.subscriptions.add(cleanupListeners);
}
initialize = () => {
this.log.debug('Initializing module');
this.render();
this.syncCursorStyle();
};
setToolVisibility = (tool: Tool, isDrawable: boolean) => {
this.brushToolPreview.setVisibility(isDrawable && tool === 'brush');
this.eraserToolPreview.setVisibility(isDrawable && tool === 'eraser');
this.colorPickerToolPreview.setVisibility(tool === 'colorPicker');
};
syncCursorStyle = () => {
const stage = this.manager.stage;
const isMouseDown = this.$isMouseDown.get();
const tool = this.$tool.get();
if (tool === 'view') {
stage.setCursor(isMouseDown ? 'grabbing' : 'grab');
} else if (this.manager.stateApi.$isTransforming.get()) {
stage.setCursor('default');
} else if (this.manager.stateApi.$isFiltering.get()) {
stage.setCursor('not-allowed');
} else if (this.manager.stagingArea.$isStaging.get()) {
stage.setCursor('not-allowed');
} else if (tool === 'bbox') {
stage.setCursor('default');
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
stage.setCursor('not-allowed');
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
stage.setCursor('not-allowed');
} else if (tool === 'colorPicker' || tool === 'brush' || tool === 'eraser') {
stage.setCursor('none');
} else if (tool === 'move') {
stage.setCursor('default');
} else if (tool === 'rect') {
stage.setCursor('crosshair');
} else {
stage.setCursor('not-allowed');
}
};
render = () => {
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
const cursorPos = this.$cursorPos.get();
const tool = this.$tool.get();
const isFiltering = this.manager.stateApi.$isFiltering.get();
const isStaging = this.manager.stagingArea.$isStaging.get();
this.syncCursorStyle();
this.manager.stage.setIsDraggable(tool === 'view');
if (!cursorPos || isFiltering || isStaging || renderedEntityCount === 0) {
this.konva.group.visible(false);
} else {
this.konva.group.visible(true);
this.brushToolPreview.render();
this.eraserToolPreview.render();
this.colorPickerToolPreview.render();
}
};
syncLastCursorPos = (): Coordinate | null => {
const pos = getScaledCursorPosition(this.konva.stage);
this.$cursorPos.set(pos);
return pos;
};
getColorUnderCursor = (): RgbColor | null => {
const pos = this.konva.stage.getPointerPosition();
if (!pos) {
return null;
}
const ctx = this.konva.stage
.toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1, imageSmoothingEnabled: false })
.getContext('2d');
if (!ctx) {
return null;
}
const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data;
if (r === undefined || g === undefined || b === undefined) {
return null;
}
return { r, g, b };
};
getClip = (
entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState
) => {
const settings = this.manager.stateApi.getSettings();
if (settings.clipToBbox) {
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
return {
x: x - entity.position.x,
y: y - entity.position.y,
width,
height,
};
} else {
const { x, y } = this.manager.stage.getPosition();
const scale = this.manager.stage.getScale();
const { width, height } = this.manager.stage.getSize();
return {
x: -x / scale - entity.position.x,
y: -y / scale - entity.position.y,
width: width / scale,
height: height / scale,
};
}
};
setEventListeners = (): (() => void) => {
this.konva.stage.on('pointerenter', this.onStagePointerEnter);
this.konva.stage.on('pointerdown', this.onStagePointerDown);
this.konva.stage.on('pointerup', this.onStagePointerUp);
this.konva.stage.on('pointermove', this.onStagePointerMove);
// The Konva stage doesn't appear to handle pointerleave events, so we need to listen to the container instead
this.manager.stage.container.addEventListener('pointerleave', this.onStagePointerLeave);
this.konva.stage.on('wheel', this.onStageMouseWheel);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
window.addEventListener('pointerup', this.onWindowPointerUp);
window.addEventListener('blur', this.onWindowBlur);
return () => {
this.konva.stage.off('pointerenter', this.onStagePointerEnter);
this.konva.stage.off('pointerdown', this.onStagePointerDown);
this.konva.stage.off('pointerup', this.onStagePointerUp);
this.konva.stage.off('pointermove', this.onStagePointerMove);
this.manager.stage.container.removeEventListener('pointerleave', this.onStagePointerLeave);
this.konva.stage.off('wheel', this.onStageMouseWheel);
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('pointerup', this.onWindowPointerUp);
window.removeEventListener('blur', this.onWindowBlur);
};
};
getCanDraw = (): boolean => {
if (this.manager.stateApi.getRenderedEntityCount() === 0) {
return false;
} else if (this.manager.$isBusy.get()) {
return false;
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
return false;
} else {
return true;
}
};
onStagePointerEnter = async (e: KonvaEventObject<PointerEvent>) => {
try {
this.$lastPointerType.set(e.evt.pointerType);
if (!this.getCanDraw()) {
return;
}
const cursorPos = this.syncLastCursorPos();
const isMouseDown = this.$isMouseDown.get();
const settings = this.manager.stateApi.getSettings();
const tool = this.$tool.get();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity.state.isLocked) {
return;
}
if (selectedEntity.bufferRenderer.state?.type !== 'rect' && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
return;
}
if (tool === 'brush') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line_with_pressure'),
type: 'brush_line_with_pressure',
points: [alignedPoint.x, alignedPoint.y, e.evt.pressure],
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
} else {
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
}
return;
}
if (tool === 'eraser') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (selectedEntity.bufferRenderer.state && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line_with_pressure'),
type: 'eraser_line_with_pressure',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
} else {
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points: [alignedPoint.x, alignedPoint.y],
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
}
return;
}
} finally {
this.render();
}
};
onStagePointerDown = async (e: KonvaEventObject<PointerEvent>) => {
try {
this.$lastPointerType.set(e.evt.pointerType);
if (!this.getCanDraw()) {
return;
}
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
const cursorPos = this.syncLastCursorPos();
const tool = this.$tool.get();
const settings = this.manager.stateApi.getSettings();
const isMouseDown = this.$isMouseDown.get();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity?.state.isLocked) {
return;
}
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
if (tool === 'brush') {
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
const lastLinePoint = getLastPointOfLastLineWithPressure(
selectedEntity.state.objects,
'brush_line_with_pressure'
);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
let points: number[];
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
points = [
lastLinePoint.x,
lastLinePoint.y,
lastLinePoint.pressure,
alignedPoint.x,
alignedPoint.y,
e.evt.pressure,
];
} else {
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
}
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line_with_pressure'),
type: 'brush_line_with_pressure',
points,
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
} else {
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
let points: number[];
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
} else {
points = [alignedPoint.x, alignedPoint.y];
}
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('brush_line'),
type: 'brush_line',
points,
strokeWidth: settings.brushWidth,
color: this.manager.stateApi.getCurrentColor(),
clip: this.getClip(selectedEntity.state),
});
}
}
if (tool === 'eraser') {
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
const lastLinePoint = getLastPointOfLastLineWithPressure(
selectedEntity.state.objects,
'eraser_line_with_pressure'
);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
let points: number[];
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
points = [
lastLinePoint.x,
lastLinePoint.y,
lastLinePoint.pressure,
alignedPoint.x,
alignedPoint.y,
e.evt.pressure,
];
} else {
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
}
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line_with_pressure'),
type: 'eraser_line_with_pressure',
points,
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
} else {
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
let points: number[];
if (e.evt.shiftKey && lastLinePoint) {
// Create a straight line from the last line point
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
} else {
points = [alignedPoint.x, alignedPoint.y];
}
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('eraser_line'),
type: 'eraser_line',
points,
strokeWidth: settings.eraserWidth,
clip: this.getClip(selectedEntity.state),
});
}
}
if (tool === 'rect') {
if (selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
}
await selectedEntity.bufferRenderer.setBuffer({
id: getPrefixedId('rect'),
type: 'rect',
rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 },
color: this.manager.stateApi.getCurrentColor(),
});
}
} finally {
this.render();
}
};
onStagePointerUp = (e: KonvaEventObject<PointerEvent>) => {
try {
this.$lastPointerType.set(e.evt.pointerType);
if (!this.getCanDraw()) {
return;
}
const tool = this.$tool.get();
const settings = this.manager.stateApi.getSettings();
if (tool === 'colorPicker') {
const color = this.getColorUnderCursor();
if (color) {
this.manager.stateApi.setColor({ ...settings.color, ...color });
}
return;
}
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked;
if (!isDrawable) {
return;
}
if (tool === 'brush') {
if (
(selectedEntity.bufferRenderer.state?.type === 'brush_line' ||
selectedEntity.bufferRenderer.state?.type === 'brush_line_with_pressure') &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.bufferRenderer.clearBuffer();
}
}
if (tool === 'eraser') {
if (
(selectedEntity.bufferRenderer.state?.type === 'eraser_line' ||
selectedEntity.bufferRenderer.state?.type === 'eraser_line_with_pressure') &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.bufferRenderer.clearBuffer();
}
}
if (tool === 'rect') {
if (selectedEntity.bufferRenderer.state?.type === 'rect' && selectedEntity.bufferRenderer.hasBuffer()) {
selectedEntity.bufferRenderer.commitBuffer();
} else {
selectedEntity.bufferRenderer.clearBuffer();
}
}
} finally {
this.render();
}
};
onStagePointerMove = async (e: KonvaEventObject<PointerEvent>) => {
try {
this.$lastPointerType.set(e.evt.pointerType);
if (!this.getCanDraw()) {
return;
}
const tool = this.$tool.get();
const cursorPos = this.syncLastCursorPos();
if (tool === 'colorPicker') {
const color = this.getColorUnderCursor();
if (color) {
this.$colorUnderCursor.set(color);
}
return;
}
const isMouseDown = this.$isMouseDown.get();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked && cursorPos && isMouseDown;
if (!isDrawable) {
return;
}
const bufferState = selectedEntity.bufferRenderer.state;
if (!bufferState) {
return;
}
const settings = this.manager.stateApi.getSettings();
if (tool === 'brush' && (bufferState.type === 'brush_line' || bufferState.type === 'brush_line_with_pressure')) {
const lastPoint = getLastPointOfLine(bufferState.points);
const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
return;
}
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
// Do not add duplicate points
return;
}
bufferState.points.push(alignedPoint.x, alignedPoint.y);
if (bufferState.type === 'brush_line_with_pressure') {
bufferState.points.push(e.evt.pressure);
}
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else if (
tool === 'eraser' &&
(bufferState.type === 'eraser_line' || bufferState.type === 'eraser_line_with_pressure')
) {
const lastPoint = getLastPointOfLine(bufferState.points);
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
return;
}
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
// Do not add duplicate points
return;
}
bufferState.points.push(alignedPoint.x, alignedPoint.y);
if (bufferState.type === 'eraser_line_with_pressure') {
bufferState.points.push(e.evt.pressure);
}
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else if (tool === 'rect' && bufferState.type === 'rect') {
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
const alignedPoint = floorCoord(normalizedPoint);
bufferState.rect.width = Math.round(alignedPoint.x - bufferState.rect.x);
bufferState.rect.height = Math.round(alignedPoint.y - bufferState.rect.y);
await selectedEntity.bufferRenderer.setBuffer(bufferState);
} else {
selectedEntity?.bufferRenderer.clearBuffer();
}
} finally {
this.render();
}
};
onStagePointerLeave = (e: PointerEvent) => {
try {
this.$lastPointerType.set(e.pointerType);
this.$cursorPos.set(null);
if (!this.getCanDraw()) {
return;
}
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (
selectedEntity &&
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.commitBuffer();
}
} finally {
this.render();
}
};
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
if (!this.getCanDraw()) {
return;
}
e.evt.preventDefault();
if (!e.evt.ctrlKey && !e.evt.metaKey) {
return;
}
const settings = this.manager.stateApi.getSettings();
const tool = this.$tool.get();
let delta = e.evt.deltaY;
if (settings.invertScrollForToolWidth) {
delta = -delta;
}
// Holding ctrl or meta while scrolling changes the brush size
if (tool === 'brush') {
this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(settings.brushWidth, delta));
} else if (tool === 'eraser') {
this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(settings.eraserWidth, delta));
}
this.render();
};
/**
* Commit the buffer on window pointer up.
*
* The user may start drawing inside the stage and then release the mouse button outside of the stage. To prevent
* whatever the user was drawing from being lost, or ending up with stale state, we need to commit the buffer
* on window pointer up.
*/
onWindowPointerUp = (_: PointerEvent) => {
try {
this.$isMouseDown.set(false);
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
selectedEntity.bufferRenderer.commitBuffer();
}
} finally {
this.render();
}
};
/**
* We want to reset any "quick-switch" tool selection on window blur. Fixes an issue where you alt-tab out of the app
* and the color picker tool is still active when you come back.
*/
onWindowBlur = () => {
this.revertToolBuffer();
};
onKeyDown = (e: KeyboardEvent) => {
if (e.repeat) {
return;
}
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (e.key === 'Escape') {
// Cancel shape drawing on escape
e.preventDefault();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (
selectedEntity &&
!selectedEntity.filterer?.$isFiltering.get() &&
!selectedEntity.transformer.$isTransforming.get() &&
selectedEntity.bufferRenderer.hasBuffer()
) {
selectedEntity.bufferRenderer.clearBuffer();
}
return;
}
if (e.key === ' ') {
// Select the view tool on space key down
e.preventDefault();
this.$toolBuffer.set(this.$tool.get());
this.$tool.set('view');
this.manager.stateApi.$spaceKey.set(true);
this.$cursorPos.set(null);
return;
}
if (e.key === 'Alt') {
// Select the color picker on alt key down
e.preventDefault();
this.$toolBuffer.set(this.$tool.get());
this.$tool.set('colorPicker');
}
};
onKeyUp = (e: KeyboardEvent) => {
if (e.repeat) {
return;
}
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (e.key === ' ') {
// Revert the tool to the previous tool on space key up
e.preventDefault();
this.revertToolBuffer();
this.manager.stateApi.$spaceKey.set(false);
return;
}
if (e.key === 'Alt') {
// Revert the tool to the previous tool on alt key up
e.preventDefault();
this.revertToolBuffer();
return;
}
};
revertToolBuffer = () => {
const toolBuffer = this.$toolBuffer.get();
if (toolBuffer) {
this.$tool.set(toolBuffer);
this.$toolBuffer.set(null);
}
};
repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
config: this.config,
$tool: this.$tool.get(),
$toolBuffer: this.$toolBuffer.get(),
$isMouseDown: this.$isMouseDown.get(),
$cursorPos: this.$cursorPos.get(),
$colorUnderCursor: this.$colorUnderCursor.get(),
brushToolPreview: this.brushToolPreview.repr(),
eraserToolPreview: this.eraserToolPreview.repr(),
colorPickerToolPreview: this.colorPickerToolPreview.repr(),
};
};
destroy = () => {
this.log.debug('Destroying module');
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions.clear();
this.konva.group.destroy();
};
}