mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): move events into modules who care about them
This commit is contained in:
parent
307885f505
commit
f86b50d18a
@ -3,11 +3,15 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||||
|
import { getLastPointOfLine } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
|
CanvasBrushLineState,
|
||||||
CanvasControlLayerState,
|
CanvasControlLayerState,
|
||||||
CanvasEntityIdentifier,
|
CanvasEntityIdentifier,
|
||||||
|
CanvasEraserLineState,
|
||||||
CanvasRasterLayerState,
|
CanvasRasterLayerState,
|
||||||
CanvasV2State,
|
CanvasV2State,
|
||||||
|
Coordinate,
|
||||||
Rect,
|
Rect,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
@ -180,6 +184,19 @@ export class CanvasLayerAdapter {
|
|||||||
return stableHash(arg);
|
return stableHash(arg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => {
|
||||||
|
const lastObject = this.state.objects[this.state.objects.length - 1];
|
||||||
|
if (!lastObject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastObject.type === type) {
|
||||||
|
return getLastPointOfLine(lastObject.points);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
logDebugInfo(msg = 'Debug info') {
|
logDebugInfo(msg = 'Debug info') {
|
||||||
const info = {
|
const info = {
|
||||||
repr: this.repr(),
|
repr: this.repr(),
|
||||||
|
@ -19,7 +19,6 @@ import type { CanvasLayerAdapter } from './CanvasLayerAdapter';
|
|||||||
import type { CanvasMaskAdapter } from './CanvasMaskAdapter';
|
import type { CanvasMaskAdapter } from './CanvasMaskAdapter';
|
||||||
import { CanvasPreviewModule } from './CanvasPreviewModule';
|
import { CanvasPreviewModule } from './CanvasPreviewModule';
|
||||||
import { CanvasStateApiModule } from './CanvasStateApiModule';
|
import { CanvasStateApiModule } from './CanvasStateApiModule';
|
||||||
import { setStageEventHandlers } from './events';
|
|
||||||
|
|
||||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||||
const TYPE = 'manager';
|
const TYPE = 'manager';
|
||||||
@ -110,7 +109,6 @@ export class CanvasManager {
|
|||||||
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
|
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
|
||||||
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
|
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
|
||||||
|
|
||||||
const cleanupEventHandlers = setStageEventHandlers(this);
|
|
||||||
const cleanupStage = this.stage.initialize();
|
const cleanupStage = this.stage.initialize();
|
||||||
const cleanupStore = this.store.subscribe(this.renderer.render);
|
const cleanupStore = this.store.subscribe(this.renderer.render);
|
||||||
|
|
||||||
@ -122,7 +120,6 @@ export class CanvasManager {
|
|||||||
this.background.destroy();
|
this.background.destroy();
|
||||||
this.preview.destroy();
|
this.preview.destroy();
|
||||||
cleanupStore();
|
cleanupStore();
|
||||||
cleanupEventHandlers();
|
|
||||||
cleanupStage();
|
cleanupStage();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,15 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||||
|
import { getLastPointOfLine } from 'features/controlLayers/konva/util';
|
||||||
import type {
|
import type {
|
||||||
|
CanvasBrushLineState,
|
||||||
CanvasEntityIdentifier,
|
CanvasEntityIdentifier,
|
||||||
|
CanvasEraserLineState,
|
||||||
CanvasInpaintMaskState,
|
CanvasInpaintMaskState,
|
||||||
CanvasRegionalGuidanceState,
|
CanvasRegionalGuidanceState,
|
||||||
CanvasV2State,
|
CanvasV2State,
|
||||||
|
Coordinate,
|
||||||
Rect,
|
Rect,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
@ -136,6 +140,19 @@ export class CanvasMaskAdapter {
|
|||||||
this.konva.layer.visible(isEnabled);
|
this.konva.layer.visible(isEnabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getLastPointOfLastLine = (type: CanvasBrushLineState['type'] | CanvasEraserLineState['type']): Coordinate | null => {
|
||||||
|
const lastObject = this.state.objects[this.state.objects.length - 1];
|
||||||
|
if (!lastObject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastObject.type === type) {
|
||||||
|
return getLastPointOfLine(lastObject.points);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
repr = () => {
|
repr = () => {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { SerializableObject } from 'common/types';
|
import type { SerializableObject } from 'common/types';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
|
import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants';
|
||||||
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
|
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
|
||||||
import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
|
import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
|
|
||||||
@ -27,6 +29,18 @@ export class CanvasStageModule {
|
|||||||
this.konva = { stage };
|
this.konva = { stage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEventListeners = () => {
|
||||||
|
this.konva.stage.on('wheel', this.onStageMouseWheel);
|
||||||
|
this.konva.stage.on('dragmove', this.onStageDragMove);
|
||||||
|
this.konva.stage.on('dragend', this.onStageDragEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.konva.stage.off('wheel', this.onStageMouseWheel);
|
||||||
|
this.konva.stage.off('dragmove', this.onStageDragMove);
|
||||||
|
this.konva.stage.off('dragend', this.onStageDragEnd);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
initialize = () => {
|
initialize = () => {
|
||||||
this.log.debug('Initializing stage');
|
this.log.debug('Initializing stage');
|
||||||
this.konva.stage.container(this.container);
|
this.konva.stage.container(this.container);
|
||||||
@ -34,11 +48,13 @@ export class CanvasStageModule {
|
|||||||
resizeObserver.observe(this.container);
|
resizeObserver.observe(this.container);
|
||||||
this.fitStageToContainer();
|
this.fitStageToContainer();
|
||||||
this.fitLayersToStage();
|
this.fitLayersToStage();
|
||||||
|
const cleanupListeners = this.setEventListeners();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.log.debug('Destroying stage');
|
this.log.debug('Destroying stage');
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
this.konva.stage.destroy();
|
this.konva.stage.destroy();
|
||||||
|
cleanupListeners();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,6 +188,54 @@ export class CanvasStageModule {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
if (e.evt.ctrlKey || e.evt.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the absolute cursor position - not the scaled position
|
||||||
|
const cursorPos = this.konva.stage.getPointerPosition();
|
||||||
|
|
||||||
|
if (cursorPos) {
|
||||||
|
// 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 scale = this.manager.stage.getScale() * CANVAS_SCALE_BY ** delta;
|
||||||
|
this.manager.stage.setScale(scale, cursorPos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (e.target !== this.konva.stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.stateApi.$stageAttrs.set({
|
||||||
|
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
||||||
|
x: Math.floor(this.konva.stage.x()),
|
||||||
|
y: Math.floor(this.konva.stage.y()),
|
||||||
|
width: this.konva.stage.width(),
|
||||||
|
height: this.konva.stage.height(),
|
||||||
|
scale: this.konva.stage.scaleX(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageDragEnd = (e: KonvaEventObject<DragEvent>) => {
|
||||||
|
if (e.target !== this.konva.stage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.stateApi.$stageAttrs.set({
|
||||||
|
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
||||||
|
x: Math.floor(this.konva.stage.x()),
|
||||||
|
y: Math.floor(this.konva.stage.y()),
|
||||||
|
width: this.konva.stage.width(),
|
||||||
|
height: this.konva.stage.height(),
|
||||||
|
scale: this.konva.stage.scaleX(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the scale of the stage. The stage is always scaled uniformly in x and y.
|
* Gets the scale of the stage. The stage is always scaled uniformly in x and y.
|
||||||
*/
|
*/
|
||||||
|
@ -2,11 +2,33 @@ import type { SerializableObject } from 'common/types';
|
|||||||
import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule';
|
import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule';
|
||||||
import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants';
|
import {
|
||||||
import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util';
|
BRUSH_BORDER_INNER_COLOR,
|
||||||
import type { Tool } from 'features/controlLayers/store/types';
|
BRUSH_BORDER_OUTER_COLOR,
|
||||||
|
BRUSH_SPACING_TARGET_SCALE,
|
||||||
|
} from 'features/controlLayers/konva/constants';
|
||||||
|
import {
|
||||||
|
alignCoordForTool,
|
||||||
|
calculateNewBrushSizeFromWheelDelta,
|
||||||
|
getIsPrimaryMouseDown,
|
||||||
|
getLastPointOfLine,
|
||||||
|
getPrefixedId,
|
||||||
|
getScaledCursorPosition,
|
||||||
|
offsetCoord,
|
||||||
|
validateCandidatePoint,
|
||||||
|
} from 'features/controlLayers/konva/util';
|
||||||
|
import type {
|
||||||
|
CanvasControlLayerState,
|
||||||
|
CanvasInpaintMaskState,
|
||||||
|
CanvasRasterLayerState,
|
||||||
|
CanvasRegionalGuidanceState,
|
||||||
|
Coordinate,
|
||||||
|
RgbColor,
|
||||||
|
Tool,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import { isDrawableEntity } from 'features/controlLayers/store/types';
|
import { isDrawableEntity } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
|
|
||||||
export class CanvasToolModule {
|
export class CanvasToolModule {
|
||||||
@ -25,6 +47,7 @@ export class CanvasToolModule {
|
|||||||
log: Logger;
|
log: Logger;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
|
stage: Konva.Stage;
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
brush: {
|
brush: {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
@ -67,6 +90,7 @@ export class CanvasToolModule {
|
|||||||
this.path = this.manager.path.concat(this.id);
|
this.path = this.manager.path.concat(this.id);
|
||||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
this.konva = {
|
this.konva = {
|
||||||
|
stage: this.manager.stage.konva.stage,
|
||||||
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
||||||
brush: {
|
brush: {
|
||||||
group: new Konva.Group({ name: `${this.type}:brush_group`, listening: false }),
|
group: new Konva.Group({ name: `${this.type}:brush_group`, listening: false }),
|
||||||
@ -218,6 +242,10 @@ export class CanvasToolModule {
|
|||||||
this.render();
|
this.render();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cleanupListeners = this.setEventListeners();
|
||||||
|
|
||||||
|
this.subscriptions.add(cleanupListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy = () => {
|
destroy = () => {
|
||||||
@ -277,7 +305,7 @@ export class CanvasToolModule {
|
|||||||
|
|
||||||
stage.setIsDraggable(tool === 'view');
|
stage.setIsDraggable(tool === 'view');
|
||||||
|
|
||||||
if (!cursorPos || renderedEntityCount === 0 || !isDrawable) {
|
if (!cursorPos || renderedEntityCount === 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
|
||||||
this.konva.group.visible(false);
|
this.konva.group.visible(false);
|
||||||
} else {
|
} else {
|
||||||
@ -421,6 +449,445 @@ export class CanvasToolModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncLastCursorPos = (): Coordinate | null => {
|
||||||
|
const pos = getScaledCursorPosition(this.konva.stage);
|
||||||
|
if (!pos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.manager.stateApi.$lastCursorPos.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('mouseenter', this.onStageMouseEnter);
|
||||||
|
this.konva.stage.on('mousedown', this.onStageMouseDown);
|
||||||
|
this.konva.stage.on('mouseup', this.onStageMouseUp);
|
||||||
|
this.konva.stage.on('mousemove', this.onStageMouseMove);
|
||||||
|
this.konva.stage.on('mouseleave', this.onStageMouseLeave);
|
||||||
|
this.konva.stage.on('wheel', this.onStageMouseWheel);
|
||||||
|
|
||||||
|
window.addEventListener('keydown', this.onKeyDown);
|
||||||
|
window.addEventListener('keyup', this.onKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.konva.stage.off('mouseenter', this.onStageMouseEnter);
|
||||||
|
this.konva.stage.off('mousedown', this.onStageMouseDown);
|
||||||
|
this.konva.stage.off('mouseup', this.onStageMouseUp);
|
||||||
|
this.konva.stage.off('mousemove', this.onStageMouseMove);
|
||||||
|
this.konva.stage.off('mouseleave', this.onStageMouseLeave);
|
||||||
|
|
||||||
|
this.konva.stage.off('wheel', this.onStageMouseWheel);
|
||||||
|
window.removeEventListener('keydown', this.onKeyDown);
|
||||||
|
window.removeEventListener('keyup', this.onKeyUp);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseEnter = (_: KonvaEventObject<MouseEvent>) => {
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
this.manager.stateApi.$isMouseDown.set(true);
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const pos = this.syncLastCursorPos();
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
|
||||||
|
if (toolState.selected === 'colorPicker') {
|
||||||
|
const color = this.getColorUnderCursor();
|
||||||
|
if (color) {
|
||||||
|
this.manager.stateApi.$colorUnderCursor.set(color);
|
||||||
|
}
|
||||||
|
if (color) {
|
||||||
|
this.manager.stateApi.setFill({ ...toolState.fill, ...color });
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
const isDrawable = selectedEntity?.state.isEnabled;
|
||||||
|
if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
||||||
|
this.manager.stateApi.$lastMouseDownPos.set(pos);
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
|
||||||
|
if (toolState.selected === 'brush') {
|
||||||
|
const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('brush_line');
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
|
if (e.evt.shiftKey && lastLinePoint) {
|
||||||
|
// Create a straight line from the last line point
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('brush_line'),
|
||||||
|
type: 'brush_line',
|
||||||
|
points: [
|
||||||
|
// The last point of the last line is already normalized to the entity's coordinates
|
||||||
|
lastLinePoint.x,
|
||||||
|
lastLinePoint.y,
|
||||||
|
alignedPoint.x,
|
||||||
|
alignedPoint.y,
|
||||||
|
],
|
||||||
|
strokeWidth: toolState.brush.width,
|
||||||
|
color: this.manager.stateApi.getCurrentFill(),
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('brush_line'),
|
||||||
|
type: 'brush_line',
|
||||||
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
|
strokeWidth: toolState.brush.width,
|
||||||
|
color: this.manager.stateApi.getCurrentFill(),
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'eraser') {
|
||||||
|
const lastLinePoint = selectedEntity.adapter.getLastPointOfLastLine('eraser_line');
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
|
if (e.evt.shiftKey && lastLinePoint) {
|
||||||
|
// Create a straight line from the last line point
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('eraser_line'),
|
||||||
|
type: 'eraser_line',
|
||||||
|
points: [
|
||||||
|
// The last point of the last line is already normalized to the entity's coordinates
|
||||||
|
lastLinePoint.x,
|
||||||
|
lastLinePoint.y,
|
||||||
|
alignedPoint.x,
|
||||||
|
alignedPoint.y,
|
||||||
|
],
|
||||||
|
strokeWidth: toolState.eraser.width,
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('eraser_line'),
|
||||||
|
type: 'eraser_line',
|
||||||
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
|
strokeWidth: toolState.eraser.width,
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'rect') {
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
await selectedEntity.adapter.renderer.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.getCurrentFill(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseUp = (_: KonvaEventObject<MouseEvent>) => {
|
||||||
|
this.manager.stateApi.$isMouseDown.set(false);
|
||||||
|
const pos = this.manager.stateApi.$lastCursorPos.get();
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
const isDrawable = selectedEntity?.state.isEnabled;
|
||||||
|
|
||||||
|
if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get()) {
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
|
||||||
|
if (toolState.selected === 'brush') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer?.type === 'brush_line') {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'eraser') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer?.type === 'eraser_line') {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'rect') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer?.type === 'rect') {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseMove = async (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const pos = this.syncLastCursorPos();
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
|
||||||
|
if (toolState.selected === 'colorPicker') {
|
||||||
|
const color = this.getColorUnderCursor();
|
||||||
|
if (color) {
|
||||||
|
this.manager.stateApi.$colorUnderCursor.set(color);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isDrawable = selectedEntity?.state.isEnabled;
|
||||||
|
if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
||||||
|
if (toolState.selected === 'brush') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer) {
|
||||||
|
if (drawingBuffer.type === 'brush_line') {
|
||||||
|
const lastPoint = getLastPointOfLine(drawingBuffer.points);
|
||||||
|
const minDistance = toolState.brush.width * BRUSH_SPACING_TARGET_SCALE;
|
||||||
|
if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) {
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
|
// Do not add duplicate points
|
||||||
|
if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) {
|
||||||
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('brush_line'),
|
||||||
|
type: 'brush_line',
|
||||||
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
|
strokeWidth: toolState.brush.width,
|
||||||
|
color: this.manager.stateApi.getCurrentFill(),
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'eraser') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer) {
|
||||||
|
if (drawingBuffer.type === 'eraser_line') {
|
||||||
|
const lastPoint = getLastPointOfLine(drawingBuffer.points);
|
||||||
|
const minDistance = toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE;
|
||||||
|
if (lastPoint && validateCandidatePoint(pos, lastPoint, minDistance)) {
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
|
// Do not add duplicate points
|
||||||
|
if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) {
|
||||||
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedEntity.adapter.renderer.bufferState) {
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
|
id: getPrefixedId('eraser_line'),
|
||||||
|
type: 'eraser_line',
|
||||||
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
|
strokeWidth: toolState.eraser.width,
|
||||||
|
clip: this.getClip(selectedEntity.state),
|
||||||
|
});
|
||||||
|
this.manager.stateApi.$lastAddedPoint.set(alignedPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolState.selected === 'rect') {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
if (drawingBuffer) {
|
||||||
|
if (drawingBuffer.type === 'rect') {
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x);
|
||||||
|
drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
} else {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseLeave = async (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const pos = this.syncLastCursorPos();
|
||||||
|
this.manager.stateApi.$lastCursorPos.set(null);
|
||||||
|
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const isDrawable = selectedEntity?.state.isEnabled;
|
||||||
|
|
||||||
|
if (pos && isDrawable && !this.manager.stateApi.$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
||||||
|
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
||||||
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
||||||
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
|
||||||
|
drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x);
|
||||||
|
drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y);
|
||||||
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
|
selectedEntity.adapter.renderer.commitBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
if (!e.evt.ctrlKey && !e.evt.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
|
||||||
|
let delta = e.evt.deltaY;
|
||||||
|
|
||||||
|
if (toolState.invertScroll) {
|
||||||
|
delta = -delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holding ctrl or meta while scrolling changes the brush size
|
||||||
|
if (toolState.selected === 'brush') {
|
||||||
|
this.manager.stateApi.setBrushWidth(calculateNewBrushSizeFromWheelDelta(toolState.brush.width, delta));
|
||||||
|
} else if (toolState.selected === 'eraser') {
|
||||||
|
this.manager.stateApi.setEraserWidth(calculateNewBrushSizeFromWheelDelta(toolState.eraser.width, delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||||
|
if (selectedEntity) {
|
||||||
|
selectedEntity.adapter.renderer.clearBuffer();
|
||||||
|
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||||
|
}
|
||||||
|
} else if (e.key === ' ') {
|
||||||
|
// Select the view tool on space key down
|
||||||
|
this.manager.stateApi.setToolBuffer(this.manager.stateApi.getToolState().selected);
|
||||||
|
this.manager.stateApi.setTool('view');
|
||||||
|
this.manager.stateApi.$spaceKey.set(true);
|
||||||
|
this.manager.stateApi.$lastCursorPos.set(null);
|
||||||
|
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
const toolBuffer = this.manager.stateApi.getToolState().selectedBuffer;
|
||||||
|
this.manager.stateApi.setTool(toolBuffer ?? 'move');
|
||||||
|
this.manager.stateApi.setToolBuffer(null);
|
||||||
|
this.manager.stateApi.$spaceKey.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getLoggingContext = (): SerializableObject => {
|
getLoggingContext = (): SerializableObject => {
|
||||||
return { ...this.manager.getLoggingContext(), path: this.path.join('.') };
|
return { ...this.manager.getLoggingContext(), path: this.path.join('.') };
|
||||||
};
|
};
|
||||||
|
@ -1,588 +0,0 @@
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|
||||||
import {
|
|
||||||
alignCoordForTool,
|
|
||||||
getPrefixedId,
|
|
||||||
getScaledCursorPosition,
|
|
||||||
offsetCoord,
|
|
||||||
} from 'features/controlLayers/konva/util';
|
|
||||||
import type {
|
|
||||||
CanvasControlLayerState,
|
|
||||||
CanvasInpaintMaskState,
|
|
||||||
CanvasRasterLayerState,
|
|
||||||
CanvasRegionalGuidanceState,
|
|
||||||
CanvasV2State,
|
|
||||||
Coordinate,
|
|
||||||
RgbColor,
|
|
||||||
Tool,
|
|
||||||
} from 'features/controlLayers/store/types';
|
|
||||||
import type Konva from 'konva';
|
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
|
|
||||||
import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY } from './constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
|
|
||||||
* cursor is not over the stage.
|
|
||||||
* @param stage The konva stage
|
|
||||||
* @param setLastCursorPos The callback to store the cursor pos
|
|
||||||
*/
|
|
||||||
const updateLastCursorPos = (
|
|
||||||
stage: Konva.Stage,
|
|
||||||
setLastCursorPos: CanvasManager['stateApi']['$lastCursorPos']['set']
|
|
||||||
) => {
|
|
||||||
const pos = getScaledCursorPosition(stage);
|
|
||||||
if (!pos) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
setLastCursorPos(pos);
|
|
||||||
return pos;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateNewBrushSize = (brushSize: number, delta: number) => {
|
|
||||||
// This equation was derived by fitting a curve to the desired brush sizes and deltas
|
|
||||||
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
|
|
||||||
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
|
|
||||||
// This needs to be clamped to prevent the delta from getting too large
|
|
||||||
const finalDelta = clamp(targetDelta, -20, 20);
|
|
||||||
// The new brush size is also clamped to prevent it from getting too large or small
|
|
||||||
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);
|
|
||||||
|
|
||||||
return newBrushSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextPoint = (
|
|
||||||
currentPos: Coordinate,
|
|
||||||
toolState: CanvasV2State['tool'],
|
|
||||||
lastAddedPoint: Coordinate | null
|
|
||||||
): Coordinate | null => {
|
|
||||||
// Continue the last line
|
|
||||||
const minSpacingPx =
|
|
||||||
toolState.selected === 'brush'
|
|
||||||
? toolState.brush.width * BRUSH_SPACING_TARGET_SCALE
|
|
||||||
: toolState.eraser.width * BRUSH_SPACING_TARGET_SCALE;
|
|
||||||
|
|
||||||
if (lastAddedPoint) {
|
|
||||||
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
|
|
||||||
if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < minSpacingPx) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentPos;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLastPointOfLine = (points: number[]): Coordinate | null => {
|
|
||||||
if (points.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const x = points[points.length - 2];
|
|
||||||
const y = points[points.length - 1];
|
|
||||||
if (x === undefined || y === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { x, y };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLastPointOfLastLineOfEntity = (
|
|
||||||
entity: CanvasRasterLayerState | CanvasControlLayerState | CanvasRegionalGuidanceState | CanvasInpaintMaskState,
|
|
||||||
tool: Tool
|
|
||||||
): Coordinate | null => {
|
|
||||||
const lastObject = entity.objects[entity.objects.length - 1];
|
|
||||||
|
|
||||||
if (!lastObject) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
(lastObject.type === 'brush_line' && tool === 'brush') ||
|
|
||||||
(lastObject.type === 'eraser_line' && tool === 'eraser')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// If the last object type and current tool do not match, we cannot continue the line
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastObject.points.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const x = lastObject.points[lastObject.points.length - 2];
|
|
||||||
const y = lastObject.points[lastObject.points.length - 1];
|
|
||||||
if (x === undefined || y === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { x, y };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColorUnderCursor = (stage: Konva.Stage): RgbColor | null => {
|
|
||||||
const pos = stage.getPointerPosition();
|
|
||||||
if (!pos) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ctx = 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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|
||||||
const stage = manager.stage.konva.stage;
|
|
||||||
const {
|
|
||||||
getToolState,
|
|
||||||
setTool,
|
|
||||||
setToolBuffer,
|
|
||||||
$isMouseDown,
|
|
||||||
$lastMouseDownPos,
|
|
||||||
$lastCursorPos,
|
|
||||||
$lastAddedPoint,
|
|
||||||
$stageAttrs,
|
|
||||||
$spaceKey,
|
|
||||||
getBbox,
|
|
||||||
getSettings,
|
|
||||||
setBrushWidth,
|
|
||||||
setEraserWidth,
|
|
||||||
getCurrentFill,
|
|
||||||
getSelectedEntity,
|
|
||||||
} = manager.stateApi;
|
|
||||||
|
|
||||||
function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
|
||||||
return e.evt.buttons === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getClip(
|
|
||||||
entity: CanvasRegionalGuidanceState | CanvasControlLayerState | CanvasRasterLayerState | CanvasInpaintMaskState
|
|
||||||
) {
|
|
||||||
const settings = getSettings();
|
|
||||||
const bboxRect = getBbox().rect;
|
|
||||||
|
|
||||||
if (settings.clipToBbox) {
|
|
||||||
return {
|
|
||||||
x: bboxRect.x - entity.position.x,
|
|
||||||
y: bboxRect.y - entity.position.y,
|
|
||||||
width: bboxRect.width,
|
|
||||||
height: bboxRect.height,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
x: -stage.x() / stage.scaleX() - entity.position.x,
|
|
||||||
y: -stage.y() / stage.scaleY() - entity.position.y,
|
|
||||||
width: stage.width() / stage.scaleX(),
|
|
||||||
height: stage.height() / stage.scaleY(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region mouseenter
|
|
||||||
stage.on('mouseenter', () => {
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region mousedown
|
|
||||||
stage.on('mousedown', async (e) => {
|
|
||||||
$isMouseDown.set(true);
|
|
||||||
const toolState = getToolState();
|
|
||||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
|
|
||||||
if (toolState.selected === 'colorPicker') {
|
|
||||||
const color = getColorUnderCursor(stage);
|
|
||||||
if (color) {
|
|
||||||
manager.stateApi.$colorUnderCursor.set(color);
|
|
||||||
}
|
|
||||||
if (color) {
|
|
||||||
manager.stateApi.setFill({ ...toolState.fill, ...color });
|
|
||||||
}
|
|
||||||
manager.preview.tool.render();
|
|
||||||
} else {
|
|
||||||
const isDrawable = selectedEntity?.state.isEnabled;
|
|
||||||
if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
|
||||||
$lastMouseDownPos.set(pos);
|
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
|
||||||
|
|
||||||
if (toolState.selected === 'brush') {
|
|
||||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
|
||||||
if (e.evt.shiftKey && lastLinePoint) {
|
|
||||||
// Create a straight line from the last line point
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('brush_line'),
|
|
||||||
type: 'brush_line',
|
|
||||||
points: [
|
|
||||||
// The last point of the last line is already normalized to the entity's coordinates
|
|
||||||
lastLinePoint.x,
|
|
||||||
lastLinePoint.y,
|
|
||||||
alignedPoint.x,
|
|
||||||
alignedPoint.y,
|
|
||||||
],
|
|
||||||
strokeWidth: toolState.brush.width,
|
|
||||||
color: getCurrentFill(),
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('brush_line'),
|
|
||||||
type: 'brush_line',
|
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
|
||||||
strokeWidth: toolState.brush.width,
|
|
||||||
color: getCurrentFill(),
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
|
||||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
|
||||||
if (e.evt.shiftKey && lastLinePoint) {
|
|
||||||
// Create a straight line from the last line point
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('eraser_line'),
|
|
||||||
type: 'eraser_line',
|
|
||||||
points: [
|
|
||||||
// The last point of the last line is already normalized to the entity's coordinates
|
|
||||||
lastLinePoint.x,
|
|
||||||
lastLinePoint.y,
|
|
||||||
alignedPoint.x,
|
|
||||||
alignedPoint.y,
|
|
||||||
],
|
|
||||||
strokeWidth: toolState.eraser.width,
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('eraser_line'),
|
|
||||||
type: 'eraser_line',
|
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
|
||||||
strokeWidth: toolState.eraser.width,
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('rect'),
|
|
||||||
type: 'rect',
|
|
||||||
rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 },
|
|
||||||
color: getCurrentFill(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region mouseup
|
|
||||||
stage.on('mouseup', () => {
|
|
||||||
$isMouseDown.set(false);
|
|
||||||
const pos = $lastCursorPos.get();
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
const isDrawable = selectedEntity?.state.isEnabled;
|
|
||||||
if (pos && isDrawable && !$spaceKey.get()) {
|
|
||||||
const toolState = getToolState();
|
|
||||||
|
|
||||||
if (toolState.selected === 'brush') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer?.type === 'brush_line') {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer?.type === 'eraser_line') {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer?.type === 'rect') {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
}
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region mousemove
|
|
||||||
stage.on('mousemove', async (e) => {
|
|
||||||
const toolState = getToolState();
|
|
||||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
|
|
||||||
if (toolState.selected === 'colorPicker') {
|
|
||||||
const color = getColorUnderCursor(stage);
|
|
||||||
if (color) {
|
|
||||||
manager.stateApi.$colorUnderCursor.set(color);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const isDrawable = selectedEntity?.state.isEnabled;
|
|
||||||
if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
|
||||||
if (toolState.selected === 'brush') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer) {
|
|
||||||
if (drawingBuffer.type === 'brush_line') {
|
|
||||||
const lastPoint = getLastPointOfLine(drawingBuffer.points);
|
|
||||||
const nextPoint = getNextPoint(pos, toolState, lastPoint);
|
|
||||||
if (lastPoint && nextPoint) {
|
|
||||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
|
||||||
// Do not add duplicate points
|
|
||||||
if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) {
|
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('brush_line'),
|
|
||||||
type: 'brush_line',
|
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
|
||||||
strokeWidth: toolState.brush.width,
|
|
||||||
color: getCurrentFill(),
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer) {
|
|
||||||
if (drawingBuffer.type === 'eraser_line') {
|
|
||||||
const lastPoint = getLastPointOfLine(drawingBuffer.points);
|
|
||||||
const nextPoint = getNextPoint(pos, toolState, lastPoint);
|
|
||||||
if (lastPoint && nextPoint) {
|
|
||||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
|
||||||
// Do not add duplicate points
|
|
||||||
if (lastPoint.x !== alignedPoint.x || lastPoint.y !== alignedPoint.y) {
|
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedEntity.adapter.renderer.bufferState) {
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer({
|
|
||||||
id: getPrefixedId('eraser_line'),
|
|
||||||
type: 'eraser_line',
|
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
|
||||||
strokeWidth: toolState.eraser.width,
|
|
||||||
clip: getClip(selectedEntity.state),
|
|
||||||
});
|
|
||||||
$lastAddedPoint.set(alignedPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
if (drawingBuffer) {
|
|
||||||
if (drawingBuffer.type === 'rect') {
|
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
|
||||||
drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x);
|
|
||||||
drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
} else {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region mouseleave
|
|
||||||
stage.on('mouseleave', async (e) => {
|
|
||||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
|
||||||
$lastCursorPos.set(null);
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
const toolState = getToolState();
|
|
||||||
const isDrawable = selectedEntity?.state.isEnabled;
|
|
||||||
|
|
||||||
if (pos && isDrawable && !$spaceKey.get() && getIsPrimaryMouseDown(e)) {
|
|
||||||
const drawingBuffer = selectedEntity.adapter.renderer.bufferState;
|
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
|
||||||
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
|
|
||||||
drawingBuffer.rect.width = Math.round(normalizedPoint.x - drawingBuffer.rect.x);
|
|
||||||
drawingBuffer.rect.height = Math.round(normalizedPoint.y - drawingBuffer.rect.y);
|
|
||||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
|
||||||
selectedEntity.adapter.renderer.commitBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region wheel
|
|
||||||
stage.on('wheel', (e) => {
|
|
||||||
e.evt.preventDefault();
|
|
||||||
|
|
||||||
if (e.evt.ctrlKey || e.evt.metaKey) {
|
|
||||||
const toolState = getToolState();
|
|
||||||
let delta = e.evt.deltaY;
|
|
||||||
if (toolState.invertScroll) {
|
|
||||||
delta = -delta;
|
|
||||||
}
|
|
||||||
// Holding ctrl or meta while scrolling changes the brush size
|
|
||||||
if (toolState.selected === 'brush') {
|
|
||||||
setBrushWidth(calculateNewBrushSize(toolState.brush.width, delta));
|
|
||||||
} else if (toolState.selected === 'eraser') {
|
|
||||||
setEraserWidth(calculateNewBrushSize(toolState.eraser.width, delta));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We need the absolute cursor position - not the scaled position
|
|
||||||
const cursorPos = stage.getPointerPosition();
|
|
||||||
if (cursorPos) {
|
|
||||||
// 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 scale = manager.stage.getScale() * CANVAS_SCALE_BY ** delta;
|
|
||||||
manager.stage.setScale(scale, cursorPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region dragmove
|
|
||||||
stage.on('dragmove', (e) => {
|
|
||||||
if (e.target !== stage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$stageAttrs.set({
|
|
||||||
x: Math.floor(stage.x()),
|
|
||||||
y: Math.floor(stage.y()),
|
|
||||||
width: stage.width(),
|
|
||||||
height: stage.height(),
|
|
||||||
scale: stage.scaleX(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region dragend
|
|
||||||
stage.on('dragend', (e) => {
|
|
||||||
if (e.target !== stage) {
|
|
||||||
return;
|
|
||||||
} // Stage position should always be an integer, else we get fractional pixels which are blurry
|
|
||||||
$stageAttrs.set({
|
|
||||||
x: Math.floor(stage.x()),
|
|
||||||
y: Math.floor(stage.y()),
|
|
||||||
width: stage.width(),
|
|
||||||
height: stage.height(),
|
|
||||||
scale: stage.scaleX(),
|
|
||||||
});
|
|
||||||
manager.preview.tool.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
//#region key
|
|
||||||
const 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
|
|
||||||
const selectedEntity = getSelectedEntity();
|
|
||||||
if (selectedEntity) {
|
|
||||||
selectedEntity.adapter.renderer.clearBuffer();
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
}
|
|
||||||
} else if (e.key === ' ') {
|
|
||||||
// Select the view tool on space key down
|
|
||||||
setToolBuffer(getToolState().selected);
|
|
||||||
setTool('view');
|
|
||||||
$spaceKey.set(true);
|
|
||||||
$lastCursorPos.set(null);
|
|
||||||
$lastMouseDownPos.set(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', onKeyDown);
|
|
||||||
|
|
||||||
const 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
|
|
||||||
const toolBuffer = getToolState().selectedBuffer;
|
|
||||||
setTool(toolBuffer ?? 'move');
|
|
||||||
setToolBuffer(null);
|
|
||||||
$spaceKey.set(false);
|
|
||||||
}
|
|
||||||
manager.preview.tool.render();
|
|
||||||
};
|
|
||||||
window.addEventListener('keyup', onKeyUp);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stage.off('mousedown mouseup mousemove mouseenter mouseleave wheel dragend');
|
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
|
||||||
};
|
|
||||||
};
|
|
@ -2,6 +2,7 @@ import type { CanvasEntityIdentifier, Coordinate, Rect } from 'features/controlL
|
|||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -129,6 +130,64 @@ export const getIsMouseDown = (e: KonvaEventObject<MouseEvent>): boolean => e.ev
|
|||||||
*/
|
*/
|
||||||
export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement);
|
export const getIsFocused = (stage: Konva.Stage): boolean => stage.container().contains(document.activeElement);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last point of a line as a coordinate.
|
||||||
|
* @param points An array of numbers representing points as [x1, y1, x2, y2, ...]
|
||||||
|
* @returns The last point of the line as a coordinate, or null if the line has less than 1 point
|
||||||
|
*/
|
||||||
|
export const getLastPointOfLine = (points: number[]): Coordinate | null => {
|
||||||
|
if (points.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const x = points[points.length - 2];
|
||||||
|
const y = points[points.length - 1];
|
||||||
|
if (x === undefined || y === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
||||||
|
return e.evt.buttons === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the new brush size based on the current brush size and the wheel delta from a mouse wheel event.
|
||||||
|
* @param brushSize The current brush size
|
||||||
|
* @param delta The wheel delta
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const calculateNewBrushSizeFromWheelDelta = (brushSize: number, delta: number) => {
|
||||||
|
// This equation was derived by fitting a curve to the desired brush sizes and deltas
|
||||||
|
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
|
||||||
|
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
|
||||||
|
// This needs to be clamped to prevent the delta from getting too large
|
||||||
|
const finalDelta = clamp(targetDelta, -20, 20);
|
||||||
|
// The new brush size is also clamped to prevent it from getting too large or small
|
||||||
|
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);
|
||||||
|
|
||||||
|
return newBrushSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a candidate point by checking if it is at least `minDistance` away from the last point.
|
||||||
|
* @param candidatePoint The candidate point
|
||||||
|
* @param lastPoint The last point
|
||||||
|
* @param minDistance The minimum distance between points
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const validateCandidatePoint = (
|
||||||
|
candidatePoint: Coordinate,
|
||||||
|
lastPoint: Coordinate | null,
|
||||||
|
minDistance: number
|
||||||
|
): boolean => {
|
||||||
|
if (!lastPoint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.hypot(lastPoint.x - candidatePoint.x, lastPoint.y - candidatePoint.y) >= minDistance;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback
|
* Simple util to map an object to its id property. Serves as a minor optimization to avoid recreating a map callback
|
||||||
* every time we need to map an object to its id, which happens very often.
|
* every time we need to map an object to its id, which happens very often.
|
||||||
|
Loading…
Reference in New Issue
Block a user