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 { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||
import { getLastPointOfLine } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEraserLineState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
@ -180,6 +184,19 @@ export class CanvasLayerAdapter {
|
||||
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') {
|
||||
const info = {
|
||||
repr: this.repr(),
|
||||
|
@ -19,7 +19,6 @@ import type { CanvasLayerAdapter } from './CanvasLayerAdapter';
|
||||
import type { CanvasMaskAdapter } from './CanvasMaskAdapter';
|
||||
import { CanvasPreviewModule } from './CanvasPreviewModule';
|
||||
import { CanvasStateApiModule } from './CanvasStateApiModule';
|
||||
import { setStageEventHandlers } from './events';
|
||||
|
||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||
const TYPE = 'manager';
|
||||
@ -110,7 +109,6 @@ export class CanvasManager {
|
||||
this.stateApi.$currentFill.set(this.stateApi.getCurrentFill());
|
||||
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
|
||||
|
||||
const cleanupEventHandlers = setStageEventHandlers(this);
|
||||
const cleanupStage = this.stage.initialize();
|
||||
const cleanupStore = this.store.subscribe(this.renderer.render);
|
||||
|
||||
@ -122,7 +120,6 @@ export class CanvasManager {
|
||||
this.background.destroy();
|
||||
this.preview.destroy();
|
||||
cleanupStore();
|
||||
cleanupEventHandlers();
|
||||
cleanupStage();
|
||||
};
|
||||
};
|
||||
|
@ -3,11 +3,15 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||
import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer';
|
||||
import { getLastPointOfLine } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEraserLineState,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
@ -136,6 +140,19 @@ export class CanvasMaskAdapter {
|
||||
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 = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
|
@ -1,8 +1,10 @@
|
||||
import type { SerializableObject } from 'common/types';
|
||||
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 type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { clamp } from 'lodash-es';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
@ -27,6 +29,18 @@ export class CanvasStageModule {
|
||||
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 = () => {
|
||||
this.log.debug('Initializing stage');
|
||||
this.konva.stage.container(this.container);
|
||||
@ -34,11 +48,13 @@ export class CanvasStageModule {
|
||||
resizeObserver.observe(this.container);
|
||||
this.fitStageToContainer();
|
||||
this.fitLayersToStage();
|
||||
const cleanupListeners = this.setEventListeners();
|
||||
|
||||
return () => {
|
||||
this.log.debug('Destroying stage');
|
||||
resizeObserver.disconnect();
|
||||
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.
|
||||
*/
|
||||
|
@ -2,11 +2,33 @@ import type { SerializableObject } from 'common/types';
|
||||
import { rgbaColorToString, rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule';
|
||||
import { BRUSH_BORDER_INNER_COLOR, BRUSH_BORDER_OUTER_COLOR } from 'features/controlLayers/konva/constants';
|
||||
import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Tool } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
BRUSH_BORDER_INNER_COLOR,
|
||||
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 Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasToolModule {
|
||||
@ -25,6 +47,7 @@ export class CanvasToolModule {
|
||||
log: Logger;
|
||||
|
||||
konva: {
|
||||
stage: Konva.Stage;
|
||||
group: Konva.Group;
|
||||
brush: {
|
||||
group: Konva.Group;
|
||||
@ -67,6 +90,7 @@ export class CanvasToolModule {
|
||||
this.path = this.manager.path.concat(this.id);
|
||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||
this.konva = {
|
||||
stage: this.manager.stage.konva.stage,
|
||||
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
||||
brush: {
|
||||
group: new Konva.Group({ name: `${this.type}:brush_group`, listening: false }),
|
||||
@ -218,6 +242,10 @@ export class CanvasToolModule {
|
||||
this.render();
|
||||
})
|
||||
);
|
||||
|
||||
const cleanupListeners = this.setEventListeners();
|
||||
|
||||
this.subscriptions.add(cleanupListeners);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
@ -277,7 +305,7 @@ export class CanvasToolModule {
|
||||
|
||||
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
|
||||
this.konva.group.visible(false);
|
||||
} 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 => {
|
||||
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 { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* every time we need to map an object to its id, which happens very often.
|
||||
|
Loading…
Reference in New Issue
Block a user