mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip transform mode 2
This commit is contained in:
parent
7f8a1d8d20
commit
7d4342bbff
@ -24,7 +24,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const l of canvasManager.layers.values()) {
|
for (const l of canvasManager.layers.values()) {
|
||||||
l.getBbox();
|
l.calculateBbox();
|
||||||
}
|
}
|
||||||
}, [canvasManager]);
|
}, [canvasManager]);
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import type { BrushLine } from 'features/controlLayers/store/types';
|
import type { BrushLine } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
@ -7,18 +8,25 @@ export class CanvasBrushLine {
|
|||||||
static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasBrushLine.NAME_PREFIX}_group`;
|
||||||
static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`;
|
static LINE_NAME = `${CanvasBrushLine.NAME_PREFIX}_line`;
|
||||||
|
|
||||||
private state: BrushLine;
|
state: BrushLine;
|
||||||
|
|
||||||
|
type = 'brush_line';
|
||||||
id: string;
|
id: string;
|
||||||
konva: {
|
konva: {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
line: Konva.Line;
|
line: Konva.Line;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(state: BrushLine) {
|
parent: CanvasLayer;
|
||||||
this.state = state;
|
|
||||||
const { id, strokeWidth, clip, color, points } = this.state;
|
constructor(state: BrushLine, parent: CanvasLayer) {
|
||||||
|
const { id, strokeWidth, clip, color, points } = state;
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
|
this.parent.log.trace(`Creating brush line ${this.id}`);
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
group: new Konva.Group({
|
group: new Konva.Group({
|
||||||
name: CanvasBrushLine.GROUP_NAME,
|
name: CanvasBrushLine.GROUP_NAME,
|
||||||
@ -46,6 +54,7 @@ export class CanvasBrushLine {
|
|||||||
|
|
||||||
async update(state: BrushLine, force?: boolean): Promise<boolean> {
|
async update(state: BrushLine, force?: boolean): Promise<boolean> {
|
||||||
if (force || this.state !== state) {
|
if (force || this.state !== state) {
|
||||||
|
this.parent.log.trace(`Updating brush line ${this.id}`);
|
||||||
const { points, color, clip, strokeWidth } = state;
|
const { points, color, clip, strokeWidth } = state;
|
||||||
this.konva.line.setAttrs({
|
this.konva.line.setAttrs({
|
||||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||||
@ -62,6 +71,7 @@ export class CanvasBrushLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.parent.log.trace(`Destroying brush line ${this.id}`);
|
||||||
this.konva.group.destroy();
|
this.konva.group.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import type { EraserLine } from 'features/controlLayers/store/types';
|
import type { EraserLine } from 'features/controlLayers/store/types';
|
||||||
import { RGBA_RED } from 'features/controlLayers/store/types';
|
import { RGBA_RED } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
@ -8,17 +9,25 @@ export class CanvasEraserLine {
|
|||||||
static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasEraserLine.NAME_PREFIX}_group`;
|
||||||
static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`;
|
static LINE_NAME = `${CanvasEraserLine.NAME_PREFIX}_line`;
|
||||||
|
|
||||||
private state: EraserLine;
|
state: EraserLine;
|
||||||
|
|
||||||
|
type = 'eraser_line';
|
||||||
id: string;
|
id: string;
|
||||||
konva: {
|
konva: {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
line: Konva.Line;
|
line: Konva.Line;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(state: EraserLine) {
|
parent: CanvasLayer;
|
||||||
|
|
||||||
|
constructor(state: EraserLine, parent: CanvasLayer) {
|
||||||
const { id, strokeWidth, clip, points } = state;
|
const { id, strokeWidth, clip, points } = state;
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
|
this.parent.log.trace(`Creating eraser line ${this.id}`);
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
group: new Konva.Group({
|
group: new Konva.Group({
|
||||||
name: CanvasEraserLine.GROUP_NAME,
|
name: CanvasEraserLine.GROUP_NAME,
|
||||||
@ -46,6 +55,7 @@ export class CanvasEraserLine {
|
|||||||
|
|
||||||
async update(state: EraserLine, force?: boolean): Promise<boolean> {
|
async update(state: EraserLine, force?: boolean): Promise<boolean> {
|
||||||
if (force || this.state !== state) {
|
if (force || this.state !== state) {
|
||||||
|
this.parent.log.trace(`Updating eraser line ${this.id}`);
|
||||||
const { points, clip, strokeWidth } = state;
|
const { points, clip, strokeWidth } = state;
|
||||||
this.konva.line.setAttrs({
|
this.konva.line.setAttrs({
|
||||||
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
// A line with only one point will not be rendered, so we duplicate the points to make it visible
|
||||||
@ -61,6 +71,7 @@ export class CanvasEraserLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.parent.log.trace(`Destroying eraser line ${this.id}`);
|
||||||
this.konva.group.destroy();
|
this.konva.group.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import { FILTER_MAP } from 'features/controlLayers/konva/filters';
|
import { FILTER_MAP } from 'features/controlLayers/konva/filters';
|
||||||
import { loadImage } from 'features/controlLayers/konva/util';
|
import { loadImage } from 'features/controlLayers/konva/util';
|
||||||
import type { ImageObject } from 'features/controlLayers/store/types';
|
import type { ImageObject } from 'features/controlLayers/store/types';
|
||||||
@ -14,7 +15,9 @@ export class CanvasImage {
|
|||||||
static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`;
|
static PLACEHOLDER_RECT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-rect`;
|
||||||
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`;
|
static PLACEHOLDER_TEXT_NAME = `${CanvasImage.NAME_PREFIX}_placeholder-text`;
|
||||||
|
|
||||||
private state: ImageObject;
|
state: ImageObject;
|
||||||
|
|
||||||
|
type = 'image';
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
konva: {
|
konva: {
|
||||||
@ -26,8 +29,15 @@ export class CanvasImage {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
|
|
||||||
constructor(state: ImageObject) {
|
parent: CanvasLayer;
|
||||||
|
|
||||||
|
constructor(state: ImageObject, parent: CanvasLayer) {
|
||||||
const { id, width, height, x, y } = state;
|
const { id, width, height, x, y } = state;
|
||||||
|
this.id = id;
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
|
this.parent.log.trace(`Creating image ${this.id}`);
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
|
group: new Konva.Group({ name: CanvasImage.GROUP_NAME, listening: false, x, y }),
|
||||||
placeholder: {
|
placeholder: {
|
||||||
@ -58,7 +68,6 @@ export class CanvasImage {
|
|||||||
this.konva.placeholder.group.add(this.konva.placeholder.text);
|
this.konva.placeholder.group.add(this.konva.placeholder.text);
|
||||||
this.konva.group.add(this.konva.placeholder.group);
|
this.konva.group.add(this.konva.placeholder.group);
|
||||||
|
|
||||||
this.id = id;
|
|
||||||
this.imageName = null;
|
this.imageName = null;
|
||||||
this.image = null;
|
this.image = null;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@ -68,6 +77,8 @@ export class CanvasImage {
|
|||||||
|
|
||||||
async updateImageSource(imageName: string) {
|
async updateImageSource(imageName: string) {
|
||||||
try {
|
try {
|
||||||
|
this.parent.log.trace(`Updating image source ${this.id}`);
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.konva.group.visible(true);
|
this.konva.group.visible(true);
|
||||||
|
|
||||||
@ -119,6 +130,8 @@ export class CanvasImage {
|
|||||||
|
|
||||||
async update(state: ImageObject, force?: boolean): Promise<boolean> {
|
async update(state: ImageObject, force?: boolean): Promise<boolean> {
|
||||||
if (this.state !== state || force) {
|
if (this.state !== state || force) {
|
||||||
|
this.parent.log.trace(`Updating image ${this.id}`);
|
||||||
|
|
||||||
const { width, height, x, y, image, filters } = state;
|
const { width, height, x, y, image, filters } = state;
|
||||||
if (this.state.image.name !== image.name || force) {
|
if (this.state.image.name !== image.name || force) {
|
||||||
await this.updateImageSource(image.name);
|
await this.updateImageSource(image.name);
|
||||||
@ -141,6 +154,7 @@ export class CanvasImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.parent.log.trace(`Destroying image ${this.id}`);
|
||||||
this.konva.group.destroy();
|
this.konva.group.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { CanvasEraserLine } from 'features/controlLayers/konva/CanvasEraserLine'
|
|||||||
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
|
import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
|
||||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
|
import { CanvasRect } from 'features/controlLayers/konva/CanvasRect';
|
||||||
|
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
||||||
import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
|
import { konvaNodeToBlob, mapId, previewBlob } from 'features/controlLayers/konva/util';
|
||||||
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
|
import { layerRasterized } from 'features/controlLayers/store/canvasV2Slice';
|
||||||
import type {
|
import type {
|
||||||
@ -19,6 +20,7 @@ import { debounce, get } from 'lodash-es';
|
|||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
import { uploadImage } from 'services/api/endpoints/images';
|
import { uploadImage } from 'services/api/endpoints/images';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export class CanvasLayer {
|
export class CanvasLayer {
|
||||||
static NAME_PREFIX = 'layer';
|
static NAME_PREFIX = 'layer';
|
||||||
@ -82,7 +84,7 @@ export class CanvasLayer {
|
|||||||
name: CanvasLayer.INTERACTION_RECT_NAME,
|
name: CanvasLayer.INTERACTION_RECT_NAME,
|
||||||
listening: false,
|
listening: false,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
fill: 'rgba(255,0,0,0.5)',
|
// fill: 'rgba(255,0,0,0.5)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,6 +152,7 @@ export class CanvasLayer {
|
|||||||
scaleY: this.konva.interactionRect.scaleY(),
|
scaleY: this.konva.interactionRect.scaleY(),
|
||||||
rotation: this.konva.interactionRect.rotation(),
|
rotation: this.konva.interactionRect.rotation(),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('objectGroup', {
|
console.log('objectGroup', {
|
||||||
x: this.konva.objectGroup.x(),
|
x: this.konva.objectGroup.x(),
|
||||||
y: this.konva.objectGroup.y(),
|
y: this.konva.objectGroup.y(),
|
||||||
@ -241,7 +244,6 @@ export class CanvasLayer {
|
|||||||
getDrawingBuffer() {
|
getDrawingBuffer() {
|
||||||
return this.drawingBuffer;
|
return this.drawingBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
|
async setDrawingBuffer(obj: BrushLine | EraserLine | RectShape | null) {
|
||||||
if (obj) {
|
if (obj) {
|
||||||
this.drawingBuffer = obj;
|
this.drawingBuffer = obj;
|
||||||
@ -258,11 +260,17 @@ export class CanvasLayer {
|
|||||||
const drawingBuffer = this.drawingBuffer;
|
const drawingBuffer = this.drawingBuffer;
|
||||||
this.setDrawingBuffer(null);
|
this.setDrawingBuffer(null);
|
||||||
|
|
||||||
|
// We need to give the objects a fresh ID else they will be considered the same object when they are re-rendered as
|
||||||
|
// a non-buffer object, and we won't trigger things like bbox calculation
|
||||||
|
|
||||||
if (drawingBuffer.type === 'brush_line') {
|
if (drawingBuffer.type === 'brush_line') {
|
||||||
|
drawingBuffer.id = getBrushLineId(this.id, uuidv4());
|
||||||
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
|
this.manager.stateApi.onBrushLineAdded({ id: this.id, brushLine: drawingBuffer }, 'layer');
|
||||||
} else if (drawingBuffer.type === 'eraser_line') {
|
} else if (drawingBuffer.type === 'eraser_line') {
|
||||||
|
drawingBuffer.id = getEraserLineId(this.id, uuidv4());
|
||||||
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
|
this.manager.stateApi.onEraserLineAdded({ id: this.id, eraserLine: drawingBuffer }, 'layer');
|
||||||
} else if (drawingBuffer.type === 'rect_shape') {
|
} else if (drawingBuffer.type === 'rect_shape') {
|
||||||
|
drawingBuffer.id = getRectShapeId(this.id, uuidv4());
|
||||||
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
|
this.manager.stateApi.onRectShapeAdded({ id: this.id, rectShape: drawingBuffer }, 'layer');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,26 +336,33 @@ export class CanvasLayer {
|
|||||||
const objects = get(arg, 'objects', this.state.objects);
|
const objects = get(arg, 'objects', this.state.objects);
|
||||||
|
|
||||||
const objectIds = objects.map(mapId);
|
const objectIds = objects.map(mapId);
|
||||||
|
|
||||||
|
let didUpdate = false;
|
||||||
|
|
||||||
// Destroy any objects that are no longer in state
|
// Destroy any objects that are no longer in state
|
||||||
for (const object of this.objects.values()) {
|
for (const object of this.objects.values()) {
|
||||||
if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
|
if (!objectIds.includes(object.id) && object.id !== this.drawingBuffer?.id) {
|
||||||
this.objects.delete(object.id);
|
this.objects.delete(object.id);
|
||||||
object.destroy();
|
object.destroy();
|
||||||
this.bboxNeedsUpdate = true;
|
didUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
if (await this._renderObject(obj)) {
|
if (await this._renderObject(obj)) {
|
||||||
this.bboxNeedsUpdate = true;
|
didUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.drawingBuffer) {
|
if (this.drawingBuffer) {
|
||||||
if (await this._renderObject(this.drawingBuffer)) {
|
if (await this._renderObject(this.drawingBuffer)) {
|
||||||
this.bboxNeedsUpdate = true;
|
didUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
this.calculateBbox();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOpacity(arg?: { opacity: number }) {
|
async updateOpacity(arg?: { opacity: number }) {
|
||||||
@ -410,6 +425,14 @@ export class CanvasLayer {
|
|||||||
async updateBbox() {
|
async updateBbox() {
|
||||||
this.log.trace('Updating bbox');
|
this.log.trace('Updating bbox');
|
||||||
|
|
||||||
|
// If the bbox has no width or height, that means the layer is fully transparent. This can happen if it is only
|
||||||
|
// eraser lines, fully clipped brush lines or if it has been fully erased. In this case, we should reset the layer
|
||||||
|
// so we aren't drawing shapes that do not render anything.
|
||||||
|
if (this.width === 0 || this.height === 0) {
|
||||||
|
this.manager.stateApi.onEntityReset({ id: this.id }, 'layer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onePixel = this.manager.getScaledPixel();
|
const onePixel = this.manager.getScaledPixel();
|
||||||
const bboxPadding = this.manager.getScaledBboxPadding();
|
const bboxPadding = this.manager.getScaledBboxPadding();
|
||||||
|
|
||||||
@ -450,23 +473,19 @@ export class CanvasLayer {
|
|||||||
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
|
assert(brushLine instanceof CanvasBrushLine || brushLine === undefined);
|
||||||
|
|
||||||
if (!brushLine) {
|
if (!brushLine) {
|
||||||
console.log('creating new brush line');
|
brushLine = new CanvasBrushLine(obj, this);
|
||||||
brushLine = new CanvasBrushLine(obj);
|
|
||||||
this.objects.set(brushLine.id, brushLine);
|
this.objects.set(brushLine.id, brushLine);
|
||||||
this.konva.objectGroup.add(brushLine.konva.group);
|
this.konva.objectGroup.add(brushLine.konva.group);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.log('updating brush line');
|
return await brushLine.update(obj, force);
|
||||||
if (await brushLine.update(obj, force)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'eraser_line') {
|
} else if (obj.type === 'eraser_line') {
|
||||||
let eraserLine = this.objects.get(obj.id);
|
let eraserLine = this.objects.get(obj.id);
|
||||||
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
|
assert(eraserLine instanceof CanvasEraserLine || eraserLine === undefined);
|
||||||
|
|
||||||
if (!eraserLine) {
|
if (!eraserLine) {
|
||||||
eraserLine = new CanvasEraserLine(obj);
|
eraserLine = new CanvasEraserLine(obj, this);
|
||||||
this.objects.set(eraserLine.id, eraserLine);
|
this.objects.set(eraserLine.id, eraserLine);
|
||||||
this.konva.objectGroup.add(eraserLine.konva.group);
|
this.konva.objectGroup.add(eraserLine.konva.group);
|
||||||
return true;
|
return true;
|
||||||
@ -480,12 +499,12 @@ export class CanvasLayer {
|
|||||||
assert(rect instanceof CanvasRect || rect === undefined);
|
assert(rect instanceof CanvasRect || rect === undefined);
|
||||||
|
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
rect = new CanvasRect(obj);
|
rect = new CanvasRect(obj, this);
|
||||||
this.objects.set(rect.id, rect);
|
this.objects.set(rect.id, rect);
|
||||||
this.konva.objectGroup.add(rect.konva.group);
|
this.konva.objectGroup.add(rect.konva.group);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (rect.update(obj, force)) {
|
if (await rect.update(obj, force)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,7 +513,7 @@ export class CanvasLayer {
|
|||||||
assert(image instanceof CanvasImage || image === undefined);
|
assert(image instanceof CanvasImage || image === undefined);
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
image = new CanvasImage(obj);
|
image = new CanvasImage(obj, this);
|
||||||
this.objects.set(image.id, image);
|
this.objects.set(image.id, image);
|
||||||
this.konva.objectGroup.add(image.konva.group);
|
this.konva.objectGroup.add(image.konva.group);
|
||||||
await image.updateImageSource(obj.image.name);
|
await image.updateImageSource(obj.image.name);
|
||||||
@ -510,6 +529,7 @@ export class CanvasLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startTransform() {
|
async startTransform() {
|
||||||
|
this.log.debug('Starting transform');
|
||||||
this.isTransforming = true;
|
this.isTransforming = true;
|
||||||
|
|
||||||
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
// When transforming, we want the stage to still be movable if the view tool is selected. If the transformer or
|
||||||
@ -538,6 +558,8 @@ export class CanvasLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async applyTransform() {
|
async applyTransform() {
|
||||||
|
this.log.debug('Applying transform');
|
||||||
|
|
||||||
this.isTransforming = false;
|
this.isTransforming = false;
|
||||||
const objectGroupClone = this.konva.objectGroup.clone();
|
const objectGroupClone = this.konva.objectGroup.clone();
|
||||||
const rect = {
|
const rect = {
|
||||||
@ -556,6 +578,8 @@ export class CanvasLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cancelTransform() {
|
async cancelTransform() {
|
||||||
|
this.log.debug('Canceling transform');
|
||||||
|
|
||||||
this.isTransforming = false;
|
this.isTransforming = false;
|
||||||
this.resetScale();
|
this.resetScale();
|
||||||
await this.updatePosition({ position: this.state.position });
|
await this.updatePosition({ position: this.state.position });
|
||||||
@ -566,7 +590,9 @@ export class CanvasLayer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBbox = debounce(() => {
|
calculateBbox = debounce(() => {
|
||||||
|
this.log.debug('Calculating bbox');
|
||||||
|
|
||||||
if (this.objects.size === 0) {
|
if (this.objects.size === 0) {
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
this.offsetY = 0;
|
this.offsetY = 0;
|
||||||
@ -581,9 +607,21 @@ export class CanvasLayer {
|
|||||||
|
|
||||||
console.log('getBbox rect', rect);
|
console.log('getBbox rect', rect);
|
||||||
|
|
||||||
// If there are no eraser strokes, we can use the client rect directly
|
/**
|
||||||
|
* In some cases, we can use konva's getClientRect as the bbox, but there are some cases where we need to calculate
|
||||||
|
* the bbox using pixel data:
|
||||||
|
*
|
||||||
|
* - Eraser lines are normal lines, except they composite as transparency. Konva's getClientRect includes them when
|
||||||
|
* calculating the bbox.
|
||||||
|
* - Clipped portions of lines will be included in the client rect.
|
||||||
|
*
|
||||||
|
* TODO(psyche): Using pixel data is slow. Is it possible to be clever and somehow subtract the eraser lines and
|
||||||
|
* clipped areas from the client rect?
|
||||||
|
*/
|
||||||
for (const obj of this.objects.values()) {
|
for (const obj of this.objects.values()) {
|
||||||
if (obj instanceof CanvasEraserLine) {
|
const isEraserLine = obj instanceof CanvasEraserLine;
|
||||||
|
const hasClip = obj instanceof CanvasBrushLine && obj.state.clip;
|
||||||
|
if (isEraserLine || hasClip) {
|
||||||
needsPixelBbox = true;
|
needsPixelBbox = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
|
import type { CanvasLayer } from 'features/controlLayers/konva/CanvasLayer';
|
||||||
import type { RectShape } from 'features/controlLayers/store/types';
|
import type { RectShape } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
@ -7,7 +8,9 @@ export class CanvasRect {
|
|||||||
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
|
static GROUP_NAME = `${CanvasRect.NAME_PREFIX}_group`;
|
||||||
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
|
static RECT_NAME = `${CanvasRect.NAME_PREFIX}_rect`;
|
||||||
|
|
||||||
private state: RectShape;
|
state: RectShape;
|
||||||
|
|
||||||
|
type = 'rect';
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
konva: {
|
konva: {
|
||||||
@ -15,9 +18,16 @@ export class CanvasRect {
|
|||||||
rect: Konva.Rect;
|
rect: Konva.Rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(state: RectShape) {
|
parent: CanvasLayer;
|
||||||
|
|
||||||
|
constructor(state: RectShape, parent: CanvasLayer) {
|
||||||
const { id, x, y, width, height, color } = state;
|
const { id, x, y, width, height, color } = state;
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
|
this.parent.log.trace(`Creating rect ${this.id}`);
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
|
group: new Konva.Group({ name: CanvasRect.GROUP_NAME, listening: false }),
|
||||||
rect: new Konva.Rect({
|
rect: new Konva.Rect({
|
||||||
@ -35,8 +45,9 @@ export class CanvasRect {
|
|||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(state: RectShape, force?: boolean): boolean {
|
async update(state: RectShape, force?: boolean): Promise<boolean> {
|
||||||
if (this.state !== state || force) {
|
if (this.state !== state || force) {
|
||||||
|
this.parent.log.trace(`Updating rect ${this.id}`);
|
||||||
const { x, y, width, height, color } = state;
|
const { x, y, width, height, color } = state;
|
||||||
this.konva.rect.setAttrs({
|
this.konva.rect.setAttrs({
|
||||||
x,
|
x,
|
||||||
@ -53,6 +64,7 @@ export class CanvasRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.parent.log.trace(`Destroying rect ${this.id}`);
|
||||||
this.konva.group.destroy();
|
this.konva.group.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
layerEraserLineAdded,
|
layerEraserLineAdded,
|
||||||
layerImageCacheChanged,
|
layerImageCacheChanged,
|
||||||
layerRectShapeAdded,
|
layerRectShapeAdded,
|
||||||
|
layerReset,
|
||||||
layerScaled,
|
layerScaled,
|
||||||
layerTranslated,
|
layerTranslated,
|
||||||
rgBboxChanged,
|
rgBboxChanged,
|
||||||
@ -70,7 +71,12 @@ export class CanvasStateApi {
|
|||||||
getState = () => {
|
getState = () => {
|
||||||
return this.store.getState().canvasV2;
|
return this.store.getState().canvasV2;
|
||||||
};
|
};
|
||||||
|
onEntityReset = (arg: { id: string }, entityType: CanvasEntity['type']) => {
|
||||||
|
log.debug('onEntityReset');
|
||||||
|
if (entityType === 'layer') {
|
||||||
|
this.store.dispatch(layerReset(arg));
|
||||||
|
}
|
||||||
|
};
|
||||||
onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => {
|
onPosChanged = (arg: PositionChangedArg, entityType: CanvasEntity['type']) => {
|
||||||
log.debug('onPosChanged');
|
log.debug('onPosChanged');
|
||||||
if (entityType === 'layer') {
|
if (entityType === 'layer') {
|
||||||
|
@ -188,7 +188,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [
|
points: [
|
||||||
// The last point of the last line is already normalized to the entity's coordinates
|
// The last point of the last line is already normalized to the entity's coordinates
|
||||||
@ -206,7 +206,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||||
strokeWidth: toolState.brush.width,
|
strokeWidth: toolState.brush.width,
|
||||||
@ -225,7 +225,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [
|
points: [
|
||||||
// The last point of the last line is already normalized to the entity's coordinates
|
// The last point of the last line is already normalized to the entity's coordinates
|
||||||
@ -242,7 +242,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||||
strokeWidth: toolState.eraser.width,
|
strokeWidth: toolState.eraser.width,
|
||||||
@ -257,7 +257,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getRectShapeId(selectedEntityAdapter.id, uuidv4()),
|
id: getRectShapeId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'rect_shape',
|
type: 'rect_shape',
|
||||||
x: pos.x - selectedEntity.position.x,
|
x: pos.x - selectedEntity.position.x,
|
||||||
y: pos.y - selectedEntity.position.y,
|
y: pos.y - selectedEntity.position.y,
|
||||||
@ -357,7 +357,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getBrushLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getBrushLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'brush_line',
|
type: 'brush_line',
|
||||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||||
strokeWidth: toolState.brush.width,
|
strokeWidth: toolState.brush.width,
|
||||||
@ -389,7 +389,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
|
|||||||
await selectedEntityAdapter.finalizeDrawingBuffer();
|
await selectedEntityAdapter.finalizeDrawingBuffer();
|
||||||
}
|
}
|
||||||
await selectedEntityAdapter.setDrawingBuffer({
|
await selectedEntityAdapter.setDrawingBuffer({
|
||||||
id: getEraserLineId(selectedEntityAdapter.id, uuidv4()),
|
id: getEraserLineId(selectedEntityAdapter.id, uuidv4(), true),
|
||||||
type: 'eraser_line',
|
type: 'eraser_line',
|
||||||
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
points: [pos.x - selectedEntity.position.x, pos.y - selectedEntity.position.y],
|
||||||
strokeWidth: toolState.eraser.width,
|
strokeWidth: toolState.eraser.width,
|
||||||
|
@ -5,9 +5,12 @@
|
|||||||
// Getters for non-singleton layer and object IDs
|
// Getters for non-singleton layer and object IDs
|
||||||
export const getRGId = (entityId: string) => `region_${entityId}`;
|
export const getRGId = (entityId: string) => `region_${entityId}`;
|
||||||
export const getLayerId = (entityId: string) => `layer_${entityId}`;
|
export const getLayerId = (entityId: string) => `layer_${entityId}`;
|
||||||
export const getBrushLineId = (entityId: string, lineId: string) => `${entityId}.brush_line_${lineId}`;
|
export const getBrushLineId = (entityId: string, lineId: string, isBuffer?: boolean) =>
|
||||||
export const getEraserLineId = (entityId: string, lineId: string) => `${entityId}.eraser_line_${lineId}`;
|
`${entityId}.${isBuffer ? 'buffer_' : ''}brush_line_${lineId}`;
|
||||||
export const getRectShapeId = (entityId: string, rectId: string) => `${entityId}.rect_${rectId}`;
|
export const getEraserLineId = (entityId: string, lineId: string, isBuffer?: boolean) =>
|
||||||
|
`${entityId}.${isBuffer ? 'buffer_' : ''}eraser_line_${lineId}`;
|
||||||
|
export const getRectShapeId = (entityId: string, rectId: string, isBuffer?: boolean) =>
|
||||||
|
`${entityId}.${isBuffer ? 'buffer_' : ''}rect_${rectId}`;
|
||||||
export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`;
|
export const getImageObjectId = (entityId: string, imageId: string) => `${entityId}.image_${imageId}`;
|
||||||
export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`;
|
export const getObjectGroupId = (entityId: string, groupId: string) => `${entityId}.objectGroup_${groupId}`;
|
||||||
export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`;
|
export const getLayerBboxId = (entityId: string) => `${entityId}.bbox`;
|
||||||
|
Loading…
Reference in New Issue
Block a user