mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): move tool fixes, add transform tool
This commit is contained in:
parent
7bdfd3ef5f
commit
f405e472ea
invokeai/frontend/web/src/features/controlLayers
@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt
|
||||
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
||||
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
||||
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
||||
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
|
||||
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
||||
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||
@ -21,6 +22,7 @@ export const ToolChooser: React.FC = () => {
|
||||
<EraserToolButton />
|
||||
<RectToolButton />
|
||||
<MoveToolButton />
|
||||
<TransformToolButton />
|
||||
<ViewToolButton />
|
||||
<BboxToolButton />
|
||||
</ButtonGroup>
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { toolChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { memo, useCallback } 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 isSelected = useAppSelector((s) => s.canvasV2.tool.selected === 'transform');
|
||||
const isDisabled = useAppSelector(
|
||||
(s) => s.canvasV2.selectedEntityIdentifier === null || s.canvasV2.session.isStaging
|
||||
);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(toolChanged('transform'));
|
||||
}, [dispatch]);
|
||||
|
||||
useHotkeys(['ctrl+t', 'meta+t'], onClick, { enabled: !isDisabled }, [isDisabled, onClick]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('unifiedCanvas.transform')} (Ctrl+T)`}
|
||||
tooltip={`${t('unifiedCanvas.transform')} (Ctrl+T)`}
|
||||
icon={<PiResizeBold />}
|
||||
variant={isSelected ? 'solid' : 'outline'}
|
||||
onClick={onClick}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
TransformToolButton.displayName = 'TransformToolButton';
|
@ -14,6 +14,7 @@ export class CanvasLayer {
|
||||
static NAME_PREFIX = 'layer';
|
||||
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
|
||||
static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`;
|
||||
static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`;
|
||||
static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`;
|
||||
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
|
||||
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
|
||||
@ -26,13 +27,15 @@ export class CanvasLayer {
|
||||
|
||||
konva: {
|
||||
layer: Konva.Layer;
|
||||
bbox: Konva.Rect;
|
||||
group: Konva.Group;
|
||||
bbox: Konva.Rect;
|
||||
|
||||
objectGroup: Konva.Group;
|
||||
transformer: Konva.Transformer;
|
||||
interactionRect: Konva.Rect;
|
||||
};
|
||||
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
|
||||
bbox: Rect | null;
|
||||
bbox: Rect;
|
||||
|
||||
getBbox = debounce(this._getBbox, 300);
|
||||
|
||||
@ -41,38 +44,63 @@ export class CanvasLayer {
|
||||
this.manager = manager;
|
||||
this.konva = {
|
||||
layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }),
|
||||
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: false, draggable: true }),
|
||||
group: new Konva.Group({ name: CanvasLayer.GROUP_NAME, listening: true, draggable: true }),
|
||||
bbox: new Konva.Rect({
|
||||
listening: false,
|
||||
draggable: false,
|
||||
name: CanvasLayer.BBOX_NAME,
|
||||
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
|
||||
fill: '',
|
||||
perfectDrawEnabled: false,
|
||||
strokeHitEnabled: false,
|
||||
}),
|
||||
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
|
||||
transformer: new Konva.Transformer({
|
||||
name: CanvasLayer.TRANSFORMER_NAME,
|
||||
shouldOverdrawWholeArea: true,
|
||||
draggable: false,
|
||||
dragDistance: 0,
|
||||
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||
rotateEnabled: false,
|
||||
flipEnabled: false,
|
||||
listening: false,
|
||||
}),
|
||||
interactionRect: new Konva.Rect({
|
||||
name: CanvasLayer.INTERACTION_RECT_NAME,
|
||||
listening: false,
|
||||
draggable: false,
|
||||
fill: 'rgba(255,0,0,0.5)',
|
||||
}),
|
||||
};
|
||||
|
||||
this.konva.group.add(this.konva.objectGroup);
|
||||
this.konva.group.add(this.konva.bbox);
|
||||
this.konva.layer.add(this.konva.group);
|
||||
this.konva.layer.add(this.konva.transformer);
|
||||
this.konva.group.add(this.konva.objectGroup);
|
||||
this.konva.group.add(this.konva.interactionRect);
|
||||
this.konva.group.add(this.konva.bbox);
|
||||
|
||||
this.konva.transformer.on('transform', () => {
|
||||
console.log(this.konva.interactionRect.position());
|
||||
this.konva.objectGroup.setAttrs({
|
||||
scaleX: this.konva.interactionRect.scaleX(),
|
||||
scaleY: this.konva.interactionRect.scaleY(),
|
||||
// rotation: this.konva.interactionRect.rotation(),
|
||||
x: this.konva.interactionRect.x(),
|
||||
t: this.konva.interactionRect.y(),
|
||||
});
|
||||
});
|
||||
this.konva.transformer.on('transformend', () => {
|
||||
console.log(this.bbox);
|
||||
this.bbox = {
|
||||
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(),
|
||||
};
|
||||
console.log(this.bbox);
|
||||
this.renderBbox();
|
||||
this.manager.stateApi.onScaleChanged(
|
||||
{
|
||||
id: this.id,
|
||||
scale: this.konva.group.scaleX(),
|
||||
position: { x: this.konva.group.x(), y: this.konva.group.y() },
|
||||
scale: this.konva.objectGroup.scaleX(),
|
||||
position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
|
||||
},
|
||||
'layer'
|
||||
);
|
||||
@ -83,12 +111,15 @@ export class CanvasLayer {
|
||||
'layer'
|
||||
);
|
||||
});
|
||||
this.konva.layer.add(this.konva.transformer);
|
||||
|
||||
this.objects = new Map();
|
||||
this.drawingBuffer = null;
|
||||
this.state = state;
|
||||
this.bbox = null;
|
||||
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
}
|
||||
|
||||
private static get DEFAULT_BBOX_RECT() {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
@ -235,48 +266,50 @@ export class CanvasLayer {
|
||||
if (this.objects.size > 0) {
|
||||
this.getBbox();
|
||||
} else {
|
||||
this.bbox = null;
|
||||
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
this.renderBbox();
|
||||
}
|
||||
}
|
||||
|
||||
this.konva.layer.visible(true);
|
||||
this.konva.group.opacity(this.state.opacity);
|
||||
this.konva.objectGroup.opacity(this.state.opacity);
|
||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||
|
||||
const transformerListening = selectedTool === 'transform' && isSelected;
|
||||
const bboxListening = selectedTool === 'move' && isSelected;
|
||||
const isTransforming = selectedTool === 'transform' && isSelected;
|
||||
const isMoving = selectedTool === 'move' && isSelected;
|
||||
|
||||
this.konva.layer.listening(transformerListening || bboxListening);
|
||||
this.konva.transformer.listening(transformerListening);
|
||||
this.konva.group.listening(bboxListening);
|
||||
this.konva.bbox.listening(bboxListening);
|
||||
this.konva.layer.listening(isTransforming || isMoving);
|
||||
this.konva.transformer.listening(isTransforming);
|
||||
this.konva.bbox.visible(isMoving);
|
||||
this.konva.interactionRect.listening(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.group.isCached()) {
|
||||
this.konva.group.clearCache();
|
||||
if (this.konva.objectGroup.isCached()) {
|
||||
this.konva.objectGroup.clearCache();
|
||||
}
|
||||
} else if (isSelected && selectedTool === 'transform') {
|
||||
// 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.group.isCached() || didDraw) {
|
||||
// this.konva.group.cache();
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
// Activate the transformer
|
||||
this.konva.transformer.nodes([this.konva.group]);
|
||||
this.konva.transformer.nodes([this.konva.interactionRect]);
|
||||
this.konva.transformer.enabledAnchors(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
|
||||
this.konva.transformer.forceUpdate();
|
||||
this.konva.transformer.visible(true);
|
||||
} else if (selectedTool === '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.group.isCached() || didDraw) {
|
||||
// this.konva.group.cache();
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
// Activate the transformer
|
||||
this.konva.transformer.nodes([]);
|
||||
this.konva.transformer.nodes([this.konva.interactionRect]);
|
||||
this.konva.transformer.enabledAnchors([]);
|
||||
this.konva.transformer.forceUpdate();
|
||||
this.konva.transformer.visible(false);
|
||||
} else if (isSelected) {
|
||||
@ -286,14 +319,14 @@ export class CanvasLayer {
|
||||
if (isDrawingTool(selectedTool)) {
|
||||
// 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.group.isCached()) {
|
||||
this.konva.group.clearCache();
|
||||
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.group.isCached() || didDraw) {
|
||||
// this.konva.group.cache();
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
}
|
||||
} else if (!isSelected) {
|
||||
@ -301,8 +334,8 @@ export class CanvasLayer {
|
||||
// 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.group.isCached() || didDraw) {
|
||||
// this.konva.group.cache();
|
||||
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||
// this.konva.objectGroup.cache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -310,17 +343,33 @@ export class CanvasLayer {
|
||||
renderBbox() {
|
||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||
const hasBbox = this.bbox.width !== 0 && this.bbox.height !== 0;
|
||||
|
||||
this.konva.bbox.visible(hasBbox);
|
||||
this.konva.interactionRect.visible(hasBbox);
|
||||
|
||||
this.konva.bbox.setAttrs({
|
||||
...this.bbox,
|
||||
x: this.bbox.x,
|
||||
y: this.bbox.y,
|
||||
width: this.bbox.width,
|
||||
height: this.bbox.height,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
strokeWidth: 1 / this.manager.stage.scaleX(),
|
||||
visible: this.bbox !== null && selectedTool === 'move' && isSelected,
|
||||
});
|
||||
this.konva.interactionRect.setAttrs({
|
||||
x: this.bbox.x,
|
||||
y: this.bbox.y,
|
||||
width: this.bbox.width,
|
||||
height: this.bbox.height,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private _getBbox() {
|
||||
if (this.objects.size === 0) {
|
||||
this.bbox = null;
|
||||
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
this.renderBbox();
|
||||
return;
|
||||
}
|
||||
@ -338,7 +387,7 @@ export class CanvasLayer {
|
||||
|
||||
if (!needsPixelBbox) {
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
this.bbox = null;
|
||||
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
} else {
|
||||
this.bbox = rect;
|
||||
}
|
||||
@ -370,13 +419,13 @@ export class CanvasLayer {
|
||||
if (extents) {
|
||||
const { minX, minY, maxX, maxY } = extents;
|
||||
this.bbox = {
|
||||
x: minX + rect.x - Math.floor(this.konva.layer.x()),
|
||||
y: minY + rect.y - Math.floor(this.konva.layer.y()),
|
||||
x: rect.x + minX,
|
||||
y: rect.y + minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
} else {
|
||||
this.bbox = null;
|
||||
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||
}
|
||||
this.renderBbox();
|
||||
clone.destroy();
|
||||
|
Loading…
x
Reference in New Issue
Block a user