mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip transform mode
This commit is contained in:
parent
65353ac1e1
commit
7f8a1d8d20
@ -1,6 +1,6 @@
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
|
||||
@ -17,6 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
const model = state.canvasV2.params.model;
|
||||
const { prepend } = action.payload;
|
||||
|
||||
const manager = $canvasManager.get();
|
||||
assert(manager, 'No model found in state');
|
||||
|
||||
let didStartStaging = false;
|
||||
if (!state.canvasV2.session.isStaging && state.canvasV2.session.isActive) {
|
||||
dispatch(sessionStartedStaging());
|
||||
@ -26,7 +29,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
try {
|
||||
let g;
|
||||
|
||||
const manager = getCanvasManager();
|
||||
assert(model, 'No model found in state');
|
||||
const base = model.base;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { BrushWidth } from 'features/controlLayers/components/BrushWidth';
|
||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||
@ -10,19 +11,22 @@ import { NewSessionButton } from 'features/controlLayers/components/NewSessionBu
|
||||
import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton';
|
||||
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||
import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const ControlLayersToolbar = memo(() => {
|
||||
const tool = useAppSelector((s) => s.canvasV2.tool.selected);
|
||||
const canvasManager = useStore($canvasManager);
|
||||
const bbox = useCallback(() => {
|
||||
const manager = getCanvasManager();
|
||||
for (const l of manager.layers.values()) {
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
for (const l of canvasManager.layers.values()) {
|
||||
l.getBbox();
|
||||
}
|
||||
}, []);
|
||||
}, [canvasManager]);
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
|
@ -2,7 +2,7 @@ import { Flex } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||
import { CanvasManager, setCanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
@ -28,7 +28,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
|
||||
}
|
||||
|
||||
const manager = new CanvasManager(stage, container, store);
|
||||
setCanvasManager(manager);
|
||||
$canvasManager.set(manager);
|
||||
console.log(manager);
|
||||
const cleanup = manager.initialize();
|
||||
return cleanup;
|
||||
|
@ -1,38 +1,58 @@
|
||||
import { Button, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { toolIsTransformingChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiResizeBold } from 'react-icons/pi';
|
||||
|
||||
export const TransformToolButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming);
|
||||
const canvasManager = useStore($canvasManager);
|
||||
const [isTransforming, setIsTransforming] = useState(false);
|
||||
const isDisabled = useAppSelector(
|
||||
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
canvasManager.onTransform = setIsTransforming;
|
||||
return () => {
|
||||
canvasManager.onTransform = null;
|
||||
};
|
||||
}, [canvasManager]);
|
||||
|
||||
const onTransform = useCallback(() => {
|
||||
dispatch(toolIsTransformingChanged(true));
|
||||
}, [dispatch]);
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
canvasManager.startTransform();
|
||||
}, [canvasManager]);
|
||||
|
||||
const onApplyTransformation = useCallback(() => {
|
||||
false && dispatch(toolIsTransformingChanged(true));
|
||||
}, [dispatch]);
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
canvasManager.applyTransform();
|
||||
}, [canvasManager]);
|
||||
|
||||
const onCancelTransformation = useCallback(() => {
|
||||
dispatch(toolIsTransformingChanged(false));
|
||||
}, [dispatch]);
|
||||
if (!canvasManager) {
|
||||
return;
|
||||
}
|
||||
canvasManager.cancelTransform();
|
||||
}, [canvasManager]);
|
||||
|
||||
useHotkeys(['ctrl+t', 'meta+t'], onTransform, { enabled: !isDisabled }, [isDisabled, onTransform]);
|
||||
|
||||
if (isTransforming) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onApplyTransformation}>Apply</Button>
|
||||
<Button onClick={onCancelTransformation}>Cancel</Button>
|
||||
<Button onClick={onApplyTransformation}>{t('common.apply')}</Button>
|
||||
<Button onClick={onCancelTransformation}>{t('common.cancel')}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -44,8 +44,8 @@ export class CanvasBrushLine {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: BrushLine, force?: boolean): boolean {
|
||||
if (this.state !== state || force) {
|
||||
async update(state: BrushLine, force?: boolean): Promise<boolean> {
|
||||
if (force || this.state !== state) {
|
||||
const { points, color, clip, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||
|
@ -44,8 +44,8 @@ export class CanvasEraserLine {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: EraserLine, force?: boolean): boolean {
|
||||
if (this.state !== state || force) {
|
||||
async update(state: EraserLine, force?: boolean): Promise<boolean> {
|
||||
if (force || this.state !== state) {
|
||||
const { points, clip, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||
|
@ -1,14 +1,23 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { CanvasBrushLine } from 'features/controlLayers/konva/CanvasBrushLine';
|
||||
import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine';
|
||||
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import type { BrushLine, EraserLine, LayerEntity, RectShape } from 'features/controlLayers/store/types';
|
||||
import { isDrawingTool } from 'features/controlLayers/store/types';
|
||||
import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
|
||||
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type {
|
||||
BrushLine,
|
||||
CanvasV2State,
|
||||
Coordinate,
|
||||
EraserLine,
|
||||
LayerEntity,
|
||||
RectShape,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { debounce, get } from 'lodash-es';
|
||||
import type { Logger } from 'roarr';
|
||||
import { uploadImage } from 'services/api/endpoints/images';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export class CanvasLayer {
|
||||
@ -20,8 +29,6 @@ export class CanvasLayer {
|
||||
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
|
||||
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
|
||||
|
||||
private static BBOX_PADDING_PX = 5;
|
||||
|
||||
private drawingBuffer: BrushLine | EraserLine | RectShape | null;
|
||||
private state: LayerEntity;
|
||||
|
||||
@ -41,8 +48,10 @@ export class CanvasLayer {
|
||||
offsetY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
getBbox = debounce(this._getBbox, 300);
|
||||
log: Logger;
|
||||
bboxNeedsUpdate: boolean;
|
||||
isTransforming: boolean;
|
||||
isFirstRender: boolean;
|
||||
|
||||
constructor(state: LayerEntity, manager: CanvasManager) {
|
||||
this.id = state.id;
|
||||
@ -60,12 +69,12 @@ export class CanvasLayer {
|
||||
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
|
||||
transformer: new Konva.Transformer({
|
||||
name: CanvasLayer.TRANSFORMER_NAME,
|
||||
draggable: true,
|
||||
draggable: false,
|
||||
// enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||
rotateEnabled: true,
|
||||
flipEnabled: true,
|
||||
listening: false,
|
||||
padding: CanvasLayer.BBOX_PADDING_PX,
|
||||
padding: this.manager.getScaledBboxPadding(),
|
||||
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
|
||||
keepRatio: false,
|
||||
}),
|
||||
@ -135,19 +144,30 @@ export class CanvasLayer {
|
||||
});
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: this.konva.interactionRect.x(),
|
||||
y: this.konva.interactionRect.y(),
|
||||
x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
|
||||
y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
|
||||
scaleX: this.konva.interactionRect.scaleX(),
|
||||
scaleY: this.konva.interactionRect.scaleY(),
|
||||
rotation: this.konva.interactionRect.rotation(),
|
||||
});
|
||||
console.log('objectGroup', {
|
||||
x: this.konva.objectGroup.x(),
|
||||
y: this.konva.objectGroup.y(),
|
||||
scaleX: this.konva.objectGroup.scaleX(),
|
||||
scaleY: this.konva.objectGroup.scaleY(),
|
||||
offsetX: this.offsetX,
|
||||
offsetY: this.offsetY,
|
||||
width: this.konva.objectGroup.width(),
|
||||
height: this.konva.objectGroup.height(),
|
||||
rotation: this.konva.objectGroup.rotation(),
|
||||
});
|
||||
});
|
||||
|
||||
this.konva.transformer.on('transformend', () => {
|
||||
this.offsetX = this.konva.interactionRect.x() - this.state.position.x;
|
||||
this.offsetY = this.konva.interactionRect.y() - this.state.position.y;
|
||||
this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
|
||||
this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
|
||||
// this.offsetX = this.konva.interactionRect.x() - this.state.position.x;
|
||||
// this.offsetY = this.konva.interactionRect.y() - this.state.position.y;
|
||||
// this.width = Math.round(this.konva.interactionRect.width() * this.konva.interactionRect.scaleX());
|
||||
// this.height = Math.round(this.konva.interactionRect.height() * this.konva.interactionRect.scaleY());
|
||||
// this.manager.stateApi.onPosChanged(
|
||||
// {
|
||||
// id: this.id,
|
||||
@ -166,26 +186,33 @@ export class CanvasLayer {
|
||||
// The bbox should be updated to reflect the new position of the interaction rect, taking into account its padding
|
||||
// and border
|
||||
this.konva.bbox.setAttrs({
|
||||
x: this.konva.interactionRect.x() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
|
||||
y: this.konva.interactionRect.y() - CanvasLayer.BBOX_PADDING_PX / this.manager.stage.scaleX(),
|
||||
x: this.konva.interactionRect.x() - this.manager.getScaledBboxPadding(),
|
||||
y: this.konva.interactionRect.y() - this.manager.getScaledBboxPadding(),
|
||||
});
|
||||
|
||||
// The object group is translated by the difference between the interaction rect's new and old positions (which is
|
||||
// stored as this.bbox)
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: this.konva.interactionRect.x(),
|
||||
y: this.konva.interactionRect.y(),
|
||||
x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
|
||||
y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
|
||||
});
|
||||
});
|
||||
this.konva.interactionRect.on('dragend', () => {
|
||||
this.logBbox('dragend bbox');
|
||||
|
||||
// Update internal state
|
||||
// this.state.position = { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() };
|
||||
if (this.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.
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager.stateApi.onPosChanged(
|
||||
{
|
||||
id: this.id,
|
||||
position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
|
||||
position: {
|
||||
x: this.konva.interactionRect.x() - this.offsetX * this.konva.interactionRect.scaleX(),
|
||||
y: this.konva.interactionRect.y() - this.offsetY * this.konva.interactionRect.scaleY(),
|
||||
},
|
||||
},
|
||||
'layer'
|
||||
);
|
||||
@ -198,15 +225,16 @@ export class CanvasLayer {
|
||||
this.offsetY = 0;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.bboxNeedsUpdate = true;
|
||||
this.isTransforming = false;
|
||||
this.isFirstRender = true;
|
||||
this.log = this.manager.getLogger(`layer_${this.id}`);
|
||||
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
private static get DEFAULT_BBOX_RECT() {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.log.debug(`Layer ${this.id} - destroying`);
|
||||
this.konva.layer.destroy();
|
||||
}
|
||||
|
||||
@ -214,99 +242,222 @@ export class CanvasLayer {
|
||||
return this.drawingBuffer;
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
const scale = this.manager.stage.scaleX();
|
||||
const onePixel = 1 / scale;
|
||||
const bboxPadding = CanvasLayer.BBOX_PADDING_PX / scale;
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: this.state.position.x,
|
||||
y: this.state.position.y,
|
||||
offsetX: this.offsetX,
|
||||
offsetY: this.offsetY,
|
||||
});
|
||||
this.konva.bbox.setAttrs({
|
||||
x: this.state.position.x - bboxPadding,
|
||||
y: this.state.position.y - bboxPadding,
|
||||
width: this.width + bboxPadding * 2,
|
||||
height: this.height + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
this.konva.interactionRect.setAttrs({
|
||||
x: this.state.position.x,
|
||||
y: this.state.position.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
|
||||
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
|
||||
if (obj) {
|
||||
this.drawingBuffer = obj;
|
||||
await this.renderObject(this.drawingBuffer, true);
|
||||
this.updateGroup(true);
|
||||
await this._renderObject(this.drawingBuffer, true);
|
||||
} else {
|
||||
this.drawingBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
finalizeDrawingBuffer() {
|
||||
async finalizeDrawingBuffer() {
|
||||
if (!this.drawingBuffer) {
|
||||
return;
|
||||
}
|
||||
if (this.drawingBuffer.type === 'brush_line') {
|
||||
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: this.drawingBuffer }, 'layer');
|
||||
} else if (this.drawingBuffer.type === 'eraser_line') {
|
||||
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: this.drawingBuffer }, 'layer');
|
||||
} else if (this.drawingBuffer.type === 'rect_shape') {
|
||||
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: this.drawingBuffer }, 'layer');
|
||||
}
|
||||
const drawingBuffer = this.drawingBuffer;
|
||||
this.setDrawingBuffer(null);
|
||||
|
||||
if (drawingBuffer.type === 'brush_line') {
|
||||
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
|
||||
} else if (drawingBuffer.type === 'eraser_line') {
|
||||
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
|
||||
} else if (drawingBuffer.type === 'rect_shape') {
|
||||
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
|
||||
}
|
||||
}
|
||||
|
||||
async render(state: LayerEntity) {
|
||||
this.state = deepClone(state);
|
||||
async update(arg?: { state: LayerEntity; 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));
|
||||
|
||||
let didDraw = false;
|
||||
if (!this.isFirstRender && state === this.state) {
|
||||
this.log.trace('State unchanged, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
const objectIds = state.objects.map(mapId);
|
||||
this.log.debug('Updating');
|
||||
const { position, objects, opacity, isEnabled } = state;
|
||||
|
||||
if (this.isFirstRender || position !== this.state.position) {
|
||||
await this.updatePosition({ position });
|
||||
}
|
||||
if (this.isFirstRender || objects !== this.state.objects) {
|
||||
await this.updateObjects({ objects });
|
||||
}
|
||||
if (this.isFirstRender || opacity !== this.state.opacity) {
|
||||
await this.updateOpacity({ opacity });
|
||||
}
|
||||
if (this.isFirstRender || isEnabled !== this.state.isEnabled) {
|
||||
await this.updateVisibility({ isEnabled });
|
||||
}
|
||||
await this.updateInteraction({ toolState, isSelected });
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async updateVisibility(arg?: { isEnabled: boolean }) {
|
||||
this.log.trace('Updating visibility');
|
||||
const isEnabled = get(arg, 'isEnabled', this.state.isEnabled);
|
||||
const hasObjects = this.objects.size > 0 || this.drawingBuffer !== null;
|
||||
this.konva.layer.visible(isEnabled || hasObjects);
|
||||
}
|
||||
|
||||
async updatePosition(arg?: { position: Coordinate }) {
|
||||
this.log.trace('Updating position');
|
||||
const position = get(arg, 'position', this.state.position);
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.objectGroup.setAttrs({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
this.konva.bbox.setAttrs({
|
||||
x: position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding,
|
||||
y: position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding,
|
||||
});
|
||||
this.konva.interactionRect.setAttrs({
|
||||
x: position.x + this.offsetX * this.konva.interactionRect.scaleX(),
|
||||
y: position.y + this.offsetY * this.konva.interactionRect.scaleY(),
|
||||
});
|
||||
}
|
||||
|
||||
async updateObjects(arg?: { objects: LayerEntity['objects'] }) {
|
||||
this.log.trace('Updating objects');
|
||||
|
||||
const objects = get(arg, 'objects', this.state.objects);
|
||||
|
||||
const objectIds = objects.map(mapId);
|
||||
// Destroy any objects that are no longer in state
|
||||
for (const object of this.objects.values()) {
|
||||
if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
|
||||
this.objects.delete(object.id);
|
||||
object.destroy();
|
||||
didDraw = true;
|
||||
this.bboxNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const obj of state.objects) {
|
||||
if (await this.renderObject(obj)) {
|
||||
didDraw = true;
|
||||
for (const obj of objects) {
|
||||
if (await this._renderObject(obj)) {
|
||||
this.bboxNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.drawingBuffer) {
|
||||
if (await this.renderObject(this.drawingBuffer)) {
|
||||
didDraw = true;
|
||||
if (await this._renderObject(this.drawingBuffer)) {
|
||||
this.bboxNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.renderBbox();
|
||||
this.updateGroup(didDraw);
|
||||
}
|
||||
|
||||
private async renderObject(obj: LayerEntity['objects'][number], force = false): Promise<boolean> {
|
||||
async updateOpacity(arg?: { opacity: number }) {
|
||||
this.log.trace('Updating opacity');
|
||||
|
||||
const opacity = get(arg, 'opacity', this.state.opacity);
|
||||
|
||||
this.konva.objectGroup.opacity(opacity);
|
||||
}
|
||||
|
||||
async 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.objects.size === 0) {
|
||||
// The layer is totally empty, we can just disable the layer
|
||||
this.konva.layer.listening(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && !this.isTransforming && toolState.selected === 'move') {
|
||||
// We are moving this layer, it must be listening
|
||||
this.konva.layer.listening(true);
|
||||
|
||||
// The transformer is not needed
|
||||
this.konva.transformer.listening(false);
|
||||
this.konva.transformer.nodes([]);
|
||||
|
||||
// The bbox rect should be visible and interaction rect listening for dragging
|
||||
this.konva.bbox.visible(true);
|
||||
this.konva.interactionRect.listening(true);
|
||||
} else if (isSelected && this.isTransforming) {
|
||||
// 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 listening = toolState.selected !== 'view';
|
||||
this.konva.layer.listening(listening);
|
||||
this.konva.interactionRect.listening(listening);
|
||||
this.konva.transformer.listening(listening);
|
||||
|
||||
// The transformer transforms the interaction rect, not the object group
|
||||
this.konva.transformer.nodes([this.konva.interactionRect]);
|
||||
|
||||
// Hide the bbox rect, the transformer will has its own bbox
|
||||
this.konva.bbox.visible(false);
|
||||
} 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);
|
||||
|
||||
// The transformer, bbox and interaction rect should be inactive
|
||||
this.konva.transformer.listening(false);
|
||||
this.konva.transformer.nodes([]);
|
||||
this.konva.bbox.visible(false);
|
||||
this.konva.interactionRect.listening(false);
|
||||
}
|
||||
}
|
||||
|
||||
async updateBbox() {
|
||||
this.log.trace('Updating bbox');
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.bbox.setAttrs({
|
||||
x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX() - bboxPadding,
|
||||
y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY() - bboxPadding,
|
||||
width: this.width + bboxPadding * 2,
|
||||
height: this.height + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
this.konva.interactionRect.setAttrs({
|
||||
x: this.state.position.x + this.offsetX * this.konva.interactionRect.scaleX(),
|
||||
y: this.state.position.y + this.offsetY * this.konva.interactionRect.scaleY(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
|
||||
async syncStageScale() {
|
||||
this.log.trace('Syncing scale to stage');
|
||||
|
||||
const onePixel = this.manager.getScaledPixel();
|
||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||
|
||||
this.konva.bbox.setAttrs({
|
||||
x: this.konva.interactionRect.x() - bboxPadding,
|
||||
y: this.konva.interactionRect.y() - bboxPadding,
|
||||
width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX() + bboxPadding * 2,
|
||||
height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY() + bboxPadding * 2,
|
||||
strokeWidth: onePixel,
|
||||
});
|
||||
this.konva.transformer.forceUpdate();
|
||||
}
|
||||
|
||||
async _renderObject(obj: LayerEntity['objects'][number], force = false): Promise<boolean> {
|
||||
if (obj.type === 'brush_line') {
|
||||
let brushLine = this.objects.get(obj.id);
|
||||
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
|
||||
|
||||
if (!brushLine) {
|
||||
console.log('creating new brush line');
|
||||
brushLine = new CanvasBrushLine(obj);
|
||||
this.objects.set(brushLine.id, brushLine);
|
||||
this.konva.objectGroup.add(brushLine.konva.group);
|
||||
return true;
|
||||
} else {
|
||||
if (brushLine.update(obj, force)) {
|
||||
console.log('updating brush line');
|
||||
if (await brushLine.update(obj, force)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -320,7 +471,7 @@ export class CanvasLayer {
|
||||
this.konva.objectGroup.add(eraserLine.konva.group);
|
||||
return true;
|
||||
} else {
|
||||
if (eraserLine.update(obj, force)) {
|
||||
if (await eraserLine.update(obj, force)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -358,109 +509,70 @@ export class CanvasLayer {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateGroup(didDraw: boolean) {
|
||||
if (!this.state.isEnabled) {
|
||||
this.konva.layer.visible(false);
|
||||
return;
|
||||
}
|
||||
async startTransform() {
|
||||
this.isTransforming = true;
|
||||
|
||||
if (didDraw) {
|
||||
if (this.objects.size > 0) {
|
||||
this.getBbox();
|
||||
} else {
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.renderBbox();
|
||||
}
|
||||
}
|
||||
// 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 listening = this.manager.stateApi.getToolState().selected !== 'view';
|
||||
|
||||
this.konva.layer.visible(true);
|
||||
this.konva.objectGroup.opacity(this.state.opacity);
|
||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
this.konva.layer.listening(listening);
|
||||
this.konva.interactionRect.listening(listening);
|
||||
this.konva.transformer.listening(listening);
|
||||
|
||||
const isMoving = toolState.selected === 'move' && isSelected;
|
||||
// The transformer transforms the interaction rect, not the object group
|
||||
this.konva.transformer.nodes([this.konva.interactionRect]);
|
||||
|
||||
this.konva.layer.listening(toolState.isTransforming || isMoving);
|
||||
this.konva.transformer.listening(toolState.isTransforming);
|
||||
this.konva.bbox.visible(isMoving);
|
||||
this.konva.interactionRect.listening(toolState.isTransforming || isMoving);
|
||||
|
||||
if (this.objects.size === 0) {
|
||||
// If the layer is totally empty, reset the cache and bail out.
|
||||
this.konva.transformer.nodes([]);
|
||||
if (this.konva.objectGroup.isCached()) {
|
||||
this.konva.objectGroup.clearCache();
|
||||
}
|
||||
} else if (isSelected && toolState.isTransforming) {
|
||||
// When the layer is selected and being moved, we should always cache it.
|
||||
// We should update the cache if we drew to the layer.
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
// Activate the transformer - it *must* be transforming the interactionRect, not the group!
|
||||
this.konva.transformer.nodes([this.konva.interactionRect]);
|
||||
this.konva.transformer.forceUpdate();
|
||||
this.konva.transformer.visible(true);
|
||||
} else if (toolState.selected === 'move') {
|
||||
// When the layer is selected and being moved, we should always cache it.
|
||||
// We should update the cache if we drew to the layer.
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
// Activate the transformer
|
||||
this.konva.transformer.nodes([]);
|
||||
this.konva.transformer.forceUpdate();
|
||||
this.konva.transformer.visible(false);
|
||||
} else if (isSelected) {
|
||||
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
|
||||
// The transformer also does not need to be active.
|
||||
this.konva.transformer.nodes([]);
|
||||
if (isDrawingTool(toolState.selected)) {
|
||||
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
||||
// should never be cached.
|
||||
if (this.konva.objectGroup.isCached()) {
|
||||
this.konva.objectGroup.clearCache();
|
||||
}
|
||||
} else {
|
||||
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
|
||||
// We should update the cache if we drew to the layer.
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
}
|
||||
} else if (!isSelected) {
|
||||
// Unselected layers should not be listening
|
||||
// The transformer also does not need to be active.
|
||||
this.konva.transformer.nodes([]);
|
||||
// Update the layer's cache if it's not already cached or we drew to it.
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
}
|
||||
// Hide the bbox rect, the transformer will has its own bbox
|
||||
this.konva.bbox.visible(false);
|
||||
}
|
||||
|
||||
renderBbox() {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
if (toolState.isTransforming) {
|
||||
return;
|
||||
}
|
||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||
const hasBbox = this.width !== 0 && this.height !== 0;
|
||||
this.konva.bbox.visible(hasBbox && isSelected && toolState.selected === 'move');
|
||||
this.konva.interactionRect.visible(hasBbox);
|
||||
this.updatePosition();
|
||||
async resetScale() {
|
||||
this.konva.objectGroup.scaleX(1);
|
||||
this.konva.objectGroup.scaleY(1);
|
||||
this.konva.bbox.scaleX(1);
|
||||
this.konva.bbox.scaleY(1);
|
||||
this.konva.interactionRect.scaleX(1);
|
||||
this.konva.interactionRect.scaleY(1);
|
||||
}
|
||||
|
||||
private _getBbox() {
|
||||
async applyTransform() {
|
||||
this.isTransforming = false;
|
||||
const objectGroupClone = this.konva.objectGroup.clone();
|
||||
const rect = {
|
||||
x: this.konva.interactionRect.x(),
|
||||
y: this.konva.interactionRect.y(),
|
||||
width: this.konva.interactionRect.width() * this.konva.interactionRect.scaleX(),
|
||||
height: this.konva.interactionRect.height() * this.konva.interactionRect.scaleY(),
|
||||
};
|
||||
const blob = await konvaNodeToBlob(objectGroupClone, rect);
|
||||
previewBlob(blob, 'transformed layer');
|
||||
const imageDTO = await uploadImage(blob, `${this.id}_transform.png`, 'other', true, true);
|
||||
const { dispatch } = getStore();
|
||||
dispatch(layerRasterized({ id: this.id, imageDTO, position: this.konva.interactionRect.position() }));
|
||||
this.isTransforming = false;
|
||||
this.resetScale();
|
||||
}
|
||||
|
||||
async cancelTransform() {
|
||||
this.isTransforming = false;
|
||||
this.resetScale();
|
||||
await this.updatePosition({ position: this.state.position });
|
||||
await this.updateBbox();
|
||||
await this.updateInteraction({
|
||||
toolState: this.manager.stateApi.getToolState(),
|
||||
isSelected: this.manager.stateApi.getIsSelected(this.id),
|
||||
});
|
||||
}
|
||||
|
||||
getBbox = debounce(() => {
|
||||
if (this.objects.size === 0) {
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.renderBbox();
|
||||
this.updateBbox();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -482,18 +594,8 @@ export class CanvasLayer {
|
||||
this.offsetY = rect.y;
|
||||
this.width = rect.width;
|
||||
this.height = rect.height;
|
||||
// if (rect.width === 0 || rect.height === 0) {
|
||||
// this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
// } else {
|
||||
// this.bbox = {
|
||||
// x: rect.x,
|
||||
// y: rect.y,
|
||||
// width: rect.width,
|
||||
// height: rect.height,
|
||||
// };
|
||||
// }
|
||||
this.logBbox('new bbox from client rect');
|
||||
this.renderBbox();
|
||||
this.updateBbox();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -523,11 +625,11 @@ export class CanvasLayer {
|
||||
this.height = 0;
|
||||
}
|
||||
this.logBbox('new bbox from worker');
|
||||
this.renderBbox();
|
||||
this.updateBbox();
|
||||
clone.destroy();
|
||||
}
|
||||
);
|
||||
}
|
||||
}, CanvasManager.BBOX_DEBOUNCE_MS);
|
||||
|
||||
logBbox(msg: string = 'bbox') {
|
||||
console.log(msg, {
|
||||
@ -539,4 +641,13 @@ export class CanvasLayer {
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
|
||||
getLayerRect() {
|
||||
return {
|
||||
x: this.state.position.x + this.offsetX,
|
||||
y: this.state.position.y + this.offsetY,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLaye
|
||||
import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
|
||||
import type { ImageCategory, ImageDTO } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
@ -32,9 +33,6 @@ import { CanvasStateApi } from './CanvasStateApi';
|
||||
import { CanvasTool } from './CanvasTool';
|
||||
import { setStageEventHandlers } from './events';
|
||||
|
||||
const log = logger('canvas');
|
||||
const workerLog = logger('worker');
|
||||
|
||||
// type Extents = {
|
||||
// minX: number;
|
||||
// minY: number;
|
||||
@ -63,17 +61,12 @@ type Util = {
|
||||
) => Promise<ImageDTO>;
|
||||
};
|
||||
|
||||
const $canvasManager = atom<CanvasManager | null>(null);
|
||||
export function getCanvasManager() {
|
||||
const nodeManager = $canvasManager.get();
|
||||
assert(nodeManager !== null, 'Node manager not initialized');
|
||||
return nodeManager;
|
||||
}
|
||||
export function setCanvasManager(nodeManager: CanvasManager) {
|
||||
$canvasManager.set(nodeManager);
|
||||
}
|
||||
export const $canvasManager = atom<CanvasManager | null>(null);
|
||||
|
||||
export class CanvasManager {
|
||||
private static BBOX_PADDING_PX = 5;
|
||||
static BBOX_DEBOUNCE_MS = 300;
|
||||
|
||||
stage: Konva.Stage;
|
||||
container: HTMLDivElement;
|
||||
controlAdapters: Map<string, CanvasControlAdapter>;
|
||||
@ -86,6 +79,11 @@ export class CanvasManager {
|
||||
preview: CanvasPreview;
|
||||
background: CanvasBackground;
|
||||
|
||||
log: Logger;
|
||||
workerLog: Logger;
|
||||
|
||||
onTransform: ((isTransforming: boolean) => void) | null;
|
||||
|
||||
private store: Store<RootState>;
|
||||
private isFirstRender: boolean;
|
||||
private prevState: CanvasV2State;
|
||||
@ -106,6 +104,9 @@ export class CanvasManager {
|
||||
this.prevState = this.stateApi.getState();
|
||||
this.isFirstRender = true;
|
||||
|
||||
this.log = logger('canvas');
|
||||
this.workerLog = logger('worker');
|
||||
|
||||
this.util = {
|
||||
getImageDTO,
|
||||
uploadImage,
|
||||
@ -138,9 +139,9 @@ export class CanvasManager {
|
||||
const { type, data } = event.data;
|
||||
if (type === 'log') {
|
||||
if (data.ctx) {
|
||||
workerLog[data.level](data.ctx, data.message);
|
||||
this.workerLog[data.level](data.ctx, data.message);
|
||||
} else {
|
||||
workerLog[data.level](data.message);
|
||||
this.workerLog[data.level](data.message);
|
||||
}
|
||||
} else if (type === 'extents') {
|
||||
const task = this.tasks.get(data.id);
|
||||
@ -151,11 +152,17 @@ export class CanvasManager {
|
||||
}
|
||||
};
|
||||
this.worker.onerror = (event) => {
|
||||
log.error({ message: event.message }, 'Worker error');
|
||||
this.log.error({ message: event.message }, 'Worker error');
|
||||
};
|
||||
this.worker.onmessageerror = () => {
|
||||
log.error('Worker message error');
|
||||
this.log.error('Worker message error');
|
||||
};
|
||||
this.onTransform = null;
|
||||
}
|
||||
|
||||
getLogger(namespace: string) {
|
||||
const managerNamespace = this.log.getContext().namespace;
|
||||
return this.log.child({ namespace: `${managerNamespace}.${namespace}` });
|
||||
}
|
||||
|
||||
requestBbox(data: Omit<GetBboxTask['data'], 'id'>, onComplete: (extents: Extents | null) => void) {
|
||||
@ -172,27 +179,6 @@ export class CanvasManager {
|
||||
await this.initialImage.render(this.stateApi.getInitialImageState());
|
||||
}
|
||||
|
||||
async renderLayers() {
|
||||
const { entities } = this.stateApi.getLayersState();
|
||||
|
||||
for (const canvasLayer of this.layers.values()) {
|
||||
if (!entities.find((l) => l.id === canvasLayer.id)) {
|
||||
canvasLayer.destroy();
|
||||
this.layers.delete(canvasLayer.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
let adapter = this.layers.get(entity.id);
|
||||
if (!adapter) {
|
||||
adapter = new CanvasLayer(entity, this);
|
||||
this.layers.set(adapter.id, adapter);
|
||||
this.stage.add(adapter.konva.layer);
|
||||
}
|
||||
await adapter.render(entity);
|
||||
}
|
||||
}
|
||||
|
||||
async renderRegions() {
|
||||
const { entities } = this.stateApi.getRegionsState();
|
||||
|
||||
@ -245,9 +231,9 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
renderBboxes() {
|
||||
syncStageScale() {
|
||||
for (const layer of this.layers.values()) {
|
||||
layer.renderBbox();
|
||||
layer.syncStageScale();
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,22 +269,84 @@ export class CanvasManager {
|
||||
this.background.render();
|
||||
}
|
||||
|
||||
getTransformingLayer() {
|
||||
return Array.from(this.layers.values()).find((layer) => layer.isTransforming);
|
||||
}
|
||||
|
||||
getIsTransforming() {
|
||||
return Boolean(this.getTransformingLayer());
|
||||
}
|
||||
|
||||
startTransform() {
|
||||
if (this.getIsTransforming()) {
|
||||
return;
|
||||
}
|
||||
const layer = this.getSelectedEntityAdapter();
|
||||
assert(layer instanceof CanvasLayer, 'No selected layer');
|
||||
layer.startTransform();
|
||||
this.onTransform?.(true);
|
||||
}
|
||||
|
||||
applyTransform() {
|
||||
const layer = this.getTransformingLayer();
|
||||
if (layer) {
|
||||
layer.applyTransform();
|
||||
}
|
||||
this.onTransform?.(false);
|
||||
}
|
||||
|
||||
cancelTransform() {
|
||||
const layer = this.getTransformingLayer();
|
||||
if (layer) {
|
||||
layer.cancelTransform();
|
||||
}
|
||||
this.onTransform?.(false);
|
||||
}
|
||||
|
||||
render = async () => {
|
||||
const state = this.stateApi.getState();
|
||||
|
||||
if (this.prevState === state && !this.isFirstRender) {
|
||||
log.trace('No changes detected, skipping render');
|
||||
this.log.trace('No changes detected, skipping render');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.layers.entities !== this.prevState.layers.entities) {
|
||||
this.log.debug('Rendering layers');
|
||||
|
||||
for (const canvasLayer of this.layers.values()) {
|
||||
if (!state.layers.entities.find((l) => l.id === canvasLayer.id)) {
|
||||
this.log.debug(`Destroying deleted layer ${canvasLayer.id}`);
|
||||
canvasLayer.destroy();
|
||||
this.layers.delete(canvasLayer.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entityState of state.layers.entities) {
|
||||
let adapter = this.layers.get(entityState.id);
|
||||
if (!adapter) {
|
||||
this.log.debug(`Creating layer layer ${entityState.id}`);
|
||||
adapter = new CanvasLayer(entityState, this);
|
||||
this.layers.set(adapter.id, adapter);
|
||||
this.stage.add(adapter.konva.layer);
|
||||
}
|
||||
await adapter.update({
|
||||
state: entityState,
|
||||
toolState: state.tool,
|
||||
isSelected: state.selectedEntityIdentifier?.id === entityState.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFirstRender ||
|
||||
state.layers.entities !== this.prevState.layers.entities ||
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Rendering layers');
|
||||
await this.renderLayers();
|
||||
this.log.debug('Updating interaction');
|
||||
for (const layer of this.layers.values()) {
|
||||
layer.updateInteraction({ toolState: state.tool, isSelected: state.selectedEntityIdentifier?.id === layer.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@ -308,7 +356,7 @@ export class CanvasManager {
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Rendering initial image');
|
||||
this.log.debug('Rendering initial image');
|
||||
await this.renderInitialImage();
|
||||
}
|
||||
|
||||
@ -319,7 +367,7 @@ export class CanvasManager {
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Rendering regions');
|
||||
this.log.debug('Rendering regions');
|
||||
await this.renderRegions();
|
||||
}
|
||||
|
||||
@ -330,7 +378,7 @@ export class CanvasManager {
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Rendering inpaint mask');
|
||||
this.log.debug('Rendering inpaint mask');
|
||||
await this.renderInpaintMask();
|
||||
}
|
||||
|
||||
@ -340,7 +388,7 @@ export class CanvasManager {
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Rendering control adapters');
|
||||
this.log.debug('Rendering control adapters');
|
||||
await this.renderControlAdapters();
|
||||
}
|
||||
|
||||
@ -350,7 +398,7 @@ export class CanvasManager {
|
||||
state.tool.selected !== this.prevState.tool.selected ||
|
||||
state.session.isActive !== this.prevState.session.isActive
|
||||
) {
|
||||
log.debug('Rendering generation bbox');
|
||||
this.log.debug('Rendering generation bbox');
|
||||
await this.preview.bbox.render();
|
||||
}
|
||||
|
||||
@ -360,12 +408,12 @@ export class CanvasManager {
|
||||
state.controlAdapters !== this.prevState.controlAdapters ||
|
||||
state.regions !== this.prevState.regions
|
||||
) {
|
||||
// log.debug('Updating entity bboxes');
|
||||
// this.log.debug('Updating entity bboxes');
|
||||
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
|
||||
}
|
||||
|
||||
if (this.isFirstRender || state.session !== this.prevState.session) {
|
||||
log.debug('Rendering staging area');
|
||||
this.log.debug('Rendering staging area');
|
||||
await this.preview.stagingArea.render();
|
||||
}
|
||||
|
||||
@ -377,7 +425,7 @@ export class CanvasManager {
|
||||
state.inpaintMask !== this.prevState.inpaintMask ||
|
||||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
log.debug('Arranging entities');
|
||||
this.log.debug('Arranging entities');
|
||||
await this.arrangeEntities();
|
||||
}
|
||||
|
||||
@ -389,7 +437,7 @@ export class CanvasManager {
|
||||
};
|
||||
|
||||
initialize = () => {
|
||||
log.debug('Initializing renderer');
|
||||
this.log.debug('Initializing renderer');
|
||||
this.stage.container(this.container);
|
||||
|
||||
const cleanupListeners = setStageEventHandlers(this);
|
||||
@ -405,24 +453,24 @@ export class CanvasManager {
|
||||
// When we this flag, we need to render the staging area
|
||||
$shouldShowStagedImage.subscribe(async (shouldShowStagedImage, prevShouldShowStagedImage) => {
|
||||
if (shouldShowStagedImage !== prevShouldShowStagedImage) {
|
||||
log.debug('Rendering staging area');
|
||||
this.log.debug('Rendering staging area');
|
||||
await this.preview.stagingArea.render();
|
||||
}
|
||||
});
|
||||
|
||||
$lastProgressEvent.subscribe(async (lastProgressEvent, prevLastProgressEvent) => {
|
||||
if (lastProgressEvent !== prevLastProgressEvent) {
|
||||
log.debug('Rendering progress image');
|
||||
this.log.debug('Rendering progress image');
|
||||
await this.preview.progressPreview.render(lastProgressEvent);
|
||||
}
|
||||
});
|
||||
|
||||
log.debug('First render of konva stage');
|
||||
this.log.debug('First render of konva stage');
|
||||
this.preview.tool.render();
|
||||
this.render();
|
||||
|
||||
return () => {
|
||||
log.debug('Cleaning up konva renderer');
|
||||
this.log.debug('Cleaning up konva renderer');
|
||||
unsubscribeRenderer();
|
||||
cleanupListeners();
|
||||
$shouldShowStagedImage.off();
|
||||
@ -430,6 +478,19 @@ export class CanvasManager {
|
||||
};
|
||||
};
|
||||
|
||||
getStageScale(): number {
|
||||
// The stage is never scaled differently in x and y
|
||||
return this.stage.scaleX();
|
||||
}
|
||||
|
||||
getScaledPixel(): number {
|
||||
return 1 / this.getStageScale();
|
||||
}
|
||||
|
||||
getScaledBboxPadding(): number {
|
||||
return CanvasManager.BBOX_PADDING_PX / this.getStageScale();
|
||||
}
|
||||
|
||||
getSelectedEntityAdapter = (): CanvasLayer | CanvasRegion | CanvasControlAdapter | CanvasInpaintMask | null => {
|
||||
const state = this.stateApi.getState();
|
||||
const identifier = state.selectedEntityIdentifier;
|
||||
|
@ -185,7 +185,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -203,7 +203,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
});
|
||||
} else {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -222,7 +222,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -239,7 +239,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
});
|
||||
} else {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -254,7 +254,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
|
||||
if (toolState.selected === 'rect') {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getRectShapeId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -290,7 +290,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (toolState.selected === 'brush') {
|
||||
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
|
||||
if (drawingBuffer?.type === 'brush_line') {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
}
|
||||
@ -299,7 +299,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (toolState.selected === 'eraser') {
|
||||
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
|
||||
if (drawingBuffer?.type === 'eraser_line') {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
}
|
||||
@ -308,7 +308,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (toolState.selected === 'rect') {
|
||||
const drawingBuffer = selectedEntityAdapter.getDrawingBuffer();
|
||||
if (drawingBuffer?.type === 'rect_shape') {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else {
|
||||
await selectedEntityAdapter.setDrawingBuffer(null);
|
||||
}
|
||||
@ -354,7 +354,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
}
|
||||
} else {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -386,7 +386,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
}
|
||||
} else {
|
||||
if (selectedEntityAdapter.getDrawingBuffer()) {
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
await selectedEntityAdapter.setDrawingBuffer({
|
||||
id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
|
||||
@ -437,16 +437,16 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
if (toolState.selected === 'brush' && drawingBuffer?.type === 'brush_line') {
|
||||
drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else if (toolState.selected === 'eraser' && drawingBuffer?.type === 'eraser_line') {
|
||||
drawingBuffer.points.push(pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y);
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
} else if (toolState.selected === 'rect' && drawingBuffer?.type === 'rect_shape') {
|
||||
drawingBuffer.width = pos.x - selectedEntity.position.x - drawingBuffer.x;
|
||||
drawingBuffer.height = pos.y - selectedEntity.position.y - drawingBuffer.y;
|
||||
await selectedEntityAdapter.setDrawingBuffer(drawingBuffer);
|
||||
selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,7 +496,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
||||
scale: newScale,
|
||||
});
|
||||
manager.background.render();
|
||||
manager.renderBboxes();
|
||||
manager.syncStageScale();
|
||||
}
|
||||
}
|
||||
manager.preview.tool.render();
|
||||
|
@ -66,6 +66,7 @@ const initialState: CanvasV2State = {
|
||||
eraser: {
|
||||
width: 50,
|
||||
},
|
||||
isTransforming: false,
|
||||
},
|
||||
bbox: {
|
||||
rect: { x: 0, y: 0, width: 512, height: 512 },
|
||||
@ -194,7 +195,6 @@ export const {
|
||||
allEntitiesDeleted,
|
||||
clipToBboxChanged,
|
||||
canvasReset,
|
||||
toolIsTransformingChanged,
|
||||
// bbox
|
||||
bboxChanged,
|
||||
bboxScaledSizeChanged,
|
||||
@ -226,6 +226,7 @@ export const {
|
||||
layerBrushLineAdded,
|
||||
layerEraserLineAdded,
|
||||
layerRectShapeAdded,
|
||||
layerRasterized,
|
||||
// IP Adapters
|
||||
ipaAdded,
|
||||
ipaRecalled,
|
||||
@ -396,3 +397,6 @@ export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionReque
|
||||
export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
|
||||
`${canvasV2Slice.name}/sessionStagingAreaImageAccepted`
|
||||
);
|
||||
export const transformationApplied = createAction<CanvasEntityIdentifier>(
|
||||
`${canvasV2Slice.name}/transformationApplied`
|
||||
);
|
||||
|
@ -34,8 +34,6 @@ export const layersReducers = {
|
||||
id,
|
||||
type: 'layer',
|
||||
isEnabled: true,
|
||||
bbox: null,
|
||||
bboxNeedsUpdate: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
@ -57,8 +55,6 @@ export const layersReducers = {
|
||||
id,
|
||||
type: 'layer',
|
||||
isEnabled: true,
|
||||
bbox: null,
|
||||
bboxNeedsUpdate: true,
|
||||
objects: [imageObject],
|
||||
opacity: 1,
|
||||
position: { x: position.x + offsetX, y: position.y + offsetY },
|
||||
@ -100,8 +96,6 @@ export const layersReducers = {
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
layer.bbox = bbox;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
if (bbox === null) {
|
||||
// TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers
|
||||
// doesn't work - always returns null
|
||||
@ -116,8 +110,6 @@ export const layersReducers = {
|
||||
}
|
||||
layer.isEnabled = true;
|
||||
layer.objects = [];
|
||||
layer.bbox = null;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
state.layers.imageCache = null;
|
||||
layer.position = { x: 0, y: 0 };
|
||||
},
|
||||
@ -183,7 +175,6 @@ export const layersReducers = {
|
||||
}
|
||||
|
||||
layer.objects.push(brushLine);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: EraserLine }>) => {
|
||||
@ -194,7 +185,6 @@ export const layersReducers = {
|
||||
}
|
||||
|
||||
layer.objects.push(eraserLine);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
layerRectShapeAdded: (state, action: PayloadAction<{ id: string; rectShape: RectShape }>) => {
|
||||
@ -205,7 +195,6 @@ export const layersReducers = {
|
||||
}
|
||||
|
||||
layer.objects.push(rectShape);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
layerScaled: (state, action: PayloadAction<ScaleChangedArg>) => {
|
||||
@ -235,7 +224,6 @@ export const layersReducers = {
|
||||
}
|
||||
layer.position.x = Math.round(position.x);
|
||||
layer.position.y = Math.round(position.y);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
layerImageAdded: {
|
||||
@ -254,7 +242,6 @@ export const layersReducers = {
|
||||
imageObject.y = pos.y;
|
||||
}
|
||||
layer.objects.push(imageObject);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
prepare: (payload: ImageObjectAddedArg & { pos?: { x: number; y: number } }) => ({
|
||||
@ -265,6 +252,16 @@ export const layersReducers = {
|
||||
const { imageDTO } = action.payload;
|
||||
state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||
},
|
||||
layerRasterized: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO; position: Coordinate }>) => {
|
||||
const { id, imageDTO, position } = action.payload;
|
||||
const layer = selectLayer(state, id);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
layer.objects = [imageDTOToImageObject(id, uuidv4(), imageDTO)];
|
||||
layer.position = position;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
} satisfies SliceCaseReducers<CanvasV2State>;
|
||||
|
||||
const scalePoints = (points: number[], scaleX: number, scaleY: number) => {
|
||||
|
@ -20,7 +20,4 @@ export const toolReducers = {
|
||||
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
|
||||
state.tool.selectedBuffer = action.payload;
|
||||
},
|
||||
toolIsTransformingChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.tool.isTransforming = action.payload;
|
||||
},
|
||||
} satisfies SliceCaseReducers<CanvasV2State>;
|
||||
|
@ -579,8 +579,6 @@ export const zLayerEntity = z.object({
|
||||
type: z.literal('layer'),
|
||||
isEnabled: z.boolean(),
|
||||
position: zCoordinate,
|
||||
bbox: zRect.nullable(),
|
||||
bboxNeedsUpdate: z.boolean(),
|
||||
opacity: zOpacity,
|
||||
objects: z.array(zRenderableObject),
|
||||
});
|
||||
@ -850,7 +848,6 @@ export type CanvasV2State = {
|
||||
brush: { width: number };
|
||||
eraser: { width: number };
|
||||
fill: RgbaColor;
|
||||
isTransforming: boolean;
|
||||
};
|
||||
settings: {
|
||||
imageSmoothing: boolean;
|
||||
|
@ -590,11 +590,14 @@ export const uploadImage = async (
|
||||
blob: Blob,
|
||||
fileName: string,
|
||||
image_category: ImageCategory,
|
||||
is_intermediate: boolean
|
||||
is_intermediate: boolean,
|
||||
crop_visible: boolean = false
|
||||
): Promise<ImageDTO> => {
|
||||
const { dispatch } = getStore();
|
||||
const file = new File([blob], fileName, { type: 'image/png' });
|
||||
const req = dispatch(imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate }));
|
||||
const req = dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate, crop_visible })
|
||||
);
|
||||
req.reset();
|
||||
return await req.unwrap();
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user