mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): transformable layers
This commit is contained in:
parent
6a10d31b19
commit
49371ddec9
@ -13,6 +13,7 @@ import type {
|
||||
Rect,
|
||||
RectShapeAddedArg,
|
||||
RgbaColor,
|
||||
ScaleChangedArg,
|
||||
StageAttrs,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@ -63,6 +64,8 @@ export type StateApi = {
|
||||
onBrushWidthChanged: (size: number) => void;
|
||||
onEraserWidthChanged: (size: number) => void;
|
||||
getMaskOpacity: () => number;
|
||||
getIsSelected: (id: string) => boolean;
|
||||
onScaleChanged: (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => void;
|
||||
onPosChanged: (arg: PosChangedArg, entityType: CanvasEntity['type']) => void;
|
||||
onBboxTransformed: (bbox: Rect) => void;
|
||||
getShiftKey: () => boolean;
|
||||
@ -155,9 +158,8 @@ export class KonvaNodeManager {
|
||||
this.controlAdapters = new Map();
|
||||
}
|
||||
|
||||
renderLayers() {
|
||||
async renderLayers() {
|
||||
const { entities } = this.stateApi.getLayersState();
|
||||
const toolState = this.stateApi.getToolState();
|
||||
|
||||
for (const canvasLayer of this.layers.values()) {
|
||||
if (!entities.find((l) => l.id === canvasLayer.id)) {
|
||||
@ -169,11 +171,11 @@ export class KonvaNodeManager {
|
||||
for (const entity of entities) {
|
||||
let adapter = this.layers.get(entity.id);
|
||||
if (!adapter) {
|
||||
adapter = new CanvasLayer(entity, this.stateApi.onPosChanged);
|
||||
adapter = new CanvasLayer(entity, this);
|
||||
this.layers.set(adapter.id, adapter);
|
||||
this.stage.add(adapter.layer);
|
||||
}
|
||||
adapter.render(entity, toolState.selected);
|
||||
await adapter.render(entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,8 +43,7 @@ export class CanvasControlAdapter {
|
||||
const filters = entity.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [];
|
||||
|
||||
if (!this.image) {
|
||||
this.image = await new KonvaImage({
|
||||
imageObject,
|
||||
this.image = await new KonvaImage(imageObject, {
|
||||
onLoad: (konvaImage) => {
|
||||
konvaImage.filters(filters);
|
||||
konvaImage.cache();
|
||||
|
@ -79,7 +79,7 @@ export class CanvasInpaintMask {
|
||||
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||
|
||||
if (!brushLine) {
|
||||
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||
brushLine = new KonvaBrushLine(obj);
|
||||
this.objects.set(brushLine.id, brushLine);
|
||||
this.group.add(brushLine.konvaLineGroup);
|
||||
groupNeedsCache = true;
|
||||
@ -94,7 +94,7 @@ export class CanvasInpaintMask {
|
||||
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||
|
||||
if (!eraserLine) {
|
||||
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||
eraserLine = new KonvaEraserLine(obj);
|
||||
this.objects.set(eraserLine.id, eraserLine);
|
||||
this.group.add(eraserLine.konvaLineGroup);
|
||||
groupNeedsCache = true;
|
||||
@ -109,7 +109,7 @@ export class CanvasInpaintMask {
|
||||
assert(rect instanceof KonvaRect || rect === undefined);
|
||||
|
||||
if (!rect) {
|
||||
rect = new KonvaRect({ rectShape: obj });
|
||||
rect = new KonvaRect(obj);
|
||||
this.objects.set(rect.id, rect);
|
||||
this.group.add(rect.konvaRect);
|
||||
groupNeedsCache = true;
|
||||
@ -129,7 +129,6 @@ export class CanvasInpaintMask {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
|
||||
if (this.group.isCached()) {
|
||||
this.group.clearCache();
|
||||
|
@ -1,38 +1,53 @@
|
||||
import { getObjectGroupId } from 'features/controlLayers/konva/naming';
|
||||
import type { StateApi } from 'features/controlLayers/konva/nodeManager';
|
||||
import type { KonvaNodeManager } from 'features/controlLayers/konva/nodeManager';
|
||||
import { KonvaBrushLine, KonvaEraserLine, KonvaImage, KonvaRect } from 'features/controlLayers/konva/renderers/objects';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import type { LayerEntity, Tool } from 'features/controlLayers/store/types';
|
||||
import { isDrawingTool, type LayerEntity } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class CanvasLayer {
|
||||
id: string;
|
||||
manager: KonvaNodeManager;
|
||||
layer: Konva.Layer;
|
||||
group: Konva.Group;
|
||||
transformer: Konva.Transformer;
|
||||
objects: Map<string, KonvaBrushLine | KonvaEraserLine | KonvaRect | KonvaImage>;
|
||||
|
||||
constructor(entity: LayerEntity, onPosChanged: StateApi['onPosChanged']) {
|
||||
constructor(entity: LayerEntity, manager: KonvaNodeManager) {
|
||||
this.id = entity.id;
|
||||
|
||||
this.manager = manager;
|
||||
this.layer = new Konva.Layer({
|
||||
id: entity.id,
|
||||
draggable: true,
|
||||
dragDistance: 0,
|
||||
listening: false,
|
||||
});
|
||||
|
||||
// When a drag on the layer finishes, update the layer's position in state. During the drag, konva handles changing
|
||||
// the position - we do not need to call this on the `dragmove` event.
|
||||
this.layer.on('dragend', function (e) {
|
||||
onPosChanged({ id: entity.id, x: Math.floor(e.target.x()), y: Math.floor(e.target.y()) }, 'layer');
|
||||
});
|
||||
const group = new Konva.Group({
|
||||
this.group = new Konva.Group({
|
||||
id: getObjectGroupId(this.layer.id(), uuidv4()),
|
||||
listening: false,
|
||||
});
|
||||
this.group = group;
|
||||
this.layer.add(this.group);
|
||||
|
||||
this.transformer = new Konva.Transformer({
|
||||
shouldOverdrawWholeArea: true,
|
||||
draggable: true,
|
||||
dragDistance: 0,
|
||||
enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||
rotateEnabled: false,
|
||||
flipEnabled: false,
|
||||
});
|
||||
this.transformer.on('transformend', () => {
|
||||
this.manager.stateApi.onScaleChanged(
|
||||
{ id: this.id, scale: this.group.scaleX(), x: this.group.x(), y: this.group.y() },
|
||||
'layer'
|
||||
);
|
||||
});
|
||||
this.transformer.on('dragend', () => {
|
||||
this.manager.stateApi.onPosChanged({ id: this.id, x: this.group.x(), y: this.group.y() }, 'layer');
|
||||
});
|
||||
this.layer.add(this.transformer);
|
||||
|
||||
this.objects = new Map();
|
||||
}
|
||||
|
||||
@ -40,20 +55,24 @@ export class CanvasLayer {
|
||||
this.layer.destroy();
|
||||
}
|
||||
|
||||
async render(layerState: LayerEntity, selectedTool: Tool) {
|
||||
async render(layerState: LayerEntity) {
|
||||
// Update the layer's position and listening state
|
||||
this.layer.setAttrs({
|
||||
listening: selectedTool === 'move', // The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
||||
x: Math.floor(layerState.x),
|
||||
y: Math.floor(layerState.y),
|
||||
this.group.setAttrs({
|
||||
x: layerState.x,
|
||||
y: layerState.y,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
let didDraw = false;
|
||||
|
||||
const objectIds = layerState.objects.map(mapId);
|
||||
// Destroy any objects that are no longer in state
|
||||
for (const object of this.objects.values()) {
|
||||
if (!objectIds.includes(object.id)) {
|
||||
this.objects.delete(object.id);
|
||||
object.destroy();
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,45 +82,60 @@ export class CanvasLayer {
|
||||
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||
|
||||
if (!brushLine) {
|
||||
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||
brushLine = new KonvaBrushLine(obj);
|
||||
this.objects.set(brushLine.id, brushLine);
|
||||
this.group.add(brushLine.konvaLineGroup);
|
||||
}
|
||||
if (obj.points.length !== brushLine.konvaLine.points().length) {
|
||||
brushLine.konvaLine.points(obj.points);
|
||||
didDraw = true;
|
||||
} else {
|
||||
if (brushLine.update(obj)) {
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
} else if (obj.type === 'eraser_line') {
|
||||
let eraserLine = this.objects.get(obj.id);
|
||||
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||
|
||||
if (!eraserLine) {
|
||||
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||
eraserLine = new KonvaEraserLine(obj);
|
||||
this.objects.set(eraserLine.id, eraserLine);
|
||||
this.group.add(eraserLine.konvaLineGroup);
|
||||
}
|
||||
if (obj.points.length !== eraserLine.konvaLine.points().length) {
|
||||
eraserLine.konvaLine.points(obj.points);
|
||||
didDraw = true;
|
||||
} else {
|
||||
if (eraserLine.update(obj)) {
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
} else if (obj.type === 'rect_shape') {
|
||||
let rect = this.objects.get(obj.id);
|
||||
assert(rect instanceof KonvaRect || rect === undefined);
|
||||
|
||||
if (!rect) {
|
||||
rect = new KonvaRect({ rectShape: obj });
|
||||
rect = new KonvaRect(obj);
|
||||
this.objects.set(rect.id, rect);
|
||||
this.group.add(rect.konvaRect);
|
||||
didDraw = true;
|
||||
} else {
|
||||
if (rect.update(obj)) {
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
} else if (obj.type === 'image') {
|
||||
let image = this.objects.get(obj.id);
|
||||
assert(image instanceof KonvaImage || image === undefined);
|
||||
|
||||
if (!image) {
|
||||
image = await new KonvaImage({ imageObject: obj });
|
||||
image = await new KonvaImage(obj, {
|
||||
onLoad: () => {
|
||||
this.updateGroup(true);
|
||||
},
|
||||
});
|
||||
this.objects.set(image.id, image);
|
||||
this.group.add(image.konvaImageGroup);
|
||||
}
|
||||
if (image.imageName !== obj.image.name) {
|
||||
image.updateImageSource(obj.image.name);
|
||||
await image.updateImageSource(obj.image.name);
|
||||
} else {
|
||||
if (await image.update(obj)) {
|
||||
didDraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,22 +145,71 @@ export class CanvasLayer {
|
||||
this.layer.visible(layerState.isEnabled);
|
||||
}
|
||||
|
||||
// const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
|
||||
// if (layerState.bbox) {
|
||||
// const active = !layerState.bboxNeedsUpdate && layerState.isSelected && tool === 'move';
|
||||
// bboxRect.setAttrs({
|
||||
// visible: active,
|
||||
// listening: active,
|
||||
// x: layerState.bbox.x,
|
||||
// y: layerState.bbox.y,
|
||||
// width: layerState.bbox.width,
|
||||
// height: layerState.bbox.height,
|
||||
// stroke: layerState.isSelected ? BBOX_SELECTED_STROKE : '',
|
||||
// strokeWidth: 1 / stage.scaleX(),
|
||||
// });
|
||||
// } else {
|
||||
// bboxRect.visible(false);
|
||||
// }
|
||||
this.group.opacity(layerState.opacity);
|
||||
|
||||
// The layer only listens when using the move tool - otherwise the stage is handling mouse events
|
||||
this.updateGroup(didDraw);
|
||||
}
|
||||
|
||||
updateGroup(didDraw: boolean) {
|
||||
const isSelected = this.manager.stateApi.getIsSelected(this.id);
|
||||
const selectedTool = this.manager.stateApi.getToolState().selected;
|
||||
|
||||
if (this.objects.size === 0) {
|
||||
// If the layer is totally empty, reset the cache and bail out.
|
||||
this.layer.listening(false);
|
||||
this.transformer.nodes([]);
|
||||
if (this.group.isCached()) {
|
||||
this.group.clearCache();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && 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.group.isCached() || didDraw) {
|
||||
this.group.cache();
|
||||
}
|
||||
// Activate the transformer
|
||||
this.layer.listening(true);
|
||||
this.transformer.nodes([this.group]);
|
||||
this.transformer.forceUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && selectedTool !== 'move') {
|
||||
// If the layer is selected but not using the move tool, we don't want the layer to be listening.
|
||||
this.layer.listening(false);
|
||||
// The transformer also does not need to be active.
|
||||
this.transformer.nodes([]);
|
||||
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.group.isCached()) {
|
||||
this.group.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.group.isCached() || didDraw) {
|
||||
this.group.cache();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSelected) {
|
||||
// Unselected layers should not be listening
|
||||
this.layer.listening(false);
|
||||
// The transformer also does not need to be active.
|
||||
this.transformer.nodes([]);
|
||||
// Update the layer's cache if it's not already cached or we drew to it.
|
||||
if (!this.group.isCached() || didDraw) {
|
||||
this.group.cache();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ export class KonvaBrushLine {
|
||||
id: string;
|
||||
konvaLineGroup: Konva.Group;
|
||||
konvaLine: Konva.Line;
|
||||
lastBrushLine: BrushLine;
|
||||
|
||||
constructor(arg: { brushLine: BrushLine }) {
|
||||
const { brushLine } = arg;
|
||||
const { id, strokeWidth, clip, color } = brushLine;
|
||||
constructor(brushLine: BrushLine) {
|
||||
const { id, strokeWidth, clip, color, points } = brushLine;
|
||||
this.id = id;
|
||||
this.konvaLineGroup = new Konva.Group({
|
||||
clip,
|
||||
@ -46,8 +46,26 @@ export class KonvaBrushLine {
|
||||
lineJoin: 'round',
|
||||
globalCompositeOperation: 'source-over',
|
||||
stroke: rgbaColorToString(color),
|
||||
points,
|
||||
});
|
||||
this.konvaLineGroup.add(this.konvaLine);
|
||||
this.lastBrushLine = brushLine;
|
||||
}
|
||||
|
||||
update(brushLine: BrushLine, force?: boolean): boolean {
|
||||
if (this.lastBrushLine !== brushLine || force) {
|
||||
const { points, color, clip, strokeWidth } = brushLine;
|
||||
this.konvaLine.setAttrs({
|
||||
points,
|
||||
stroke: rgbaColorToString(color),
|
||||
clip,
|
||||
strokeWidth,
|
||||
});
|
||||
this.lastBrushLine = brushLine;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -59,10 +77,10 @@ export class KonvaEraserLine {
|
||||
id: string;
|
||||
konvaLineGroup: Konva.Group;
|
||||
konvaLine: Konva.Line;
|
||||
lastEraserLine: EraserLine;
|
||||
|
||||
constructor(arg: { eraserLine: EraserLine }) {
|
||||
const { eraserLine } = arg;
|
||||
const { id, strokeWidth, clip } = eraserLine;
|
||||
constructor(eraserLine: EraserLine) {
|
||||
const { id, strokeWidth, clip, points } = eraserLine;
|
||||
this.id = id;
|
||||
this.konvaLineGroup = new Konva.Group({
|
||||
clip,
|
||||
@ -78,8 +96,25 @@ export class KonvaEraserLine {
|
||||
lineJoin: 'round',
|
||||
globalCompositeOperation: 'destination-out',
|
||||
stroke: rgbaColorToString(RGBA_RED),
|
||||
points,
|
||||
});
|
||||
this.konvaLineGroup.add(this.konvaLine);
|
||||
this.lastEraserLine = eraserLine;
|
||||
}
|
||||
|
||||
update(eraserLine: EraserLine, force?: boolean): boolean {
|
||||
if (this.lastEraserLine !== eraserLine || force) {
|
||||
const { points, clip, strokeWidth } = eraserLine;
|
||||
this.konvaLine.setAttrs({
|
||||
points,
|
||||
clip,
|
||||
strokeWidth,
|
||||
});
|
||||
this.lastEraserLine = eraserLine;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -90,9 +125,9 @@ export class KonvaEraserLine {
|
||||
export class KonvaRect {
|
||||
id: string;
|
||||
konvaRect: Konva.Rect;
|
||||
lastRectShape: RectShape;
|
||||
|
||||
constructor(arg: { rectShape: RectShape }) {
|
||||
const { rectShape } = arg;
|
||||
constructor(rectShape: RectShape) {
|
||||
const { id, x, y, width, height } = rectShape;
|
||||
this.id = id;
|
||||
const konvaRect = new Konva.Rect({
|
||||
@ -105,6 +140,24 @@ export class KonvaRect {
|
||||
fill: rgbaColorToString(rectShape.color),
|
||||
});
|
||||
this.konvaRect = konvaRect;
|
||||
this.lastRectShape = rectShape;
|
||||
}
|
||||
|
||||
update(rectShape: RectShape, force?: boolean): boolean {
|
||||
if (this.lastRectShape !== rectShape || force) {
|
||||
const { x, y, width, height, color } = rectShape;
|
||||
this.konvaRect.setAttrs({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
fill: rgbaColorToString(color),
|
||||
});
|
||||
this.lastRectShape = rectShape;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -126,15 +179,18 @@ export class KonvaImage {
|
||||
onLoading: () => void;
|
||||
onLoad: (imageName: string, imageEl: HTMLImageElement) => void;
|
||||
onError: () => void;
|
||||
lastImageObject: ImageObject;
|
||||
|
||||
constructor(arg: {
|
||||
imageObject: ImageObject;
|
||||
getImageDTO?: (imageName: string) => Promise<ImageDTO | null>;
|
||||
onLoading?: () => void;
|
||||
onLoad?: (konvaImage: Konva.Image) => void;
|
||||
onError?: () => void;
|
||||
}) {
|
||||
const { imageObject, getImageDTO, onLoading, onLoad, onError } = arg;
|
||||
constructor(
|
||||
imageObject: ImageObject,
|
||||
options: {
|
||||
getImageDTO?: (imageName: string) => Promise<ImageDTO | null>;
|
||||
onLoading?: () => void;
|
||||
onLoad?: (konvaImage: Konva.Image) => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
) {
|
||||
const { getImageDTO, onLoading, onLoad, onError } = options;
|
||||
const { id, width, height, x, y } = imageObject;
|
||||
this.konvaImageGroup = new Konva.Group({ id, listening: false, x, y });
|
||||
this.konvaPlaceholderGroup = new Konva.Group({ listening: false });
|
||||
@ -188,6 +244,8 @@ export class KonvaImage {
|
||||
id: this.id,
|
||||
listening: false,
|
||||
image: imageEl,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
this.konvaImageGroup.add(this.konvaImage);
|
||||
}
|
||||
@ -213,6 +271,7 @@ export class KonvaImage {
|
||||
onError();
|
||||
}
|
||||
};
|
||||
this.lastImageObject = imageObject;
|
||||
}
|
||||
|
||||
async updateImageSource(imageName: string) {
|
||||
@ -238,6 +297,22 @@ export class KonvaImage {
|
||||
}
|
||||
}
|
||||
|
||||
async update(imageObject: ImageObject, force?: boolean): Promise<boolean> {
|
||||
if (this.lastImageObject !== imageObject || force) {
|
||||
const { width, height, x, y, image } = imageObject;
|
||||
if (this.lastImageObject.image.name !== image.name || force) {
|
||||
await this.updateImageSource(image.name);
|
||||
}
|
||||
this.konvaImage?.setAttrs({ x, y, width, height });
|
||||
this.konvaPlaceholderRect.setAttrs({ width, height });
|
||||
this.konvaPlaceholderText.setAttrs({ width, height, fontSize: width / 16 });
|
||||
this.lastImageObject = imageObject;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.konvaImageGroup.destroy();
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ export class CanvasRegion {
|
||||
assert(brushLine instanceof KonvaBrushLine || brushLine === undefined);
|
||||
|
||||
if (!brushLine) {
|
||||
brushLine = new KonvaBrushLine({ brushLine: obj });
|
||||
brushLine = new KonvaBrushLine(obj);
|
||||
this.objects.set(brushLine.id, brushLine);
|
||||
this.group.add(brushLine.konvaLineGroup);
|
||||
groupNeedsCache = true;
|
||||
@ -94,7 +94,7 @@ export class CanvasRegion {
|
||||
assert(eraserLine instanceof KonvaEraserLine || eraserLine === undefined);
|
||||
|
||||
if (!eraserLine) {
|
||||
eraserLine = new KonvaEraserLine({ eraserLine: obj });
|
||||
eraserLine = new KonvaEraserLine(obj);
|
||||
this.objects.set(eraserLine.id, eraserLine);
|
||||
this.group.add(eraserLine.konvaLineGroup);
|
||||
groupNeedsCache = true;
|
||||
@ -109,7 +109,7 @@ export class CanvasRegion {
|
||||
assert(rect instanceof KonvaRect || rect === undefined);
|
||||
|
||||
if (!rect) {
|
||||
rect = new KonvaRect({ rectShape: obj });
|
||||
rect = new KonvaRect(obj);
|
||||
this.objects.set(rect.id, rect);
|
||||
this.group.add(rect.konvaRect);
|
||||
groupNeedsCache = true;
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
layerImageCacheChanged,
|
||||
layerLinePointAdded,
|
||||
layerRectAdded,
|
||||
layerScaled,
|
||||
layerTranslated,
|
||||
rgBboxChanged,
|
||||
rgBrushLineAdded,
|
||||
@ -48,6 +49,7 @@ import type {
|
||||
PointAddedToLineArg,
|
||||
PosChangedArg,
|
||||
RectShapeAddedArg,
|
||||
ScaleChangedArg,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
@ -90,7 +92,7 @@ export const initializeRenderer = (
|
||||
|
||||
// Set up callbacks for various events
|
||||
const onPosChanged = (arg: PosChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Position changed');
|
||||
logIfDebugging('onPosChanged');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerTranslated(arg));
|
||||
} else if (entityType === 'control_adapter') {
|
||||
@ -101,6 +103,12 @@ export const initializeRenderer = (
|
||||
dispatch(imTranslated(arg));
|
||||
}
|
||||
};
|
||||
const onScaleChanged = (arg: ScaleChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('onScaleChanged');
|
||||
if (entityType === 'layer') {
|
||||
dispatch(layerScaled(arg));
|
||||
}
|
||||
};
|
||||
const onBboxChanged = (arg: BboxChangedArg, entityType: CanvasEntity['type']) => {
|
||||
logIfDebugging('Entity bbox changed');
|
||||
if (entityType === 'layer') {
|
||||
@ -249,7 +257,12 @@ export const initializeRenderer = (
|
||||
const getInpaintMaskState = () => canvasV2.inpaintMask;
|
||||
const getMaskOpacity = () => canvasV2.settings.maskOpacity;
|
||||
const getStagingAreaState = () => canvasV2.stagingArea;
|
||||
const getIsSelected = (id: string) => getSelectedEntity()?.id === id;
|
||||
|
||||
// Read-only state, derived from nanostores
|
||||
const resetLastProgressEvent = () => {
|
||||
$lastProgressEvent.set(null);
|
||||
};
|
||||
// Read-write state, ephemeral interaction state
|
||||
let isDrawing = false;
|
||||
const getIsDrawing = () => isDrawing;
|
||||
@ -307,9 +320,8 @@ export const initializeRenderer = (
|
||||
getStagingAreaState,
|
||||
getShouldShowStagedImage: $shouldShowStagedImage.get,
|
||||
getLastProgressEvent: $lastProgressEvent.get,
|
||||
resetLastProgressEvent: () => {
|
||||
$lastProgressEvent.set(null);
|
||||
},
|
||||
resetLastProgressEvent,
|
||||
getIsSelected,
|
||||
|
||||
// Read-write state
|
||||
setTool,
|
||||
@ -340,6 +352,7 @@ export const initializeRenderer = (
|
||||
onRegionMaskImageCached,
|
||||
onInpaintMaskImageCached,
|
||||
onLayerImageCached,
|
||||
onScaleChanged,
|
||||
};
|
||||
|
||||
const manager = new KonvaNodeManager(stage, container, stateApi);
|
||||
@ -367,7 +380,8 @@ export const initializeRenderer = (
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
prevSelectedEntity?.id !== selectedEntity?.id
|
||||
) {
|
||||
logIfDebugging('Rendering layers');
|
||||
manager.renderLayers();
|
||||
@ -377,7 +391,8 @@ export const initializeRenderer = (
|
||||
isFirstRender ||
|
||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
prevSelectedEntity?.id !== selectedEntity?.id
|
||||
) {
|
||||
logIfDebugging('Rendering regions');
|
||||
manager.renderRegions();
|
||||
@ -387,13 +402,18 @@ export const initializeRenderer = (
|
||||
isFirstRender ||
|
||||
canvasV2.inpaintMask !== prevCanvasV2.inpaintMask ||
|
||||
canvasV2.settings.maskOpacity !== prevCanvasV2.settings.maskOpacity ||
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected
|
||||
canvasV2.tool.selected !== prevCanvasV2.tool.selected ||
|
||||
prevSelectedEntity?.id !== selectedEntity?.id
|
||||
) {
|
||||
logIfDebugging('Rendering inpaint mask');
|
||||
manager.renderInpaintMask();
|
||||
}
|
||||
|
||||
if (isFirstRender || canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities) {
|
||||
if (
|
||||
isFirstRender ||
|
||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
||||
prevSelectedEntity?.id !== selectedEntity?.id
|
||||
) {
|
||||
logIfDebugging('Rendering control adapters');
|
||||
manager.renderControlAdapters();
|
||||
}
|
||||
@ -427,7 +447,8 @@ export const initializeRenderer = (
|
||||
isFirstRender ||
|
||||
canvasV2.layers.entities !== prevCanvasV2.layers.entities ||
|
||||
canvasV2.controlAdapters.entities !== prevCanvasV2.controlAdapters.entities ||
|
||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities
|
||||
canvasV2.regions.entities !== prevCanvasV2.regions.entities ||
|
||||
prevSelectedEntity?.id !== selectedEntity?.id
|
||||
) {
|
||||
logIfDebugging('Arranging entities');
|
||||
manager.arrangeEntities();
|
||||
|
@ -215,6 +215,7 @@ export const {
|
||||
layerImageAdded,
|
||||
layerAllDeleted,
|
||||
layerImageCacheChanged,
|
||||
layerScaled,
|
||||
// IP Adapters
|
||||
ipaAdded,
|
||||
ipaRecalled,
|
||||
|
@ -172,6 +172,36 @@ export const layersReducers = {
|
||||
payload: { ...payload, lineId: uuidv4() },
|
||||
}),
|
||||
},
|
||||
layerScaled: (state, action: PayloadAction<{ id: string; scale: number; x: number; y: number }>) => {
|
||||
const { id, scale, x, y } = action.payload;
|
||||
const layer = selectLayer(state, id);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
for (const obj of layer.objects) {
|
||||
if (obj.type === 'brush_line') {
|
||||
obj.points = obj.points.map((point) => point * scale);
|
||||
obj.strokeWidth *= scale;
|
||||
} else if (obj.type === 'eraser_line') {
|
||||
obj.points = obj.points.map((point) => point * scale);
|
||||
obj.strokeWidth *= scale;
|
||||
} else if (obj.type === 'rect_shape') {
|
||||
obj.x *= scale;
|
||||
obj.y *= scale;
|
||||
obj.height *= scale;
|
||||
obj.width *= scale;
|
||||
} else if (obj.type === 'image') {
|
||||
obj.x *= scale;
|
||||
obj.y *= scale;
|
||||
obj.height *= scale;
|
||||
obj.width *= scale;
|
||||
}
|
||||
}
|
||||
layer.x = x;
|
||||
layer.y = y;
|
||||
layer.bboxNeedsUpdate = true;
|
||||
state.layers.imageCache = null;
|
||||
},
|
||||
layerEraserLineAdded: {
|
||||
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
|
||||
const { id, points, lineId, width, clip } = action.payload;
|
||||
|
@ -462,6 +462,9 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
||||
|
||||
const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']);
|
||||
export type Tool = z.infer<typeof zTool>;
|
||||
export function isDrawingTool(tool: Tool): tool is 'brush' | 'eraser' | 'rect' {
|
||||
return tool === 'brush' || tool === 'eraser' || tool === 'rect';
|
||||
}
|
||||
|
||||
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
||||
|
||||
@ -891,6 +894,7 @@ export type CanvasV2State = {
|
||||
|
||||
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };
|
||||
export type PosChangedArg = { id: string; x: number; y: number };
|
||||
export type ScaleChangedArg = { id: string; scale: number; x: number; y: number };
|
||||
export type BboxChangedArg = { id: string; bbox: Rect | null };
|
||||
export type EraserLineAddedArg = {
|
||||
id: string;
|
||||
|
Loading…
Reference in New Issue
Block a user