mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revised event pubsub, transformer logic split out
This commit is contained in:
parent
54f2acf5b9
commit
fa94979ab6
@ -90,7 +90,7 @@ export class CanvasBbox {
|
|||||||
assert(stage, 'Stage must exist');
|
assert(stage, 'Stage must exist');
|
||||||
|
|
||||||
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
||||||
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||||
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
||||||
const scaledGridSize = gridSize * stage.scaleX();
|
const scaledGridSize = gridSize * stage.scaleX();
|
||||||
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
||||||
@ -107,7 +107,7 @@ export class CanvasBbox {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
this.konva.rect.on('dragmove', () => {
|
this.konva.rect.on('dragmove', () => {
|
||||||
const gridSize = this.manager.stateApi.getCtrlKey() || this.manager.stateApi.getMetaKey() ? 8 : 64;
|
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||||
const bbox = this.manager.stateApi.getBbox();
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
const bboxRect: Rect = {
|
const bboxRect: Rect = {
|
||||||
...bbox.rect,
|
...bbox.rect,
|
||||||
@ -129,10 +129,10 @@ export class CanvasBbox {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alt = this.manager.stateApi.getAltKey();
|
const alt = this.manager.stateApi.$altKey.get();
|
||||||
const ctrl = this.manager.stateApi.getCtrlKey();
|
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
||||||
const meta = this.manager.stateApi.getMetaKey();
|
const meta = this.manager.stateApi.$metaKey.get();
|
||||||
const shift = this.manager.stateApi.getShiftKey();
|
const shift = this.manager.stateApi.$shiftKey.get();
|
||||||
|
|
||||||
// Grid size depends on the modifier keys
|
// Grid size depends on the modifier keys
|
||||||
let gridSize = ctrl || meta ? 8 : 64;
|
let gridSize = ctrl || meta ? 8 : 64;
|
||||||
@ -141,7 +141,7 @@ export class CanvasBbox {
|
|||||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||||
if (this.manager.stateApi.getAltKey()) {
|
if (this.manager.stateApi.$altKey.get()) {
|
||||||
gridSize = gridSize * 2;
|
gridSize = gridSize * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@ export class CanvasControlAdapter extends CanvasEntity {
|
|||||||
static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`;
|
static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`;
|
||||||
static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`;
|
||||||
static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`;
|
static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`;
|
||||||
|
static TYPE = 'control_adapter' as const;
|
||||||
|
|
||||||
type = 'control_adapter';
|
type = CanvasControlAdapter.TYPE;
|
||||||
_state: CanvasControlAdapterState;
|
_state: CanvasControlAdapterState;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
|
@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|||||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
||||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type { CanvasBrushLineState, CanvasEraserLineState, CanvasInpaintMaskState, CanvasRectState } from 'features/controlLayers/store/types';
|
import type {
|
||||||
|
CanvasBrushLineState,
|
||||||
|
CanvasEraserLineState,
|
||||||
|
CanvasInpaintMaskState,
|
||||||
|
CanvasRectState,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
|
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -17,11 +22,12 @@ export class CanvasInpaintMask {
|
|||||||
static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`;
|
||||||
static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`;
|
static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`;
|
||||||
static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`;
|
static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`;
|
||||||
|
static TYPE = 'inpaint_mask' as const;
|
||||||
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
||||||
private state: CanvasInpaintMaskState;
|
private state: CanvasInpaintMaskState;
|
||||||
|
|
||||||
id = 'inpaint_mask';
|
id = CanvasInpaintMask.TYPE;
|
||||||
|
type = CanvasInpaintMask.TYPE;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
|
@ -3,7 +3,7 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { 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 { konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util';
|
import { getEmptyRect, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util';
|
||||||
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
|
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import type {
|
import type {
|
||||||
CanvasLayerState,
|
CanvasLayerState,
|
||||||
@ -19,11 +19,12 @@ import type { Logger } from 'roarr';
|
|||||||
import { uploadImage } from 'services/api/endpoints/images';
|
import { uploadImage } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
export class CanvasLayer {
|
export class CanvasLayer {
|
||||||
static TYPE = 'layer';
|
static TYPE = 'layer' as const;
|
||||||
static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`;
|
static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`;
|
||||||
static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`;
|
static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
|
type = CanvasLayer.TYPE;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
getLoggingContext: GetLoggingContext;
|
getLoggingContext: GetLoggingContext;
|
||||||
@ -38,12 +39,11 @@ export class CanvasLayer {
|
|||||||
renderer: CanvasObjectRenderer;
|
renderer: CanvasObjectRenderer;
|
||||||
|
|
||||||
isFirstRender: boolean = true;
|
isFirstRender: boolean = true;
|
||||||
bboxNeedsUpdate: boolean;
|
bboxNeedsUpdate: boolean = true;
|
||||||
isTransforming: boolean;
|
isPendingBboxCalculation: boolean = false;
|
||||||
isPendingBboxCalculation: boolean;
|
|
||||||
|
|
||||||
rect: Rect;
|
rect: Rect = getEmptyRect();
|
||||||
bbox: Rect;
|
bbox: Rect = getEmptyRect();
|
||||||
|
|
||||||
constructor(state: CanvasLayerState, manager: CanvasManager) {
|
constructor(state: CanvasLayerState, manager: CanvasManager) {
|
||||||
this.id = state.id;
|
this.id = state.id;
|
||||||
@ -69,11 +69,6 @@ export class CanvasLayer {
|
|||||||
this.konva.layer.add(...this.transformer.getNodes());
|
this.konva.layer.add(...this.transformer.getNodes());
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.rect = this.getDefaultRect();
|
|
||||||
this.bbox = this.getDefaultRect();
|
|
||||||
this.bboxNeedsUpdate = true;
|
|
||||||
this.isTransforming = false;
|
|
||||||
this.isPendingBboxCalculation = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy = (): void => {
|
destroy = (): void => {
|
||||||
@ -86,8 +81,6 @@ export class CanvasLayer {
|
|||||||
|
|
||||||
update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
||||||
const state = get(arg, 'state', this.state);
|
const state = get(arg, 'state', this.state);
|
||||||
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
|
|
||||||
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
|
|
||||||
|
|
||||||
if (!this.isFirstRender && state === this.state) {
|
if (!this.isFirstRender && state === this.state) {
|
||||||
this.log.trace('State unchanged, skipping update');
|
this.log.trace('State unchanged, skipping update');
|
||||||
@ -109,7 +102,7 @@ export class CanvasLayer {
|
|||||||
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||||
await this.updateVisibility({ isEnabled });
|
await this.updateVisibility({ isEnabled });
|
||||||
}
|
}
|
||||||
await this.updateInteraction({ toolState, isSelected });
|
// this.transformer.syncInteractionState();
|
||||||
|
|
||||||
if (this.isFirstRender) {
|
if (this.isFirstRender) {
|
||||||
await this.updateBbox();
|
await this.updateBbox();
|
||||||
@ -159,40 +152,6 @@ export class CanvasLayer {
|
|||||||
this.konva.objectGroup.opacity(opacity);
|
this.konva.objectGroup.opacity(opacity);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateInteraction = (arg?: { toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
|
||||||
this.log.trace('Updating interaction');
|
|
||||||
|
|
||||||
const toolState = get(arg, 'toolState', this.manager.stateApi.getToolState());
|
|
||||||
const isSelected = get(arg, 'isSelected', this.manager.stateApi.getIsSelected(this.id));
|
|
||||||
|
|
||||||
if (!this.renderer.hasObjects()) {
|
|
||||||
// The layer is totally empty, we can just disable the layer
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
this.transformer.setMode('off');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelected && !this.isTransforming && toolState.selected === 'move') {
|
|
||||||
// We are moving this layer, it must be listening
|
|
||||||
this.konva.layer.listening(true);
|
|
||||||
this.transformer.setMode('drag');
|
|
||||||
} else if (isSelected && this.isTransforming) {
|
|
||||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is
|
|
||||||
// active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected.
|
|
||||||
if (toolState.selected !== 'view') {
|
|
||||||
this.konva.layer.listening(true);
|
|
||||||
this.transformer.setMode('transform');
|
|
||||||
} else {
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
this.transformer.setMode('off');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff
|
|
||||||
this.konva.layer.listening(false);
|
|
||||||
this.transformer.setMode('off');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateBbox = () => {
|
updateBbox = () => {
|
||||||
this.log.trace('Updating bbox');
|
this.log.trace('Updating bbox');
|
||||||
|
|
||||||
@ -208,11 +167,11 @@ export class CanvasLayer {
|
|||||||
// The layer is fully transparent but has objects - reset it
|
// The layer is fully transparent but has objects - reset it
|
||||||
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
||||||
}
|
}
|
||||||
this.transformer.setMode('off');
|
this.transformer.syncInteractionState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transformer.setMode('drag');
|
this.transformer.syncInteractionState();
|
||||||
this.transformer.update(this.state.position, this.bbox);
|
this.transformer.update(this.state.position, this.bbox);
|
||||||
this.konva.objectGroup.setAttrs({
|
this.konva.objectGroup.setAttrs({
|
||||||
x: this.state.position.x + this.bbox.x,
|
x: this.state.position.x + this.bbox.x,
|
||||||
@ -222,18 +181,6 @@ export class CanvasLayer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
startTransform = () => {
|
|
||||||
this.log.debug('Starting transform');
|
|
||||||
this.isTransforming = true;
|
|
||||||
|
|
||||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
|
||||||
// interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
|
|
||||||
// when the view tool is selected
|
|
||||||
const shouldListen = this.manager.stateApi.getToolState().selected !== 'view';
|
|
||||||
this.konva.layer.listening(shouldListen);
|
|
||||||
this.transformer.setMode('transform');
|
|
||||||
};
|
|
||||||
|
|
||||||
resetScale = () => {
|
resetScale = () => {
|
||||||
const attrs = {
|
const attrs = {
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
@ -245,7 +192,7 @@ export class CanvasLayer {
|
|||||||
this.transformer.konva.proxyRect.setAttrs(attrs);
|
this.transformer.konva.proxyRect.setAttrs(attrs);
|
||||||
};
|
};
|
||||||
|
|
||||||
rasterizeLayer = async () => {
|
rasterize = async () => {
|
||||||
this.log.debug('Rasterizing layer');
|
this.log.debug('Rasterizing layer');
|
||||||
|
|
||||||
const objectGroupClone = this.konva.objectGroup.clone();
|
const objectGroupClone = this.konva.objectGroup.clone();
|
||||||
@ -263,20 +210,6 @@ export class CanvasLayer {
|
|||||||
dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }));
|
dispatch(layerRasterized({ id: this.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
stopTransform = () => {
|
|
||||||
this.log.debug('Stopping transform');
|
|
||||||
|
|
||||||
this.isTransforming = false;
|
|
||||||
this.resetScale();
|
|
||||||
this.updatePosition();
|
|
||||||
this.updateBbox();
|
|
||||||
this.updateInteraction();
|
|
||||||
};
|
|
||||||
|
|
||||||
getDefaultRect = (): Rect => {
|
|
||||||
return { x: 0, y: 0, width: 0, height: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateBbox = debounce(() => {
|
calculateBbox = debounce(() => {
|
||||||
this.log.debug('Calculating bbox');
|
this.log.debug('Calculating bbox');
|
||||||
|
|
||||||
@ -284,8 +217,8 @@ export class CanvasLayer {
|
|||||||
|
|
||||||
if (!this.renderer.hasObjects()) {
|
if (!this.renderer.hasObjects()) {
|
||||||
this.log.trace('No objects, resetting bbox');
|
this.log.trace('No objects, resetting bbox');
|
||||||
this.rect = this.getDefaultRect();
|
this.rect = getEmptyRect();
|
||||||
this.bbox = this.getDefaultRect();
|
this.bbox = getEmptyRect();
|
||||||
this.isPendingBboxCalculation = false;
|
this.isPendingBboxCalculation = false;
|
||||||
this.updateBbox();
|
this.updateBbox();
|
||||||
return;
|
return;
|
||||||
@ -324,8 +257,8 @@ export class CanvasLayer {
|
|||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.bbox = this.getDefaultRect();
|
this.bbox = getEmptyRect();
|
||||||
this.rect = this.getDefaultRect();
|
this.rect = getEmptyRect();
|
||||||
}
|
}
|
||||||
this.isPendingBboxCalculation = false;
|
this.isPendingBboxCalculation = false;
|
||||||
this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`);
|
this.log.trace({ bbox: this.bbox, rect: this.rect, extents }, `Got bbox from worker`);
|
||||||
@ -343,7 +276,6 @@ export class CanvasLayer {
|
|||||||
rect: deepClone(this.rect),
|
rect: deepClone(this.rect),
|
||||||
bbox: deepClone(this.bbox),
|
bbox: deepClone(this.bbox),
|
||||||
bboxNeedsUpdate: this.bboxNeedsUpdate,
|
bboxNeedsUpdate: this.bboxNeedsUpdate,
|
||||||
isTransforming: this.isTransforming,
|
|
||||||
isPendingBboxCalculation: this.isPendingBboxCalculation,
|
isPendingBboxCalculation: this.isPendingBboxCalculation,
|
||||||
transformer: this.transformer.repr(),
|
transformer: this.transformer.repr(),
|
||||||
renderer: this.renderer.repr(),
|
renderer: this.renderer.repr(),
|
||||||
|
@ -2,6 +2,7 @@ import type { Store } from '@reduxjs/toolkit';
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import type { JSONObject } from 'common/types';
|
import type { JSONObject } from 'common/types';
|
||||||
|
import { PubSub } from 'common/util/PubSub/PubSub';
|
||||||
import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
||||||
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
||||||
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
|
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
|
||||||
@ -22,7 +23,19 @@ import {
|
|||||||
} from 'features/controlLayers/konva/util';
|
} from 'features/controlLayers/konva/util';
|
||||||
import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker';
|
import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker';
|
||||||
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
|
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import type { CanvasV2State, Coordinate, GenerationMode, GetLoggingContext } from 'features/controlLayers/store/types';
|
import type {
|
||||||
|
CanvasControlAdapterState,
|
||||||
|
CanvasEntity,
|
||||||
|
CanvasEntityIdentifier,
|
||||||
|
CanvasInpaintMaskState,
|
||||||
|
CanvasLayerState,
|
||||||
|
CanvasRegionalGuidanceState,
|
||||||
|
CanvasV2State,
|
||||||
|
Coordinate,
|
||||||
|
GenerationMode,
|
||||||
|
GetLoggingContext,
|
||||||
|
RgbaColor,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
@ -70,6 +83,24 @@ type Util = {
|
|||||||
) => Promise<ImageDTO>;
|
) => Promise<ImageDTO>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EntityStateAndAdapter =
|
||||||
|
| {
|
||||||
|
state: CanvasLayerState;
|
||||||
|
adapter: CanvasLayer;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: CanvasInpaintMaskState;
|
||||||
|
adapter: CanvasInpaintMask;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: CanvasControlAdapterState;
|
||||||
|
adapter: CanvasControlAdapter;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: CanvasRegionalGuidanceState;
|
||||||
|
adapter: CanvasRegion;
|
||||||
|
};
|
||||||
|
|
||||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||||
|
|
||||||
export class CanvasManager {
|
export class CanvasManager {
|
||||||
@ -101,6 +132,11 @@ export class CanvasManager {
|
|||||||
_worker: Worker;
|
_worker: Worker;
|
||||||
_tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>;
|
_tasks: Map<string, { task: GetBboxTask; onComplete: (extents: Extents | null) => void }>;
|
||||||
|
|
||||||
|
toolState: PubSub<CanvasV2State['tool']>;
|
||||||
|
currentFill: PubSub<RgbaColor>;
|
||||||
|
selectedEntity: PubSub<EntityStateAndAdapter | null>;
|
||||||
|
selectedEntityIdentifier: PubSub<CanvasEntityIdentifier | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
container: HTMLDivElement,
|
container: HTMLDivElement,
|
||||||
@ -111,7 +147,7 @@ export class CanvasManager {
|
|||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this._store = store;
|
this._store = store;
|
||||||
this.stateApi = new CanvasStateApi(this._store);
|
this.stateApi = new CanvasStateApi(this._store, this);
|
||||||
this._prevState = this.stateApi.getState();
|
this._prevState = this.stateApi.getState();
|
||||||
this._isFirstRender = true;
|
this._isFirstRender = true;
|
||||||
|
|
||||||
@ -178,6 +214,17 @@ export class CanvasManager {
|
|||||||
};
|
};
|
||||||
this.onTransform = null;
|
this.onTransform = null;
|
||||||
this._isDebugging = false;
|
this._isDebugging = false;
|
||||||
|
|
||||||
|
this.toolState = new PubSub<CanvasV2State['tool']>(this.stateApi.getToolState());
|
||||||
|
this.currentFill = new PubSub<RgbaColor>(this.getCurrentFill());
|
||||||
|
this.selectedEntityIdentifier = new PubSub<CanvasEntityIdentifier | null>(
|
||||||
|
this.stateApi.getState().selectedEntityIdentifier,
|
||||||
|
(a, b) => a?.id === b?.id
|
||||||
|
);
|
||||||
|
this.selectedEntity = new PubSub<EntityStateAndAdapter | null>(
|
||||||
|
this.getSelectedEntity(),
|
||||||
|
(a, b) => a?.state === b?.state && a?.adapter === b?.adapter
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enableDebugging() {
|
enableDebugging() {
|
||||||
@ -226,7 +273,7 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderProgressPreview() {
|
async renderProgressPreview() {
|
||||||
await this.preview.progressPreview.render(this.stateApi.getLastProgressEvent());
|
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderInpaintMask() {
|
async renderInpaintMask() {
|
||||||
@ -279,7 +326,7 @@ export class CanvasManager {
|
|||||||
fitStageToContainer() {
|
fitStageToContainer() {
|
||||||
this.stage.width(this.container.offsetWidth);
|
this.stage.width(this.container.offsetWidth);
|
||||||
this.stage.height(this.container.offsetHeight);
|
this.stage.height(this.container.offsetHeight);
|
||||||
this.stateApi.setStageAttrs({
|
this.stateApi.$stageAttrs.set({
|
||||||
position: { x: this.stage.x(), y: this.stage.y() },
|
position: { x: this.stage.x(), y: this.stage.y() },
|
||||||
dimensions: { width: this.stage.width(), height: this.stage.height() },
|
dimensions: { width: this.stage.width(), height: this.stage.height() },
|
||||||
scale: this.stage.scaleX(),
|
scale: this.stage.scaleX(),
|
||||||
@ -287,8 +334,57 @@ export class CanvasManager {
|
|||||||
this.background.render();
|
this.background.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null {
|
||||||
|
const state = this.stateApi.getState();
|
||||||
|
|
||||||
|
let entityState: CanvasEntity | null = null;
|
||||||
|
let entityAdapter: CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null = null;
|
||||||
|
|
||||||
|
if (identifier.type === 'layer') {
|
||||||
|
entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
entityAdapter = this.layers.get(identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'control_adapter') {
|
||||||
|
entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
entityAdapter = this.controlAdapters.get(identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'regional_guidance') {
|
||||||
|
entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
||||||
|
entityAdapter = this.regions.get(identifier.id) ?? null;
|
||||||
|
} else if (identifier.type === 'inpaint_mask') {
|
||||||
|
entityState = state.inpaintMask;
|
||||||
|
entityAdapter = this.inpaintMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityState && entityAdapter && entityState.type === entityAdapter.type) {
|
||||||
|
return { state: entityState, adapter: entityAdapter } as EntityStateAndAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedEntity = () => {
|
||||||
|
const state = this.stateApi.getState();
|
||||||
|
if (state.selectedEntityIdentifier) {
|
||||||
|
return this.getEntity(state.selectedEntityIdentifier);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
getCurrentFill = () => {
|
||||||
|
const state = this.stateApi.getState();
|
||||||
|
let currentFill: RgbaColor = state.tool.fill;
|
||||||
|
const selectedEntity = this.getSelectedEntity();
|
||||||
|
if (selectedEntity) {
|
||||||
|
if (selectedEntity.state.type === 'regional_guidance') {
|
||||||
|
currentFill = { ...selectedEntity.state.fill, a: state.settings.maskOpacity };
|
||||||
|
} else if (selectedEntity.state.type === 'inpaint_mask') {
|
||||||
|
currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentFill;
|
||||||
|
};
|
||||||
|
|
||||||
getTransformingLayer() {
|
getTransformingLayer() {
|
||||||
return Array.from(this.layers.values()).find((layer) => layer.isTransforming);
|
return Array.from(this.layers.values()).find((layer) => layer.transformer.isTransforming);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIsTransforming() {
|
getIsTransforming() {
|
||||||
@ -299,17 +395,17 @@ export class CanvasManager {
|
|||||||
if (this.getIsTransforming()) {
|
if (this.getIsTransforming()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const layer = this.getSelectedEntityAdapter();
|
const layer = this.getSelectedEntity();
|
||||||
assert(layer instanceof CanvasLayer, 'No selected layer');
|
// TODO(psyche): Support other entity types
|
||||||
layer.startTransform();
|
assert(layer?.adapter instanceof CanvasLayer, 'No selected layer');
|
||||||
|
layer.adapter.transformer.startTransform();
|
||||||
this.onTransform?.(true);
|
this.onTransform?.(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyTransform() {
|
async applyTransform() {
|
||||||
const layer = this.getTransformingLayer();
|
const layer = this.getTransformingLayer();
|
||||||
if (layer) {
|
if (layer) {
|
||||||
await layer.rasterizeLayer();
|
await layer.transformer.applyTransform();
|
||||||
layer.stopTransform();
|
|
||||||
}
|
}
|
||||||
this.onTransform?.(false);
|
this.onTransform?.(false);
|
||||||
}
|
}
|
||||||
@ -317,7 +413,7 @@ export class CanvasManager {
|
|||||||
cancelTransform() {
|
cancelTransform() {
|
||||||
const layer = this.getTransformingLayer();
|
const layer = this.getTransformingLayer();
|
||||||
if (layer) {
|
if (layer) {
|
||||||
layer.stopTransform();
|
layer.transformer.stopTransform();
|
||||||
}
|
}
|
||||||
this.onTransform?.(false);
|
this.onTransform?.(false);
|
||||||
}
|
}
|
||||||
@ -355,16 +451,10 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
this.toolState.publish(state.tool);
|
||||||
this._isFirstRender ||
|
this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier);
|
||||||
state.tool.selected !== this._prevState.tool.selected ||
|
this.selectedEntity.publish(this.getSelectedEntity());
|
||||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
this.currentFill.publish(this.getCurrentFill());
|
||||||
) {
|
|
||||||
this.log.debug('Updating interaction');
|
|
||||||
for (const layer of this.layers.values()) {
|
|
||||||
layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this._isFirstRender ||
|
this._isFirstRender ||
|
||||||
@ -521,24 +611,6 @@ export class CanvasManager {
|
|||||||
return CanvasManager.BBOX_PADDING_PX;
|
return CanvasManager.BBOX_PADDING_PX;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => {
|
|
||||||
const state = this.stateApi.getState();
|
|
||||||
const identifier = state.selectedEntityIdentifier;
|
|
||||||
if (!identifier) {
|
|
||||||
return null;
|
|
||||||
} else if (identifier.type === 'layer') {
|
|
||||||
return this.layers.get(identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'control_adapter') {
|
|
||||||
return this.controlAdapters.get(identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'regional_guidance') {
|
|
||||||
return this.regions.get(identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'inpaint_mask') {
|
|
||||||
return this.inpaintMask;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getGenerationMode(): GenerationMode {
|
getGenerationMode(): GenerationMode {
|
||||||
const session = this.stateApi.getSession();
|
const session = this.stateApi.getSession();
|
||||||
if (session.isActive) {
|
if (session.isActive) {
|
||||||
|
@ -25,6 +25,9 @@ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | Ca
|
|||||||
*/
|
*/
|
||||||
type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles rendering of objects for a canvas entity.
|
||||||
|
*/
|
||||||
export class CanvasObjectRenderer {
|
export class CanvasObjectRenderer {
|
||||||
static TYPE = 'object_renderer';
|
static TYPE = 'object_renderer';
|
||||||
|
|
||||||
|
@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|||||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
||||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||||
import { mapId } from 'features/controlLayers/konva/util';
|
import { mapId } from 'features/controlLayers/konva/util';
|
||||||
import type { CanvasBrushLineState, CanvasEraserLineState, CanvasRectState, CanvasRegionalGuidanceState } from 'features/controlLayers/store/types';
|
import type {
|
||||||
|
CanvasBrushLineState,
|
||||||
|
CanvasEraserLineState,
|
||||||
|
CanvasRectState,
|
||||||
|
CanvasRegionalGuidanceState,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
|
import { isDrawingTool, RGBA_RED } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -17,11 +22,13 @@ export class CanvasRegion {
|
|||||||
static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`;
|
||||||
static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`;
|
static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`;
|
||||||
static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`;
|
static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`;
|
||||||
|
static TYPE = 'regional_guidance' as const;
|
||||||
|
|
||||||
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
||||||
private state: CanvasRegionalGuidanceState;
|
private state: CanvasRegionalGuidanceState;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
|
type = CanvasRegion.TYPE;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
|
@ -34,7 +34,7 @@ export class CanvasStagingArea {
|
|||||||
render = async () => {
|
render = async () => {
|
||||||
const session = this.manager.stateApi.getSession();
|
const session = this.manager.stateApi.getSession();
|
||||||
const bboxRect = this.manager.stateApi.getBbox().rect;
|
const bboxRect = this.manager.stateApi.getBbox().rect;
|
||||||
const shouldShowStagedImage = this.manager.stateApi.getShouldShowStagedImage();
|
const shouldShowStagedImage = this.manager.stateApi.$shouldShowStagedImage.get();
|
||||||
|
|
||||||
this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null;
|
this.selectedImage = session.stagedImages[session.selectedStagedImageIndex] ?? null;
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export class CanvasStagingArea {
|
|||||||
this.image.konva.group.x(bboxRect.x + offsetX);
|
this.image.konva.group.x(bboxRect.x + offsetX);
|
||||||
this.image.konva.group.y(bboxRect.y + offsetY);
|
this.image.konva.group.y(bboxRect.y + offsetY);
|
||||||
await this.image.updateImageSource(imageDTO.image_name);
|
await this.image.updateImageSource(imageDTO.image_name);
|
||||||
this.manager.stateApi.resetLastProgressEvent();
|
this.manager.stateApi.$lastProgressEvent.set(null);
|
||||||
}
|
}
|
||||||
this.image.konva.group.visible(shouldShowStagedImage);
|
this.image.konva.group.visible(shouldShowStagedImage);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,7 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library';
|
|||||||
import type { Store } from '@reduxjs/toolkit';
|
import type { Store } from '@reduxjs/toolkit';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { buildSubscribe } from 'features/controlLayers/konva/util';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import {
|
import {
|
||||||
$isDrawing,
|
$isDrawing,
|
||||||
$isMouseDown,
|
$isMouseDown,
|
||||||
@ -49,173 +49,143 @@ import type {
|
|||||||
CanvasBrushLineState,
|
CanvasBrushLineState,
|
||||||
CanvasEntity,
|
CanvasEntity,
|
||||||
CanvasEraserLineState,
|
CanvasEraserLineState,
|
||||||
PositionChangedArg,
|
|
||||||
CanvasRectState,
|
CanvasRectState,
|
||||||
|
PositionChangedArg,
|
||||||
ScaleChangedArg,
|
ScaleChangedArg,
|
||||||
Tool,
|
Tool,
|
||||||
} from 'features/controlLayers/store/types';
|
} from 'features/controlLayers/store/types';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
const log = logger('canvas');
|
const log = logger('canvas');
|
||||||
|
|
||||||
export class CanvasStateApi {
|
|
||||||
private store: Store<RootState>;
|
|
||||||
|
|
||||||
constructor(store: Store<RootState>) {
|
export class CanvasStateApi {
|
||||||
this.store = store;
|
_store: Store<RootState>;
|
||||||
|
manager: CanvasManager;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(store: Store<RootState>, manager: CanvasManager) {
|
||||||
|
this._store = store;
|
||||||
|
this.manager = manager;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reminder - use arrow functions to avoid binding issues
|
// Reminder - use arrow functions to avoid binding issues
|
||||||
getState = () => {
|
getState = () => {
|
||||||
return this.store.getState().canvasV2;
|
return this._store.getState().canvasV2;
|
||||||
};
|
};
|
||||||
onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => {
|
onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('onEntityReset');
|
log.debug('onEntityReset');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerReset(arg));
|
this._store.dispatch(layerReset(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => {
|
onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('onPosChanged');
|
log.debug('onPosChanged');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerTranslated(arg));
|
this._store.dispatch(layerTranslated(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgTranslated(arg));
|
this._store.dispatch(rgTranslated(arg));
|
||||||
} else if (entityType === 'inpaint_mask') {
|
} else if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imTranslated(arg));
|
this._store.dispatch(imTranslated(arg));
|
||||||
} else if (entityType === 'control_adapter') {
|
} else if (entityType === 'control_adapter') {
|
||||||
this.store.dispatch(caTranslated(arg));
|
this._store.dispatch(caTranslated(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('onScaleChanged');
|
log.debug('onScaleChanged');
|
||||||
if (entityType === 'inpaint_mask') {
|
if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imScaled(arg));
|
this._store.dispatch(imScaled(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgScaled(arg));
|
this._store.dispatch(rgScaled(arg));
|
||||||
} else if (entityType === 'control_adapter') {
|
} else if (entityType === 'control_adapter') {
|
||||||
this.store.dispatch(caScaled(arg));
|
this._store.dispatch(caScaled(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('Entity bbox changed');
|
log.debug('Entity bbox changed');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerBboxChanged(arg));
|
this._store.dispatch(layerBboxChanged(arg));
|
||||||
} else if (entityType === 'control_adapter') {
|
} else if (entityType === 'control_adapter') {
|
||||||
this.store.dispatch(caBboxChanged(arg));
|
this._store.dispatch(caBboxChanged(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgBboxChanged(arg));
|
this._store.dispatch(rgBboxChanged(arg));
|
||||||
} else if (entityType === 'inpaint_mask') {
|
} else if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imBboxChanged(arg));
|
this._store.dispatch(imBboxChanged(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => {
|
onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('Brush line added');
|
log.debug('Brush line added');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerBrushLineAdded(arg));
|
this._store.dispatch(layerBrushLineAdded(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgBrushLineAdded(arg));
|
this._store.dispatch(rgBrushLineAdded(arg));
|
||||||
} else if (entityType === 'inpaint_mask') {
|
} else if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imBrushLineAdded(arg));
|
this._store.dispatch(imBrushLineAdded(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => {
|
onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('Eraser line added');
|
log.debug('Eraser line added');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerEraserLineAdded(arg));
|
this._store.dispatch(layerEraserLineAdded(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgEraserLineAdded(arg));
|
this._store.dispatch(rgEraserLineAdded(arg));
|
||||||
} else if (entityType === 'inpaint_mask') {
|
} else if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imEraserLineAdded(arg));
|
this._store.dispatch(imEraserLineAdded(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => {
|
onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('Rect shape added');
|
log.debug('Rect shape added');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
this.store.dispatch(layerRectShapeAdded(arg));
|
this._store.dispatch(layerRectShapeAdded(arg));
|
||||||
} else if (entityType === 'regional_guidance') {
|
} else if (entityType === 'regional_guidance') {
|
||||||
this.store.dispatch(rgRectShapeAdded(arg));
|
this._store.dispatch(rgRectShapeAdded(arg));
|
||||||
} else if (entityType === 'inpaint_mask') {
|
} else if (entityType === 'inpaint_mask') {
|
||||||
this.store.dispatch(imRectShapeAdded(arg));
|
this._store.dispatch(imRectShapeAdded(arg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => {
|
onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => {
|
||||||
log.debug('Entity selected');
|
log.debug('Entity selected');
|
||||||
this.store.dispatch(entitySelected(arg));
|
this._store.dispatch(entitySelected(arg));
|
||||||
};
|
};
|
||||||
onBboxTransformed = (bbox: IRect) => {
|
onBboxTransformed = (bbox: IRect) => {
|
||||||
log.debug('Generation bbox transformed');
|
log.debug('Generation bbox transformed');
|
||||||
this.store.dispatch(bboxChanged(bbox));
|
this._store.dispatch(bboxChanged(bbox));
|
||||||
};
|
};
|
||||||
onBrushWidthChanged = (width: number) => {
|
onBrushWidthChanged = (width: number) => {
|
||||||
log.debug('Brush width changed');
|
log.debug('Brush width changed');
|
||||||
this.store.dispatch(brushWidthChanged(width));
|
this._store.dispatch(brushWidthChanged(width));
|
||||||
};
|
};
|
||||||
onEraserWidthChanged = (width: number) => {
|
onEraserWidthChanged = (width: number) => {
|
||||||
log.debug('Eraser width changed');
|
log.debug('Eraser width changed');
|
||||||
this.store.dispatch(eraserWidthChanged(width));
|
this._store.dispatch(eraserWidthChanged(width));
|
||||||
};
|
};
|
||||||
onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
||||||
log.debug('Region mask image cached');
|
log.debug('Region mask image cached');
|
||||||
this.store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
this._store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||||
};
|
};
|
||||||
onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
||||||
log.debug('Inpaint mask image cached');
|
log.debug('Inpaint mask image cached');
|
||||||
this.store.dispatch(imImageCacheChanged({ imageDTO }));
|
this._store.dispatch(imImageCacheChanged({ imageDTO }));
|
||||||
};
|
};
|
||||||
onLayerImageCached = (imageDTO: ImageDTO) => {
|
onLayerImageCached = (imageDTO: ImageDTO) => {
|
||||||
log.debug('Layer image cached');
|
log.debug('Layer image cached');
|
||||||
this.store.dispatch(layerImageCacheChanged({ imageDTO }));
|
this._store.dispatch(layerImageCacheChanged({ imageDTO }));
|
||||||
};
|
};
|
||||||
setTool = (tool: Tool) => {
|
setTool = (tool: Tool) => {
|
||||||
log.debug('Tool selection changed');
|
log.debug('Tool selection changed');
|
||||||
this.store.dispatch(toolChanged(tool));
|
this._store.dispatch(toolChanged(tool));
|
||||||
};
|
};
|
||||||
setToolBuffer = (toolBuffer: Tool | null) => {
|
setToolBuffer = (toolBuffer: Tool | null) => {
|
||||||
log.debug('Tool buffer changed');
|
log.debug('Tool buffer changed');
|
||||||
this.store.dispatch(toolBufferChanged(toolBuffer));
|
this._store.dispatch(toolBufferChanged(toolBuffer));
|
||||||
};
|
};
|
||||||
|
|
||||||
getSelectedEntity = (): CanvasEntity | null => {
|
|
||||||
const state = this.getState();
|
|
||||||
const identifier = state.selectedEntityIdentifier;
|
|
||||||
if (!identifier) {
|
|
||||||
return null;
|
|
||||||
} else if (identifier.type === 'layer') {
|
|
||||||
return state.layers.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'control_adapter') {
|
|
||||||
return state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'ip_adapter') {
|
|
||||||
return state.ipAdapters.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'regional_guidance') {
|
|
||||||
return state.regions.entities.find((i) => i.id === identifier.id) ?? null;
|
|
||||||
} else if (identifier.type === 'inpaint_mask') {
|
|
||||||
return state.inpaintMask;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getCurrentFill = () => {
|
|
||||||
const state = this.getState();
|
|
||||||
const selectedEntity = this.getSelectedEntity();
|
|
||||||
let currentFill: RgbaColor = state.tool.fill;
|
|
||||||
if (selectedEntity) {
|
|
||||||
if (selectedEntity.type === 'regional_guidance') {
|
|
||||||
currentFill = { ...selectedEntity.fill, a: state.settings.maskOpacity };
|
|
||||||
} else if (selectedEntity.type === 'inpaint_mask') {
|
|
||||||
currentFill = { ...state.inpaintMask.fill, a: state.settings.maskOpacity };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentFill = state.tool.fill;
|
|
||||||
}
|
|
||||||
return currentFill;
|
|
||||||
};
|
|
||||||
getBbox = () => {
|
getBbox = () => {
|
||||||
return this.getState().bbox;
|
return this.getState().bbox;
|
||||||
};
|
};
|
||||||
|
|
||||||
getToolState = () => {
|
getToolState = () => {
|
||||||
return this.getState().tool;
|
return this.getState().tool;
|
||||||
};
|
};
|
||||||
@ -244,61 +214,24 @@ export class CanvasStateApi {
|
|||||||
return this.getState().session;
|
return this.getState().session;
|
||||||
};
|
};
|
||||||
getIsSelected = (id: string) => {
|
getIsSelected = (id: string) => {
|
||||||
return this.getSelectedEntity()?.id === id;
|
return this.getState().selectedEntityIdentifier?.id === id;
|
||||||
};
|
};
|
||||||
getLogLevel = () => {
|
getLogLevel = () => {
|
||||||
return this.store.getState().system.consoleLogLevel;
|
return this._store.getState().system.consoleLogLevel;
|
||||||
};
|
|
||||||
|
|
||||||
// Read-only state, derived from nanostores
|
|
||||||
resetLastProgressEvent = () => {
|
|
||||||
$lastProgressEvent.set(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read-write state, ephemeral interaction state
|
// Read-write state, ephemeral interaction state
|
||||||
getIsDrawing = $isDrawing.get;
|
$isDrawing = $isDrawing;
|
||||||
setIsDrawing = $isDrawing.set;
|
$isMouseDown = $isMouseDown;
|
||||||
onIsDrawingChanged = $isDrawing.subscribe;
|
$lastAddedPoint = $lastAddedPoint;
|
||||||
|
$lastMouseDownPos = $lastMouseDownPos;
|
||||||
getIsMouseDown = $isMouseDown.get;
|
$lastCursorPos = $lastCursorPos;
|
||||||
setIsMouseDown = $isMouseDown.set;
|
$lastProgressEvent = $lastProgressEvent;
|
||||||
onIsMouseDownChanged = $isMouseDown.subscribe;
|
$spaceKey = $spaceKey;
|
||||||
|
$altKey = $alt;
|
||||||
getLastAddedPoint = $lastAddedPoint.get;
|
$ctrlKey = $ctrl;
|
||||||
setLastAddedPoint = $lastAddedPoint.set;
|
$metaKey = $meta;
|
||||||
onLastAddedPointChanged = $lastAddedPoint.subscribe;
|
$shiftKey = $shift;
|
||||||
|
$shouldShowStagedImage = $shouldShowStagedImage;
|
||||||
getLastMouseDownPos = $lastMouseDownPos.get;
|
$stageAttrs = $stageAttrs;
|
||||||
setLastMouseDownPos = $lastMouseDownPos.set;
|
|
||||||
onLastMouseDownPosChanged = $lastMouseDownPos.subscribe;
|
|
||||||
|
|
||||||
getLastCursorPos = $lastCursorPos.get;
|
|
||||||
setLastCursorPos = $lastCursorPos.set;
|
|
||||||
onLastCursorPosChanged = $lastCursorPos.subscribe;
|
|
||||||
|
|
||||||
getSpaceKey = $spaceKey.get;
|
|
||||||
setSpaceKey = $spaceKey.set;
|
|
||||||
onSpaceKeyChanged = $spaceKey.subscribe;
|
|
||||||
|
|
||||||
getLastProgressEvent = $lastProgressEvent.get;
|
|
||||||
setLastProgressEvent = $lastProgressEvent.set;
|
|
||||||
onLastProgressEventChanged = $lastProgressEvent.subscribe;
|
|
||||||
|
|
||||||
getAltKey = $alt.get;
|
|
||||||
onAltChanged = $alt.subscribe;
|
|
||||||
|
|
||||||
getCtrlKey = $ctrl.get;
|
|
||||||
onCtrlChanged = $ctrl.subscribe;
|
|
||||||
|
|
||||||
getMetaKey = $meta.get;
|
|
||||||
onMetaChanged = $meta.subscribe;
|
|
||||||
|
|
||||||
getShiftKey = $shift.get;
|
|
||||||
onShiftChanged = buildSubscribe($shift.subscribe, 'onShiftChanged');
|
|
||||||
|
|
||||||
getShouldShowStagedImage = $shouldShowStagedImage.get;
|
|
||||||
onGetShouldShowStagedImageChanged = $shouldShowStagedImage.subscribe;
|
|
||||||
|
|
||||||
setStageAttrs = $stageAttrs.set;
|
|
||||||
onStageAttrsChanged = buildSubscribe($stageAttrs.subscribe, 'onStageAttrsChanged');
|
|
||||||
}
|
}
|
||||||
|
@ -139,17 +139,17 @@ export class CanvasTool {
|
|||||||
const stage = this.manager.stage;
|
const stage = this.manager.stage;
|
||||||
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||||
const toolState = this.manager.stateApi.getToolState();
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
const currentFill = this.manager.stateApi.getCurrentFill();
|
const currentFill = this.manager.getCurrentFill();
|
||||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
const selectedEntity = this.manager.getSelectedEntity();
|
||||||
const cursorPos = this.manager.stateApi.getLastCursorPos();
|
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||||
const isDrawing = this.manager.stateApi.getIsDrawing();
|
const isDrawing = this.manager.stateApi.$isDrawing.get();
|
||||||
const isMouseDown = this.manager.stateApi.getIsMouseDown();
|
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||||
|
|
||||||
const tool = toolState.selected;
|
const tool = toolState.selected;
|
||||||
const isDrawableEntity =
|
const isDrawableEntity =
|
||||||
selectedEntity?.type === 'regional_guidance' ||
|
selectedEntity?.state.type === 'regional_guidance' ||
|
||||||
selectedEntity?.type === 'layer' ||
|
selectedEntity?.state.type === 'layer' ||
|
||||||
selectedEntity?.type === 'inpaint_mask';
|
selectedEntity?.state.type === 'inpaint_mask';
|
||||||
|
|
||||||
// Update the stage's pointer style
|
// Update the stage's pointer style
|
||||||
if (tool === 'view') {
|
if (tool === 'view') {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import type { Subscription } from 'features/controlLayers/konva/util';
|
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types';
|
import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
@ -19,32 +18,50 @@ export class CanvasTransformer {
|
|||||||
static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`;
|
static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`;
|
||||||
static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`;
|
static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`;
|
||||||
static BBOX_OUTLINE_NAME = `${CanvasTransformer.TYPE}:bbox_outline`;
|
static BBOX_OUTLINE_NAME = `${CanvasTransformer.TYPE}:bbox_outline`;
|
||||||
static STROKE_COLOR = 'hsl(200deg 76% 59%)'; // `invokeBlue.400
|
static STROKE_COLOR = 'hsl(200 76% 50% / 1)'; // invokeBlue.500
|
||||||
|
static ANCHOR_FILL_COLOR = CanvasTransformer.STROKE_COLOR;
|
||||||
|
static ANCHOR_STROKE_COLOR = 'hsl(200 76% 77% / 1)'; // invokeBlue.200
|
||||||
|
static RESIZE_ANCHOR_SIZE = 8;
|
||||||
|
static ROTATE_ANCHOR_FILL_COLOR = 'hsl(200 76% 95% / 1)'; // invokeBlue.50
|
||||||
|
static ROTATE_ANCHOR_STROKE_COLOR = 'hsl(200 76% 40% / 1)'; // invokeBlue.700
|
||||||
|
static ROTATE_ANCHOR_SIZE = 12;
|
||||||
|
static ANCHOR_CORNER_RADIUS_RATIO = 0.5;
|
||||||
|
static ANCHOR_STROKE_WIDTH = 2;
|
||||||
|
static ANCHOR_HIT_PADDING = 10;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
parent: CanvasLayer;
|
parent: CanvasLayer;
|
||||||
manager: CanvasManager;
|
manager: CanvasManager;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
getLoggingContext: GetLoggingContext;
|
getLoggingContext: GetLoggingContext;
|
||||||
subscriptions: Subscription[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current mode of the transformer:
|
* A list of subscriptions that should be cleaned up when the transformer is destroyed.
|
||||||
* - 'transform': The entity can be moved, resized, and rotated
|
|
||||||
* - 'drag': The entity can only be moved
|
|
||||||
* - 'off': The transformer is disabled
|
|
||||||
*/
|
*/
|
||||||
mode: 'transform' | 'drag' | 'off';
|
subscriptions: (() => void)[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether dragging is enabled. Dragging is enabled in both 'transform' and 'drag' modes.
|
* Whether the transformer is currently transforming the entity.
|
||||||
*/
|
*/
|
||||||
isDragEnabled: boolean;
|
isTransforming: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether transforming is enabled. Transforming is enabled only in 'transform' mode.
|
* The current interaction mode of the transformer:
|
||||||
|
* - 'all': The entity can be moved, resized, and rotated.
|
||||||
|
* - 'drag': The entity can be moved.
|
||||||
|
* - 'off': The transformer is not interactable.
|
||||||
*/
|
*/
|
||||||
isTransformEnabled: boolean;
|
interactionMode: 'all' | 'drag' | 'off' = 'off';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether dragging is enabled. Dragging is enabled in both 'all' and 'drag' interaction modes.
|
||||||
|
*/
|
||||||
|
isDragEnabled: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether transforming is enabled. Transforming is enabled only in 'all' interaction mode.
|
||||||
|
*/
|
||||||
|
isTransformEnabled: boolean = false;
|
||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
@ -59,11 +76,6 @@ export class CanvasTransformer {
|
|||||||
|
|
||||||
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
|
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
|
||||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
this.subscriptions = [];
|
|
||||||
|
|
||||||
this.mode = 'off';
|
|
||||||
this.isDragEnabled = false;
|
|
||||||
this.isTransformEnabled = false;
|
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
bboxOutline: new Konva.Rect({
|
bboxOutline: new Konva.Rect({
|
||||||
@ -89,6 +101,35 @@ export class CanvasTransformer {
|
|||||||
padding: this.manager.getTransformerPadding(),
|
padding: this.manager.getTransformerPadding(),
|
||||||
// This is `invokeBlue.400`
|
// This is `invokeBlue.400`
|
||||||
stroke: CanvasTransformer.STROKE_COLOR,
|
stroke: CanvasTransformer.STROKE_COLOR,
|
||||||
|
anchorFill: CanvasTransformer.ANCHOR_FILL_COLOR,
|
||||||
|
anchorStroke: CanvasTransformer.ANCHOR_STROKE_COLOR,
|
||||||
|
anchorStrokeWidth: CanvasTransformer.ANCHOR_STROKE_WIDTH,
|
||||||
|
anchorSize: CanvasTransformer.RESIZE_ANCHOR_SIZE,
|
||||||
|
anchorCornerRadius: CanvasTransformer.RESIZE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO,
|
||||||
|
anchorStyleFunc: (anchor) => {
|
||||||
|
if (anchor.hasName('rotater')) {
|
||||||
|
anchor.setAttrs({
|
||||||
|
height: CanvasTransformer.ROTATE_ANCHOR_SIZE,
|
||||||
|
width: CanvasTransformer.ROTATE_ANCHOR_SIZE,
|
||||||
|
cornerRadius: CanvasTransformer.ROTATE_ANCHOR_SIZE * CanvasTransformer.ANCHOR_CORNER_RADIUS_RATIO,
|
||||||
|
fill: CanvasTransformer.ROTATE_ANCHOR_FILL_COLOR,
|
||||||
|
stroke: CanvasTransformer.ANCHOR_FILL_COLOR,
|
||||||
|
offsetX: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2,
|
||||||
|
offsetY: CanvasTransformer.ROTATE_ANCHOR_SIZE / 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
anchor.hitFunc((context) => {
|
||||||
|
context.beginPath();
|
||||||
|
context.rect(
|
||||||
|
-CanvasTransformer.ANCHOR_HIT_PADDING,
|
||||||
|
-CanvasTransformer.ANCHOR_HIT_PADDING,
|
||||||
|
anchor.width() + CanvasTransformer.ANCHOR_HIT_PADDING * 2,
|
||||||
|
anchor.height() + CanvasTransformer.ANCHOR_HIT_PADDING * 2
|
||||||
|
);
|
||||||
|
context.closePath();
|
||||||
|
context.fillStrokeShape(anchor);
|
||||||
|
});
|
||||||
|
},
|
||||||
// TODO(psyche): The konva Vector2D type is is apparently not compatible with the JSONObject type that the log
|
// TODO(psyche): The konva Vector2D type is is apparently not compatible with the JSONObject type that the log
|
||||||
// function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and
|
// function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and
|
||||||
// TypeScript is happy with it.
|
// TypeScript is happy with it.
|
||||||
@ -152,7 +193,7 @@ export class CanvasTransformer {
|
|||||||
// This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and
|
// This transform constraint operates on the bounding box of the transformer. This box has x, y, width, and
|
||||||
// height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to
|
// height in stage coordinates, and rotation in radians. This can be used to snap the transformer rotation to
|
||||||
// the nearest 45 degrees when shift is held.
|
// the nearest 45 degrees when shift is held.
|
||||||
if (this.manager.stateApi.getShiftKey()) {
|
if (this.manager.stateApi.$shiftKey.get()) {
|
||||||
if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) {
|
if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) {
|
||||||
return oldBoundBox;
|
return oldBoundBox;
|
||||||
}
|
}
|
||||||
@ -278,9 +319,9 @@ export class CanvasTransformer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.konva.proxyRect.on('dragend', () => {
|
this.konva.proxyRect.on('dragend', () => {
|
||||||
if (this.parent.isTransforming) {
|
if (this.isTransforming) {
|
||||||
// When the user cancels the transformation, we need to reset the layer, so we should not update the layer's
|
// If we are transforming the entity, we should not push the new position to the state. This will trigger a
|
||||||
// positition while we are transforming - bail out early.
|
// re-render of the entity and bork the transformation.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,9 +337,9 @@ export class CanvasTransformer {
|
|||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
// When the stage scale changes, we may need to re-scale some of the transformer's components. For example,
|
// When the stage scale changes, we may need to re-scale some of the transformer's components. For example,
|
||||||
// the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width.
|
// the bbox outline should always be 1 screen pixel wide, so we need to update its stroke width.
|
||||||
this.manager.stateApi.onStageAttrsChanged((newAttrs, oldAttrs) => {
|
this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => {
|
||||||
if (newAttrs.scale !== oldAttrs?.scale) {
|
if (newVal.scale !== oldVal.scale) {
|
||||||
this.scale();
|
this.syncScale();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -306,8 +347,24 @@ export class CanvasTransformer {
|
|||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
// While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state
|
// While the user holds shift, we want to snap rotation to 45 degree increments. Listen for the shift key state
|
||||||
// and update the snap angles accordingly.
|
// and update the snap angles accordingly.
|
||||||
this.manager.stateApi.onShiftChanged((isPressed) => {
|
this.manager.stateApi.$shiftKey.listen((newVal) => {
|
||||||
this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []);
|
this.konva.transformer.rotationSnaps(newVal ? [0, 45, 90, 135, 180, 225, 270, 315] : []);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
// When the selected tool changes, we need to update the transformer's interaction state.
|
||||||
|
this.manager.toolState.subscribe((newVal, oldVal) => {
|
||||||
|
if (newVal.selected !== oldVal.selected) {
|
||||||
|
this.syncInteractionState();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
// When the selected entity changes, we need to update the transformer's interaction state.
|
||||||
|
this.manager.selectedEntityIdentifier.subscribe(() => {
|
||||||
|
this.syncInteractionState();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -336,10 +393,48 @@ export class CanvasTransformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the transformer's interaction state with the application and entity's states. This is called when the entity
|
||||||
|
* is selected or deselected, or when the user changes the selected tool.
|
||||||
|
*/
|
||||||
|
syncInteractionState = () => {
|
||||||
|
this.log.trace('Syncing interaction state');
|
||||||
|
|
||||||
|
const toolState = this.manager.stateApi.getToolState();
|
||||||
|
const isSelected = this.manager.stateApi.getIsSelected(this.parent.id);
|
||||||
|
|
||||||
|
if (!this.parent.renderer.hasObjects()) {
|
||||||
|
// The layer is totally empty, we can just disable the layer
|
||||||
|
this.parent.konva.layer.listening(false);
|
||||||
|
this.setInteractionMode('off');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected && !this.isTransforming && toolState.selected === 'move') {
|
||||||
|
// We are moving this layer, it must be listening
|
||||||
|
this.parent.konva.layer.listening(true);
|
||||||
|
this.setInteractionMode('drag');
|
||||||
|
} else if (isSelected && this.isTransforming) {
|
||||||
|
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer is
|
||||||
|
// active, it will interrupt the stage drag events. So we should disable listening when the view tool is selected.
|
||||||
|
if (toolState.selected !== 'view') {
|
||||||
|
this.parent.konva.layer.listening(true);
|
||||||
|
this.setInteractionMode('all');
|
||||||
|
} else {
|
||||||
|
this.parent.konva.layer.listening(false);
|
||||||
|
this.setInteractionMode('off');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The layer is not selected, or we are using a tool that doesn't need the layer to be listening - disable interaction stuff
|
||||||
|
this.parent.konva.layer.listening(false);
|
||||||
|
this.setInteractionMode('off');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the transformer's scale. This is called when the stage is scaled.
|
* Updates the transformer's scale. This is called when the stage is scaled.
|
||||||
*/
|
*/
|
||||||
scale = () => {
|
syncScale = () => {
|
||||||
const onePixel = this.manager.getScaledPixel();
|
const onePixel = this.manager.getScaledPixel();
|
||||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||||
|
|
||||||
@ -353,24 +448,53 @@ export class CanvasTransformer {
|
|||||||
this.konva.transformer.forceUpdate();
|
this.konva.transformer.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
startTransform = () => {
|
||||||
|
this.log.debug('Starting transform');
|
||||||
|
this.isTransforming = true;
|
||||||
|
|
||||||
|
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
||||||
|
// interaction rect are listening, it will interrupt the stage's drag events. So we should disable listening
|
||||||
|
// when the view tool is selected
|
||||||
|
const shouldListen = this.manager.stateApi.getToolState().selected !== 'view';
|
||||||
|
this.parent.konva.layer.listening(shouldListen);
|
||||||
|
this.setInteractionMode('all');
|
||||||
|
};
|
||||||
|
|
||||||
|
applyTransform = async () => {
|
||||||
|
this.log.debug('Applying transform');
|
||||||
|
await this.parent.rasterize();
|
||||||
|
this.stopTransform();
|
||||||
|
};
|
||||||
|
|
||||||
|
stopTransform = () => {
|
||||||
|
this.log.debug('Stopping transform');
|
||||||
|
|
||||||
|
this.isTransforming = false;
|
||||||
|
this.setInteractionMode('off');
|
||||||
|
this.parent.resetScale();
|
||||||
|
this.parent.updatePosition();
|
||||||
|
this.parent.updateBbox();
|
||||||
|
this.syncInteractionState();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the transformer to a specific mode.
|
* Sets the transformer to a specific interaction mode.
|
||||||
* @param mode The mode to set the transformer to. The transformer can be in one of three modes:
|
* @param interactionMode The mode to set the transformer to. The transformer can be in one of three modes:
|
||||||
* - 'transform': The entity can be moved, resized, and rotated
|
* - 'all': The entity can be moved, resized, and rotated.
|
||||||
* - 'drag': The entity can only be moved
|
* - 'drag': The entity can be moved.
|
||||||
* - 'off': The transformer is disabled
|
* - 'off': The transformer is not interactable.
|
||||||
*/
|
*/
|
||||||
setMode = (mode: 'transform' | 'drag' | 'off') => {
|
setInteractionMode = (interactionMode: 'all' | 'drag' | 'off') => {
|
||||||
this.mode = mode;
|
this.interactionMode = interactionMode;
|
||||||
if (mode === 'drag') {
|
if (interactionMode === 'drag') {
|
||||||
this._enableDrag();
|
this._enableDrag();
|
||||||
this._disableTransform();
|
this._disableTransform();
|
||||||
this._showBboxOutline();
|
this._showBboxOutline();
|
||||||
} else if (mode === 'transform') {
|
} else if (interactionMode === 'all') {
|
||||||
this._enableDrag();
|
this._enableDrag();
|
||||||
this._enableTransform();
|
this._enableTransform();
|
||||||
this._hideBboxOutline();
|
this._hideBboxOutline();
|
||||||
} else if (mode === 'off') {
|
} else if (interactionMode === 'off') {
|
||||||
this._disableDrag();
|
this._disableDrag();
|
||||||
this._disableTransform();
|
this._disableTransform();
|
||||||
this._hideBboxOutline();
|
this._hideBboxOutline();
|
||||||
@ -411,13 +535,13 @@ export class CanvasTransformer {
|
|||||||
this.konva.bboxOutline.visible(false);
|
this.konva.bboxOutline.visible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
getNodes = () => [this.konva.transformer, this.konva.proxyRect, this.konva.bboxOutline];
|
getNodes = () => [this.konva.bboxOutline, this.konva.proxyRect, this.konva.transformer];
|
||||||
|
|
||||||
repr = () => {
|
repr = () => {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
type: CanvasTransformer.TYPE,
|
type: CanvasTransformer.TYPE,
|
||||||
mode: this.mode,
|
mode: this.interactionMode,
|
||||||
isTransformEnabled: this.isTransformEnabled,
|
isTransformEnabled: this.isTransformEnabled,
|
||||||
isDragEnabled: this.isDragEnabled,
|
isDragEnabled: this.isDragEnabled,
|
||||||
};
|
};
|
||||||
@ -425,9 +549,9 @@ export class CanvasTransformer {
|
|||||||
|
|
||||||
destroy = () => {
|
destroy = () => {
|
||||||
this.log.trace('Destroying transformer');
|
this.log.trace('Destroying transformer');
|
||||||
for (const { name, unsubscribe } of this.subscriptions) {
|
for (const cleanup of this.subscriptions) {
|
||||||
this.log.trace({ name }, 'Cleaning up listener');
|
this.log.trace('Cleaning up listener');
|
||||||
unsubscribe();
|
cleanup();
|
||||||
}
|
}
|
||||||
this.konva.bboxOutline.destroy();
|
this.konva.bboxOutline.destroy();
|
||||||
this.konva.transformer.destroy();
|
this.konva.transformer.destroy();
|
||||||
|
@ -26,7 +26,10 @@ import { BRUSH_SPACING_TARGET_SCALE, CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANV
|
|||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
* @param setLastCursorPos The callback to store the cursor pos
|
* @param setLastCursorPos The callback to store the cursor pos
|
||||||
*/
|
*/
|
||||||
const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: CanvasManager['stateApi']['setLastCursorPos']) => {
|
const updateLastCursorPos = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
setLastCursorPos: CanvasManager['stateApi']['$lastCursorPos']['set']
|
||||||
|
) => {
|
||||||
const pos = getScaledCursorPosition(stage);
|
const pos = getScaledCursorPosition(stage);
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
return null;
|
return null;
|
||||||
@ -112,22 +115,17 @@ const getLastPointOfLastLineOfEntity = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||||
const { stage, stateApi, getSelectedEntityAdapter } = manager;
|
const { stage, stateApi, getCurrentFill, getSelectedEntity } = manager;
|
||||||
const {
|
const {
|
||||||
getToolState,
|
getToolState,
|
||||||
getCurrentFill,
|
|
||||||
setTool,
|
setTool,
|
||||||
setToolBuffer,
|
setToolBuffer,
|
||||||
setIsMouseDown,
|
$isMouseDown,
|
||||||
setLastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
getLastCursorPos,
|
$lastCursorPos,
|
||||||
setLastCursorPos,
|
$lastAddedPoint,
|
||||||
// getLastAddedPoint,
|
$stageAttrs,
|
||||||
setLastAddedPoint,
|
$spaceKey,
|
||||||
setStageAttrs,
|
|
||||||
getSelectedEntity,
|
|
||||||
getSpaceKey,
|
|
||||||
setSpaceKey,
|
|
||||||
getBbox,
|
getBbox,
|
||||||
getSettings,
|
getSettings,
|
||||||
onBrushWidthChanged,
|
onBrushWidthChanged,
|
||||||
@ -166,34 +164,31 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
|
|
||||||
//#region mousedown
|
//#region mousedown
|
||||||
stage.on('mousedown', async (e) => {
|
stage.on('mousedown', async (e) => {
|
||||||
setIsMouseDown(true);
|
$isMouseDown.set(true);
|
||||||
const toolState = getToolState();
|
const toolState = getToolState();
|
||||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pos &&
|
pos &&
|
||||||
selectedEntity &&
|
selectedEntity &&
|
||||||
isDrawableEntity(selectedEntity) &&
|
isDrawableEntity(selectedEntity.state) &&
|
||||||
selectedEntityAdapter &&
|
!$spaceKey.get() &&
|
||||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
|
||||||
!getSpaceKey() &&
|
|
||||||
getIsPrimaryMouseDown(e)
|
getIsPrimaryMouseDown(e)
|
||||||
) {
|
) {
|
||||||
setLastMouseDownPos(pos);
|
$lastMouseDownPos.set(pos);
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
|
|
||||||
if (toolState.selected === 'brush') {
|
if (toolState.selected === 'brush') {
|
||||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
if (e.evt.shiftKey && lastLinePoint) {
|
if (e.evt.shiftKey && lastLinePoint) {
|
||||||
// Create a straight line from the last line point
|
// Create a straight line from the last line point
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('brush_line', true),
|
id: getObjectId('brush_line', true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [
|
points: [
|
||||||
@ -205,33 +200,33 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
],
|
],
|
||||||
strokeWidth: toolState.brush.width,
|
strokeWidth: toolState.brush.width,
|
||||||
color: getCurrentFill(),
|
color: getCurrentFill(),
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('brush_line', true),
|
id: getObjectId('brush_line', true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
strokeWidth: toolState.brush.width,
|
strokeWidth: toolState.brush.width,
|
||||||
color: getCurrentFill(),
|
color: getCurrentFill(),
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
if (toolState.selected === 'eraser') {
|
||||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
if (e.evt.shiftKey && lastLinePoint) {
|
if (e.evt.shiftKey && lastLinePoint) {
|
||||||
// Create a straight line from the last line point
|
// Create a straight line from the last line point
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('eraser_line', true),
|
id: getObjectId('eraser_line', true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [
|
points: [
|
||||||
@ -242,28 +237,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
alignedPoint.y,
|
alignedPoint.y,
|
||||||
],
|
],
|
||||||
strokeWidth: toolState.eraser.width,
|
strokeWidth: toolState.eraser.width,
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('eraser_line', true),
|
id: getObjectId('eraser_line', true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
strokeWidth: toolState.eraser.width,
|
strokeWidth: toolState.eraser.width,
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
if (toolState.selected === 'rect') {
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('rect', true),
|
id: getObjectId('rect', true),
|
||||||
type: 'rect',
|
type: 'rect',
|
||||||
x: Math.round(normalizedPoint.x),
|
x: Math.round(normalizedPoint.x),
|
||||||
@ -279,49 +274,41 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
|
|
||||||
//#region mouseup
|
//#region mouseup
|
||||||
stage.on('mouseup', async () => {
|
stage.on('mouseup', async () => {
|
||||||
setIsMouseDown(false);
|
$isMouseDown.set(false);
|
||||||
const pos = getLastCursorPos();
|
const pos = $lastCursorPos.get();
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
|
||||||
|
|
||||||
if (
|
if (pos && selectedEntity && isDrawableEntity(selectedEntity.state) && !$spaceKey.get()) {
|
||||||
pos &&
|
|
||||||
selectedEntity &&
|
|
||||||
isDrawableEntity(selectedEntity) &&
|
|
||||||
selectedEntityAdapter &&
|
|
||||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
|
||||||
!getSpaceKey()
|
|
||||||
) {
|
|
||||||
const toolState = getToolState();
|
const toolState = getToolState();
|
||||||
|
|
||||||
if (toolState.selected === 'brush') {
|
if (toolState.selected === 'brush') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer?.type === 'brush_line') {
|
if (drawingBuffer?.type === 'brush_line') {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
if (toolState.selected === 'eraser') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer?.type === 'eraser_line') {
|
if (drawingBuffer?.type === 'eraser_line') {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
if (toolState.selected === 'rect') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer?.type === 'rect') {
|
if (drawingBuffer?.type === 'rect') {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLastMouseDownPos(null);
|
$lastMouseDownPos.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.preview.tool.render();
|
manager.preview.tool.render();
|
||||||
@ -330,94 +317,93 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
//#region mousemove
|
//#region mousemove
|
||||||
stage.on('mousemove', async (e) => {
|
stage.on('mousemove', async (e) => {
|
||||||
const toolState = getToolState();
|
const toolState = getToolState();
|
||||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pos &&
|
pos &&
|
||||||
selectedEntity &&
|
selectedEntity &&
|
||||||
isDrawableEntity(selectedEntity) &&
|
isDrawableEntity(selectedEntity.state) &&
|
||||||
selectedEntityAdapter &&
|
selectedEntity.adapter &&
|
||||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
isDrawableEntityAdapter(selectedEntity.adapter) &&
|
||||||
!getSpaceKey() &&
|
!$spaceKey.get() &&
|
||||||
getIsPrimaryMouseDown(e)
|
getIsPrimaryMouseDown(e)
|
||||||
) {
|
) {
|
||||||
if (toolState.selected === 'brush') {
|
if (toolState.selected === 'brush') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer) {
|
if (drawingBuffer) {
|
||||||
if (drawingBuffer?.type === 'brush_line') {
|
if (drawingBuffer?.type === 'brush_line') {
|
||||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||||
if (nextPoint) {
|
if (nextPoint) {
|
||||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('brush_line', true),
|
id: getObjectId('brush_line', true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
strokeWidth: toolState.brush.width,
|
strokeWidth: toolState.brush.width,
|
||||||
color: getCurrentFill(),
|
color: getCurrentFill(),
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'eraser') {
|
if (toolState.selected === 'eraser') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer) {
|
if (drawingBuffer) {
|
||||||
if (drawingBuffer.type === 'eraser_line') {
|
if (drawingBuffer.type === 'eraser_line') {
|
||||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||||
if (nextPoint) {
|
if (nextPoint) {
|
||||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selectedEntityAdapter.renderer.buffer) {
|
if (selectedEntity.adapter.renderer.buffer) {
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
await selectedEntityAdapter.renderer.setBuffer({
|
await selectedEntity.adapter.renderer.setBuffer({
|
||||||
id: getObjectId('eraser_line', true),
|
id: getObjectId('eraser_line', true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [alignedPoint.x, alignedPoint.y],
|
points: [alignedPoint.x, alignedPoint.y],
|
||||||
strokeWidth: toolState.eraser.width,
|
strokeWidth: toolState.eraser.width,
|
||||||
clip: getClip(selectedEntity),
|
clip: getClip(selectedEntity.state),
|
||||||
});
|
});
|
||||||
setLastAddedPoint(alignedPoint);
|
$lastAddedPoint.set(alignedPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolState.selected === 'rect') {
|
if (toolState.selected === 'rect') {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
if (drawingBuffer) {
|
if (drawingBuffer) {
|
||||||
if (drawingBuffer.type === 'rect') {
|
if (drawingBuffer.type === 'rect') {
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
||||||
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
} else {
|
} else {
|
||||||
await selectedEntityAdapter.renderer.clearBuffer();
|
await selectedEntity.adapter.renderer.clearBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,39 +413,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
|
|
||||||
//#region mouseleave
|
//#region mouseleave
|
||||||
stage.on('mouseleave', async (e) => {
|
stage.on('mouseleave', async (e) => {
|
||||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||||
setLastCursorPos(null);
|
$lastCursorPos.set(null);
|
||||||
setLastMouseDownPos(null);
|
$lastMouseDownPos.set(null);
|
||||||
const selectedEntity = getSelectedEntity();
|
const selectedEntity = getSelectedEntity();
|
||||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
|
||||||
const toolState = getToolState();
|
const toolState = getToolState();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pos &&
|
pos &&
|
||||||
selectedEntity &&
|
selectedEntity &&
|
||||||
isDrawableEntity(selectedEntity) &&
|
isDrawableEntity(selectedEntity.state) &&
|
||||||
selectedEntityAdapter &&
|
!$spaceKey.get() &&
|
||||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
|
||||||
!getSpaceKey() &&
|
|
||||||
getIsPrimaryMouseDown(e)
|
getIsPrimaryMouseDown(e)
|
||||||
) {
|
) {
|
||||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||||
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
||||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
|
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
|
||||||
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
||||||
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||||
await selectedEntityAdapter.renderer.commitBuffer();
|
await selectedEntity.adapter.renderer.commitBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,7 +486,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
stage.scaleX(newScale);
|
stage.scaleX(newScale);
|
||||||
stage.scaleY(newScale);
|
stage.scaleY(newScale);
|
||||||
stage.position(newPos);
|
stage.position(newPos);
|
||||||
setStageAttrs({
|
$stageAttrs.set({
|
||||||
position: newPos,
|
position: newPos,
|
||||||
dimensions: { width: stage.width(), height: stage.height() },
|
dimensions: { width: stage.width(), height: stage.height() },
|
||||||
scale: newScale,
|
scale: newScale,
|
||||||
@ -516,7 +499,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
|
|
||||||
//#region dragmove
|
//#region dragmove
|
||||||
stage.on('dragmove', () => {
|
stage.on('dragmove', () => {
|
||||||
setStageAttrs({
|
$stageAttrs.set({
|
||||||
position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) },
|
position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) },
|
||||||
dimensions: { width: stage.width(), height: stage.height() },
|
dimensions: { width: stage.width(), height: stage.height() },
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
@ -528,7 +511,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
//#region dragend
|
//#region dragend
|
||||||
stage.on('dragend', () => {
|
stage.on('dragend', () => {
|
||||||
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
// Stage position should always be an integer, else we get fractional pixels which are blurry
|
||||||
setStageAttrs({
|
$stageAttrs.set({
|
||||||
position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) },
|
position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) },
|
||||||
dimensions: { width: stage.width(), height: stage.height() },
|
dimensions: { width: stage.width(), height: stage.height() },
|
||||||
scale: stage.scaleX(),
|
scale: stage.scaleX(),
|
||||||
@ -546,17 +529,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
}
|
}
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
// Cancel shape drawing on escape
|
// Cancel shape drawing on escape
|
||||||
setLastMouseDownPos(null);
|
$lastMouseDownPos.set(null);
|
||||||
} else if (e.key === ' ') {
|
} else if (e.key === ' ') {
|
||||||
// Select the view tool on space key down
|
// Select the view tool on space key down
|
||||||
setToolBuffer(getToolState().selected);
|
setToolBuffer(getToolState().selected);
|
||||||
setTool('view');
|
setTool('view');
|
||||||
setSpaceKey(true);
|
$spaceKey.set(true);
|
||||||
setLastCursorPos(null);
|
$lastCursorPos.set(null);
|
||||||
setLastMouseDownPos(null);
|
$lastMouseDownPos.set(null);
|
||||||
} else if (e.key === 'r') {
|
} else if (e.key === 'r') {
|
||||||
setLastCursorPos(null);
|
$lastCursorPos.set(null);
|
||||||
setLastMouseDownPos(null);
|
$lastMouseDownPos.set(null);
|
||||||
manager.background.render();
|
manager.background.render();
|
||||||
// TODO(psyche): restore some kind of fit
|
// TODO(psyche): restore some kind of fit
|
||||||
}
|
}
|
||||||
@ -576,7 +559,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
const toolBuffer = getToolState().selectedBuffer;
|
const toolBuffer = getToolState().selectedBuffer;
|
||||||
setTool(toolBuffer ?? 'move');
|
setTool(toolBuffer ?? 'move');
|
||||||
setToolBuffer(null);
|
setToolBuffer(null);
|
||||||
setSpaceKey(false);
|
$spaceKey.set(false);
|
||||||
}
|
}
|
||||||
manager.preview.tool.render();
|
manager.preview.tool.render();
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import type { CanvasObjectState, Coordinate, GenerationMode, Rect, RgbaColor } from 'features/controlLayers/store/types';
|
import type {
|
||||||
|
CanvasObjectState,
|
||||||
|
Coordinate,
|
||||||
|
GenerationMode,
|
||||||
|
Rect,
|
||||||
|
RgbaColor,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
import { isValidLayer } from 'features/nodes/util/graph/generation/addLayers';
|
||||||
import Konva from 'konva';
|
import 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 { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import type { WritableAtom } from 'nanostores';
|
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -623,22 +628,6 @@ export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Subscription = {
|
export const getEmptyRect = (): Rect => {
|
||||||
name: string;
|
return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
unsubscribe: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a subscribe function for a nanostores atom.
|
|
||||||
* @param subscribe The subscribe function of the atom
|
|
||||||
* @param name The name of the atom
|
|
||||||
* @returns A subscribe function that returns an object with the name and unsubscribe function
|
|
||||||
*/
|
|
||||||
export const buildSubscribe = <T>(subscribe: WritableAtom<T>['subscribe'], name: string) => {
|
|
||||||
return (cb: Parameters<WritableAtom<T>['subscribe']>[0]): Subscription => {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
unsubscribe: subscribe(cb),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user