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
@ -5,6 +5,7 @@ import { BrushToolButton } from 'features/controlLayers/components/BrushToolButt
|
|||||||
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
import { EraserToolButton } from 'features/controlLayers/components/EraserToolButton';
|
||||||
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
import { MoveToolButton } from 'features/controlLayers/components/MoveToolButton';
|
||||||
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
import { RectToolButton } from 'features/controlLayers/components/RectToolButton';
|
||||||
|
import { TransformToolButton } from 'features/controlLayers/components/TransformToolButton';
|
||||||
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
import { ViewToolButton } from 'features/controlLayers/components/ViewToolButton';
|
||||||
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
||||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||||
@ -21,6 +22,7 @@ export const ToolChooser: React.FC = () => {
|
|||||||
<EraserToolButton />
|
<EraserToolButton />
|
||||||
<RectToolButton />
|
<RectToolButton />
|
||||||
<MoveToolButton />
|
<MoveToolButton />
|
||||||
|
<TransformToolButton />
|
||||||
<ViewToolButton />
|
<ViewToolButton />
|
||||||
<BboxToolButton />
|
<BboxToolButton />
|
||||||
</ButtonGroup>
|
</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 NAME_PREFIX = 'layer';
|
||||||
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
|
static LAYER_NAME = `${CanvasLayer.NAME_PREFIX}_layer`;
|
||||||
static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`;
|
static TRANSFORMER_NAME = `${CanvasLayer.NAME_PREFIX}_transformer`;
|
||||||
|
static INTERACTION_RECT_NAME = `${CanvasLayer.NAME_PREFIX}_interaction-rect`;
|
||||||
static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_group`;
|
||||||
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
|
static OBJECT_GROUP_NAME = `${CanvasLayer.NAME_PREFIX}_object-group`;
|
||||||
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
|
static BBOX_NAME = `${CanvasLayer.NAME_PREFIX}_bbox`;
|
||||||
@ -26,13 +27,15 @@ export class CanvasLayer {
|
|||||||
|
|
||||||
konva: {
|
konva: {
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
bbox: Konva.Rect;
|
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
|
bbox: Konva.Rect;
|
||||||
|
|
||||||
objectGroup: Konva.Group;
|
objectGroup: Konva.Group;
|
||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
|
interactionRect: Konva.Rect;
|
||||||
};
|
};
|
||||||
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
|
objects: Map<string, CanvasBrushLine | CanvasEraserLine | CanvasRect | CanvasImage>;
|
||||||
bbox: Rect | null;
|
bbox: Rect;
|
||||||
|
|
||||||
getBbox = debounce(this._getBbox, 300);
|
getBbox = debounce(this._getBbox, 300);
|
||||||
|
|
||||||
@ -41,38 +44,63 @@ export class CanvasLayer {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.konva = {
|
this.konva = {
|
||||||
layer: new Konva.Layer({ id: this.id, name: CanvasLayer.LAYER_NAME, listening: false }),
|
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({
|
bbox: new Konva.Rect({
|
||||||
listening: false,
|
listening: false,
|
||||||
|
draggable: false,
|
||||||
name: CanvasLayer.BBOX_NAME,
|
name: CanvasLayer.BBOX_NAME,
|
||||||
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
|
stroke: 'hsl(200deg 76% 59%)', // invokeBlue.400
|
||||||
fill: '',
|
|
||||||
perfectDrawEnabled: false,
|
perfectDrawEnabled: false,
|
||||||
strokeHitEnabled: false,
|
strokeHitEnabled: false,
|
||||||
}),
|
}),
|
||||||
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
|
objectGroup: new Konva.Group({ name: CanvasLayer.OBJECT_GROUP_NAME, listening: false }),
|
||||||
transformer: new Konva.Transformer({
|
transformer: new Konva.Transformer({
|
||||||
name: CanvasLayer.TRANSFORMER_NAME,
|
name: CanvasLayer.TRANSFORMER_NAME,
|
||||||
shouldOverdrawWholeArea: true,
|
|
||||||
draggable: false,
|
draggable: false,
|
||||||
dragDistance: 0,
|
|
||||||
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
rotateEnabled: false,
|
rotateEnabled: false,
|
||||||
flipEnabled: false,
|
flipEnabled: false,
|
||||||
listening: 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.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', () => {
|
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(
|
this.manager.stateApi.onScaleChanged(
|
||||||
{
|
{
|
||||||
id: this.id,
|
id: this.id,
|
||||||
scale: this.konva.group.scaleX(),
|
scale: this.konva.objectGroup.scaleX(),
|
||||||
position: { x: this.konva.group.x(), y: this.konva.group.y() },
|
position: { x: this.konva.objectGroup.x(), y: this.konva.objectGroup.y() },
|
||||||
},
|
},
|
||||||
'layer'
|
'layer'
|
||||||
);
|
);
|
||||||
@ -83,12 +111,15 @@ export class CanvasLayer {
|
|||||||
'layer'
|
'layer'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.konva.layer.add(this.konva.transformer);
|
|
||||||
|
|
||||||
this.objects = new Map();
|
this.objects = new Map();
|
||||||
this.drawingBuffer = null;
|
this.drawingBuffer = null;
|
||||||
this.state = state;
|
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 {
|
destroy(): void {
|
||||||
@ -235,48 +266,50 @@ export class CanvasLayer {
|
|||||||
if (this.objects.size > 0) {
|
if (this.objects.size > 0) {
|
||||||
this.getBbox();
|
this.getBbox();
|
||||||
} else {
|
} else {
|
||||||
this.bbox = null;
|
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||||
this.renderBbox();
|
this.renderBbox();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.konva.layer.visible(true);
|
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 isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||||
|
|
||||||
const transformerListening = selectedTool === 'transform' && isSelected;
|
const isTransforming = selectedTool === 'transform' && isSelected;
|
||||||
const bboxListening = selectedTool === 'move' && isSelected;
|
const isMoving = selectedTool === 'move' && isSelected;
|
||||||
|
|
||||||
this.konva.layer.listening(transformerListening || bboxListening);
|
this.konva.layer.listening(isTransforming || isMoving);
|
||||||
this.konva.transformer.listening(transformerListening);
|
this.konva.transformer.listening(isTransforming);
|
||||||
this.konva.group.listening(bboxListening);
|
this.konva.bbox.visible(isMoving);
|
||||||
this.konva.bbox.listening(bboxListening);
|
this.konva.interactionRect.listening(isMoving);
|
||||||
|
|
||||||
if (this.objects.size === 0) {
|
if (this.objects.size === 0) {
|
||||||
// If the layer is totally empty, reset the cache and bail out.
|
// If the layer is totally empty, reset the cache and bail out.
|
||||||
this.konva.transformer.nodes([]);
|
this.konva.transformer.nodes([]);
|
||||||
if (this.konva.group.isCached()) {
|
if (this.konva.objectGroup.isCached()) {
|
||||||
this.konva.group.clearCache();
|
this.konva.objectGroup.clearCache();
|
||||||
}
|
}
|
||||||
} else if (isSelected && selectedTool === 'transform') {
|
} else if (isSelected && selectedTool === 'transform') {
|
||||||
// When the layer is selected and being moved, we should always cache it.
|
// When the layer is selected and being moved, we should always cache it.
|
||||||
// We should update the cache if we drew to the layer.
|
// We should update the cache if we drew to the layer.
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||||
// this.konva.group.cache();
|
// this.konva.objectGroup.cache();
|
||||||
}
|
}
|
||||||
// Activate the transformer
|
// 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.forceUpdate();
|
||||||
this.konva.transformer.visible(true);
|
this.konva.transformer.visible(true);
|
||||||
} else if (selectedTool === 'move') {
|
} else if (selectedTool === 'move') {
|
||||||
// When the layer is selected and being moved, we should always cache it.
|
// When the layer is selected and being moved, we should always cache it.
|
||||||
// We should update the cache if we drew to the layer.
|
// We should update the cache if we drew to the layer.
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||||
// this.konva.group.cache();
|
// this.konva.objectGroup.cache();
|
||||||
}
|
}
|
||||||
// Activate the transformer
|
// 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.forceUpdate();
|
||||||
this.konva.transformer.visible(false);
|
this.konva.transformer.visible(false);
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
@ -286,14 +319,14 @@ export class CanvasLayer {
|
|||||||
if (isDrawingTool(selectedTool)) {
|
if (isDrawingTool(selectedTool)) {
|
||||||
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
// We are using a drawing tool (brush, eraser, rect). These tools change the layer's rendered appearance, so we
|
||||||
// should never be cached.
|
// should never be cached.
|
||||||
if (this.konva.group.isCached()) {
|
if (this.konva.objectGroup.isCached()) {
|
||||||
this.konva.group.clearCache();
|
this.konva.objectGroup.clearCache();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We are using a non-drawing tool (move, view, bbox), so we should cache the layer.
|
// 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.
|
// We should update the cache if we drew to the layer.
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||||
// this.konva.group.cache();
|
// this.konva.objectGroup.cache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!isSelected) {
|
} else if (!isSelected) {
|
||||||
@ -301,8 +334,8 @@ export class CanvasLayer {
|
|||||||
// The transformer also does not need to be active.
|
// The transformer also does not need to be active.
|
||||||
this.konva.transformer.nodes([]);
|
this.konva.transformer.nodes([]);
|
||||||
// Update the layer's cache if it's not already cached or we drew to it.
|
// Update the layer's cache if it's not already cached or we drew to it.
|
||||||
if (!this.konva.group.isCached() || didDraw) {
|
if (!this.konva.objectGroup.isCached() || didDraw) {
|
||||||
// this.konva.group.cache();
|
// this.konva.objectGroup.cache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,17 +343,33 @@ export class CanvasLayer {
|
|||||||
renderBbox() {
|
renderBbox() {
|
||||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
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.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(),
|
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() {
|
private _getBbox() {
|
||||||
if (this.objects.size === 0) {
|
if (this.objects.size === 0) {
|
||||||
this.bbox = null;
|
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||||
this.renderBbox();
|
this.renderBbox();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -338,7 +387,7 @@ export class CanvasLayer {
|
|||||||
|
|
||||||
if (!needsPixelBbox) {
|
if (!needsPixelBbox) {
|
||||||
if (rect.width === 0 || rect.height === 0) {
|
if (rect.width === 0 || rect.height === 0) {
|
||||||
this.bbox = null;
|
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||||
} else {
|
} else {
|
||||||
this.bbox = rect;
|
this.bbox = rect;
|
||||||
}
|
}
|
||||||
@ -370,13 +419,13 @@ export class CanvasLayer {
|
|||||||
if (extents) {
|
if (extents) {
|
||||||
const { minX, minY, maxX, maxY } = extents;
|
const { minX, minY, maxX, maxY } = extents;
|
||||||
this.bbox = {
|
this.bbox = {
|
||||||
x: minX + rect.x - Math.floor(this.konva.layer.x()),
|
x: rect.x + minX,
|
||||||
y: minY + rect.y - Math.floor(this.konva.layer.y()),
|
y: rect.y + minY,
|
||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.bbox = null;
|
this.bbox = CanvasLayer.DEFAULT_BBOX_RECT;
|
||||||
}
|
}
|
||||||
this.renderBbox();
|
this.renderBbox();
|
||||||
clone.destroy();
|
clone.destroy();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user