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');
|
||||
|
||||
// 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.
|
||||
const scaledGridSize = gridSize * stage.scaleX();
|
||||
// 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', () => {
|
||||
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 bboxRect: Rect = {
|
||||
...bbox.rect,
|
||||
@ -129,10 +129,10 @@ export class CanvasBbox {
|
||||
return;
|
||||
}
|
||||
|
||||
const alt = this.manager.stateApi.getAltKey();
|
||||
const ctrl = this.manager.stateApi.getCtrlKey();
|
||||
const meta = this.manager.stateApi.getMetaKey();
|
||||
const shift = this.manager.stateApi.getShiftKey();
|
||||
const alt = this.manager.stateApi.$altKey.get();
|
||||
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
||||
const meta = this.manager.stateApi.$metaKey.get();
|
||||
const shift = this.manager.stateApi.$shiftKey.get();
|
||||
|
||||
// Grid size depends on the modifier keys
|
||||
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
|
||||
// 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.
|
||||
if (this.manager.stateApi.getAltKey()) {
|
||||
if (this.manager.stateApi.$altKey.get()) {
|
||||
gridSize = gridSize * 2;
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,9 @@ export class CanvasControlAdapter extends CanvasEntity {
|
||||
static TRANSFORMER_NAME = `${CanvasControlAdapter.NAME_PREFIX}_transformer`;
|
||||
static GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_group`;
|
||||
static OBJECT_GROUP_NAME = `${CanvasControlAdapter.NAME_PREFIX}_object-group`;
|
||||
static TYPE = 'control_adapter' as const;
|
||||
|
||||
type = 'control_adapter';
|
||||
type = CanvasControlAdapter.TYPE;
|
||||
_state: CanvasControlAdapterState;
|
||||
|
||||
konva: {
|
||||
|
@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||
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 Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
@ -17,11 +22,12 @@ export class CanvasInpaintMask {
|
||||
static GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_group`;
|
||||
static OBJECT_GROUP_NAME = `${CanvasInpaintMask.NAME_PREFIX}_object-group`;
|
||||
static COMPOSITING_RECT_NAME = `${CanvasInpaintMask.NAME_PREFIX}_compositing-rect`;
|
||||
|
||||
static TYPE = 'inpaint_mask' as const;
|
||||
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
||||
private state: CanvasInpaintMaskState;
|
||||
|
||||
id = 'inpaint_mask';
|
||||
id = CanvasInpaintMask.TYPE;
|
||||
type = CanvasInpaintMask.TYPE;
|
||||
manager: CanvasManager;
|
||||
|
||||
konva: {
|
||||
|
@ -3,7 +3,7 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer';
|
||||
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 type {
|
||||
CanvasLayerState,
|
||||
@ -19,11 +19,12 @@ import type { Logger } from 'roarr';
|
||||
import { uploadImage } from 'services/api/endpoints/images';
|
||||
|
||||
export class CanvasLayer {
|
||||
static TYPE = 'layer';
|
||||
static TYPE = 'layer' as const;
|
||||
static KONVA_LAYER_NAME = `${CanvasLayer.TYPE}_layer`;
|
||||
static KONVA_OBJECT_GROUP_NAME = `${CanvasLayer.TYPE}_object-group`;
|
||||
|
||||
id: string;
|
||||
type = CanvasLayer.TYPE;
|
||||
manager: CanvasManager;
|
||||
log: Logger;
|
||||
getLoggingContext: GetLoggingContext;
|
||||
@ -38,12 +39,11 @@ export class CanvasLayer {
|
||||
renderer: CanvasObjectRenderer;
|
||||
|
||||
isFirstRender: boolean = true;
|
||||
bboxNeedsUpdate: boolean;
|
||||
isTransforming: boolean;
|
||||
isPendingBboxCalculation: boolean;
|
||||
bboxNeedsUpdate: boolean = true;
|
||||
isPendingBboxCalculation: boolean = false;
|
||||
|
||||
rect: Rect;
|
||||
bbox: Rect;
|
||||
rect: Rect = getEmptyRect();
|
||||
bbox: Rect = getEmptyRect();
|
||||
|
||||
constructor(state: CanvasLayerState, manager: CanvasManager) {
|
||||
this.id = state.id;
|
||||
@ -69,11 +69,6 @@ export class CanvasLayer {
|
||||
this.konva.layer.add(...this.transformer.getNodes());
|
||||
|
||||
this.state = state;
|
||||
this.rect = this.getDefaultRect();
|
||||
this.bbox = this.getDefaultRect();
|
||||
this.bboxNeedsUpdate = true;
|
||||
this.isTransforming = false;
|
||||
this.isPendingBboxCalculation = false;
|
||||
}
|
||||
|
||||
destroy = (): void => {
|
||||
@ -86,8 +81,6 @@ export class CanvasLayer {
|
||||
|
||||
update = async (arg?: { state: CanvasLayerState; toolState: CanvasV2State['tool']; isSelected: boolean }) => {
|
||||
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) {
|
||||
this.log.trace('State unchanged, skipping update');
|
||||
@ -109,7 +102,7 @@ export class CanvasLayer {
|
||||
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||
await this.updateVisibility({ isEnabled });
|
||||
}
|
||||
await this.updateInteraction({ toolState, isSelected });
|
||||
// this.transformer.syncInteractionState();
|
||||
|
||||
if (this.isFirstRender) {
|
||||
await this.updateBbox();
|
||||
@ -159,40 +152,6 @@ export class CanvasLayer {
|
||||
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 = () => {
|
||||
this.log.trace('Updating bbox');
|
||||
|
||||
@ -208,11 +167,11 @@ export class CanvasLayer {
|
||||
// The layer is fully transparent but has objects - reset it
|
||||
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
||||
}
|
||||
this.transformer.setMode('off');
|
||||
this.transformer.syncInteractionState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.transformer.setMode('drag');
|
||||
this.transformer.syncInteractionState();
|
||||
this.transformer.update(this.state.position, this.bbox);
|
||||
this.konva.objectGroup.setAttrs({
|
||||
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 = () => {
|
||||
const attrs = {
|
||||
scaleX: 1,
|
||||
@ -245,7 +192,7 @@ export class CanvasLayer {
|
||||
this.transformer.konva.proxyRect.setAttrs(attrs);
|
||||
};
|
||||
|
||||
rasterizeLayer = async () => {
|
||||
rasterize = async () => {
|
||||
this.log.debug('Rasterizing layer');
|
||||
|
||||
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) } }));
|
||||
};
|
||||
|
||||
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(() => {
|
||||
this.log.debug('Calculating bbox');
|
||||
|
||||
@ -284,8 +217,8 @@ export class CanvasLayer {
|
||||
|
||||
if (!this.renderer.hasObjects()) {
|
||||
this.log.trace('No objects, resetting bbox');
|
||||
this.rect = this.getDefaultRect();
|
||||
this.bbox = this.getDefaultRect();
|
||||
this.rect = getEmptyRect();
|
||||
this.bbox = getEmptyRect();
|
||||
this.isPendingBboxCalculation = false;
|
||||
this.updateBbox();
|
||||
return;
|
||||
@ -324,8 +257,8 @@ export class CanvasLayer {
|
||||
height: maxY - minY,
|
||||
};
|
||||
} else {
|
||||
this.bbox = this.getDefaultRect();
|
||||
this.rect = this.getDefaultRect();
|
||||
this.bbox = getEmptyRect();
|
||||
this.rect = getEmptyRect();
|
||||
}
|
||||
this.isPendingBboxCalculation = false;
|
||||
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),
|
||||
bbox: deepClone(this.bbox),
|
||||
bboxNeedsUpdate: this.bboxNeedsUpdate,
|
||||
isTransforming: this.isTransforming,
|
||||
isPendingBboxCalculation: this.isPendingBboxCalculation,
|
||||
transformer: this.transformer.repr(),
|
||||
renderer: this.renderer.repr(),
|
||||
|
@ -2,6 +2,7 @@ import type { Store } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { JSONObject } from 'common/types';
|
||||
import { PubSub } from 'common/util/PubSub/PubSub';
|
||||
import type { CanvasBrushLineRenderer } from 'features/controlLayers/konva/CanvasBrushLine';
|
||||
import type { CanvasEraserLineRenderer } from 'features/controlLayers/konva/CanvasEraserLine';
|
||||
import type { CanvasImageRenderer } from 'features/controlLayers/konva/CanvasImage';
|
||||
@ -22,7 +23,19 @@ import {
|
||||
} from 'features/controlLayers/konva/util';
|
||||
import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker';
|
||||
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 { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@ -70,6 +83,24 @@ type Util = {
|
||||
) => 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 class CanvasManager {
|
||||
@ -101,6 +132,11 @@ export class CanvasManager {
|
||||
_worker: Worker;
|
||||
_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(
|
||||
stage: Konva.Stage,
|
||||
container: HTMLDivElement,
|
||||
@ -111,7 +147,7 @@ export class CanvasManager {
|
||||
this.stage = stage;
|
||||
this.container = container;
|
||||
this._store = store;
|
||||
this.stateApi = new CanvasStateApi(this._store);
|
||||
this.stateApi = new CanvasStateApi(this._store, this);
|
||||
this._prevState = this.stateApi.getState();
|
||||
this._isFirstRender = true;
|
||||
|
||||
@ -178,6 +214,17 @@ export class CanvasManager {
|
||||
};
|
||||
this.onTransform = null;
|
||||
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() {
|
||||
@ -226,7 +273,7 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
async renderProgressPreview() {
|
||||
await this.preview.progressPreview.render(this.stateApi.getLastProgressEvent());
|
||||
await this.preview.progressPreview.render(this.stateApi.$lastProgressEvent.get());
|
||||
}
|
||||
|
||||
async renderInpaintMask() {
|
||||
@ -279,7 +326,7 @@ export class CanvasManager {
|
||||
fitStageToContainer() {
|
||||
this.stage.width(this.container.offsetWidth);
|
||||
this.stage.height(this.container.offsetHeight);
|
||||
this.stateApi.setStageAttrs({
|
||||
this.stateApi.$stageAttrs.set({
|
||||
position: { x: this.stage.x(), y: this.stage.y() },
|
||||
dimensions: { width: this.stage.width(), height: this.stage.height() },
|
||||
scale: this.stage.scaleX(),
|
||||
@ -287,8 +334,57 @@ export class CanvasManager {
|
||||
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() {
|
||||
return Array.from(this.layers.values()).find((layer) => layer.isTransforming);
|
||||
return Array.from(this.layers.values()).find((layer) => layer.transformer.isTransforming);
|
||||
}
|
||||
|
||||
getIsTransforming() {
|
||||
@ -299,17 +395,17 @@ export class CanvasManager {
|
||||
if (this.getIsTransforming()) {
|
||||
return;
|
||||
}
|
||||
const layer = this.getSelectedEntityAdapter();
|
||||
assert(layer instanceof CanvasLayer, 'No selected layer');
|
||||
layer.startTransform();
|
||||
const layer = this.getSelectedEntity();
|
||||
// TODO(psyche): Support other entity types
|
||||
assert(layer?.adapter instanceof CanvasLayer, 'No selected layer');
|
||||
layer.adapter.transformer.startTransform();
|
||||
this.onTransform?.(true);
|
||||
}
|
||||
|
||||
async applyTransform() {
|
||||
const layer = this.getTransformingLayer();
|
||||
if (layer) {
|
||||
await layer.rasterizeLayer();
|
||||
layer.stopTransform();
|
||||
await layer.transformer.applyTransform();
|
||||
}
|
||||
this.onTransform?.(false);
|
||||
}
|
||||
@ -317,7 +413,7 @@ export class CanvasManager {
|
||||
cancelTransform() {
|
||||
const layer = this.getTransformingLayer();
|
||||
if (layer) {
|
||||
layer.stopTransform();
|
||||
layer.transformer.stopTransform();
|
||||
}
|
||||
this.onTransform?.(false);
|
||||
}
|
||||
@ -355,16 +451,10 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this._isFirstRender ||
|
||||
state.tool.selected !== this._prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
this.log.debug('Updating interaction');
|
||||
for (const layer of this.layers.values()) {
|
||||
layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id });
|
||||
}
|
||||
}
|
||||
this.toolState.publish(state.tool);
|
||||
this.selectedEntityIdentifier.publish(state.selectedEntityIdentifier);
|
||||
this.selectedEntity.publish(this.getSelectedEntity());
|
||||
this.currentFill.publish(this.getCurrentFill());
|
||||
|
||||
if (
|
||||
this._isFirstRender ||
|
||||
@ -521,24 +611,6 @@ export class CanvasManager {
|
||||
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 {
|
||||
const session = this.stateApi.getSession();
|
||||
if (session.isActive) {
|
||||
|
@ -25,6 +25,9 @@ type AnyObjectRenderer = CanvasBrushLineRenderer | CanvasEraserLineRenderer | Ca
|
||||
*/
|
||||
type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
||||
|
||||
/**
|
||||
* Handles rendering of objects for a canvas entity.
|
||||
*/
|
||||
export class CanvasObjectRenderer {
|
||||
static TYPE = 'object_renderer';
|
||||
|
||||
|
@ -5,7 +5,12 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect';
|
||||
import { getNodeBboxFast } from 'features/controlLayers/konva/entityBbox';
|
||||
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 Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
@ -17,11 +22,13 @@ export class CanvasRegion {
|
||||
static GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_group`;
|
||||
static OBJECT_GROUP_NAME = `${CanvasRegion.NAME_PREFIX}_object-group`;
|
||||
static COMPOSITING_RECT_NAME = `${CanvasRegion.NAME_PREFIX}_compositing-rect`;
|
||||
static TYPE = 'regional_guidance' as const;
|
||||
|
||||
private drawingBuffer: CanvasBrushLineState | CanvasEraserLineState | CanvasRectState | null;
|
||||
private state: CanvasRegionalGuidanceState;
|
||||
|
||||
id: string;
|
||||
type = CanvasRegion.TYPE;
|
||||
manager: CanvasManager;
|
||||
|
||||
konva: {
|
||||
|
@ -34,7 +34,7 @@ export class CanvasStagingArea {
|
||||
render = async () => {
|
||||
const session = this.manager.stateApi.getSession();
|
||||
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;
|
||||
|
||||
@ -69,7 +69,7 @@ export class CanvasStagingArea {
|
||||
this.image.konva.group.x(bboxRect.x + offsetX);
|
||||
this.image.konva.group.y(bboxRect.y + offsetY);
|
||||
await this.image.updateImageSource(imageDTO.image_name);
|
||||
this.manager.stateApi.resetLastProgressEvent();
|
||||
this.manager.stateApi.$lastProgressEvent.set(null);
|
||||
}
|
||||
this.image.konva.group.visible(shouldShowStagedImage);
|
||||
} else {
|
||||
|
@ -2,7 +2,7 @@ import { $alt, $ctrl, $meta, $shift } from '@invoke-ai/ui-library';
|
||||
import type { Store } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { buildSubscribe } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import {
|
||||
$isDrawing,
|
||||
$isMouseDown,
|
||||
@ -49,173 +49,143 @@ import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasEntity,
|
||||
CanvasEraserLineState,
|
||||
PositionChangedArg,
|
||||
CanvasRectState,
|
||||
PositionChangedArg,
|
||||
ScaleChangedArg,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
export class CanvasStateApi {
|
||||
private store: Store<RootState>;
|
||||
|
||||
constructor(store: Store<RootState>) {
|
||||
this.store = store;
|
||||
export class CanvasStateApi {
|
||||
_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
|
||||
getState = () => {
|
||||
return this.store.getState().canvasV2;
|
||||
return this._store.getState().canvasV2;
|
||||
};
|
||||
onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => {
|
||||
log.debug('onEntityReset');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerReset(arg));
|
||||
this._store.dispatch(layerReset(arg));
|
||||
}
|
||||
};
|
||||
onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => {
|
||||
log.debug('onPosChanged');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerTranslated(arg));
|
||||
this._store.dispatch(layerTranslated(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgTranslated(arg));
|
||||
this._store.dispatch(rgTranslated(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imTranslated(arg));
|
||||
this._store.dispatch(imTranslated(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
this.store.dispatch(caTranslated(arg));
|
||||
this._store.dispatch(caTranslated(arg));
|
||||
}
|
||||
};
|
||||
onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||
log.debug('onScaleChanged');
|
||||
if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imScaled(arg));
|
||||
this._store.dispatch(imScaled(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgScaled(arg));
|
||||
this._store.dispatch(rgScaled(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
this.store.dispatch(caScaled(arg));
|
||||
this._store.dispatch(caScaled(arg));
|
||||
}
|
||||
};
|
||||
onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||
log.debug('Entity bbox changed');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerBboxChanged(arg));
|
||||
this._store.dispatch(layerBboxChanged(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
this.store.dispatch(caBboxChanged(arg));
|
||||
this._store.dispatch(caBboxChanged(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgBboxChanged(arg));
|
||||
this._store.dispatch(rgBboxChanged(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imBboxChanged(arg));
|
||||
this._store.dispatch(imBboxChanged(arg));
|
||||
}
|
||||
};
|
||||
onBrushLineAdded = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntity['type']) => {
|
||||
log.debug('Brush line added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerBrushLineAdded(arg));
|
||||
this._store.dispatch(layerBrushLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgBrushLineAdded(arg));
|
||||
this._store.dispatch(rgBrushLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imBrushLineAdded(arg));
|
||||
this._store.dispatch(imBrushLineAdded(arg));
|
||||
}
|
||||
};
|
||||
onEraserLineAdded = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntity['type']) => {
|
||||
log.debug('Eraser line added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerEraserLineAdded(arg));
|
||||
this._store.dispatch(layerEraserLineAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgEraserLineAdded(arg));
|
||||
this._store.dispatch(rgEraserLineAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imEraserLineAdded(arg));
|
||||
this._store.dispatch(imEraserLineAdded(arg));
|
||||
}
|
||||
};
|
||||
onRectShapeAdded = (arg: { id: string; rectShape: CanvasRectState }, entityType: CanvasEntity['type']) => {
|
||||
log.debug('Rect shape added');
|
||||
if (entityType === 'layer') {
|
||||
this.store.dispatch(layerRectShapeAdded(arg));
|
||||
this._store.dispatch(layerRectShapeAdded(arg));
|
||||
} else if (entityType === 'regional_guidance') {
|
||||
this.store.dispatch(rgRectShapeAdded(arg));
|
||||
this._store.dispatch(rgRectShapeAdded(arg));
|
||||
} else if (entityType === 'inpaint_mask') {
|
||||
this.store.dispatch(imRectShapeAdded(arg));
|
||||
this._store.dispatch(imRectShapeAdded(arg));
|
||||
}
|
||||
};
|
||||
onEntitySelected = (arg: { id: string; type: CanvasEntity['type'] }) => {
|
||||
log.debug('Entity selected');
|
||||
this.store.dispatch(entitySelected(arg));
|
||||
this._store.dispatch(entitySelected(arg));
|
||||
};
|
||||
onBboxTransformed = (bbox: IRect) => {
|
||||
log.debug('Generation bbox transformed');
|
||||
this.store.dispatch(bboxChanged(bbox));
|
||||
this._store.dispatch(bboxChanged(bbox));
|
||||
};
|
||||
onBrushWidthChanged = (width: number) => {
|
||||
log.debug('Brush width changed');
|
||||
this.store.dispatch(brushWidthChanged(width));
|
||||
this._store.dispatch(brushWidthChanged(width));
|
||||
};
|
||||
onEraserWidthChanged = (width: number) => {
|
||||
log.debug('Eraser width changed');
|
||||
this.store.dispatch(eraserWidthChanged(width));
|
||||
this._store.dispatch(eraserWidthChanged(width));
|
||||
};
|
||||
onRegionMaskImageCached = (id: string, imageDTO: ImageDTO) => {
|
||||
log.debug('Region mask image cached');
|
||||
this.store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||
this._store.dispatch(rgImageCacheChanged({ id, imageDTO }));
|
||||
};
|
||||
onInpaintMaskImageCached = (imageDTO: ImageDTO) => {
|
||||
log.debug('Inpaint mask image cached');
|
||||
this.store.dispatch(imImageCacheChanged({ imageDTO }));
|
||||
this._store.dispatch(imImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
onLayerImageCached = (imageDTO: ImageDTO) => {
|
||||
log.debug('Layer image cached');
|
||||
this.store.dispatch(layerImageCacheChanged({ imageDTO }));
|
||||
this._store.dispatch(layerImageCacheChanged({ imageDTO }));
|
||||
};
|
||||
setTool = (tool: Tool) => {
|
||||
log.debug('Tool selection changed');
|
||||
this.store.dispatch(toolChanged(tool));
|
||||
this._store.dispatch(toolChanged(tool));
|
||||
};
|
||||
setToolBuffer = (toolBuffer: Tool | null) => {
|
||||
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 = () => {
|
||||
return this.getState().bbox;
|
||||
};
|
||||
|
||||
getToolState = () => {
|
||||
return this.getState().tool;
|
||||
};
|
||||
@ -244,61 +214,24 @@ export class CanvasStateApi {
|
||||
return this.getState().session;
|
||||
};
|
||||
getIsSelected = (id: string) => {
|
||||
return this.getSelectedEntity()?.id === id;
|
||||
return this.getState().selectedEntityIdentifier?.id === id;
|
||||
};
|
||||
getLogLevel = () => {
|
||||
return this.store.getState().system.consoleLogLevel;
|
||||
};
|
||||
|
||||
// Read-only state, derived from nanostores
|
||||
resetLastProgressEvent = () => {
|
||||
$lastProgressEvent.set(null);
|
||||
return this._store.getState().system.consoleLogLevel;
|
||||
};
|
||||
|
||||
// Read-write state, ephemeral interaction state
|
||||
getIsDrawing = $isDrawing.get;
|
||||
setIsDrawing = $isDrawing.set;
|
||||
onIsDrawingChanged = $isDrawing.subscribe;
|
||||
|
||||
getIsMouseDown = $isMouseDown.get;
|
||||
setIsMouseDown = $isMouseDown.set;
|
||||
onIsMouseDownChanged = $isMouseDown.subscribe;
|
||||
|
||||
getLastAddedPoint = $lastAddedPoint.get;
|
||||
setLastAddedPoint = $lastAddedPoint.set;
|
||||
onLastAddedPointChanged = $lastAddedPoint.subscribe;
|
||||
|
||||
getLastMouseDownPos = $lastMouseDownPos.get;
|
||||
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');
|
||||
$isDrawing = $isDrawing;
|
||||
$isMouseDown = $isMouseDown;
|
||||
$lastAddedPoint = $lastAddedPoint;
|
||||
$lastMouseDownPos = $lastMouseDownPos;
|
||||
$lastCursorPos = $lastCursorPos;
|
||||
$lastProgressEvent = $lastProgressEvent;
|
||||
$spaceKey = $spaceKey;
|
||||
$altKey = $alt;
|
||||
$ctrlKey = $ctrl;
|
||||
$metaKey = $meta;
|
||||
$shiftKey = $shift;
|
||||
$shouldShowStagedImage = $shouldShowStagedImage;
|
||||
$stageAttrs = $stageAttrs;
|
||||
}
|
||||
|
@ -139,17 +139,17 @@ export class CanvasTool {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount: number = 1; // TODO(psyche): this.manager should be renderable entity count
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const currentFill = this.manager.stateApi.getCurrentFill();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.getLastCursorPos();
|
||||
const isDrawing = this.manager.stateApi.getIsDrawing();
|
||||
const isMouseDown = this.manager.stateApi.getIsMouseDown();
|
||||
const currentFill = this.manager.getCurrentFill();
|
||||
const selectedEntity = this.manager.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const isDrawing = this.manager.stateApi.$isDrawing.get();
|
||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||
|
||||
const tool = toolState.selected;
|
||||
const isDrawableEntity =
|
||||
selectedEntity?.type === 'regional_guidance' ||
|
||||
selectedEntity?.type === 'layer' ||
|
||||
selectedEntity?.type === 'inpaint_mask';
|
||||
selectedEntity?.state.type === 'regional_guidance' ||
|
||||
selectedEntity?.state.type === 'layer' ||
|
||||
selectedEntity?.state.type === 'inpaint_mask';
|
||||
|
||||
// Update the stage's pointer style
|
||||
if (tool === 'view') {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { Subscription } from 'features/controlLayers/konva/util';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Coordinate, GetLoggingContext, Rect } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
@ -19,32 +18,50 @@ export class CanvasTransformer {
|
||||
static TRANSFORMER_NAME = `${CanvasTransformer.TYPE}:transformer`;
|
||||
static PROXY_RECT_NAME = `${CanvasTransformer.TYPE}:proxy_rect`;
|
||||
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;
|
||||
parent: CanvasLayer;
|
||||
manager: CanvasManager;
|
||||
log: Logger;
|
||||
getLoggingContext: GetLoggingContext;
|
||||
subscriptions: Subscription[];
|
||||
|
||||
/**
|
||||
* The current mode of the transformer:
|
||||
* - 'transform': The entity can be moved, resized, and rotated
|
||||
* - 'drag': The entity can only be moved
|
||||
* - 'off': The transformer is disabled
|
||||
* A list of subscriptions that should be cleaned up when the transformer is destroyed.
|
||||
*/
|
||||
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: {
|
||||
transformer: Konva.Transformer;
|
||||
@ -59,11 +76,6 @@ export class CanvasTransformer {
|
||||
|
||||
this.getLoggingContext = this.manager.buildGetLoggingContext(this);
|
||||
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||
this.subscriptions = [];
|
||||
|
||||
this.mode = 'off';
|
||||
this.isDragEnabled = false;
|
||||
this.isTransformEnabled = false;
|
||||
|
||||
this.konva = {
|
||||
bboxOutline: new Konva.Rect({
|
||||
@ -89,6 +101,35 @@ export class CanvasTransformer {
|
||||
padding: this.manager.getTransformerPadding(),
|
||||
// This is `invokeBlue.400`
|
||||
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
|
||||
// function expects. The in-house Coordinate type is functionally the same - `{x: number; y: number}` - and
|
||||
// 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
|
||||
// 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.
|
||||
if (this.manager.stateApi.getShiftKey()) {
|
||||
if (this.manager.stateApi.$shiftKey.get()) {
|
||||
if (Math.abs(newBoundBox.rotation % (Math.PI / 4)) > 0) {
|
||||
return oldBoundBox;
|
||||
}
|
||||
@ -278,9 +319,9 @@ export class CanvasTransformer {
|
||||
});
|
||||
});
|
||||
this.konva.proxyRect.on('dragend', () => {
|
||||
if (this.parent.isTransforming) {
|
||||
// When the user cancels the transformation, we need to reset the layer, so we should not update the layer's
|
||||
// positition while we are transforming - bail out early.
|
||||
if (this.isTransforming) {
|
||||
// If we are transforming the entity, we should not push the new position to the state. This will trigger a
|
||||
// re-render of the entity and bork the transformation.
|
||||
return;
|
||||
}
|
||||
|
||||
@ -296,9 +337,9 @@ export class CanvasTransformer {
|
||||
this.subscriptions.push(
|
||||
// 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.
|
||||
this.manager.stateApi.onStageAttrsChanged((newAttrs, oldAttrs) => {
|
||||
if (newAttrs.scale !== oldAttrs?.scale) {
|
||||
this.scale();
|
||||
this.manager.stateApi.$stageAttrs.listen((newVal, oldVal) => {
|
||||
if (newVal.scale !== oldVal.scale) {
|
||||
this.syncScale();
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -306,8 +347,24 @@ export class CanvasTransformer {
|
||||
this.subscriptions.push(
|
||||
// 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.
|
||||
this.manager.stateApi.onShiftChanged((isPressed) => {
|
||||
this.konva.transformer.rotationSnaps(isPressed ? [0, 45, 90, 135, 180, 225, 270, 315] : []);
|
||||
this.manager.stateApi.$shiftKey.listen((newVal) => {
|
||||
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.
|
||||
*/
|
||||
scale = () => {
|
||||
syncScale = () => {
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
@ -353,24 +448,53 @@ export class CanvasTransformer {
|
||||
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.
|
||||
* @param mode 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
|
||||
* - 'drag': The entity can only be moved
|
||||
* - 'off': The transformer is disabled
|
||||
* Sets the transformer to a specific interaction mode.
|
||||
* @param interactionMode The mode to set the transformer to. The transformer can be in one of three modes:
|
||||
* - 'all': The entity can be moved, resized, and rotated.
|
||||
* - 'drag': The entity can be moved.
|
||||
* - 'off': The transformer is not interactable.
|
||||
*/
|
||||
setMode = (mode: 'transform' | 'drag' | 'off') => {
|
||||
this.mode = mode;
|
||||
if (mode === 'drag') {
|
||||
setInteractionMode = (interactionMode: 'all' | 'drag' | 'off') => {
|
||||
this.interactionMode = interactionMode;
|
||||
if (interactionMode === 'drag') {
|
||||
this._enableDrag();
|
||||
this._disableTransform();
|
||||
this._showBboxOutline();
|
||||
} else if (mode === 'transform') {
|
||||
} else if (interactionMode === 'all') {
|
||||
this._enableDrag();
|
||||
this._enableTransform();
|
||||
this._hideBboxOutline();
|
||||
} else if (mode === 'off') {
|
||||
} else if (interactionMode === 'off') {
|
||||
this._disableDrag();
|
||||
this._disableTransform();
|
||||
this._hideBboxOutline();
|
||||
@ -411,13 +535,13 @@ export class CanvasTransformer {
|
||||
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 = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: CanvasTransformer.TYPE,
|
||||
mode: this.mode,
|
||||
mode: this.interactionMode,
|
||||
isTransformEnabled: this.isTransformEnabled,
|
||||
isDragEnabled: this.isDragEnabled,
|
||||
};
|
||||
@ -425,9 +549,9 @@ export class CanvasTransformer {
|
||||
|
||||
destroy = () => {
|
||||
this.log.trace('Destroying transformer');
|
||||
for (const { name, unsubscribe } of this.subscriptions) {
|
||||
this.log.trace({ name }, 'Cleaning up listener');
|
||||
unsubscribe();
|
||||
for (const cleanup of this.subscriptions) {
|
||||
this.log.trace('Cleaning up listener');
|
||||
cleanup();
|
||||
}
|
||||
this.konva.bboxOutline.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 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);
|
||||
if (!pos) {
|
||||
return null;
|
||||
@ -112,22 +115,17 @@ const getLastPointOfLastLineOfEntity = (
|
||||
};
|
||||
|
||||
export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
const { stage, stateApi, getSelectedEntityAdapter } = manager;
|
||||
const { stage, stateApi, getCurrentFill, getSelectedEntity } = manager;
|
||||
const {
|
||||
getToolState,
|
||||
getCurrentFill,
|
||||
setTool,
|
||||
setToolBuffer,
|
||||
setIsMouseDown,
|
||||
setLastMouseDownPos,
|
||||
getLastCursorPos,
|
||||
setLastCursorPos,
|
||||
// getLastAddedPoint,
|
||||
setLastAddedPoint,
|
||||
setStageAttrs,
|
||||
getSelectedEntity,
|
||||
getSpaceKey,
|
||||
setSpaceKey,
|
||||
$isMouseDown,
|
||||
$lastMouseDownPos,
|
||||
$lastCursorPos,
|
||||
$lastAddedPoint,
|
||||
$stageAttrs,
|
||||
$spaceKey,
|
||||
getBbox,
|
||||
getSettings,
|
||||
onBrushWidthChanged,
|
||||
@ -166,34 +164,31 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
//#region mousedown
|
||||
stage.on('mousedown', async (e) => {
|
||||
setIsMouseDown(true);
|
||||
$isMouseDown.set(true);
|
||||
const toolState = getToolState();
|
||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||
const selectedEntity = getSelectedEntity();
|
||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
isDrawableEntity(selectedEntity) &&
|
||||
selectedEntityAdapter &&
|
||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
||||
!getSpaceKey() &&
|
||||
isDrawableEntity(selectedEntity.state) &&
|
||||
!$spaceKey.get() &&
|
||||
getIsPrimaryMouseDown(e)
|
||||
) {
|
||||
setLastMouseDownPos(pos);
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
$lastMouseDownPos.set(pos);
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||
|
||||
if (toolState.selected === 'brush') {
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
points: [
|
||||
@ -205,33 +200,33 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
setLastAddedPoint(alignedPoint);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
|
||||
if (toolState.selected === 'eraser') {
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity, toolState.selected);
|
||||
const lastLinePoint = getLastPointOfLastLineOfEntity(selectedEntity.state, toolState.selected);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('eraser_line', true),
|
||||
type: 'eraser_line',
|
||||
points: [
|
||||
@ -242,28 +237,28 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
alignedPoint.y,
|
||||
],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('eraser_line', true),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
setLastAddedPoint(alignedPoint);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
|
||||
if (toolState.selected === 'rect') {
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('rect', true),
|
||||
type: 'rect',
|
||||
x: Math.round(normalizedPoint.x),
|
||||
@ -279,49 +274,41 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
//#region mouseup
|
||||
stage.on('mouseup', async () => {
|
||||
setIsMouseDown(false);
|
||||
const pos = getLastCursorPos();
|
||||
$isMouseDown.set(false);
|
||||
const pos = $lastCursorPos.get();
|
||||
const selectedEntity = getSelectedEntity();
|
||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
isDrawableEntity(selectedEntity) &&
|
||||
selectedEntityAdapter &&
|
||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
||||
!getSpaceKey()
|
||||
) {
|
||||
if (pos && selectedEntity && isDrawableEntity(selectedEntity.state) && !$spaceKey.get()) {
|
||||
const toolState = getToolState();
|
||||
|
||||
if (toolState.selected === 'brush') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer?.type === 'brush_line') {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolState.selected === 'eraser') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer?.type === 'eraser_line') {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolState.selected === 'rect') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer?.type === 'rect') {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
setLastMouseDownPos(null);
|
||||
$lastMouseDownPos.set(null);
|
||||
}
|
||||
|
||||
manager.preview.tool.render();
|
||||
@ -330,94 +317,93 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
//#region mousemove
|
||||
stage.on('mousemove', async (e) => {
|
||||
const toolState = getToolState();
|
||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||
const selectedEntity = getSelectedEntity();
|
||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
isDrawableEntity(selectedEntity) &&
|
||||
selectedEntityAdapter &&
|
||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
||||
!getSpaceKey() &&
|
||||
isDrawableEntity(selectedEntity.state) &&
|
||||
selectedEntity.adapter &&
|
||||
isDrawableEntityAdapter(selectedEntity.adapter) &&
|
||||
!$spaceKey.get() &&
|
||||
getIsPrimaryMouseDown(e)
|
||||
) {
|
||||
if (toolState.selected === 'brush') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer) {
|
||||
if (drawingBuffer?.type === 'brush_line') {
|
||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||
if (nextPoint) {
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
} else {
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
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);
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('brush_line', true),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.brush.width,
|
||||
color: getCurrentFill(),
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
setLastAddedPoint(alignedPoint);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (toolState.selected === 'eraser') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer) {
|
||||
if (drawingBuffer.type === 'eraser_line') {
|
||||
const nextPoint = getNextPoint(pos, toolState, getLastPointOfLine(drawingBuffer.points));
|
||||
if (nextPoint) {
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.position);
|
||||
const normalizedPoint = offsetCoord(nextPoint, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
setLastAddedPoint(alignedPoint);
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
} else {
|
||||
if (selectedEntityAdapter.renderer.buffer) {
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
if (selectedEntity.adapter.renderer.buffer) {
|
||||
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);
|
||||
await selectedEntityAdapter.renderer.setBuffer({
|
||||
await selectedEntity.adapter.renderer.setBuffer({
|
||||
id: getObjectId('eraser_line', true),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: toolState.eraser.width,
|
||||
clip: getClip(selectedEntity),
|
||||
clip: getClip(selectedEntity.state),
|
||||
});
|
||||
setLastAddedPoint(alignedPoint);
|
||||
$lastAddedPoint.set(alignedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (toolState.selected === 'rect') {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
if (drawingBuffer) {
|
||||
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.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
} else {
|
||||
await selectedEntityAdapter.renderer.clearBuffer();
|
||||
await selectedEntity.adapter.renderer.clearBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -427,39 +413,36 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
//#region mouseleave
|
||||
stage.on('mouseleave', async (e) => {
|
||||
const pos = updateLastCursorPos(stage, setLastCursorPos);
|
||||
setLastCursorPos(null);
|
||||
setLastMouseDownPos(null);
|
||||
const pos = updateLastCursorPos(stage, $lastCursorPos.set);
|
||||
$lastCursorPos.set(null);
|
||||
$lastMouseDownPos.set(null);
|
||||
const selectedEntity = getSelectedEntity();
|
||||
const selectedEntityAdapter = getSelectedEntityAdapter();
|
||||
const toolState = getToolState();
|
||||
|
||||
if (
|
||||
pos &&
|
||||
selectedEntity &&
|
||||
isDrawableEntity(selectedEntity) &&
|
||||
selectedEntityAdapter &&
|
||||
isDrawableEntityAdapter(selectedEntityAdapter) &&
|
||||
!getSpaceKey() &&
|
||||
isDrawableEntity(selectedEntity.state) &&
|
||||
!$spaceKey.get() &&
|
||||
getIsPrimaryMouseDown(e)
|
||||
) {
|
||||
const drawingBuffer = selectedEntityAdapter.renderer.buffer;
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.position);
|
||||
const drawingBuffer = selectedEntity.adapter.renderer.buffer;
|
||||
const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
|
||||
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
|
||||
drawingBuffer.points.push(alignedPoint.x, alignedPoint.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect') {
|
||||
drawingBuffer.width = Math.round(normalizedPoint.x - drawingBuffer.x);
|
||||
drawingBuffer.height = Math.round(normalizedPoint.y - drawingBuffer.y);
|
||||
await selectedEntityAdapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntityAdapter.renderer.commitBuffer();
|
||||
await selectedEntity.adapter.renderer.setBuffer(drawingBuffer);
|
||||
await selectedEntity.adapter.renderer.commitBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@ -503,7 +486,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
stage.scaleX(newScale);
|
||||
stage.scaleY(newScale);
|
||||
stage.position(newPos);
|
||||
setStageAttrs({
|
||||
$stageAttrs.set({
|
||||
position: newPos,
|
||||
dimensions: { width: stage.width(), height: stage.height() },
|
||||
scale: newScale,
|
||||
@ -516,7 +499,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
//#region dragmove
|
||||
stage.on('dragmove', () => {
|
||||
setStageAttrs({
|
||||
$stageAttrs.set({
|
||||
position: { x: Math.floor(stage.x()), y: Math.floor(stage.y()) },
|
||||
dimensions: { width: stage.width(), height: stage.height() },
|
||||
scale: stage.scaleX(),
|
||||
@ -528,7 +511,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
//#region dragend
|
||||
stage.on('dragend', () => {
|
||||
// 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()) },
|
||||
dimensions: { width: stage.width(), height: stage.height() },
|
||||
scale: stage.scaleX(),
|
||||
@ -546,17 +529,17 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
// Cancel shape drawing on escape
|
||||
setLastMouseDownPos(null);
|
||||
$lastMouseDownPos.set(null);
|
||||
} else if (e.key === ' ') {
|
||||
// Select the view tool on space key down
|
||||
setToolBuffer(getToolState().selected);
|
||||
setTool('view');
|
||||
setSpaceKey(true);
|
||||
setLastCursorPos(null);
|
||||
setLastMouseDownPos(null);
|
||||
$spaceKey.set(true);
|
||||
$lastCursorPos.set(null);
|
||||
$lastMouseDownPos.set(null);
|
||||
} else if (e.key === 'r') {
|
||||
setLastCursorPos(null);
|
||||
setLastMouseDownPos(null);
|
||||
$lastCursorPos.set(null);
|
||||
$lastMouseDownPos.set(null);
|
||||
manager.background.render();
|
||||
// TODO(psyche): restore some kind of fit
|
||||
}
|
||||
@ -576,7 +559,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
const toolBuffer = getToolState().selectedBuffer;
|
||||
setTool(toolBuffer ?? 'move');
|
||||
setToolBuffer(null);
|
||||
setSpaceKey(false);
|
||||
$spaceKey.set(false);
|
||||
}
|
||||
manager.preview.tool.render();
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { getImageDataTransparency } from 'common/util/arrayBuffer';
|
||||
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 Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@ -64,7 +69,7 @@ export const alignCoordForTool = (coord: Coordinate, toolWidth: number): Coordin
|
||||
* Offsets a point by the given offset. The offset is subtracted from the point.
|
||||
* @param coord The coordinate to offset
|
||||
* @param offset The offset to apply
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export const offsetCoord = (coord: Coordinate, offset: Coordinate): Coordinate => {
|
||||
return {
|
||||
@ -623,22 +628,6 @@ export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean)
|
||||
}
|
||||
}
|
||||
|
||||
export type Subscription = {
|
||||
name: string;
|
||||
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),
|
||||
};
|
||||
};
|
||||
export const getEmptyRect = (): Rect => {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user