feat(ui): wip transform mode

This commit is contained in:
psychedelicious 2024-07-29 23:51:03 +10:00
parent 65353ac1e1
commit 7f8a1d8d20
14 changed files with 497 additions and 301 deletions

View File

@ -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;

View File

@ -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">

View File

@ -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;

View File

@ -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>
</>
);
}

View File

@ -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

View File

@ -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

View File

@ -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,
};
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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`
);

View File

@ -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) => {

View File

@ -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>;

View File

@ -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;

View File

@ -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();
};